Skip to content
Open
6 changes: 6 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# 포맷만 바꾼 커밋 목록. git blame에서 제외해 실제 코드 작성자를 추적할 수 있게 한다.
# GitHub은 이 파일을 자동으로 인식한다.
# 로컬 적용: git config blame.ignoreRevsFile .git-blame-ignore-revs
#
# Squash Merge가 기본이라 브랜치 커밋 해시는 main에 남지 않는다.
# 포맷 PR을 머지한 뒤, main에 생긴 squash 커밋 해시를 여기에 추가한다.
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@
public record CursorSliceResponse<T>(List<T> content, boolean hasNext, Long nextCursor) {

public static <T> CursorSliceResponse<T> from(CursorSliceResult<T> result) {
return new CursorSliceResponse<>(
result.content(),
result.hasNext(),
result.nextCursor()
);
return new CursorSliceResponse<>(result.content(), result.hasNext(), result.nextCursor());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,4 @@ public class StreamServerApplication {
public static void main(String[] args) {
SpringApplication.run(StreamServerApplication.class, args);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,4 @@ void verify() {
void writeDocs() {
new Documenter(modules).writeDocumentation();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,4 @@ class StreamServerApplicationTests {
@Test
void contextLoads() {
}

}
13 changes: 13 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import com.diffplug.gradle.spotless.SpotlessExtension

plugins {
alias(libs.plugins.springBoot) apply false
alias(libs.plugins.spotless) apply false
}

allprojects {
Expand All @@ -13,6 +16,7 @@ allprojects {

subprojects {
apply(plugin = "java")
apply(plugin = "com.diffplug.spotless")

// Java 21 (LTS) baseline — docs/conventions/architecture.md §1
extensions.configure<JavaPluginExtension> {
Expand All @@ -33,4 +37,13 @@ subprojects {
tasks.withType<Test> {
useJUnitPlatform()
}

extensions.configure<SpotlessExtension> {
java {
removeUnusedImports()
importOrder("", "\\#")
// IntelliJ 포맷 결과를 CI에서 그대로 강제하기 위한 Eclipse JDT 프로파일
eclipse().configFile(rootProject.file("config/eclipse-formatter.xml"))
}
}
}
237 changes: 237 additions & 0 deletions config/eclipse-formatter.xml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ public BusinessException(ErrorCode errorCode) {
}

public BusinessException(ErrorCode errorCode, Object... formatArgs) {
super(formatArgs.length == 0 ? errorCode.message() : errorCode.message().formatted(formatArgs));
super(
formatArgs.length == 0
? errorCode.message()
: errorCode.message().formatted(formatArgs));
this.errorCode = errorCode;
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package kr.ac.kookmin.stream.common;

/**
* 학생회 부서. ADMIN에게만 부여되며, member 도메인의 학부(Department)와는 다른 개념이다.
*/
/** 학생회 부서. ADMIN에게만 부여되며, member 도메인의 학부(Department)와는 다른 개념이다. */
public enum CouncilDepartment {
PRESIDENCY, // 회장단
EXECUTIVE, // 집행부
GENERAL_AFFAIRS, // 총무부
PLANNING, // 기획부
PR, // 홍보부
MEDIA, // 미디어부
WELFARE, // 복지부
COMMUNICATION // 소통부
PRESIDENCY, // 회장단
EXECUTIVE, // 집행부
GENERAL_AFFAIRS, // 총무부
PLANNING, // 기획부
PR, // 홍보부
MEDIA, // 미디어부
WELFARE, // 복지부
COMMUNICATION // 소통부
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@

import java.util.List;

public record CursorSliceResult<T>(List<T> content, boolean hasNext, Long nextCursor) {}
public record CursorSliceResult<T>(List<T> content, boolean hasNext, Long nextCursor) {
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package kr.ac.kookmin.stream.common;

public interface ErrorCode {

String name();

int status();

String message();
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
import java.util.Set;

public interface PrincipalProvider {

Long userId();

Set<Role> roles();

Set<CouncilDepartment> councilDepartments();
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
package kr.ac.kookmin.stream.member;

public record Member(
Long id,
String studentNo,
String name
) {}
public record Member(Long id, String studentNo, String name) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
import java.util.Optional;

public interface MemberRepository {

Optional<Member> findById(Long id);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package kr.ac.kookmin.stream.member;

public interface MemberService {

Member getById(Long id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;

public Member getById(Long id) {
return memberRepository.findById(id)
.orElseThrow(() -> new BusinessException(MemberErrorCode.MEMBER_NOT_FOUND));
return memberRepository
.findById(id)
.orElseThrow(() -> new BusinessException(MemberErrorCode.MEMBER_NOT_FOUND));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,16 @@
import org.springframework.web.util.pattern.PathPattern;
import org.springframework.web.util.pattern.PathPatternParser;

/**
* 인증 없이 여는 엔드포인트. SecurityConfig의 permitAll 대상이며 용도별로 묶어 관리한다.
*/
/** 인증 없이 여는 엔드포인트. SecurityConfig의 permitAll 대상이며 용도별로 묶어 관리한다. */
@Getter
@Accessors(fluent = true)
public enum PublicEndpoints {

HEALTH_CHECK(List.of(
"/actuator/health"
)),
SWAGGER(List.of(
"/swagger-ui/**",
"/swagger-ui.html",
"/v3/api-docs/**"
));
HEALTH_CHECK(List.of("/actuator/health")),
SWAGGER(List.of("/swagger-ui/**", "/swagger-ui.html", "/v3/api-docs/**"));

private static final List<PathPattern> ALL_PATH_PATTERNS = Arrays.stream(values())
.flatMap(endpoints -> endpoints.pathPatterns.stream())
.toList();
.flatMap(endpoints -> endpoints.pathPatterns.stream()).toList();

private final List<String> patterns;
private final List<PathPattern> pathPatterns;
Expand All @@ -39,13 +30,13 @@ public enum PublicEndpoints {

public static String[] allPatterns() {
return Arrays.stream(values())
.flatMap(endpoints -> endpoints.patterns.stream())
.toArray(String[]::new);
.flatMap(endpoints -> endpoints.patterns.stream())
.toArray(String[]::new);
}

public static boolean isPublic(String path) {
PathContainer pathContainer = PathContainer.parsePath(path);
return ALL_PATH_PATTERNS.stream()
.anyMatch(pathPattern -> pathPattern.matches(pathContainer));
.anyMatch(pathPattern -> pathPattern.matches(pathContainer));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,26 @@ public class SecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(request -> request
.requestMatchers(PublicEndpoints.allPatterns()).permitAll()
.requestMatchers("/v1/admin/**").hasAuthority(Role.ADMIN.name())
.requestMatchers("/v1/app/**").hasAuthority(Role.STUDENT.name())
.anyRequest().authenticated())
.exceptionHandling(exception -> exception
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler))
// ExceptionTranslationFilter 뒤에 두어야 필터가 던진 인증 예외가 EntryPoint로 넘어간다
.addFilterBefore(JwtAuthFilter.of(jwtProvider), AuthorizationFilter.class)
.build();
return http.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.sessionManagement(
session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(
request -> request.requestMatchers(PublicEndpoints.allPatterns())
.permitAll()
.requestMatchers("/v1/admin/**")
.hasAuthority(Role.ADMIN.name())
.requestMatchers("/v1/app/**")
.hasAuthority(Role.STUDENT.name())
.anyRequest()
.authenticated())
.exceptionHandling(
exception -> exception
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler))
// ExceptionTranslationFilter 뒤에 두어야 필터가 던진 인증 예외가 EntryPoint로 넘어간다
.addFilterBefore(JwtAuthFilter.of(jwtProvider), AuthorizationFilter.class)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;

/**
* 인증은 됐으나 권한이 없는 요청(403)을 공통 에러 응답으로 내보낸다.
*/
/** 인증은 됐으나 권한이 없는 요청(403)을 공통 에러 응답으로 내보낸다. */
@Component
@RequiredArgsConstructor
public class RestAccessDeniedHandler implements AccessDeniedHandler {
Expand All @@ -23,11 +21,13 @@ public class RestAccessDeniedHandler implements AccessDeniedHandler {

@Override
public void handle(
HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException
) {
HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) {
handlerExceptionResolver.resolveException(
request, response, null, new BusinessException(CommonErrorCode.FORBIDDEN));
request,
response,
null,
new BusinessException(CommonErrorCode.FORBIDDEN));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;

/**
* 인증되지 않은 요청(401). 로그인 페이지로 리다이렉트하는 기본 동작 대신 공통 에러 응답으로 내보낸다.
*/
/** 인증되지 않은 요청(401). 로그인 페이지로 리다이렉트하는 기본 동작 대신 공통 에러 응답으로 내보낸다. */
@Component
@RequiredArgsConstructor
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
Expand All @@ -23,11 +21,13 @@ public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {

@Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authenticationException
) {
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authenticationException) {
handlerExceptionResolver.resolveException(
request, response, null, new BusinessException(CommonErrorCode.UNAUTHORIZED));
request,
response,
null,
new BusinessException(CommonErrorCode.UNAUTHORIZED));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import org.springframework.security.core.AuthenticationException;

/**
* 토큰이 만료됐거나 서명·형식이 올바르지 않을 때. ExceptionTranslationFilter가 잡아
* SecurityConfig에 설정된 AuthenticationEntryPoint로 넘긴다.
* 토큰이 만료됐거나 서명·형식이 올바르지 않을 때. ExceptionTranslationFilter가 잡아 SecurityConfig에 설정된
* AuthenticationEntryPoint로 넘긴다.
*/
public class InvalidTokenException extends AuthenticationException {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,14 @@ public static JwtAuthFilter of(JwtProvider jwtProvider) {

@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String token = resolveToken(request);
if (token != null) {
SecurityContextHolder.getContext().setAuthentication(UserAuthentication.from(jwtProvider.parse(token)));
SecurityContextHolder.getContext()
.setAuthentication(UserAuthentication.from(jwtProvider.parse(token)));
}
filterChain.doFilter(request, response);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,5 @@
import kr.ac.kookmin.stream.common.CouncilDepartment;
import kr.ac.kookmin.stream.common.Role;

public record JwtPayload(
Long userId,
Set<Role> roles,
Set<CouncilDepartment> councilDepartments
) {}
public record JwtPayload(Long userId, Set<Role> roles, Set<CouncilDepartment> councilDepartments) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,5 @@
import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "jwt")
public record JwtProperties(
String secretKey,
String issuer,
long accessTokenExpiry
) {}
public record JwtProperties(String secretKey, String issuer, long accessTokenExpiry) {
}
Loading