From 1aa6f2c659053038d5b39bbdb8925ffc1c9b302a Mon Sep 17 00:00:00 2001 From: coehgns Date: Wed, 30 Jul 2025 14:47:24 +0900 Subject: [PATCH 01/33] feat ( #10 ) : AwsConfig --- .../entrydsm/feed/global/config/AwsConfig.kt | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/config/AwsConfig.kt diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/config/AwsConfig.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/config/AwsConfig.kt new file mode 100644 index 0000000..cd16804 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/config/AwsConfig.kt @@ -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() + } +} From fd59d9ff583451807dba8cebcdb266c36d9339b7 Mon Sep 17 00:00:00 2001 From: coehgns Date: Wed, 30 Jul 2025 14:47:44 +0900 Subject: [PATCH 02/33] feat ( #10 ) : FilterConfig --- .../feed/global/config/FilterConfig.kt | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/config/FilterConfig.kt diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/config/FilterConfig.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/config/FilterConfig.kt new file mode 100644 index 0000000..c1aba12 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/config/FilterConfig.kt @@ -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() { + /** + * 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) + } +} From e34394e3395eca084e2183baa60a5ed3c9bdf9f9 Mon Sep 17 00:00:00 2001 From: coehgns Date: Wed, 30 Jul 2025 14:48:43 +0900 Subject: [PATCH 03/33] feat ( #10 ) : SecurityConfig --- .../feed/global/config/SecurityConfig.kt | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/config/SecurityConfig.kt diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/config/SecurityConfig.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/config/SecurityConfig.kt new file mode 100644 index 0000000..f763085 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/config/SecurityConfig.kt @@ -0,0 +1,80 @@ +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)) { } + .build() + + return http.build() + } +} From 05588ecc0ca4ae55c4650b159d2871ac8b55ae7f Mon Sep 17 00:00:00 2001 From: coehgns Date: Wed, 30 Jul 2025 14:48:55 +0900 Subject: [PATCH 04/33] feat ( #10 ) : BaseEntity --- .../entrydsm/feed/global/entity/BaseEntity.kt | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/entity/BaseEntity.kt diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/entity/BaseEntity.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/entity/BaseEntity.kt new file mode 100644 index 0000000..f791962 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/entity/BaseEntity.kt @@ -0,0 +1,25 @@ +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.* + +/** + * 모든 엔티티 클래스의 기본이 되는 추상 클래스입니다. + * 이 클래스를 상속받는 엔티티는 자동으로 생성 시간과 수정 시간을 추적할 수 있습니다. + * + * @property id 엔티티의 고유 식별자 (UUID) + * @see BaseTimeEntity 생성 시간과 수정 시간을 관리하는 부모 클래스 + */ +@MappedSuperclass +abstract class BaseEntity( + @Id + @GeneratedValue(generator = "uuid2") + @Column( + columnDefinition = "BINARY(16)", + nullable = false, + ) + val id: UUID?, +) : BaseTimeEntity() From ce613235df56a37ae733186a3482f137abc9752e Mon Sep 17 00:00:00 2001 From: coehgns Date: Wed, 30 Jul 2025 14:49:00 +0900 Subject: [PATCH 05/33] feat ( #10 ) : BaseTimeEntity --- .../feed/global/entity/BaseTimeEntity.kt | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/entity/BaseTimeEntity.kt diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/entity/BaseTimeEntity.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/entity/BaseTimeEntity.kt new file mode 100644 index 0000000..88181c1 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/entity/BaseTimeEntity.kt @@ -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) +abstract class BaseTimeEntity( + @CreatedDate + val createdAt: LocalDateTime = LocalDateTime.now(), + @LastModifiedDate + val modifiedAt: LocalDateTime = LocalDateTime.now(), +) From ff6c83a5e9c76d84ff26ce66bcef354a74819173 Mon Sep 17 00:00:00 2001 From: coehgns Date: Wed, 30 Jul 2025 14:49:07 +0900 Subject: [PATCH 06/33] feat ( #10 ) : BaseUUIDEntity --- .../feed/global/entity/BaseUUIDEntity.kt | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/entity/BaseUUIDEntity.kt diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/entity/BaseUUIDEntity.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/entity/BaseUUIDEntity.kt new file mode 100644 index 0000000..aef6dfc --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/entity/BaseUUIDEntity.kt @@ -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") + @Column(columnDefinition = "BINARY(16)", nullable = false) + val id: UUID? = if (id == UUID(0, 0)) null else id +} From 9a24c0f5138346bd1947bc0b9d4bb471848d9ebe Mon Sep 17 00:00:00 2001 From: coehgns Date: Wed, 30 Jul 2025 14:49:14 +0900 Subject: [PATCH 07/33] feat ( #10 ) : CasperException --- .../feed/global/error/exception/CasperException.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/error/exception/CasperException.kt diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/error/exception/CasperException.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/error/exception/CasperException.kt new file mode 100644 index 0000000..e3ceb4c --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/error/exception/CasperException.kt @@ -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() From a3b50d2f9d7ece2af28c5c9931a18a4dcbbea3a4 Mon Sep 17 00:00:00 2001 From: coehgns Date: Wed, 30 Jul 2025 14:49:23 +0900 Subject: [PATCH 08/33] feat ( #10 ) : ErrorCode --- .../feed/global/error/exception/ErrorCode.kt | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/error/exception/ErrorCode.kt diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/error/exception/ErrorCode.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/error/exception/ErrorCode.kt new file mode 100644 index 0000000..4839210 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/error/exception/ErrorCode.kt @@ -0,0 +1,46 @@ +package hs.kr.entrydsm.feed.global.error.exception + +/** + * 애플리케이션에서 발생할 수 있는 에러 코드를 정의한 enum 클래스입니다. + * 각 에러는 HTTP 상태 코드와 에러 메시지를 포함합니다. + * + * @property status HTTP 상태 코드 + * @property message 에러 메시지 + */ +enum class ErrorCode( + val status: Int, + val message: String +) { + // Feign + FEIGN_BAD_REQUEST(400, "Feign Bad Request"), + FEIGN_UNAUTHORIZED(401, "Feign UnAuthorized"), + FEIGN_FORBIDDEN(403, "Feign Forbidden"), + FEIGN_SERVER_ERROR(500, "Feign Server Error"), + + // Internal Server Error + INTERNAL_SERVER_ERROR(500, "Internal Server Error"), + + // UnAuthorization + INVALID_TOKEN(401, "Invalid Token"), + EXPIRED_TOKEN(401, "Expired Token"), + + // Forbidden + ACCESS_DENIED_QUESTION(403, "No Permission To Access Question"), + ACCESS_DENIED_REPLY(403, "No Permission To Comment Question"), + FEED_WRITER_MISMATCH(403, "Feed Writer Mismatch"), + + // Not Found + QUESTION_NOT_FOUND(404, "Question Not Found"), + REPLY_NOT_FOUND(404, "Reply Not Found"), + FAQ_NOT_FOUND(404, "Faq Not Found"), + NOTICE_NOT_FOUND(404, "Notice Not Found"), + SCREEN_NOT_FOUND(404, "Screen Not Found"), + ATTACH_FILE_NOT_FOUND(404, "Attach Not Found"), + + // Bad Request + FILE_IS_EMPTY(400, "File does not exist"), + BAD_FILE_EXTENSION(400, "File Extension is invalid"), + + // Conflict + REPLY_EXISTS(409, "Reply Already Exists") +} From fa8d7c56b0ff63d15c126fa3b44741d8efe8bf94 Mon Sep 17 00:00:00 2001 From: coehgns Date: Wed, 30 Jul 2025 14:49:29 +0900 Subject: [PATCH 09/33] feat ( #10 ) : ErrorResponse --- .../kr/entrydsm/feed/global/error/ErrorResponse.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/error/ErrorResponse.kt diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/error/ErrorResponse.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/error/ErrorResponse.kt new file mode 100644 index 0000000..2b04860 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/error/ErrorResponse.kt @@ -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?, +) From aa391c25202a782080df9d5eb0af6b8337f34d32 Mon Sep 17 00:00:00 2001 From: coehgns Date: Wed, 30 Jul 2025 14:49:38 +0900 Subject: [PATCH 10/33] feat ( #10 ) : GlobalExceptionFilter --- .../global/error/GlobalExceptionFilter.kt | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/error/GlobalExceptionFilter.kt diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/error/GlobalExceptionFilter.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/error/GlobalExceptionFilter.kt new file mode 100644 index 0000000..d65e10d --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/error/GlobalExceptionFilter.kt @@ -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) + } + } + + /** + * 에러 코드에 해당하는 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)) + } +} From 361159cbb04cf27f248c0ddbc850bc07e4790882 Mon Sep 17 00:00:00 2001 From: coehgns Date: Wed, 30 Jul 2025 14:49:43 +0900 Subject: [PATCH 11/33] feat ( #10 ) : GlobalExceptionHandler --- .../global/error/GlobalExceptionHandler.kt | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/error/GlobalExceptionHandler.kt diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/error/GlobalExceptionHandler.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/error/GlobalExceptionHandler.kt new file mode 100644 index 0000000..2c6dd19 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/error/GlobalExceptionHandler.kt @@ -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() { + /** + * EquusException을 처리하는 메서드입니다. + * 비즈니스 로직에서 발생한 예외를 적절한 HTTP 응답으로 변환합니다. + * + * @param e 처리할 CasperException 예외 객체 + * @return ErrorResponse를 포함하는 ResponseEntity + */ + @ExceptionHandler(CasperException::class) + fun handlingEquusException(e: CasperException): ResponseEntity { + 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 { + return ResponseEntity( + ErrorResponse( + 400, + e.bindingResult.allErrors[0].defaultMessage, + ), + HttpStatus.BAD_REQUEST, + ) + } +} From d63d97612c7dfea098a9df817fd8f4cd2248930e Mon Sep 17 00:00:00 2001 From: coehgns Date: Wed, 30 Jul 2025 14:50:12 +0900 Subject: [PATCH 12/33] feat ( #10 ) : ExpiredTokenException --- .../feed/global/exception/ExpiredTokenException.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/exception/ExpiredTokenException.kt diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/exception/ExpiredTokenException.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/exception/ExpiredTokenException.kt new file mode 100644 index 0000000..5a5955c --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/exception/ExpiredTokenException.kt @@ -0,0 +1,14 @@ +package hs.kr.entrydsm.feed.global.exception + +import hs.kr.entrydsm.feed.global.error.exception.CasperException +import hs.kr.entrydsm.feed.global.error.exception.ErrorCode + +/** + * JWT 토큰이 만료되었을 때 발생하는 예외 클래스입니다. + * + * @property status HTTP 상태 코드 (401) + * @property message 에러 메시지 + */ +object ExpiredTokenException : CasperException( + ErrorCode.EXPIRED_TOKEN, +) From c7e5a015d7f8104f113efb5c1d50483db9af68cb Mon Sep 17 00:00:00 2001 From: coehgns Date: Wed, 30 Jul 2025 14:50:19 +0900 Subject: [PATCH 13/33] feat ( #10 ) : InternalServerErrorException --- .../exception/InternalServerErrorException.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/exception/InternalServerErrorException.kt diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/exception/InternalServerErrorException.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/exception/InternalServerErrorException.kt new file mode 100644 index 0000000..7512921 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/exception/InternalServerErrorException.kt @@ -0,0 +1,14 @@ +package hs.kr.entrydsm.feed.global.exception + +import hs.kr.entrydsm.feed.global.error.exception.CasperException +import hs.kr.entrydsm.feed.global.error.exception.ErrorCode + +/** + * 서버 내부에서 예기치 않은 오류가 발생했을 때 발생하는 예외 클래스입니다. + * + * @property status HTTP 상태 코드 (500) + * @property message 에러 메시지 + */ +object InternalServerErrorException : CasperException( + ErrorCode.INTERNAL_SERVER_ERROR, +) From e5545d4c86461b48676811031d3df734fbb56489 Mon Sep 17 00:00:00 2001 From: coehgns Date: Wed, 30 Jul 2025 14:50:26 +0900 Subject: [PATCH 14/33] feat ( #10 ) : InvalidTokenException --- .../feed/global/exception/InvalidTokenException.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/exception/InvalidTokenException.kt diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/exception/InvalidTokenException.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/exception/InvalidTokenException.kt new file mode 100644 index 0000000..7b5d7b3 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/exception/InvalidTokenException.kt @@ -0,0 +1,14 @@ +package hs.kr.entrydsm.feed.global.exception + +import hs.kr.entrydsm.feed.global.error.exception.CasperException +import hs.kr.entrydsm.feed.global.error.exception.ErrorCode + +/** + * 유효하지 않은 JWT 토큰이 전달되었을 때 발생하는 예외 클래스입니다. + * + * @property status HTTP 상태 코드 (401) + * @property message 에러 메시지 + */ +object InvalidTokenException : CasperException( + ErrorCode.INVALID_TOKEN, +) From a81e0d8bda79028c903eed9a2ffea893e2b54c8b Mon Sep 17 00:00:00 2001 From: coehgns Date: Wed, 30 Jul 2025 14:50:33 +0900 Subject: [PATCH 15/33] feat ( #10 ) : JwtFilter --- .../feed/global/security/jwt/JwtFilter.kt | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/security/jwt/JwtFilter.kt diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/security/jwt/JwtFilter.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/security/jwt/JwtFilter.kt new file mode 100644 index 0000000..abc2bbb --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/security/jwt/JwtFilter.kt @@ -0,0 +1,70 @@ +package hs.kr.entrydsm.feed.global.security.jwt + +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.core.Authentication +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.core.context.SecurityContextHolder.clearContext +import org.springframework.security.core.userdetails.User +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.web.filter.OncePerRequestFilter + +/** + * JWT(JSON Web Token) 기반 인증을 처리하는 필터 클래스입니다. + * HTTP 요청 헤더에서 사용자 ID와 역할을 추출하여 Spring Security의 SecurityContext에 인증 정보를 설정합니다. + * + * 이 필터는 다음 헤더를 기반으로 인증을 수행합니다: + * - Request-User-Id: 사용자 고유 식별자 + * - Request-User-Role: 사용자 역할 (UserRole enum 값) + * + * @see OncePerRequestFilter + */ +class JwtFilter : OncePerRequestFilter() { + /** + * HTTP 요청에서 사용자 인증 정보를 추출하고 SecurityContext에 설정합니다. + * 인증 정보가 없는 경우 필터 체인을 계속 진행합니다. + * + * @param request HTTP 요청 객체 + * @param response HTTP 응답 객체 + * @param filterChain 필터 체인 + */ + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + val userId: String? = request.getHeader("Request-User-Id") + val role: UserRole? = request.getHeader("Request-User-Role")?.let { UserRole.valueOf(it) } + + if ((userId == null) || (role == null)) { + filterChain.doFilter(request, response) + return + } + + val authorities = mutableListOf(SimpleGrantedAuthority("ROLE_${role.name}")) + val userDetails: UserDetails = User(userId, "", authorities) + val authentication: Authentication = + UsernamePasswordAuthenticationToken(userDetails, "", userDetails.authorities) + + clearContext() + SecurityContextHolder.getContext().authentication = authentication + filterChain.doFilter(request, response) + } +} + +/** + * 사용자 역할을 정의한 enum 클래스입니다. + * 애플리케이션에서 사용되는 사용자 유형을 나타냅니다. + * + * @property ROOT 최고 관리자 권한 + * @property ADMIN 일반 관리자 권한 + * @property USER 일반 사용자 권한 + */ +enum class UserRole { + ROOT, + ADMIN, + USER +} From c186db611762fcfe212aaa8e7fadd22985d00f0b Mon Sep 17 00:00:00 2001 From: coehgns Date: Wed, 30 Jul 2025 14:50:38 +0900 Subject: [PATCH 16/33] feat ( #10 ) : JwtProperties --- .../feed/global/security/jwt/JwtProperties.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/security/jwt/JwtProperties.kt diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/security/jwt/JwtProperties.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/security/jwt/JwtProperties.kt new file mode 100644 index 0000000..015da7e --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/security/jwt/JwtProperties.kt @@ -0,0 +1,20 @@ +package hs.kr.entrydsm.feed.global.security.jwt + +import org.springframework.boot.context.properties.ConfigurationProperties + +/** + * JWT(JSON Web Token) 관련 설정 값을 관리하는 프로퍼티 클래스입니다. + * + * 이 클래스는 `application.yml` 또는 `application.yml` 파일에서 + * `auth.jwt` 하위의 설정 값을 주입받아 사용합니다. + * + * @property secretKey JWT 서명에 사용되는 비밀 키 + * @property header HTTP 요청 헤더에서 JWT 토큰을 식별하기 위한 헤더 이름 + * @prefix JWT 토큰의 접두사 (예: "Bearer ") + */ +@ConfigurationProperties("jwt") +class JwtProperties( + val secretKey: String, + val header: String, + val prefix: String, +) From 106b739f30488d212ecd079ee6e1e9199f371bab Mon Sep 17 00:00:00 2001 From: coehgns Date: Fri, 1 Aug 2025 17:26:39 +0900 Subject: [PATCH 17/33] feat ( #10 ) : AdminUtils --- .../feed/global/utils/admin/AdminUtils.kt | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/utils/admin/AdminUtils.kt diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/utils/admin/AdminUtils.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/utils/admin/AdminUtils.kt new file mode 100644 index 0000000..98215f1 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/utils/admin/AdminUtils.kt @@ -0,0 +1,47 @@ +package hs.kr.entrydsm.feed.global.utils.admin + +import hs.kr.entrydsm.feed.global.exception.InvalidTokenException +import hs.kr.entrydsm.feed.infrastructure.grpc.client.AdminGrpcClient +import hs.kr.entrydsm.feed.infrastructure.grpc.client.dto.response.InternalAdminResponse +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.stereotype.Component +import java.util.* + +/** + * 현재 인증된 관리자와 관련된 유틸리티 메서드를 제공하는 컴포넌트입니다. + * Spring Security의 SecurityContext를 사용하여 현재 인증된 관리자 정보를 조회하고, + * gRPC를 통해 관리자 서비스에서 추가 정보를 조회합니다. + * + * @property adminGrpcClient 관리자 정보 조회를 위한 gRPC 클라이언트 + */ +@Component +class AdminUtils( + private val adminGrpcClient: AdminGrpcClient +) { + + /** + * 현재 인증된 관리자의 상세 정보를 조회합니다. + * 내부적으로 [getCurrentAdminId]를 호출하여 관리자 ID를 가져온 후, + * gRPC를 통해 관리자 서비스에서 상세 정보를 조회합니다. + * + * @return 현재 인증된 관리자의 상세 정보 [InternalAdminResponse] + * @throws hs.kr.entrydsm.feed.global.exception.InvalidTokenException 인증 토큰이 유효하지 않은 경우 + * @throws io.grpc.StatusRuntimeException gRPC 통신 중 오류가 발생한 경우 + */ + suspend fun getCurrentAdmin(): InternalAdminResponse = adminGrpcClient.getAdminInfoByAdminId(getCurrentAdminId()) + + /** + * 현재 인증된 관리자의 고유 식별자(UUID)를 조회합니다. + * Spring Security의 SecurityContext에서 인증 정보를 추출하여 반환합니다. + * + * @return 현재 인증된 관리자의 UUID + * @throws hs.kr.entrydsm.feed.global.exception.InvalidTokenException 인증 토큰이 유효하지 않은 경우 + */ + fun getCurrentAdminId(): UUID { + try { + return UUID.fromString(SecurityContextHolder.getContext().authentication.name) + } catch (e: IllegalArgumentException) { + throw InvalidTokenException + } + } +} From 4111ac5b2bebc8ea16ff05465b68a0ed30065d39 Mon Sep 17 00:00:00 2001 From: coehgns Date: Fri, 1 Aug 2025 17:26:48 +0900 Subject: [PATCH 18/33] feat ( #10 ) : UserUtils --- .../feed/global/utils/user/UserUtils.kt | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/utils/user/UserUtils.kt diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/utils/user/UserUtils.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/utils/user/UserUtils.kt new file mode 100644 index 0000000..07d2409 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/utils/user/UserUtils.kt @@ -0,0 +1,28 @@ +package hs.kr.entrydsm.feed.global.utils.user + +import hs.kr.entrydsm.feed.global.exception.InvalidTokenException +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.stereotype.Component +import java.util.UUID + +/** + * 현재 인증된 사용자와 관련된 유틸리티 메서드를 제공하는 컴포넌트입니다. + * Spring Security의 SecurityContext를 사용하여 현재 인증된 사용자 정보를 조회합니다. + */ +@Component +class UserUtils { + + /** + * 현재 인증된 사용자의 고유 식별자(UUID)를 조회합니다. + * + * @return 현재 인증된 사용자의 UUID + * @throws hs.kr.entrydsm.feed.global.exception.InvalidTokenException 인증 토큰이 유효하지 않은 경우 + */ + fun getCurrentUserId(): UUID { + try { + return UUID.fromString(SecurityContextHolder.getContext().authentication.name) + } catch (e: IllegalArgumentException) { + throw InvalidTokenException + } + } +} From c4b257de770013f3c21ac7534169577cfe091cb7 Mon Sep 17 00:00:00 2001 From: coehgns Date: Fri, 1 Aug 2025 17:27:21 +0900 Subject: [PATCH 19/33] feat ( #10 ) : KafkaConsumerConfig --- .../configuration/KafkaConsumerConfig.kt | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/kafka/configuration/KafkaConsumerConfig.kt diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/kafka/configuration/KafkaConsumerConfig.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/kafka/configuration/KafkaConsumerConfig.kt new file mode 100644 index 0000000..b163d33 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/kafka/configuration/KafkaConsumerConfig.kt @@ -0,0 +1,94 @@ +package hs.kr.entrydsm.feed.infrastructure.kafka.configuration + +import org.apache.kafka.clients.consumer.ConsumerConfig +import org.apache.kafka.common.serialization.StringDeserializer +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.kafka.annotation.EnableKafka +import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory +import org.springframework.kafka.core.ConsumerFactory +import org.springframework.kafka.core.DefaultKafkaConsumerFactory +import org.springframework.kafka.support.serializer.JsonDeserializer + +/** + * Kafka Consumer 설정을 담당하는 클래스입니다. + * Kafka Consumer의 기본 설정과 보안 설정을 구성합니다. + * + * @property kafkaProperty Kafka 관련 설정 프로퍼티 + */ +@EnableKafka +@Configuration +class KafkaConsumerConfig( + private val kafkaProperty: KafkaProperty, +) { + /** + * Kafka ConsumerFactory를 생성하는 빈을 정의합니다. + * + * @return ConsumerFactory Kafka Consumer 인스턴스를 생성하는 팩토리 + */ + @Bean + fun consumerFactory(): ConsumerFactory { + return DefaultKafkaConsumerFactory(consumerFactoryConfig()) + } + + /** + * Kafka Listener Container Factory를 생성하는 빈을 정의합니다. + * 동시성 및 컨테이너 속성을 설정합니다. + * + * @return ConcurrentKafkaListenerContainerFactory Kafka Listener Container 팩토리 + */ + @Bean + fun kafkaListenerContainerFactory(): ConcurrentKafkaListenerContainerFactory { + return ConcurrentKafkaListenerContainerFactory().apply { + consumerFactory = consumerFactory() + setConcurrency(2) + // Spring Kafka 3.1.x에서는 setMessageConverter 제거됨 + // JSON 변환은 JsonDeserializer에서 처리 + containerProperties.apply { + pollTimeout = 500 + isMissingTopicsFatal = false + isObservationEnabled = true + } + } + } + + /** + * Kafka ConsumerFactory에 사용할 설정을 생성합니다. + * + * @return Map Kafka Consumer 설정 맵 + */ + private fun consumerFactoryConfig(): Map { + return mapOf( + // 기본 설정 + ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG to kafkaProperty.serverAddress, + ConsumerConfig.ISOLATION_LEVEL_CONFIG to "read_committed", + ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java, + ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to JsonDeserializer::class.java, + ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG to false, + ConsumerConfig.AUTO_OFFSET_RESET_CONFIG to "latest", + ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG to 5000, + ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG to 30000, + ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG to 10000, + // JsonDeserializer 설정 (3.1.x 방식) + JsonDeserializer.TRUSTED_PACKAGES to "*", + JsonDeserializer.TYPE_MAPPINGS to "", + JsonDeserializer.USE_TYPE_INFO_HEADERS to false, + JsonDeserializer.VALUE_DEFAULT_TYPE to "java.lang.Object", + // Security 설정 + "security.protocol" to "SASL_PLAINTEXT", + "sasl.mechanism" to "SCRAM-SHA-512", + "sasl.jaas.config" to buildJaasConfig(), + ) + } + + /** + * Kafka 인증을 위한 JAAS 구성을 생성합니다. + * + * @return String JAAS 구성 문자열 + */ + private fun buildJaasConfig(): String { + return "org.apache.kafka.common.security.scram.ScramLoginModule required " + + "username=\"${kafkaProperty.confluentApiKey}\" " + + "password=\"${kafkaProperty.confluentApiSecret}\";" + } +} From 6b07954cc8f0d7c428d744fa4cf62c7bb324ee6d Mon Sep 17 00:00:00 2001 From: coehgns Date: Fri, 1 Aug 2025 17:27:27 +0900 Subject: [PATCH 20/33] feat ( #10 ) : KafkaProperty --- .../kafka/configuration/KafkaProperty.kt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/kafka/configuration/KafkaProperty.kt diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/kafka/configuration/KafkaProperty.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/kafka/configuration/KafkaProperty.kt new file mode 100644 index 0000000..bd4a1c7 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/kafka/configuration/KafkaProperty.kt @@ -0,0 +1,18 @@ +package hs.kr.entrydsm.feed.infrastructure.kafka.configuration + +import org.springframework.boot.context.properties.ConfigurationProperties + +/** + * Kafka 관련 설정 프로퍼티를 담는 클래스입니다. + * application.yml에서 'kafka' 접두사로 시작하는 설정을 매핑합니다. + * + * @property serverAddress Kafka 브로커 서버 주소 + * @property confluentApiKey Confluent Cloud API 키 + * @property confluentApiSecret Confluent Cloud API 시크릿 + */ +@ConfigurationProperties("kafka") +class KafkaProperty( + val serverAddress: String, + val confluentApiKey: String, + val confluentApiSecret: String, +) From d4562eca0a286fb3b87c095631da9f5bfe791a53 Mon Sep 17 00:00:00 2001 From: coehgns Date: Fri, 1 Aug 2025 17:27:33 +0900 Subject: [PATCH 21/33] feat ( #10 ) : KafkaTopics --- .../kafka/configuration/KafkaTopics.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/kafka/configuration/KafkaTopics.kt diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/kafka/configuration/KafkaTopics.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/kafka/configuration/KafkaTopics.kt new file mode 100644 index 0000000..15ef7ea --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/kafka/configuration/KafkaTopics.kt @@ -0,0 +1,17 @@ +package hs.kr.entrydsm.feed.infrastructure.kafka.configuration + +/** + * Kafka 토픽 이름을 상수로 정의한 객체입니다. + * 애플리케이션 전체에서 일관된 토픽 이름을 사용하기 위해 상수로 관리합니다. + */ +object KafkaTopics { + /** + * 모든 테이블 데이터 삭제를 위한 토픽 이름 + */ + const val DELETE_ALL_TABLE = "delete-all-table" + + /** + * 사용자 삭제 이벤트를 위한 토픽 이름 + */ + const val DELETE_USER = "delete-user" +} From 105e190fb1a49b03854e7f9524f40d57b4543117 Mon Sep 17 00:00:00 2001 From: coehgns Date: Fri, 1 Aug 2025 17:27:41 +0900 Subject: [PATCH 22/33] feat ( #10 ) : DeleteFaqTableConsumer --- .../kafka/consumer/DeleteFaqTableConsumer.kt | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/kafka/consumer/DeleteFaqTableConsumer.kt diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/kafka/consumer/DeleteFaqTableConsumer.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/kafka/consumer/DeleteFaqTableConsumer.kt new file mode 100644 index 0000000..ed8b911 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/kafka/consumer/DeleteFaqTableConsumer.kt @@ -0,0 +1,33 @@ +package hs.kr.entrydsm.feed.infrastructure.kafka.consumer + +import hs.kr.entrydsm.feed.adapter.out.persistence.faq.repository.FaqRepository +import hs.kr.entrydsm.feed.infrastructure.kafka.configuration.KafkaTopics +import org.springframework.kafka.annotation.KafkaListener +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +/** + * FAQ 테이블 삭제를 처리하는 Kafka Consumer 클래스입니다. + * + * @property faqRepository FAQ 데이터 접근을 위한 리포지토리 + */ +@Service +class DeleteFaqTableConsumer( + private val faqRepository: FaqRepository, +) { + /** + * Kafka 토픽에서 메시지를 수신하여 FAQ 테이블의 모든 데이터를 삭제합니다. + * DELETE_ALL_TABLE 토픽으로 메시지가 수신되면 실행됩니다. + * + * @see KafkaTopics.DELETE_ALL_TABLE + */ + @KafkaListener( + topics = [KafkaTopics.DELETE_ALL_TABLE], + groupId = "delete-all-table-faq", + containerFactory = "kafkaListenerContainerFactory", + ) + @Transactional + fun execute() { + faqRepository.deleteAll() + } +} From 00a4ccad2c74df5d6bb199351b55ac7fc2fd4e2c Mon Sep 17 00:00:00 2001 From: coehgns Date: Fri, 1 Aug 2025 17:27:52 +0900 Subject: [PATCH 23/33] feat ( #10 ) : BadFileExtensionException --- .../s3/exception/BadFileExtensionException.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/s3/exception/BadFileExtensionException.kt diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/s3/exception/BadFileExtensionException.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/s3/exception/BadFileExtensionException.kt new file mode 100644 index 0000000..93eaf4d --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/s3/exception/BadFileExtensionException.kt @@ -0,0 +1,14 @@ +package hs.kr.entrydsm.feed.infrastructure.s3.exception + +import hs.kr.entrydsm.feed.global.error.exception.CasperException +import hs.kr.entrydsm.feed.global.error.exception.ErrorCode + +/** + * 잘못된 파일 확장자가 업로드되었을 때 발생하는 예외 클래스입니다. + * + * @property status HTTP 상태 코드 (400) + * @property message 에러 메시지 + */ +object BadFileExtensionException : CasperException( + ErrorCode.BAD_FILE_EXTENSION, +) From e1899fdfafa96ff91cdb550863d32e5de22191e1 Mon Sep 17 00:00:00 2001 From: coehgns Date: Fri, 1 Aug 2025 17:28:02 +0900 Subject: [PATCH 24/33] feat ( #10 ) : EmptyFileException --- .../s3/exception/EmptyFileException.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/s3/exception/EmptyFileException.kt diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/s3/exception/EmptyFileException.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/s3/exception/EmptyFileException.kt new file mode 100644 index 0000000..4ae0feb --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/s3/exception/EmptyFileException.kt @@ -0,0 +1,14 @@ +package hs.kr.entrydsm.feed.infrastructure.s3.exception + +import hs.kr.entrydsm.feed.global.error.exception.CasperException +import hs.kr.entrydsm.feed.global.error.exception.ErrorCode + +/** + * 빈 파일이 업로드되었을 때 발생하는 예외 클래스입니다. + * + * @property status HTTP 상태 코드 (400) + * @property message 에러 메시지 + */ +object EmptyFileException : CasperException( + ErrorCode.FILE_IS_EMPTY, +) From 56b8b734440f9cdb4ec346738641ad213af29526 Mon Sep 17 00:00:00 2001 From: coehgns Date: Fri, 1 Aug 2025 17:28:10 +0900 Subject: [PATCH 25/33] feat ( #10 ) : FileUtil --- .../feed/infrastructure/s3/util/FileUtil.kt | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/s3/util/FileUtil.kt diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/s3/util/FileUtil.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/s3/util/FileUtil.kt new file mode 100644 index 0000000..9181b8e --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/s3/util/FileUtil.kt @@ -0,0 +1,144 @@ +package hs.kr.entrydsm.feed.infrastructure.s3.util + +import com.amazonaws.HttpMethod +import com.amazonaws.services.s3.AmazonS3 +import com.amazonaws.services.s3.model.CannedAccessControlList +import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest +import com.amazonaws.services.s3.model.ObjectMetadata +import com.amazonaws.services.s3.model.PutObjectRequest +import hs.kr.entrydsm.feed.infrastructure.s3.exception.BadFileExtensionException +import hs.kr.entrydsm.feed.infrastructure.s3.exception.EmptyFileException +import org.springframework.beans.factory.annotation.Value +import org.springframework.http.MediaType +import org.springframework.stereotype.Service +import org.springframework.web.multipart.MultipartFile +import java.io.ByteArrayInputStream +import java.io.InputStream +import java.util.* + +/** + * AWS S3와의 상호작용을 추상화한 유틸리티 클래스입니다. + * + * 이 클래스는 파일 업로드, 다운로드 URL 생성, 파일 삭제 등 S3와 관련된 + * 공통적인 작업을 처리하기 위한 유틸리티 메서드를 제공합니다. + * + * @property amazonS3 AWS S3 클라이언트 인스턴스 + * @property bucketName S3 버킷 이름 (application.yml에서 주입됨) + * + * @throws EmptyFileException 업로드할 파일이 비어있는 경우 발생 + * @throws BadFileExtensionException 허용되지 않은 파일 확장자인 경우 발생 + */ +@Service +class FileUtil( + private val amazonS3: AmazonS3, +) { + @Value("\${cloud.aws.s3.bucket}") + lateinit var bucketName: String + + companion object { + const val EXP_TIME = 10000 * 60 * 2 + } + + /** + * 파일을 S3에 업로드하고 생성된 파일명을 반환합니다. + * + * @param file 업로드할 파일 + * @param path S3 버킷 내 저장 경로 + * @return 생성된 랜덤 파일명 (확장자 포함) + * @throws EmptyFileException 파일이 비어있는 경우 + * @throws BadFileExtensionException 허용되지 않은 확장자인 경우 + */ + fun upload( + file: MultipartFile, + path: String, + ): String { + val ext = verificationFile(file) + + val randomName = UUID.randomUUID().toString() + val filename = "$randomName.$ext" + val inputStream: InputStream = ByteArrayInputStream(file.bytes) + + val metadata = + ObjectMetadata().apply { + contentType = + when (ext) { + "pdf" -> MediaType.APPLICATION_PDF_VALUE + else -> MediaType.IMAGE_PNG_VALUE + } + contentLength = file.size + contentDisposition = "inline" + } + + inputStream.use { + amazonS3.putObject( + PutObjectRequest(bucketName, "${path}$filename", it, metadata) + .withCannedAcl(CannedAccessControlList.AuthenticatedRead), + ) + } + + return filename + } + + /** + * S3에서 지정된 파일을 삭제합니다. + * + * @param objectName 삭제할 파일명 + * @param path 파일이 위치한 S3 경로 + */ + fun delete( + objectName: String, + path: String, + ) { + amazonS3.deleteObject(bucketName, path + objectName) + } + + /** + * S3에 저장된 파일에 접근할 수 있는 임시 URL을 생성합니다. + * + * @param fileName 접근할 파일명 + * @param path 파일이 위치한 S3 경로 + * @return 임시 접근 URL + */ + fun generateObjectUrl( + fileName: String, + path: String, + ): String { + val expiration = + Date().apply { + time += EXP_TIME + } + return amazonS3.generatePresignedUrl( + GeneratePresignedUrlRequest( + bucketName, + "${path}$fileName", + ).withMethod(HttpMethod.GET).withExpiration(expiration), + ).toString() + } + + /** + * 업로드할 파일의 유효성을 검증하고 파일 확장자를 반환합니다. + * + * @param file 검증할 파일 + * @return 소문자로 변환된 파일 확장자 + * @throws EmptyFileException 파일이 비어있는 경우 + * @throws BadFileExtensionException 허용되지 않은 확장자인 경우 + */ + private fun verificationFile(file: MultipartFile): String { + if (file.isEmpty || file.originalFilename == null) throw EmptyFileException + val originalFilename = file.originalFilename!! + val ext = originalFilename.substring(originalFilename.lastIndexOf(".") + 1).lowercase(Locale.getDefault()) + + if (!( + ext == "jpg" || ext == "jpeg" || + ext == "png" || ext == "heic" || + ext == "hwp" || ext == "pptx" || + ext == "pdf" || ext == "xls" || + ext == "xlsx" + ) + ) { + throw BadFileExtensionException + } + + return ext + } +} From 9a2f3c06e4cc50316d98f20f5284cc2c64960500 Mon Sep 17 00:00:00 2001 From: coehgns Date: Fri, 1 Aug 2025 17:28:28 +0900 Subject: [PATCH 26/33] feat ( #10 ) : PathList --- .../feed/infrastructure/s3/PathList.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/s3/PathList.kt diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/s3/PathList.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/s3/PathList.kt new file mode 100644 index 0000000..f17c034 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/s3/PathList.kt @@ -0,0 +1,22 @@ +package hs.kr.entrydsm.feed.infrastructure.s3 + +/** + * S3 버킷 내의 디렉토리 경로를 관리하는 객체입니다. + * 각 상수는 S3 버킷 내의 특정 디렉토리 경로를 나타냅니다. + */ +object PathList { + /** + * 공지사항 이미지 파일이 저장되는 디렉토리 경로입니다. + */ + const val NOTICE = "notice/" + + /** + * 화면 이미지 파일이 저장되는 디렉토리 경로입니다. + */ + const val SCREEN = "screen/" + + /** + * 첨부 파일이 저장되는 디렉토리 경로입니다. + */ + const val ATTACH_FILE = "attach_file/" +} From add59a39190f7cebba6f7b8de3dad79f3639c7b2 Mon Sep 17 00:00:00 2001 From: coehgns Date: Fri, 1 Aug 2025 17:29:18 +0900 Subject: [PATCH 27/33] =?UTF-8?q?feat=20(=20#10=20)=20:=20=EC=BB=A4?= =?UTF-8?q?=EB=B0=8B=20=EB=AA=BB=ED=95=9C=20AttachFile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/entrydsm/feed/model/attachFile/AttachFile.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/model/attachFile/AttachFile.kt diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/model/attachFile/AttachFile.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/model/attachFile/AttachFile.kt new file mode 100644 index 0000000..eb8cddd --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/model/attachFile/AttachFile.kt @@ -0,0 +1,13 @@ +package hs.kr.entrydsm.feed.model.attachFile + +/** + * 첨부 파일 정보를 나타내는 데이터 클래스입니다. + * 이 클래스는 업로드된 파일의 시스템 내부 이름과 원본 파일 이름을 관리합니다. + * + * @property uploadedFileName 시스템에 저장된 파일 이름 (UUID 등으로 생성된 고유한 이름) + * @property originalAttachFileName 사용자가 업로드한 원본 파일 이름 + */ +data class AttachFile( + val uploadedFileName: String, + val originalAttachFileName: String, +) From b4cb0ad5ce02cc269a3adceaf20d6c5da4cd65df Mon Sep 17 00:00:00 2001 From: coehgns Date: Sat, 2 Aug 2025 10:29:55 +0900 Subject: [PATCH 28/33] =?UTF-8?q?refactor=20(=20#10=20)=20:=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=EB=90=9C=20build=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/hs/kr/entrydsm/feed/global/config/SecurityConfig.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/config/SecurityConfig.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/config/SecurityConfig.kt index f763085..0aaf830 100644 --- a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/config/SecurityConfig.kt +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/config/SecurityConfig.kt @@ -73,7 +73,6 @@ class SecurityConfig( .authenticated() } .with(FilterConfig(objectMapper)) { } - .build() return http.build() } From 9a2725e66a7137478255a1e2e8370e4bb4790f7f Mon Sep 17 00:00:00 2001 From: coehgns Date: Sat, 2 Aug 2025 10:42:52 +0900 Subject: [PATCH 29/33] =?UTF-8?q?refactor=20(=20#10=20)=20:=20BaseEntity?= =?UTF-8?q?=20UUID=20=EC=A0=84=EB=9E=B5=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/hs/kr/entrydsm/feed/global/entity/BaseEntity.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/entity/BaseEntity.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/entity/BaseEntity.kt index f791962..6d71a9f 100644 --- a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/entity/BaseEntity.kt +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/entity/BaseEntity.kt @@ -2,6 +2,7 @@ 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.* @@ -16,7 +17,7 @@ import java.util.* @MappedSuperclass abstract class BaseEntity( @Id - @GeneratedValue(generator = "uuid2") + @GeneratedValue(strategy = GenerationType.UUID) @Column( columnDefinition = "BINARY(16)", nullable = false, From b08a71f1522a51bab75d74d6cdc77e0907b3657c Mon Sep 17 00:00:00 2001 From: coehgns Date: Sat, 2 Aug 2025 14:27:08 +0900 Subject: [PATCH 30/33] =?UTF-8?q?refactor=20(=20#10=20)=20:=20FileUtil=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=ED=99=95=EC=9E=A5=EC=9E=90=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feed/infrastructure/s3/util/FileUtil.kt | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/s3/util/FileUtil.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/s3/util/FileUtil.kt index 9181b8e..0737acf 100644 --- a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/s3/util/FileUtil.kt +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/s3/util/FileUtil.kt @@ -37,6 +37,10 @@ class FileUtil( companion object { const val EXP_TIME = 10000 * 60 * 2 + private val ALLOWED_EXTENSIONS = setOf( + "jpg", "jpeg", "png", "heic", + "hwp", "pptx", "pdf", "xls", "xlsx" + ) } /** @@ -128,14 +132,7 @@ class FileUtil( val originalFilename = file.originalFilename!! val ext = originalFilename.substring(originalFilename.lastIndexOf(".") + 1).lowercase(Locale.getDefault()) - if (!( - ext == "jpg" || ext == "jpeg" || - ext == "png" || ext == "heic" || - ext == "hwp" || ext == "pptx" || - ext == "pdf" || ext == "xls" || - ext == "xlsx" - ) - ) { + if (ext !in ALLOWED_EXTENSIONS) { throw BadFileExtensionException } From 03dbf1f4f1d98b3875ddd7e16f2bf11b0a771dea Mon Sep 17 00:00:00 2001 From: coehgns Date: Sat, 2 Aug 2025 14:31:00 +0900 Subject: [PATCH 31/33] =?UTF-8?q?refactor=20(=20#10=20)=20:=20GlobalExcept?= =?UTF-8?q?ionHandler=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=84=A4=EC=9D=B4?= =?UTF-8?q?=EB=B0=8D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hs/kr/entrydsm/feed/global/error/GlobalExceptionHandler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/error/GlobalExceptionHandler.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/error/GlobalExceptionHandler.kt index 2c6dd19..4633528 100644 --- a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/error/GlobalExceptionHandler.kt +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/error/GlobalExceptionHandler.kt @@ -29,7 +29,7 @@ class GlobalExceptionHandler() { * @return ErrorResponse를 포함하는 ResponseEntity */ @ExceptionHandler(CasperException::class) - fun handlingEquusException(e: CasperException): ResponseEntity { + fun handlingCasperException(e: CasperException): ResponseEntity { val code = e.errorCode return ResponseEntity( ErrorResponse(code.status, code.message), From 24eae29aa2937f3b2c5127f71a3a8e6f0e0f2c93 Mon Sep 17 00:00:00 2001 From: coehgns Date: Sat, 2 Aug 2025 14:31:39 +0900 Subject: [PATCH 32/33] =?UTF-8?q?refactor=20(=20#10=20)=20:=20GlobalExcept?= =?UTF-8?q?ionHandler=20kdoc=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hs/kr/entrydsm/feed/global/error/GlobalExceptionHandler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/error/GlobalExceptionHandler.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/error/GlobalExceptionHandler.kt index 4633528..930a3d0 100644 --- a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/error/GlobalExceptionHandler.kt +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/global/error/GlobalExceptionHandler.kt @@ -22,7 +22,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice @RestControllerAdvice class GlobalExceptionHandler() { /** - * EquusException을 처리하는 메서드입니다. + * CasperException을 처리하는 메서드입니다. * 비즈니스 로직에서 발생한 예외를 적절한 HTTP 응답으로 변환합니다. * * @param e 처리할 CasperException 예외 객체 From 9934254352b608f1cfb56d4630e8f92701f6db93 Mon Sep 17 00:00:00 2001 From: coehgns Date: Fri, 15 Aug 2025 17:16:56 +0900 Subject: [PATCH 33/33] =?UTF-8?q?refactor=20(=20#10=20)=20:=20FileUtil?= =?UTF-8?q?=EC=9D=98=20verificationFile=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EC=9D=98=20=EB=B9=88=EB=AC=B8=EC=9E=90=EC=97=B4=EC=9D=B4?= =?UTF-8?q?=EA=B1=B0=EB=82=98=20=EA=B3=B5=EB=B0=B1=EB=A7=8C=20=EC=9E=88?= =?UTF-8?q?=EB=8A=94=20=EA=B2=BD=EC=9A=B0=EB=8F=84=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hs/kr/entrydsm/feed/infrastructure/s3/util/FileUtil.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/s3/util/FileUtil.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/s3/util/FileUtil.kt index 0737acf..17e522e 100644 --- a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/s3/util/FileUtil.kt +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/infrastructure/s3/util/FileUtil.kt @@ -128,7 +128,7 @@ class FileUtil( * @throws BadFileExtensionException 허용되지 않은 확장자인 경우 */ private fun verificationFile(file: MultipartFile): String { - if (file.isEmpty || file.originalFilename == null) throw EmptyFileException + if (file.isEmpty || file.originalFilename.isNullOrBlank()) throw EmptyFileException val originalFilename = file.originalFilename!! val ext = originalFilename.substring(originalFilename.lastIndexOf(".") + 1).lowercase(Locale.getDefault())