Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
1aa6f2c
feat ( #10 ) : AwsConfig
coehgns Jul 30, 2025
fd59d9f
feat ( #10 ) : FilterConfig
coehgns Jul 30, 2025
e34394e
feat ( #10 ) : SecurityConfig
coehgns Jul 30, 2025
05588ec
feat ( #10 ) : BaseEntity
coehgns Jul 30, 2025
ce61323
feat ( #10 ) : BaseTimeEntity
coehgns Jul 30, 2025
ff6c83a
feat ( #10 ) : BaseUUIDEntity
coehgns Jul 30, 2025
9a24c0f
feat ( #10 ) : CasperException
coehgns Jul 30, 2025
a3b50d2
feat ( #10 ) : ErrorCode
coehgns Jul 30, 2025
fa8d7c5
feat ( #10 ) : ErrorResponse
coehgns Jul 30, 2025
aa391c2
feat ( #10 ) : GlobalExceptionFilter
coehgns Jul 30, 2025
361159c
feat ( #10 ) : GlobalExceptionHandler
coehgns Jul 30, 2025
d63d976
feat ( #10 ) : ExpiredTokenException
coehgns Jul 30, 2025
c7e5a01
feat ( #10 ) : InternalServerErrorException
coehgns Jul 30, 2025
e5545d4
feat ( #10 ) : InvalidTokenException
coehgns Jul 30, 2025
a81e0d8
feat ( #10 ) : JwtFilter
coehgns Jul 30, 2025
c186db6
feat ( #10 ) : JwtProperties
coehgns Jul 30, 2025
106b739
feat ( #10 ) : AdminUtils
coehgns Aug 1, 2025
4111ac5
feat ( #10 ) : UserUtils
coehgns Aug 1, 2025
c4b257d
feat ( #10 ) : KafkaConsumerConfig
coehgns Aug 1, 2025
6b07954
feat ( #10 ) : KafkaProperty
coehgns Aug 1, 2025
d4562ec
feat ( #10 ) : KafkaTopics
coehgns Aug 1, 2025
105e190
feat ( #10 ) : DeleteFaqTableConsumer
coehgns Aug 1, 2025
00a4cca
feat ( #10 ) : BadFileExtensionException
coehgns Aug 1, 2025
e1899fd
feat ( #10 ) : EmptyFileException
coehgns Aug 1, 2025
56b8b73
feat ( #10 ) : FileUtil
coehgns Aug 1, 2025
9a2f3c0
feat ( #10 ) : PathList
coehgns Aug 1, 2025
add59a3
feat ( #10 ) : 커밋 못한 AttachFile
coehgns Aug 1, 2025
b4cb0ad
refactor ( #10 ) : 중복된 build 제거
coehgns Aug 2, 2025
9a2725e
refactor ( #10 ) : BaseEntity UUID 전략 수정
coehgns Aug 2, 2025
b08a71f
refactor ( #10 ) : FileUtil 파일 확장자 검증 로직 리팩토링
coehgns Aug 2, 2025
03dbf1f
refactor ( #10 ) : GlobalExceptionHandler 메서드 네이밍 수정
coehgns Aug 2, 2025
24eae29
refactor ( #10 ) : GlobalExceptionHandler kdoc 수정
coehgns Aug 2, 2025
9934254
refactor ( #10 ) : FileUtil의 verificationFile 메서드의 빈문자열이거나 공백만 있는 경우도…
coehgns Aug 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package hs.kr.entrydsm.feed.global.config

import com.amazonaws.auth.AWSCredentials
import com.amazonaws.auth.AWSStaticCredentialsProvider
import com.amazonaws.auth.BasicAWSCredentials
import com.amazonaws.services.s3.AmazonS3
import com.amazonaws.services.s3.AmazonS3ClientBuilder
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

/**
* AWS 서비스 구성을 위한 설정 클래스입니다.
* 이 클래스는 AWS S3 클라이언트를 생성하고 구성하는 책임을 담당합니다.
*
* @property accessKey AWS 액세스 키
* @property secretKey AWS 시크릿 키
* @property region AWS 리전
*/
@Configuration
class AwsConfig(
@Value("\${cloud.aws.credentials.accessKey}")
private val accessKey: String,
@Value("\${cloud.aws.credentials.secretKey}")
private val secretKey: String,
@Value("\${cloud.aws.region.static}")
private val region: String
) {
/**
* AmazonS3 클라이언트를 생성하는 빈 메서드입니다.
* AWS 자격 증명과 리전 정보를 사용하여 S3 클라이언트를 구성합니다.
*
* @return 구성된 AmazonS3 클라이언트 인스턴스
*/
@Bean
fun amazonS3(): AmazonS3 {
val awsCredentials: AWSCredentials = BasicAWSCredentials(accessKey, secretKey)
return AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(AWSStaticCredentialsProvider(awsCredentials))
.build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package hs.kr.entrydsm.feed.global.config

import com.fasterxml.jackson.databind.ObjectMapper
import hs.kr.entrydsm.feed.global.error.GlobalExceptionFilter
import hs.kr.entrydsm.feed.global.security.jwt.JwtFilter
import org.springframework.security.config.annotation.SecurityConfigurerAdapter
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.web.DefaultSecurityFilterChain
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
import org.springframework.stereotype.Component

/**
* Spring Security 필터 체인을 구성하는 설정 클래스입니다.
* JWT 인증 필터와 글로벌 예외 처리 필터를 적절한 위치에 추가합니다.
*
* @property objectMapper JSON 직렬화/역직렬화를 위한 ObjectMapper
*
* @see SecurityConfigurerAdapter
* @see JwtFilter
* @see GlobalExceptionFilter
*/
@Component
class FilterConfig(
private val objectMapper: ObjectMapper
) : SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>() {
/**
* Spring Security 필터 체인에 커스텀 필터들을 추가합니다.
* 1. JwtFilter: JWT 기반 인증 처리
* 2. GlobalExceptionFilter: 전역 예외 처리
*
* @param builder HttpSecurity 빌더 객체
*/
override fun configure(builder: HttpSecurity) {
builder.addFilterBefore(
JwtFilter(),
UsernamePasswordAuthenticationFilter::class.java
)
builder.addFilterBefore(GlobalExceptionFilter(objectMapper), JwtFilter::class.java)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package hs.kr.entrydsm.feed.global.config

import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpMethod
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.web.SecurityFilterChain

/**
* Spring Security 구성을 위한 설정 클래스입니다.
* 애플리케이션의 보안 설정(인증, 인가, CORS, CSRF 등)을 담당합니다.
*
* @property objectMapper JSON 직렬화/역직렬화를 위한 ObjectMapper
*
* @see SecurityFilterChain
* @see HttpSecurity
*/
@Configuration
class SecurityConfig(
private val objectMapper: ObjectMapper
) {
companion object {
private const val ADMIN_ROLE = "ADMIN"
}

/**
* Spring Security 필터 체인을 구성하는 빈 메서드입니다.
* CSRF, CORS, 세션 정책, 인증/인가 규칙 등을 설정합니다.
*
* @param http HttpSecurity 구성 객체
* @return 구성된 SecurityFilterChain 인스턴스
*/
@Bean
protected fun filterChain(http: HttpSecurity): SecurityFilterChain {
http
.csrf { it.disable() }
.cors { }
.formLogin { it.disable() }
.sessionManagement {
it.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
}
.authorizeHttpRequests { auth ->
auth
.requestMatchers("/")
.permitAll()
.requestMatchers(HttpMethod.POST, "/faq/**")
.hasRole(ADMIN_ROLE)
.requestMatchers(HttpMethod.PATCH, "/faq/**")
.hasRole(ADMIN_ROLE)
.requestMatchers(HttpMethod.DELETE, "/faq/**")
.hasRole(ADMIN_ROLE)
.requestMatchers(HttpMethod.GET, "/faq/all/title-type")
.hasRole(ADMIN_ROLE)
.requestMatchers(HttpMethod.GET, "/faq/**")
.permitAll()
.requestMatchers(HttpMethod.GET, "/reserve/**")
.permitAll()
.requestMatchers(HttpMethod.POST, "/notice")
.hasRole(ADMIN_ROLE)
.requestMatchers(HttpMethod.PATCH, "/notice/**")
.hasRole(ADMIN_ROLE)
.requestMatchers(HttpMethod.POST, "/notice/image")
.hasRole(ADMIN_ROLE)
.requestMatchers(HttpMethod.POST, "/screen")
.hasRole(ADMIN_ROLE)
.requestMatchers(HttpMethod.POST, "/attach-file")
.hasRole(ADMIN_ROLE)
.requestMatchers(HttpMethod.GET, "/notice/**")
.permitAll()
.anyRequest()
.authenticated()
}
.with(FilterConfig(objectMapper)) { }

return http.build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package hs.kr.entrydsm.feed.global.entity

import jakarta.persistence.Column
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.MappedSuperclass
import java.util.*

/**
* 모든 엔티티 클래스의 기본이 되는 추상 클래스입니다.
* 이 클래스를 상속받는 엔티티는 자동으로 생성 시간과 수정 시간을 추적할 수 있습니다.
*
* @property id 엔티티의 고유 식별자 (UUID)
* @see BaseTimeEntity 생성 시간과 수정 시간을 관리하는 부모 클래스
*/
@MappedSuperclass
abstract class BaseEntity(
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(
columnDefinition = "BINARY(16)",
nullable = false,
)
val id: UUID?,
Comment thread
coehgns marked this conversation as resolved.
) : BaseTimeEntity()
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package hs.kr.entrydsm.feed.global.entity

import jakarta.persistence.EntityListeners
import jakarta.persistence.MappedSuperclass
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.jpa.domain.support.AuditingEntityListener
import java.time.LocalDateTime

/**
* 엔티티의 생성 시간과 수정 시간을 자동으로 관리하는 추상 기본 클래스입니다.
* 이 클래스를 상속받은 엔티티는 자동으로 생성 시간과 수정 시간이 추적됩니다.
*
* @property createdAt 엔티티가 생성된 시간 (자동 설정됨)
* @property modifiedAt 엔티티가 마지막으로 수정된 시간 (자동 갱신됨)
*/
@MappedSuperclass
@EntityListeners(AuditingEntityListener::class)
Comment thread
coehgns marked this conversation as resolved.
abstract class BaseTimeEntity(
@CreatedDate
val createdAt: LocalDateTime = LocalDateTime.now(),
@LastModifiedDate
val modifiedAt: LocalDateTime = LocalDateTime.now(),
Comment thread
coehgns marked this conversation as resolved.
Comment thread
coehgns marked this conversation as resolved.
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package hs.kr.entrydsm.feed.global.entity

import jakarta.persistence.Column
import jakarta.persistence.GeneratedValue
import jakarta.persistence.Id
import jakarta.persistence.MappedSuperclass
import java.util.*

/**
* UUID를 기본 키로 사용하는 모든 JPA 엔티티의 기본이 되는 추상 클래스입니다.
* 이 클래스를 상속받는 엔티티는 자동으로 UUID를 ID로 가지게 됩니다.
*
* @property id 엔티티의 고유 식별자 (UUID)
* @param id 생성자로 전달된 ID (null이면 자동 생성, UUID(0,0)이면 null로 처리됨)
*/
@MappedSuperclass
abstract class BaseUUIDEntity(
id: UUID?,
) {
/**
* 엔티티의 고유 식별자입니다.
* - `@GeneratedValue(generator = "uuid2")`: UUID v4를 사용하여 자동 생성
* - `@Column(columnDefinition = "BINARY(16)")`: 데이터베이스에 BINARY(16)으로 저장
* - `nullable = false`: null을 허용하지 않음
* - `if (id == UUID(0, 0)) null else id`: UUID(0,0)이 전달되면 null로 처리
*/
@Id
@GeneratedValue(generator = "uuid2")
Comment thread
coehgns marked this conversation as resolved.
@Column(columnDefinition = "BINARY(16)", nullable = false)
val id: UUID? = if (id == UUID(0, 0)) null else id
Comment thread
coehgns marked this conversation as resolved.
Comment thread
coehgns marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package hs.kr.entrydsm.feed.global.error

/**
* 클라이언트에게 반환되는 오류 응답을 나타내는 데이터 클래스입니다.
* HTTP 상태 코드와 오류 메시지를 포함합니다.
*
* @property status HTTP 상태 코드
* @property message 사용자에게 표시할 오류 메시지 (선택 사항)
*/
data class ErrorResponse(
val status: Int,
val message: String?,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package hs.kr.entrydsm.feed.global.error

import com.fasterxml.jackson.databind.ObjectMapper
import hs.kr.entrydsm.feed.global.error.exception.CasperException
import hs.kr.entrydsm.feed.global.error.exception.ErrorCode
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.http.MediaType
import org.springframework.web.filter.OncePerRequestFilter
import java.io.IOException
import java.nio.charset.StandardCharsets

/**
* 전역 예외 처리를 담당하는 서블릿 필터입니다.
* 컨트롤러 밖에서 발생하는 예외를 잡아 적절한 에러 응답으로 변환합니다.
*
* @property objectMapper JSON 직렬화를 위한 ObjectMapper
*/
class GlobalExceptionFilter(
private val objectMapper: ObjectMapper
) : OncePerRequestFilter() {

/**
* 필터 체인을 실행하고 발생한 예외를 처리합니다.
*
* @param request HTTP 요청 객체
* @param response HTTP 응답 객체
* @param filterChain 필터 체인
* @throws IOException 입출력 예외 발생 시
*/
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
try {
filterChain.doFilter(request, response)
} catch (e: CasperException) {
println(e.errorCode)
writerErrorCode(response, e.errorCode)
} catch (e: Exception) {
e.printStackTrace()
writerErrorCode(response, ErrorCode.INTERNAL_SERVER_ERROR)
}
Comment thread
coehgns marked this conversation as resolved.
}

/**
* 에러 코드에 해당하는 HTTP 응답을 작성합니다.
*
* @param response HTTP 응답 객체
* @param errorCode 에러 코드
* @throws IOException 입출력 예외 발생 시
*/
@Throws(IOException::class)
private fun writerErrorCode(response: HttpServletResponse, errorCode: ErrorCode) {
val errorResponse = ErrorResponse(errorCode.status, errorCode.message)
response.status = errorCode.status
response.characterEncoding = StandardCharsets.UTF_8.name()
response.contentType = MediaType.APPLICATION_JSON_VALUE
response.writer.write(objectMapper.writeValueAsString(errorResponse))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package hs.kr.entrydsm.feed.global.error

import hs.kr.entrydsm.feed.global.error.exception.CasperException
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice

/**
* 전역 예외 처리를 담당하는 핸들러 클래스입니다.
*
* 이 클래스는 컨트롤러 계층에서 발생하는 예외를 중앙 집중적으로 처리하며,
* 적절한 HTTP 응답을 생성하여 클라이언트에 반환합니다.
*
* - `CasperException`: 비즈니스 로직에서 발생한 예외를 처리합니다.
* - `MethodArgumentNotValidException`: 유효성 검증 실패 시 발생하는 예외를 처리합니다.
*
* @see RestControllerAdvice
* @see ExceptionHandler
*/
@RestControllerAdvice
class GlobalExceptionHandler() {
/**
* CasperException을 처리하는 메서드입니다.
* 비즈니스 로직에서 발생한 예외를 적절한 HTTP 응답으로 변환합니다.
*
* @param e 처리할 CasperException 예외 객체
* @return ErrorResponse를 포함하는 ResponseEntity
*/
@ExceptionHandler(CasperException::class)
fun handlingCasperException(e: CasperException): ResponseEntity<ErrorResponse> {
val code = e.errorCode
return ResponseEntity(
ErrorResponse(code.status, code.message),
HttpStatus.valueOf(code.status),
)
}

/**
* MethodArgumentNotValidException을 처리하는 메서드입니다.
* 유효성 검증 실패 시 발생하는 예외를 처리합니다.
*
* @param e 처리할 MethodArgumentNotValidException 예외 객체
* @return ErrorResponse를 포함하는 ResponseEntity (400 Bad Request)
*/
@ExceptionHandler(MethodArgumentNotValidException::class)
fun validatorExceptionHandler(e: MethodArgumentNotValidException): ResponseEntity<ErrorResponse> {
return ResponseEntity(
ErrorResponse(
400,
e.bindingResult.allErrors[0].defaultMessage,
),
HttpStatus.BAD_REQUEST,
)
}
Comment thread
coehgns marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package hs.kr.entrydsm.feed.global.error.exception

import java.lang.RuntimeException

/**
* 애플리케이션에서 발생하는 예외들의 기본 추상 클래스입니다.
* 모든 커스텀 예외는 이 클래스를 상속받아 구현해야 합니다.
*
* @property errorCode 예외에 해당하는 에러 코드
*/
abstract class CasperException(
val errorCode: ErrorCode,
) : RuntimeException()
Loading