ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,96 @@
+package nextstep.security.role;
+
+import org.springframework.util.StringUtils;
+
+import java.util.*;
+
+public class RoleHierarchyImpl implements RoleHierarchy {
+
+    private static final List<GrantedAuthority> EMPTY_AUTHORITIES = Collections.emptyList();
+
+    private final Map<String, Set<GrantedAuthority>> roleHierarchy;
+
+
+    public RoleHierarchyImpl(Map<String, Set<GrantedAuthority>> roleHierarchy) {
+        this.roleHierarchy = new HashMap<>(roleHierarchy);
+    }
+
+    public static Builder builder() {
+        return new RoleHierarchyImpl.Builder();
+    }
+
+    @Override
+    public Collection<GrantedAuthority> getReachableRoles(Collection<GrantedAuthority> authorities) {
+        if (authorities == null || authorities.isEmpty()) {
+            return EMPTY_AUTHORITIES;
+        }
+
+        Set<GrantedAuthority> reachableRoles = new HashSet<>();
+        for (GrantedAuthority authority : authorities) {
+            reachableRoles.add(authority);
+
+            Collection<GrantedAuthority> impliedAuthorities = roleHierarchy.get(authority.getAuthority());
+            if (impliedAuthorities != null) {
+                reachableRoles.addAll(impliedAuthorities);
+            }
+        }
+
+        return reachableRoles;
+    }
+
+    public static final class Builder {
+
+        private final Map<String, Set<GrantedAuthority>> hierarchy;
+
+        private Builder() {
+            this.hierarchy = new HashMap<>();
+        }
+
+        public Builder.ImpliedRoles role(String role) {
+            if (!StringUtils.hasText(role)) {
+                throw new IllegalArgumentException("role must not be null or empty");
+            }
+
+            return new Builder.ImpliedRoles(role);
+        }
+
+        public RoleHierarchyImpl build() {
+            return new RoleHierarchyImpl(this.hierarchy);
+        }
+
+        private RoleHierarchyImpl.Builder addHierarchy(String role, String... impliedRoles) {
+            Set<GrantedAuthority> grantedAuthorities = new HashSet<>();
+
+            for (String impliedRole : impliedRoles) {
+                grantedAuthorities.add(new SimpleGrantedAuthority(impliedRole));
+            }
+
+            this.hierarchy.put(role, grantedAuthorities);
+            return this;
+        }
+
+        public final class ImpliedRoles {
+
+            private final String role;
+
+            private ImpliedRoles(String role) {
+                this.role = role;
+            }
+
+            public Builder addImplies(String... impliedRoles) {
+                if (impliedRoles == null) {
+                    throw new IllegalArgumentException("at least one implied role must be provided");
+                }
+
+                for (String impliedRole : impliedRoles) {
+                    if (!StringUtils.hasText(impliedRole)) {
+                        throw new IllegalArgumentException("at least one implied role must be provided");
+                    }
+                }
+
+                return Builder.this.addHierarchy(this.role, impliedRoles);
+            }
+        }
+
+    }
+}

리뷰: 수정하겠습니다!
@@ -0,0 +1,7 @@
+package nextstep.security.role;
+
+import java.util.Collection;
+
+public interface RoleHierarchy {
+    Collection<GrantedAuthority> getReachableRoles(Collection<GrantedAuthority> authorities);
+}

리뷰: 네 spring security에서는 인가처리 권한(Authority)가 있지만, 그 권한을 역할별로 계층으로 분리하려했고 그렇게 나온 것이 `RoleHierarchy`라고 보시면 될 것 같아요. 

두 정보가 동일할 수는 있는데 쓰임새 자체가 권한, 권한별 역할로 한번 더 쪼개진다고 보시면 될 것 같습니다.

https://docs.spring.io/spring-security/reference/servlet/authorization/architecture.html?utm_source=chatgpt.com#authz-hierarchical-roles
@@ -0,0 +1,31 @@
+package com.sparta.tentenbackend.domain.ai.entity;
+
+import com.sparta.tentenbackend.global.BaseEntity;
+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 java.util.UUID;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Entity
+@Getter
+@Setter
+@Table(name = "p_ai_prompting")
+@NoArgsConstructor
+public class Ai extends BaseEntity {
+
+  @Id
+  @GeneratedValue(strategy = GenerationType.UUID)
+  private UUID id;
+
+  @Column(nullable = false)
+  private String question;
+
+  @Column(nullable = false)
+  private String answer;
+}

리뷰: GenerationbType.AUTO보다 GenerationbType.UUID로 명시적으로 하는게 좋을 것 같아요!
@@ -0,0 +1,34 @@
+package com.sparta.tentenbackend.domain.review.entity;
+
+import com.sparta.tentenbackend.global.BaseEntity;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.OneToOne;
+import jakarta.persistence.Table;
+import java.util.UUID;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Entity
+@Getter
+@Setter
+@Table(name = "p_owner_review")
+@NoArgsConstructor
+public class OwnerReview extends BaseEntity {
+
+  @Id
+  @GeneratedValue(strategy = GenerationType.UUID)
+  private UUID id;
+
+  @Column(nullable = false)
+  private String content;
+
+  @OneToOne
+  @JoinColumn(name = "review_id", unique = true)
+  private Review review;
+}

리뷰: 요 부분도 GenerationType.UUID로 고치는게 나을 것 같습니다!
@@ -0,0 +1,48 @@
+package com.ioteam.order_management_platform.review.entity;
+
+import com.ioteam.order_management_platform.global.entity.BaseEntity;
+import com.ioteam.order_management_platform.review.dto.CreateReviewRequestDto;
+import com.ioteam.order_management_platform.review.dto.ReviewResponseDto;
+import jakarta.persistence.*;
+import lombok.*;
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+@Builder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+@Entity
+public class Review extends BaseEntity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.UUID)
+    private UUID reviewId;
+    private int reviewScore;
+    @Column(length = 1000)
+    private String reviewContent;
+    @Column(length = 100)
+    private String reviewImageUrl;
+    private Boolean isPublic;
+    private LocalDateTime deletedAt;
+    private UUID deletedBy;
+
+    public Review(CreateReviewRequestDto requestDto) {
+        this.reviewScore = requestDto.getReviewScore();
+        this.reviewContent = requestDto.getReviewContent();
+        this.reviewImageUrl = requestDto.getReviewImageUrl();
+        this.isPublic = requestDto.getIsPublic();
+    }
+
+    public ReviewResponseDto toResponseDto() {
+        return ReviewResponseDto
+                .builder()
+                .reviewId(this.reviewId)
+                .reviewScore(this.reviewScore)
+                .reviewContent(this.reviewContent)
+                .reviewImageUrl(this.reviewImageUrl)
+                //.reviewOrderId(this.)
+                .isPublic(this.isPublic)
+                .build();
+    }
+}

리뷰: P5: 단순 궁금증입니다! 기본 지식이 부족하여 Boolean은 default 값이 null이고, boolean은 false인 것이 차이점이라는 것만 알고 여쭤보는 점.. 양해부탁드립니다..! 😅 Boolean을 사용한 이유가 궁금합니다.

P4: 공개 여부 기본값이 true 이므로 @ColumnDefault 로 기본값 설정이 필요하진 않을까요?? 😀
@@ -0,0 +1,48 @@
+package com.ioteam.order_management_platform.review.entity;
+
+import com.ioteam.order_management_platform.global.entity.BaseEntity;
+import com.ioteam.order_management_platform.review.dto.CreateReviewRequestDto;
+import com.ioteam.order_management_platform.review.dto.ReviewResponseDto;
+import jakarta.persistence.*;
+import lombok.*;
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+@Builder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+@Entity
+public class Review extends BaseEntity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.UUID)
+    private UUID reviewId;
+    private int reviewScore;
+    @Column(length = 1000)
+    private String reviewContent;
+    @Column(length = 100)
+    private String reviewImageUrl;
+    private Boolean isPublic;
+    private LocalDateTime deletedAt;
+    private UUID deletedBy;
+
+    public Review(CreateReviewRequestDto requestDto) {
+        this.reviewScore = requestDto.getReviewScore();
+        this.reviewContent = requestDto.getReviewContent();
+        this.reviewImageUrl = requestDto.getReviewImageUrl();
+        this.isPublic = requestDto.getIsPublic();
+    }
+
+    public ReviewResponseDto toResponseDto() {
+        return ReviewResponseDto
+                .builder()
+                .reviewId(this.reviewId)
+                .reviewScore(this.reviewScore)
+                .reviewContent(this.reviewContent)
+                .reviewImageUrl(this.reviewImageUrl)
+                //.reviewOrderId(this.)
+                .isPublic(this.isPublic)
+                .build();
+    }
+}

리뷰: @Ryujy 님, 의견 감사합니다! @ColumnDefault 로 기본값 설정하는 부분을 놓쳤네요. 추가하겠습니다.
Boolean을 사용하는 이유는 특별히는 없습니다.
@@ -0,0 +1,48 @@
+package com.ioteam.order_management_platform.review.entity;
+
+import com.ioteam.order_management_platform.global.entity.BaseEntity;
+import com.ioteam.order_management_platform.review.dto.CreateReviewRequestDto;
+import com.ioteam.order_management_platform.review.dto.ReviewResponseDto;
+import jakarta.persistence.*;
+import lombok.*;
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+@Builder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+@Entity
+public class Review extends BaseEntity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.UUID)
+    private UUID reviewId;
+    private int reviewScore;
+    @Column(length = 1000)
+    private String reviewContent;
+    @Column(length = 100)
+    private String reviewImageUrl;
+    private Boolean isPublic;
+    private LocalDateTime deletedAt;
+    private UUID deletedBy;
+
+    public Review(CreateReviewRequestDto requestDto) {
+        this.reviewScore = requestDto.getReviewScore();
+        this.reviewContent = requestDto.getReviewContent();
+        this.reviewImageUrl = requestDto.getReviewImageUrl();
+        this.isPublic = requestDto.getIsPublic();
+    }
+
+    public ReviewResponseDto toResponseDto() {
+        return ReviewResponseDto
+                .builder()
+                .reviewId(this.reviewId)
+                .reviewScore(this.reviewScore)
+                .reviewContent(this.reviewContent)
+                .reviewImageUrl(this.reviewImageUrl)
+                //.reviewOrderId(this.)
+                .isPublic(this.isPublic)
+                .build();
+    }
+}

리뷰: @Ryujy 님 Boolean 사용한 이유 공부해두고 까먹고 있었네요.
- boolean으로 사용하는 경우 자바 빈 규약에 따르면 getter 메소드가 isXXX() 형태로 생성되어야 합니다. 
- 그래서 getter 메소드 자동생성(롬복, 인텔리제이 활용)하는 경우에 객체 프로퍼터의 네이밍에서 문제(isXXX 이 아닌 XXX 형태로 네이밍이 되는 문제)가 발생하게 됩니다. 
- 해결 방안으로 몇 가지가 존재하는데( [제 블로그에 간단히 정리해뒀어요.](https://ichmww2.tistory.com/87)) 저는 wrapper 클래스로 사용하는 것을 선호해서 Boolean 활용했습니다.
@@ -0,0 +1,37 @@
+package nextstep.authentication;
+
+public class UsernamePasswordAuthenticationToken implements Authentication {
+
+    private final String principal;
+    private final String credentials;
+    private boolean authenticated;
+
+    public UsernamePasswordAuthenticationToken(String principal, String credentials, boolean authenticated) {
+        this.principal = principal;
+        this.credentials = credentials;
+        this.authenticated = authenticated;
+    }
+
+    public static UsernamePasswordAuthenticationToken authenticated(String principal, String credentials) {
+        return new UsernamePasswordAuthenticationToken(principal, credentials, true);
+    }
+
+    public static UsernamePasswordAuthenticationToken unauthenticated(String principal, String credentials) {
+        return new UsernamePasswordAuthenticationToken(principal, credentials, false);
+    }
+
+    @Override
+    public String getPrincipal() {
+        return principal;
+    }
+
+    @Override
+    public String getCredentials() {
+        return credentials;
+    }
+
+    @Override
+    public boolean isAuthenticated() {
+        return authenticated;
+    }
+}

리뷰: `AuthenticationToken`은 말그대로 인증된 토큰이기 때문에 `UserDetails`는 필요로 하지 않습니다.

`UserDetails`의 사용처를 생각해보면, 최초 인입된 유저가 인증이 가능한 유저인지 확인하기 위해서 사용될 뿐이지 이미 인증된 토큰에는 별도로 `UserDetails`를 사용하지 않습니다. 현재 만들어주신 코드에도 사용처가 없기도 하구요.
@@ -0,0 +1,37 @@
+package nextstep.authentication;
+
+public class UsernamePasswordAuthenticationToken implements Authentication {
+
+    private final String principal;
+    private final String credentials;
+    private boolean authenticated;
+
+    public UsernamePasswordAuthenticationToken(String principal, String credentials, boolean authenticated) {
+        this.principal = principal;
+        this.credentials = credentials;
+        this.authenticated = authenticated;
+    }
+
+    public static UsernamePasswordAuthenticationToken authenticated(String principal, String credentials) {
+        return new UsernamePasswordAuthenticationToken(principal, credentials, true);
+    }
+
+    public static UsernamePasswordAuthenticationToken unauthenticated(String principal, String credentials) {
+        return new UsernamePasswordAuthenticationToken(principal, credentials, false);
+    }
+
+    @Override
+    public String getPrincipal() {
+        return principal;
+    }
+
+    @Override
+    public String getCredentials() {
+        return credentials;
+    }
+
+    @Override
+    public boolean isAuthenticated() {
+        return authenticated;
+    }
+}

리뷰: 사용되지 않는 메소드들은 모두 제거해주시면 좋을 것 같아요 :)
@@ -0,0 +1,29 @@
+package nextstep.authentication;
+
+import nextstep.authentication.exception.AuthenticationException;
+import nextstep.authentication.exception.ProviderNotFoundException;
+
+import java.util.List;
+import java.util.Objects;
+
+public class ProviderManager implements AuthenticationManager {
+
+    private final List<AuthenticationProvider> providers;
+
+    public ProviderManager(List<AuthenticationProvider> providers) {
+        this.providers = providers;
+    }
+
+    @Override
+    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+
+        Class<? extends Authentication> target = authentication.getClass();
+
+        return providers.stream()
+                .filter(provider -> provider.supports(target))
+                .map(provider -> provider.authenticate(authentication))
+                .filter(Objects::nonNull)
+                .findAny()
+                .orElseThrow(() -> new ProviderNotFoundException(String.format("No AuthenticationProvider found for %s", target.getName())));
+    }
+}

리뷰: `toTest`보다 적절한 네이밍은 어떨까요?
@@ -0,0 +1,29 @@
+package nextstep.authentication;
+
+import nextstep.authentication.exception.AuthenticationException;
+import nextstep.authentication.exception.ProviderNotFoundException;
+
+import java.util.List;
+import java.util.Objects;
+
+public class ProviderManager implements AuthenticationManager {
+
+    private final List<AuthenticationProvider> providers;
+
+    public ProviderManager(List<AuthenticationProvider> providers) {
+        this.providers = providers;
+    }
+
+    @Override
+    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+
+        Class<? extends Authentication> target = authentication.getClass();
+
+        return providers.stream()
+                .filter(provider -> provider.supports(target))
+                .map(provider -> provider.authenticate(authentication))
+                .filter(Objects::nonNull)
+                .findAny()
+                .orElseThrow(() -> new ProviderNotFoundException(String.format("No AuthenticationProvider found for %s", target.getName())));
+    }
+}

리뷰: ```suggestion
        return providers.stream()
                .filter(provider -> provider.supports(toTest))
                .map(provider -> provider.authenticate(authentication))
                .filter(Objects::nonNull)
                .findAny()
                .orElseThrow();
```
로 표현될 수 있을 것 같네요 :)
@@ -0,0 +1,68 @@
+package nextstep.authentication.filter;
+
+import jakarta.servlet.Filter;
+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 nextstep.authentication.Authentication;
+import nextstep.authentication.AuthenticationManager;
+import nextstep.authentication.UsernamePasswordAuthenticationToken;
+import nextstep.authentication.context.SecurityContextHolder;
+import nextstep.authentication.exception.AuthenticationException;
+import nextstep.authentication.util.matcher.MvcRequestMatcher;
+import org.springframework.http.HttpMethod;
+
+import java.io.IOException;
+
+public class FormLoginAuthenticationFilter implements Filter {
+
+    private static final MvcRequestMatcher DEFAULT_REQUEST_MATCHER = new MvcRequestMatcher(HttpMethod.POST, "/login");
+
+    private final AuthenticationManager authenticationManager;
+
+    public FormLoginAuthenticationFilter(AuthenticationManager authenticationManager) {
+        this.authenticationManager = authenticationManager;
+    }
+
+    @Override
+    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
+        if (!DEFAULT_REQUEST_MATCHER.matches((HttpServletRequest) servletRequest)) {
+            filterChain.doFilter(servletRequest, servletResponse);
+            return;
+        }
+
+        HttpServletRequest request = (HttpServletRequest) servletRequest;
+        HttpServletResponse response = (HttpServletResponse) servletResponse;
+
+        try {
+
+            Authentication authenticationResult = attemptAuthentication(request, response);
+            if (authenticationResult == null) {
+                filterChain.doFilter(servletRequest, servletResponse);
+                return;
+            }
+
+            SecurityContextHolder.getContext().setAuthentication(authenticationResult);
+
+        } catch (Exception e) {
+            SecurityContextHolder.clearContext();
+            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+
+            return;
+        }
+
+        filterChain.doFilter(servletRequest, servletResponse);
+    }
+
+    private Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
+
+        String username = request.getParameter("username");
+        String password = request.getParameter("password");
+        UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
+
+        return authenticationManager.authenticate(authRequest);
+    }
+}

리뷰: 인증이 null일 때 바로 return하면 필터 동작이 어디로 향할까요?
@@ -0,0 +1,38 @@
+package nextstep.authentication.context;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
+
+public class HttpSessionSecurityContextRepository implements SecurityContextRepository {
+
+    private static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT";
+
+    @Override
+    public SecurityContext loadContext(HttpServletRequest request) {
+
+        HttpSession httpSession = request.getSession(false);
+        if (httpSession == null) {
+            return null;
+        }
+
+        return (SecurityContext) httpSession.getAttribute(SPRING_SECURITY_CONTEXT_KEY);
+    }
+
+    @Override
+    public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
+
+        // SecurityContext가 null이거나 인증정보가 없으면 세션에서 제거
+        if (context == null || context.getAuthentication() == null) {
+            HttpSession session = request.getSession(false);
+            if (session != null) {
+                session.removeAttribute(SPRING_SECURITY_CONTEXT_KEY);
+            }
+            return;
+        }
+
+        // 세션이 없으면 생성하고, SecurityContext를 저장
+        HttpSession session = request.getSession(true);
+        session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, context);
+    }
+}

리뷰: 로직이 복잡할 필요 없이 `loadContext`는 단순히 

1. request.getSession의 값이 null이면 null을 반환한다.
2. httpSession.getAttribute(SPRING_SECURITY_CONTEXT_KEY)를 반환한다.

면 됩니다. `readSecurityContextFromSession`의 분기는 위 과정으로 이미 동일하게 로직이 구성될 수 있어 별도로 필요 없어보여요.
@@ -0,0 +1,29 @@
+package nextstep.authentication;
+
+import nextstep.authentication.exception.AuthenticationException;
+import nextstep.authentication.exception.ProviderNotFoundException;
+
+import java.util.List;
+import java.util.Objects;
+
+public class ProviderManager implements AuthenticationManager {
+
+    private final List<AuthenticationProvider> providers;
+
+    public ProviderManager(List<AuthenticationProvider> providers) {
+        this.providers = providers;
+    }
+
+    @Override
+    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+
+        Class<? extends Authentication> target = authentication.getClass();
+
+        return providers.stream()
+                .filter(provider -> provider.supports(target))
+                .map(provider -> provider.authenticate(authentication))
+                .filter(Objects::nonNull)
+                .findAny()
+                .orElseThrow(() -> new ProviderNotFoundException(String.format("No AuthenticationProvider found for %s", target.getName())));
+    }
+}

리뷰: target으로 변수명을 수정했습니다!