ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,18 @@
+package nextstep.security.authorization.manager;
+
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authorization.AccessDeniedException;
+import nextstep.security.authorization.AuthorizationDecision;
+
+@FunctionalInterface
+public interface AuthorizationManager<T> {
+
+    default void verify(Authentication authentication, T object) {
+        AuthorizationDecision decision = check(authentication, object);
+        if (decision != null && decision.isDenied()) {
+            throw new AccessDeniedException();
+        }
+    }
+
+    AuthorizationDecision check(Authentication authentication, T object);
+}

리뷰: 최근 spring security 버전에서는 `check`는 deprecated되었습니다. `AuthorizationDecision`이 지금 만들어주신 것과 같이 구현체로 되어있고, 일반적으로 오픈소스들이 커질수록 단순하게 구현해두었던 구현체들을 추상화하는 형태로 개선해나가는데요.

그래서 최근 버전에서는 구현체를 반환하는 `check`를 그대로 사용하기보다는 deprecated해둔 뒤 이를 추상화한 `AuthorizationResult`를 반환하는 메소드를 추가하여 해당 메소드 사용을 권장하고 있습니다.

https://github.com/franticticktick/spring-security/blob/main/core/src/main/java/org/springframework/security/authorization/AuthorizationManager.java

https://github.com/spring-projects/spring-security/pull/14712
https://github.com/spring-projects/spring-security/pull/14846
@@ -0,0 +1,34 @@
+package nextstep.security.authorization.manager;
+
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authorization.AuthorizationDecision;
+import nextstep.security.authorization.Secured;
+import org.aopalliance.intercept.MethodInvocation;
+
+import java.lang.reflect.Method;
+import java.util.Set;
+
+public class SecuredAuthorizationManager implements AuthorizationManager<MethodInvocation> {
+
+    private final AuthorityAuthorizationManager<Set<String>> authorityAuthorizationManager;
+
+    public SecuredAuthorizationManager(AuthorityAuthorizationManager<Set<String>> authorityAuthorizationManager) {
+        this.authorityAuthorizationManager = authorityAuthorizationManager;
+    }
+
+    @Override
+    public AuthorizationDecision check(Authentication authentication, MethodInvocation methodInvocation) {
+        if (authentication == null || !authentication.isAuthenticated()) {
+            return AuthorizationDecision.denied();
+        }
+
+        Method method = methodInvocation.getMethod();
+
+        if (method.isAnnotationPresent(Secured.class)) {
+            Secured secured = method.getAnnotation(Secured.class);
+            return authorityAuthorizationManager.check(authentication, Set.of(secured.value()));
+        }
+
+        return AuthorizationDecision.granted();
+    }
+}

리뷰: SecuredAuthorizationManager 에서 정확한 인가 처리를 AuthorityAuthorizationManager 를 위임하도록 할 수 있겠네요.
AuthorityAuthorizationManager에서도 제네릭을 통해 전달받은 파라미터에 따라서 다른 처리를 할 수 도 있을 것 같아요.

제가 이해한 내용을 정리하면 SecuredAuthorizationManager 는 인가에 필요한 값을 어노테이션 기반으로 동적으로 사용합니다. 
하지만 SecuredAuthorizationManager 는 정확히 어떤 인가 작업을 할지는 관심이 없습니다.  그래서 AuthorityAuthorizationManager 에게 인가 작업을 위임합니다.
제네릭을 사용하는 AuthorityAuthorizationManager 에서는 구현체나 제네릭 값을 기반으로 전달 받은 값과 Authentication 을 통해서 인가를 작업해주는 것 같습니다.
@@ -0,0 +1,25 @@
+package nextstep.security.authorization;
+
+public class AuthorizationDecision {
+
+    private static final AuthorizationDecision GRANTED = new AuthorizationDecision(true);
+    private static final AuthorizationDecision DENIED = new AuthorizationDecision(false);
+
+    private final boolean isGranted;
+
+    public static AuthorizationDecision granted() {
+        return GRANTED;
+    }
+
+    public static AuthorizationDecision denied() {
+        return DENIED;
+    }
+
+    protected AuthorizationDecision(final boolean isGranted) {
+        this.isGranted = isGranted;
+    }
+
+    public boolean isDenied() {
+        return !isGranted;
+    }
+}

리뷰: AuthorizationDecision 에서 성공 실패만 가지고 있기 때문에 좋은 방법이네요.
@@ -1,38 +1,28 @@
 package nextstep.security.authorization;
 
-import nextstep.security.authentication.Authentication;
-import nextstep.security.context.SecurityContextHolder;
-import org.springframework.web.filter.OncePerRequestFilter;
-
 import jakarta.servlet.FilterChain;
 import jakarta.servlet.ServletException;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authorization.manager.RequestMatcherDelegatingAuthorizationManager;
+import nextstep.security.context.SecurityContextHolder;
+import org.springframework.web.filter.OncePerRequestFilter;
+
 import java.io.IOException;
-import java.util.Set;
 
-public class CheckAuthenticationFilter extends OncePerRequestFilter {
-    private static final String DEFAULT_REQUEST_URI = "/members";
+public class AuthorizationFilter extends OncePerRequestFilter {
+
+    private final RequestMatcherDelegatingAuthorizationManager authorizationManager;
+
+    public AuthorizationFilter(RequestMatcherDelegatingAuthorizationManager authorizationManager) {
+        this.authorizationManager = authorizationManager;
+    }
 
     @Override
     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
-        if (!DEFAULT_REQUEST_URI.equals(request.getRequestURI())) {
-            filterChain.doFilter(request, response);
-            return;
-        }
-
         Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
-        if (authentication == null || !authentication.isAuthenticated()) {
-            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
-            return;
-        }
-
-        Set<String> authorities = authentication.getAuthorities();
-        if (!authorities.contains("ADMIN")) {
-            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
-            return;
-        }
-
+        authorizationManager.verifyInFilter(request, authentication);
         filterChain.doFilter(request, response);
     }
 }

리뷰: Forbidden 으로 처리하도록 변경해야겠네요.

이 부분에서도 이쪽에서도 왜 check보다 verify를 권장하는 지 보여주는 것 같네요. AuthorizationDecision 인가 결과에 대한 처리에 일관성을 보장해주는 것이 필요해 보이네요!
@@ -0,0 +1,31 @@
+package nextstep.security.matcher;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.http.HttpMethod;
+
+public class MvcRequestMatcher implements RequestMatcher {
+
+    private final HttpMethod httpMethod;
+    private final String[] requestURIs;
+
+    public MvcRequestMatcher(HttpMethod httpMethod, String... requestURIs) {
+        this.httpMethod = httpMethod;
+        this.requestURIs = requestURIs;
+    }
+
+    @Override
+    public boolean matches(HttpServletRequest request) {
+        boolean matchMethod = httpMethod.name().equalsIgnoreCase(request.getMethod());
+
+        boolean matchURI = false;
+        final String requestURI = request.getRequestURI();
+        for (String uri : requestURIs) {
+            if (uri.equalsIgnoreCase(requestURI)) {
+                matchURI = true;
+                break;
+            }
+        }
+
+        return matchMethod && matchURI;
+    }
+}

리뷰: 성능 문제와 가독성은 언제나 고민이 되는 부분입니다.
요즘은 개발하면 Stream 을 가독성이 조금 더 좋다고 생각해서 선택하는 것 같네요. 
말씀해주신 대로 ForEach 로 수정해보겠습니다!
@@ -0,0 +1,34 @@
+package nextstep.security.authorization.manager;
+
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authorization.AuthorizationDecision;
+import nextstep.security.authorization.Secured;
+import org.aopalliance.intercept.MethodInvocation;
+
+import java.lang.reflect.Method;
+import java.util.Set;
+
+public class SecuredAuthorizationManager implements AuthorizationManager<MethodInvocation> {
+
+    private final AuthorityAuthorizationManager<Set<String>> authorityAuthorizationManager;
+
+    public SecuredAuthorizationManager(AuthorityAuthorizationManager<Set<String>> authorityAuthorizationManager) {
+        this.authorityAuthorizationManager = authorityAuthorizationManager;
+    }
+
+    @Override
+    public AuthorizationDecision check(Authentication authentication, MethodInvocation methodInvocation) {
+        if (authentication == null || !authentication.isAuthenticated()) {
+            return AuthorizationDecision.denied();
+        }
+
+        Method method = methodInvocation.getMethod();
+
+        if (method.isAnnotationPresent(Secured.class)) {
+            Secured secured = method.getAnnotation(Secured.class);
+            return authorityAuthorizationManager.check(authentication, Set.of(secured.value()));
+        }
+
+        return AuthorizationDecision.granted();
+    }
+}

리뷰: 인가 부분을 위임하도록 변경해보았습니다!
@@ -0,0 +1,65 @@
+package nextstep.security.authorization.manager;
+
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authorization.AuthorizationDecision;
+import nextstep.security.authorization.ForbiddenException;
+
+import java.util.Collection;
+import java.util.Set;
+
+public class AuthorityAuthorizationManager<T> implements AuthorizationManager<T> {
+
+    @Override
+    public AuthorizationDecision check(Authentication authentication, T authorities) {
+        if (authentication == null || !authentication.isAuthenticated()) {
+            return AuthorizationDecision.denied();
+        }
+
+        boolean hasNotRole = false;
+
+        if (authorities instanceof Collection<?> collection) {
+            hasNotRole = roleCheckByCollection(authentication, collection);
+        } else if (authorities instanceof String authority) {
+            hasNotRole = roleCheckByString(authentication, authority);
+        }
+
+        if (hasNotRole) {
+            throw new ForbiddenException();
+        }
+
+        return AuthorizationDecision.granted();
+    }
+
+    private boolean roleCheckByCollection(Authentication authentication, Collection<?> requiredAuthorities) {
+        if (requiredAuthorities == null || requiredAuthorities.isEmpty()) {
+            return false;
+        }
+
+        Set<String> userAuthorities = authentication.getAuthorities();
+        if (userAuthorities == null || userAuthorities.isEmpty()) {
+            return true;
+        }
+
+        if (requiredAuthorities.iterator().next() instanceof String) {
+
+            @SuppressWarnings("unchecked")
+            Collection<String> requires = (Collection<String>) requiredAuthorities;
+            for (String requiredAuthority : requires) {
+                if (userAuthorities.contains(requiredAuthority)) {
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+
+    private boolean roleCheckByString(Authentication authentication, String requiredAuthority) {
+        Set<String> userAuthorities = authentication.getAuthorities();
+        if (userAuthorities == null || userAuthorities.isEmpty()) {
+            return true;
+        }
+
+        return !userAuthorities.contains(requiredAuthority);
+    }
+}

리뷰: 필터와 인터셉터 혹은 인터셉터에서도 여러 종류 값을 사용하기 위해서 좋은 선택 같습니다,

그렇다면 AuthorityAuthorizationManager 는 여러 종류의 구현체를 만들어서 관리할 수 도 있을 것 같습니다.

예를 들어 RequestMatcherDelegatingAuthorizationManager 처럼 여러 구현체에  support 메서드나 Matcher를 만듭니다.
이를 통해 처리할 수 있는 인가 작업인지 확인하고 그에 맞는  AuthorityAuthorizationManager  구현체가 작동하는 방식으로 쉽게 관리 될 것 같아요!
@@ -0,0 +1,18 @@
+package nextstep.security.authorization.manager;
+
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authorization.AccessDeniedException;
+import nextstep.security.authorization.AuthorizationDecision;
+
+@FunctionalInterface
+public interface AuthorizationManager<T> {
+
+    default void verify(Authentication authentication, T object) {
+        AuthorizationDecision decision = check(authentication, object);
+        if (decision != null && decision.isDenied()) {
+            throw new AccessDeniedException();
+        }
+    }
+
+    AuthorizationDecision check(Authentication authentication, T object);
+}

리뷰: 제가 이 미션을 진행하면서 느낌 점은 세 가지입니다.
1. AuthorizationDecision 의 널의 위험성
2. AuthorizationDecision 가 실패됬을 때의 예외 처리 일관성 
3. 사용도 안하는 결과를 리턴
이런 상황때문에 verify를 권장하는 것 같습니다. 
하지만 AuthorizationDecision 의 결과를 커스텀 예외나 조건부 로직이 필요한 경우만 check을 사용해 결과를 핸들링 할 것 같습니다.
@@ -0,0 +1,18 @@
+package nextstep.security.authorization.manager;
+
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authorization.AccessDeniedException;
+import nextstep.security.authorization.AuthorizationDecision;
+
+@FunctionalInterface
+public interface AuthorizationManager<T> {
+
+    default void verify(Authentication authentication, T object) {
+        AuthorizationDecision decision = check(authentication, object);
+        if (decision != null && decision.isDenied()) {
+            throw new AccessDeniedException();
+        }
+    }
+
+    AuthorizationDecision check(Authentication authentication, T object);
+}

리뷰: 미션을 하다보니 말씀해주신 대로 AuthorizationDecision 가 deprecated된 이유가 간접적으로 느껴지는 것 같습니다.
RequestMatcherDelegatingAuthorizationManager 에서도 checkInFilter 보다는 verifyInFilter를 사용한다면 
AuthorizationFilter 에서의 인가 요청, 예외처리 두가지 역할에서 인가 요청만 남게되어서 훨씬 깔끔합니다!
@@ -0,0 +1,34 @@
+package nextstep.security.authorization.manager;
+
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authorization.AuthorizationDecision;
+import nextstep.security.authorization.Secured;
+import org.aopalliance.intercept.MethodInvocation;
+
+import java.lang.reflect.Method;
+import java.util.Set;
+
+public class SecuredAuthorizationManager implements AuthorizationManager<MethodInvocation> {
+
+    private final AuthorityAuthorizationManager<Set<String>> authorityAuthorizationManager;
+
+    public SecuredAuthorizationManager(AuthorityAuthorizationManager<Set<String>> authorityAuthorizationManager) {
+        this.authorityAuthorizationManager = authorityAuthorizationManager;
+    }
+
+    @Override
+    public AuthorizationDecision check(Authentication authentication, MethodInvocation methodInvocation) {
+        if (authentication == null || !authentication.isAuthenticated()) {
+            return AuthorizationDecision.denied();
+        }
+
+        Method method = methodInvocation.getMethod();
+
+        if (method.isAnnotationPresent(Secured.class)) {
+            Secured secured = method.getAnnotation(Secured.class);
+            return authorityAuthorizationManager.check(authentication, Set.of(secured.value()));
+        }
+
+        return AuthorizationDecision.granted();
+    }
+}

리뷰: 네 이해해주신 내용이 맞습니다 :)
@@ -0,0 +1,18 @@
+package nextstep.security.authorization.manager;
+
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authorization.AccessDeniedException;
+import nextstep.security.authorization.AuthorizationDecision;
+
+@FunctionalInterface
+public interface AuthorizationManager<T> {
+
+    default void verify(Authentication authentication, T object) {
+        AuthorizationDecision decision = check(authentication, object);
+        if (decision != null && decision.isDenied()) {
+            throw new AccessDeniedException();
+        }
+    }
+
+    AuthorizationDecision check(Authentication authentication, T object);
+}

리뷰: 사실 spring security에서는 `verify`를 권장하는 것은 아니에요. (오해의 소지가 있을 수 있을 것 같아 미리 말씀드리면 정답을 요구하기 위해 남겨드린 질문은 아닙니다.)

말씀해주신 이유 중 먼저 1에 대해서 이야기하면 null 위험성을 발생시키는 것은 맞지만 `AuthorizationDecision`의 반환은 인가됨(true), 인가안됨(false)만 있는 것이 아니라 인가 상황이 없음을 나타내기도 합니다. 가령 `@Secured`가 걸린 곳이 없어 검증할 `authorities`가 없는 경우가 있겠네요.
```java
	public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation mi) {
		Set<String> authorities = getAuthorities(mi);
		return authorities.isEmpty() ? null : this.authoritiesAuthorizationManager.check(authentication, authorities);
	}

	private Set<String> getAuthorities(MethodInvocation methodInvocation) {
		Method method = methodInvocation.getMethod();
		Object target = methodInvocation.getThis();
		Class<?> targetClass = (target != null) ? target.getClass() : null;
		MethodClassKey cacheKey = new MethodClassKey(method, targetClass);
		return this.cachedAuthorities.computeIfAbsent(cacheKey, (k) -> resolveAuthorities(method, targetClass));
	}

	private Set<String> resolveAuthorities(Method method, Class<?> targetClass) {
		Secured secured = findSecuredAnnotation(method, targetClass);
		return (secured != null) ? Set.of(secured.value()) : Collections.emptySet();
	}
```

다음으로 중요한 것은 인가를 체크했을때 인가되지 않은 유저인 것이 `AuthorizationManager`입장에서는 예외상황은 아닐 수 있다에요. 예외를 발생시키는 것은 이 상황이 실패했으니 실패를 전파하는 것이 목적일텐데요. spring security 입장에서는 `AuthorizationManager`는 인가가 되었는지 확인해주는 객체이고, 실제 에러는 `AuthorizationFilter` 등과 같은 필터에서 인가가 안되었는 상황을 판단하여 예외를 발생시켜주고 있어요.

그래서 내부적으론 조금 더 복잡하지만 `AuthorizationFilter`에서는 인가 결과에 대한 이벤트를 발행한 다음 예외를 발생시키기도 해요.
```java
AuthorizationResult result = this.authorizationManager.authorize(this::getAuthentication, request);
this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, request, result);
if (result != null && !result.isGranted()) {
	throw new AuthorizationDeniedException("Access Denied", result);
}
chain.doFilter(request, response);
```

spring security의 `AuthorizationManager`를 보면 예외를 발생시키는 코드가 없는 것을 보시게 될텐데 그런 것들도 위와 같은 이유에서일 가능성이 높습니다 :)
@@ -0,0 +1,31 @@
+package nextstep.security.matcher;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.http.HttpMethod;
+
+public class MvcRequestMatcher implements RequestMatcher {
+
+    private final HttpMethod httpMethod;
+    private final String[] requestURIs;
+
+    public MvcRequestMatcher(HttpMethod httpMethod, String... requestURIs) {
+        this.httpMethod = httpMethod;
+        this.requestURIs = requestURIs;
+    }
+
+    @Override
+    public boolean matches(HttpServletRequest request) {
+        boolean matchMethod = httpMethod.name().equalsIgnoreCase(request.getMethod());
+
+        boolean matchURI = false;
+        final String requestURI = request.getRequestURI();
+        for (String uri : requestURIs) {
+            if (uri.equalsIgnoreCase(requestURI)) {
+                matchURI = true;
+                break;
+            }
+        }
+
+        return matchMethod && matchURI;
+    }
+}

리뷰: 실제로는 `httpMethod`, `requestUri`각각 하나만을 가지므로 `String[]`으로 표현되진 않습니다. 딱 하나로 지정된 method, uri만을 검증하면 되는거죠.

여러개의 uri가 필요한 경우라면 필요한 객체가 `RequestMatcher`를 여러개로 들고 있는 형태가 더 자연스럽습니다 :)
@@ -0,0 +1,25 @@
+package nextstep.security.authorization;
+
+public class AuthorizationDecision {
+
+    private static final AuthorizationDecision GRANTED = new AuthorizationDecision(true);
+    private static final AuthorizationDecision DENIED = new AuthorizationDecision(false);
+
+    private final boolean isGranted;
+
+    public static AuthorizationDecision granted() {
+        return GRANTED;
+    }
+
+    public static AuthorizationDecision denied() {
+        return DENIED;
+    }
+
+    protected AuthorizationDecision(final boolean isGranted) {
+        this.isGranted = isGranted;
+    }
+
+    public boolean isDenied() {
+        return !isGranted;
+    }
+}

리뷰: 미션에서는 spring security의 기능이 축소되어 true, false가 잘 안와닿으실 수 있으나 실제로는 그보다 더 다양한 구현체들이 생길 수 있습니다.

https://github.com/spring-projects/spring-security/blob/main/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationDecision.java
@@ -0,0 +1,18 @@
+package nextstep.security.authorization.manager;
+
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authorization.AccessDeniedException;
+import nextstep.security.authorization.AuthorizationDecision;
+
+@FunctionalInterface
+public interface AuthorizationManager<T> {
+
+    default void verify(Authentication authentication, T object) {
+        AuthorizationDecision decision = check(authentication, object);
+        if (decision != null && decision.isDenied()) {
+            throw new AccessDeniedException();
+        }
+    }
+
+    AuthorizationDecision check(Authentication authentication, T object);
+}

리뷰: 자세한 설명 감사합니다. 단순 인가 성공, 실패 뿐만 아니라 여러 케이스에 대해서는 깊이 생각 못했었는데, 고민해볼 포인트네요!

AuthorizationManager에서는 인가 확인, AuthorizationFilter 전체적인 맥락을 파악하여 필터링 해주는 것이군요!
@@ -0,0 +1,31 @@
+package nextstep.security.matcher;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.http.HttpMethod;
+
+public class MvcRequestMatcher implements RequestMatcher {
+
+    private final HttpMethod httpMethod;
+    private final String[] requestURIs;
+
+    public MvcRequestMatcher(HttpMethod httpMethod, String... requestURIs) {
+        this.httpMethod = httpMethod;
+        this.requestURIs = requestURIs;
+    }
+
+    @Override
+    public boolean matches(HttpServletRequest request) {
+        boolean matchMethod = httpMethod.name().equalsIgnoreCase(request.getMethod());
+
+        boolean matchURI = false;
+        final String requestURI = request.getRequestURI();
+        for (String uri : requestURIs) {
+            if (uri.equalsIgnoreCase(requestURI)) {
+                matchURI = true;
+                break;
+            }
+        }
+
+        return matchMethod && matchURI;
+    }
+}

리뷰: 하나의 Matcher에 하나의 URI 검증하는 것이 더 이해하기 좋겠네요!
@@ -3,6 +3,7 @@
 import healeat.server.domain.HealthPlan;
 import healeat.server.domain.HealthPlanImage;
 import healeat.server.web.dto.HealthPlanResponseDto;
+import org.springframework.data.domain.Page;
 import org.springframework.stereotype.Component;
 
 import java.time.LocalDateTime;
@@ -28,15 +29,20 @@ public static HealthPlanResponseDto.DeleteResultDto toDeleteResultDto(HealthPlan
                 .build();
     }
 
-    public static HealthPlanResponseDto.HealthPlanListDto toHealthPlanListDto(List<HealthPlan> healthPlans) {
-        List<HealthPlanResponseDto.HealthPlanOneDto> healthPlanDtoList = healthPlans.stream()
+    public static HealthPlanResponseDto toHealthPlanResponseDto(Page<HealthPlan> healthPlans) {
+
+        List<HealthPlanResponseDto.HealthPlanOneDto> healthPlanOneDtoList = healthPlans.stream()
                 .map(HealthPlanConverter::toHealthPlanOneDto)
                 .collect(Collectors.toList());
 
-        return  HealthPlanResponseDto.HealthPlanListDto.builder()
-                    .HealthPlanList(healthPlanDtoList)
-                    .build();
-
+        return HealthPlanResponseDto.builder()
+                .HealthPlanList(healthPlanOneDtoList)
+                .listSize(healthPlanOneDtoList.size())
+                .totalPage(healthPlans.getTotalPages())
+                .totalElements(healthPlans.getTotalElements())
+                .isFirst(healthPlans.isFirst())
+                .isLast(healthPlans.isLast())
+                .build();
     }
 
     public static HealthPlanResponseDto.HealthPlanOneDto toHealthPlanOneDto(HealthPlan healthPlan) {

리뷰: toList(); 가 좋음
@@ -3,7 +3,7 @@ import { VerticalLine } from "../Line";
 interface LessonInfoProps {
   startDate: string;
   endDate: string;
-  locationType: string;
+  locationType?: string;
   address?: string;
 }
 

리뷰: 이건 왜 옵셔널로 바꾸신건가요?
@@ -8,6 +8,7 @@
 import jakarta.persistence.GenerationType;
 import jakarta.persistence.Id;
 import jakarta.persistence.Table;
+import java.time.LocalDateTime;
 import lombok.AccessLevel;
 import lombok.EqualsAndHashCode;
 import lombok.Getter;
@@ -49,6 +50,8 @@ public class ReviewGroup {
     @Embedded
     private GroupAccessCode groupAccessCode;
 
+    @Column(name = "created_at", nullable = false)
+    private LocalDateTime createdAt;
 
     private ReviewGroup(@Nullable Long memberId, long templateId, String reviewee, String projectName,
                         String reviewRequestCode, String groupAccessCode) {
@@ -64,6 +67,7 @@ private ReviewGroup(@Nullable Long memberId, long templateId, String reviewee, S
         } else {
             this.groupAccessCode = null;
         }
+        this.createdAt = LocalDateTime.now();
     }
 
     public ReviewGroup(Long memberId, long templateId, String reviewee, String projectName, String reviewRequestCode) {

리뷰: 사소한 부분이지만 정렬 순서를 통일해봐요!
Column 다 나오고 Embedded 오는게 좋겠네요~
@@ -1,4 +1,4 @@
-package reviewme.review.service;
+package reviewme.util;
 
 import lombok.Getter;
 
@@ -10,7 +10,7 @@ public class PageSize {
 
     private final int size;
 
-    PageSize(Integer size) {
+    public PageSize(Integer size) {
         if (size == null || size < 1 || size > MAX_SIZE) {
             this.size = DEFAULT_SIZE;
             return;

리뷰: 패키지 변경 좋네요 ㅎㅎ