From 4b863c390d1c73d1852c481c57520efb098d646b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Fri, 15 Aug 2025 17:08:49 +0900 Subject: [PATCH 01/53] =?UTF-8?q?:rocket:=20chore:=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/document/entity/DataFile.kt | 31 +++++++++++++++++++ .../domain/document/entity/DataFileTag.kt | 21 +++++++++++++ .../ragback/domain/document/entity/Tag.kt | 17 ++++++++++ 3 files changed, 69 insertions(+) create mode 100644 src/main/kotlin/simplerag/ragback/domain/document/entity/DataFile.kt create mode 100644 src/main/kotlin/simplerag/ragback/domain/document/entity/DataFileTag.kt create mode 100644 src/main/kotlin/simplerag/ragback/domain/document/entity/Tag.kt diff --git a/src/main/kotlin/simplerag/ragback/domain/document/entity/DataFile.kt b/src/main/kotlin/simplerag/ragback/domain/document/entity/DataFile.kt new file mode 100644 index 0000000..ff7fa1d --- /dev/null +++ b/src/main/kotlin/simplerag/ragback/domain/document/entity/DataFile.kt @@ -0,0 +1,31 @@ +package simplerag.ragback.domain.document.entity + +import jakarta.persistence.* +import java.time.LocalDateTime + +@Entity +@Table( + name = "data_file", + uniqueConstraints = [UniqueConstraint(columnNames = ["sha256"])] +) +data class DataFile( + + @Column(nullable = false, length = 255) + val title: String, + + @Column(nullable = false, length = 120) + val type: String, + + @Column(name = "size_mb", nullable = false) + val sizeMb: Double, + + @Column(nullable = false, length = 128) + val sha256: String, + + val updatedAt: LocalDateTime, + + val createdAt: LocalDateTime, + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long? = null, +) diff --git a/src/main/kotlin/simplerag/ragback/domain/document/entity/DataFileTag.kt b/src/main/kotlin/simplerag/ragback/domain/document/entity/DataFileTag.kt new file mode 100644 index 0000000..0dcb435 --- /dev/null +++ b/src/main/kotlin/simplerag/ragback/domain/document/entity/DataFileTag.kt @@ -0,0 +1,21 @@ +package simplerag.ragback.domain.document.entity + +import jakarta.persistence.* + +@Entity +@Table( + name = "data_file_tags", +) +class DataFileTag( + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "tag_id", nullable = false) + var tag: Tag, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "data_file_id", nullable = false) + var dataFile: DataFile, + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long? = null, +) \ No newline at end of file diff --git a/src/main/kotlin/simplerag/ragback/domain/document/entity/Tag.kt b/src/main/kotlin/simplerag/ragback/domain/document/entity/Tag.kt new file mode 100644 index 0000000..d817108 --- /dev/null +++ b/src/main/kotlin/simplerag/ragback/domain/document/entity/Tag.kt @@ -0,0 +1,17 @@ +package simplerag.ragback.domain.document.entity + +import jakarta.persistence.* + +@Entity +@Table( + name = "tags", + uniqueConstraints = [UniqueConstraint(columnNames = ["name"])] +) +data class Tag( + + @Column(nullable = false, length = 60) + val name: String, + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long? = null, +) \ No newline at end of file From 4d6cffc1dae126f840478946ceba3648e3fc9649 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 15:37:03 +0900 Subject: [PATCH 02/53] =?UTF-8?q?:rocket:=20chore:=20S3=20=EC=84=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 6 ++ .../ragback/global/config/S3Config.kt | 69 +++++++++++++ .../MultipartJackson2HttpMessageConverter.kt | 25 +++++ .../simplerag/ragback/global/util/S3Type.kt | 8 ++ .../simplerag/ragback/global/util/S3Util.kt | 96 +++++++++++++++++++ src/main/resources/application.yml | 16 +++- 6 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/simplerag/ragback/global/config/S3Config.kt create mode 100644 src/main/kotlin/simplerag/ragback/global/util/MultipartJackson2HttpMessageConverter.kt create mode 100644 src/main/kotlin/simplerag/ragback/global/util/S3Type.kt create mode 100644 src/main/kotlin/simplerag/ragback/global/util/S3Util.kt diff --git a/build.gradle b/build.gradle index e8b033f..0b19d1e 100644 --- a/build.gradle +++ b/build.gradle @@ -43,6 +43,12 @@ dependencies { testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' runtimeOnly 'org.postgresql:postgresql' + + // swagger + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0' + + // s3 + implementation("software.amazon.awssdk:s3:2.25.70") } dependencyManagement { diff --git a/src/main/kotlin/simplerag/ragback/global/config/S3Config.kt b/src/main/kotlin/simplerag/ragback/global/config/S3Config.kt new file mode 100644 index 0000000..ea218d3 --- /dev/null +++ b/src/main/kotlin/simplerag/ragback/global/config/S3Config.kt @@ -0,0 +1,69 @@ +package simplerag.ragback.global.config + +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider +import software.amazon.awssdk.regions.Region +import software.amazon.awssdk.services.s3.S3Client +import software.amazon.awssdk.services.s3.S3Configuration +import software.amazon.awssdk.services.s3.presigner.S3Presigner + +@Configuration +class S3Config( + + @Value("\${cloud.aws.region.static}") + private val region: String, + + @Value("\${cloud.aws.s3.bucket}") + val bucket: String, + + @Value("\${cloud.aws.credentials.access-key:}") + private val accessKey: String, + + @Value("\${cloud.aws.credentials.secret-key:}") + private val secretKey: String, +) { + + @Bean + fun s3Client(): S3Client { + val regionObj = Region.of(region) + + val creds: AwsCredentialsProvider = + if (accessKey.isNotBlank() && secretKey.isNotBlank()) + StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKey, secretKey)) + else + DefaultCredentialsProvider.create() + + val builder = S3Client.builder() + .region(regionObj) + .credentialsProvider(creds) + .serviceConfiguration( + S3Configuration.builder() + .checksumValidationEnabled(true) + .build() + ) + + return builder.build() + } + + @Bean + fun s3Presigner(): S3Presigner { + val regionObj = Region.of(region) + + val creds: AwsCredentialsProvider = + if (accessKey.isNotBlank() && secretKey.isNotBlank()) + StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKey, secretKey)) + else + DefaultCredentialsProvider.create() + + val builder = S3Presigner.builder() + .region(regionObj) + .credentialsProvider(creds) + + return builder.build() + } +} \ No newline at end of file diff --git a/src/main/kotlin/simplerag/ragback/global/util/MultipartJackson2HttpMessageConverter.kt b/src/main/kotlin/simplerag/ragback/global/util/MultipartJackson2HttpMessageConverter.kt new file mode 100644 index 0000000..b5f84a3 --- /dev/null +++ b/src/main/kotlin/simplerag/ragback/global/util/MultipartJackson2HttpMessageConverter.kt @@ -0,0 +1,25 @@ +package simplerag.ragback.global.util + +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.http.MediaType +import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter +import org.springframework.stereotype.Component +import java.lang.reflect.Type + +@Component +class MultipartJackson2HttpMessageConverter + (objectMapper: ObjectMapper?) : + AbstractJackson2HttpMessageConverter(objectMapper!!, MediaType.APPLICATION_OCTET_STREAM) { + + override fun canWrite(clazz: Class<*>, mediaType: MediaType?): Boolean { + return false + } + + override fun canWrite(type: Type?, clazz: Class<*>, mediaType: MediaType?): Boolean { + return false + } + + override fun canWrite(mediaType: MediaType?): Boolean { + return false + } +} \ No newline at end of file diff --git a/src/main/kotlin/simplerag/ragback/global/util/S3Type.kt b/src/main/kotlin/simplerag/ragback/global/util/S3Type.kt new file mode 100644 index 0000000..a408188 --- /dev/null +++ b/src/main/kotlin/simplerag/ragback/global/util/S3Type.kt @@ -0,0 +1,8 @@ +package simplerag.ragback.global.util + + +enum class S3Type( + val label: String, +) { + ORIGINAL_FILE("/ORIGINAL/"),; +} \ No newline at end of file diff --git a/src/main/kotlin/simplerag/ragback/global/util/S3Util.kt b/src/main/kotlin/simplerag/ragback/global/util/S3Util.kt new file mode 100644 index 0000000..d1ac37a --- /dev/null +++ b/src/main/kotlin/simplerag/ragback/global/util/S3Util.kt @@ -0,0 +1,96 @@ +package simplerag.ragback.global.util + +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import org.springframework.web.multipart.MultipartFile +import simplerag.ragback.global.config.S3Config +import simplerag.ragback.global.error.ErrorCode +import simplerag.ragback.global.error.S3Exception +import software.amazon.awssdk.core.sync.RequestBody +import software.amazon.awssdk.services.s3.S3Client +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest +import software.amazon.awssdk.services.s3.model.ObjectCannedACL +import software.amazon.awssdk.services.s3.model.PutObjectRequest +import software.amazon.awssdk.services.s3.presigner.S3Presigner +import java.net.URI +import java.util.UUID + +@Component +class S3Util( + private val s3: S3Client, + private val presigner: S3Presigner, + private val s3Config: S3Config, + @Value("\${cloud.aws.region.static}") private val region: String +) { + private val bucket get() = s3Config.bucket + + fun upload(file: MultipartFile, dir: S3Type): String { + if (file.isEmpty) throw S3Exception(ErrorCode.S3_EMPTY_FILE) + + val key = buildKey(dir.label, file.originalFilename) + + try { + file.inputStream.use { input -> + val putReq = PutObjectRequest.builder() + .bucket(bucket) + .key(key) + .contentType(file.contentType ?: "application/octet-stream") + .build() + + val body = RequestBody.fromInputStream(input, file.size) + s3.putObject(putReq, body) + } + + return urlFromKey(key) + } catch (e: software.amazon.awssdk.services.s3.model.S3Exception) { + throw S3Exception(ErrorCode.S3_UPLOAD_FAIL) + } catch (e: Exception) { + throw S3Exception(ErrorCode.S3_UPLOAD_FAIL) + } + } + + fun urlFromKey(key: String): String = + s3.utilities() + .getUrl { it.bucket(bucket).key(key) } + .toExternalForm() + + fun deleteByUrl(url: String) { + val key = keyFromUrl(url) ?: throw S3Exception(ErrorCode.S3_INVALID_URL) + delete(key) + } + + fun delete(key: String) { + try { + val req = DeleteObjectRequest.builder() + .bucket(bucket) + .key(key) + .build() + s3.deleteObject(req) + } catch (e: software.amazon.awssdk.services.s3.model.S3Exception) { + // NoSuchKey 등 + throw S3Exception(ErrorCode.S3_OBJECT_NOT_FOUND) + } catch (e: Exception) { + throw S3Exception(ErrorCode.S3_DELETE_FAIL) + } + } + + private fun buildKey(dir: String, originalFilename: String?): String { + val cleanName = (originalFilename ?: "file") + .substringAfterLast('/') + .substringAfterLast('\\') + .ifBlank { "file" } + + val prefix = dir.trim('/') + + return "$prefix/${UUID.randomUUID()}_$cleanName" + } + + fun keyFromUrl(url: String): String? { + val path = try { + URI(url).path // e.g. "/market/menu/uuid_name.jpg" + } catch (_: Exception) { + return null + } + return path.removePrefix("/").ifBlank { null } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 081211d..872fc96 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -4,6 +4,10 @@ server: spring: application: name: rag-backend + servlet: + multipart: + max-file-size: 50MB + max-request-size: 200MB datasource: url: ${DB_URL} username: ${DB_USERNAME} @@ -24,4 +28,14 @@ logging: level: root: INFO org.hibernate.SQL: DEBUG - org.hibernate.type.descriptor.sql.BasicBinder: TRACE \ No newline at end of file + org.hibernate.type.descriptor.sql.BasicBinder: TRACE + +cloud: + aws: + region: + static: ${REGION} + s3: + bucket: ${BUCKET} + credentials: + access-key: ${AWS_ACCESS_KEY_ID} + secret-key: ${AWS_SECRET_ACCESS_KEY} \ No newline at end of file From c712b219acc674335a2aa0a69e0bb6e50775b56b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 15:37:15 +0900 Subject: [PATCH 03/53] =?UTF-8?q?:rocket:=20chore:=20Swagger=20=EC=84=B8?= =?UTF-8?q?=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ragback/global/config/SwaggerConfig.kt | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/main/kotlin/simplerag/ragback/global/config/SwaggerConfig.kt diff --git a/src/main/kotlin/simplerag/ragback/global/config/SwaggerConfig.kt b/src/main/kotlin/simplerag/ragback/global/config/SwaggerConfig.kt new file mode 100644 index 0000000..2347dfd --- /dev/null +++ b/src/main/kotlin/simplerag/ragback/global/config/SwaggerConfig.kt @@ -0,0 +1,38 @@ +package simplerag.ragback.global.config + +import io.swagger.v3.oas.models.Components +import io.swagger.v3.oas.models.OpenAPI +import io.swagger.v3.oas.models.info.Info +import io.swagger.v3.oas.models.security.SecurityRequirement +import io.swagger.v3.oas.models.security.SecurityScheme +import io.swagger.v3.oas.models.servers.Server +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class SwaggerConfig { + @Bean + fun SimpleRAG(): OpenAPI { + val info = Info().title("SimpleRAG API").description("SimpleRAG API 명세").version("0.0.1") + + val jwtSchemeName = "JWT TOKEN" + val securityRequirement = SecurityRequirement().addList(jwtSchemeName) + + val components = + Components() + .addSecuritySchemes( + jwtSchemeName, + SecurityScheme() + .name(jwtSchemeName) + .type(SecurityScheme.Type.HTTP) + .scheme("Bearer") + .bearerFormat("JWT") + ) + + return OpenAPI() + .addServersItem(Server().url("/")) + .info(info) + .addSecurityItem(securityRequirement) + .components(components) + } +} \ No newline at end of file From 9ab10be27925a073c55f8186d91dab6eb0378971 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 15:37:29 +0900 Subject: [PATCH 04/53] =?UTF-8?q?:rocket:=20chore:=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ragback/global/error/CustomException.kt | 16 +++++++++++++--- .../simplerag/ragback/global/error/ErrorCode.kt | 12 +++++++++++- .../global/error/GlobalExceptionHandler.kt | 10 ++++++++++ 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/simplerag/ragback/global/error/CustomException.kt b/src/main/kotlin/simplerag/ragback/global/error/CustomException.kt index 1fc0b21..9605746 100644 --- a/src/main/kotlin/simplerag/ragback/global/error/CustomException.kt +++ b/src/main/kotlin/simplerag/ragback/global/error/CustomException.kt @@ -1,5 +1,15 @@ package simplerag.ragback.global.error -class CustomException( - val errorCode: ErrorCode -) : RuntimeException(errorCode.message) +open class CustomException( + open val errorCode: ErrorCode, + override val message: String = errorCode.message, +) : RuntimeException(message) + +class S3Exception( + override val errorCode: ErrorCode, +) : CustomException(errorCode) + +class FileException( + override val errorCode: ErrorCode, + override val message: String, +) : CustomException(errorCode, message) \ No newline at end of file diff --git a/src/main/kotlin/simplerag/ragback/global/error/ErrorCode.kt b/src/main/kotlin/simplerag/ragback/global/error/ErrorCode.kt index 7d329f4..4bf7ffc 100644 --- a/src/main/kotlin/simplerag/ragback/global/error/ErrorCode.kt +++ b/src/main/kotlin/simplerag/ragback/global/error/ErrorCode.kt @@ -9,5 +9,15 @@ enum class ErrorCode( ) { INVALID_INPUT(HttpStatus.BAD_REQUEST, "INVALID_INPUT", "잘못된 요청입니다."), NOT_FOUND(HttpStatus.NOT_FOUND, "NOT_FOUND", "리소스를 찾을 수 없습니다."), - INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", "서버 오류가 발생했습니다.") + INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", "서버 오류가 발생했습니다."), + ALREADY_FILE (HttpStatus.BAD_REQUEST, "ALREADY_FILE", "같은 내용의 파일이 이미 존재합니다."), + + // S3 + S3_OBJECT_NOT_FOUND(HttpStatus.NOT_FOUND, "S3_001", "S3 오브젝트를 찾을 수 없습니다."), + S3_UPLOAD_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, "S3_002", "S3 업로드 실패"), + S3_DELETE_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, "S3_003", "S3 삭제 실패"), + S3_INVALID_URL(HttpStatus.BAD_REQUEST, "S3_004", "유효하지 않은 S3 URL 입니다."), + S3_EMPTY_FILE(HttpStatus.BAD_REQUEST, "S3_005", "빈 파일은 업로드할 수 없습니다."), + S3_PRESIGN_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, "S3_006", "프리사인 URL 발급 실패"), + S3_UNSUPPORTED_CONTENT_TYPE(HttpStatus.BAD_REQUEST, "S3_007", "지원하지 않는 Content-Type 입니다.") } diff --git a/src/main/kotlin/simplerag/ragback/global/error/GlobalExceptionHandler.kt b/src/main/kotlin/simplerag/ragback/global/error/GlobalExceptionHandler.kt index 5ef0541..d057a42 100644 --- a/src/main/kotlin/simplerag/ragback/global/error/GlobalExceptionHandler.kt +++ b/src/main/kotlin/simplerag/ragback/global/error/GlobalExceptionHandler.kt @@ -26,6 +26,14 @@ class GlobalExceptionHandler { .body(ApiResponse.fail(ErrorCode.INVALID_INPUT.code, message)) } + @ExceptionHandler(FileException::class) + fun handleCustomException(ex: FileException): ResponseEntity> { + val errorCode = ex.errorCode + return ResponseEntity + .status(errorCode.status) + .body(ApiResponse.fail(errorCode.code, "${errorCode.message} 파일명: ${ex.message}")) + } + @ExceptionHandler(CustomException::class) fun handleCustomException(ex: CustomException): ResponseEntity> { val errorCode = ex.errorCode @@ -36,6 +44,8 @@ class GlobalExceptionHandler { @ExceptionHandler(Exception::class) fun handleGeneralException(ex: Exception): ResponseEntity> { + ex.printStackTrace() + return ResponseEntity .status(ErrorCode.INTERNAL_ERROR.status) .body(ApiResponse.fail(ErrorCode.INTERNAL_ERROR.code, ErrorCode.INTERNAL_ERROR.message)) From 4fb07df5762bf85d734081180f3b4cbbd47141fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 15:37:43 +0900 Subject: [PATCH 05/53] =?UTF-8?q?:rocket:=20chore:=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/simplerag/ragback/domain/document/entity/DataFile.kt | 2 ++ .../simplerag/ragback/domain/document/entity/DataFileTag.kt | 1 + 2 files changed, 3 insertions(+) diff --git a/src/main/kotlin/simplerag/ragback/domain/document/entity/DataFile.kt b/src/main/kotlin/simplerag/ragback/domain/document/entity/DataFile.kt index ff7fa1d..fe48378 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/entity/DataFile.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/entity/DataFile.kt @@ -22,6 +22,8 @@ data class DataFile( @Column(nullable = false, length = 128) val sha256: String, + val fileUrl: String, + val updatedAt: LocalDateTime, val createdAt: LocalDateTime, diff --git a/src/main/kotlin/simplerag/ragback/domain/document/entity/DataFileTag.kt b/src/main/kotlin/simplerag/ragback/domain/document/entity/DataFileTag.kt index 0dcb435..a9771db 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/entity/DataFileTag.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/entity/DataFileTag.kt @@ -5,6 +5,7 @@ import jakarta.persistence.* @Entity @Table( name = "data_file_tags", + uniqueConstraints = [UniqueConstraint(columnNames = ["data_file_id", "tag_id"])] ) class DataFileTag( From 3cc591ca1adc33046ed29e9f5aca8c98295c79be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 15:38:29 +0900 Subject: [PATCH 06/53] =?UTF-8?q?:sparkles:=20feat:=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../document/controller/DataFileController.kt | 27 ++++++ .../domain/document/dto/DataFileRequestDTO.kt | 17 ++++ .../document/dto/DataFileResponseDTO.kt | 10 ++ .../document/repository/DataFileRepository.kt | 8 ++ .../repository/DataFileTagRepository.kt | 8 ++ .../document/repository/TagRepository.kt | 10 ++ .../document/service/DataFileService.kt | 96 +++++++++++++++++++ 7 files changed, 176 insertions(+) create mode 100644 src/main/kotlin/simplerag/ragback/domain/document/controller/DataFileController.kt create mode 100644 src/main/kotlin/simplerag/ragback/domain/document/dto/DataFileRequestDTO.kt create mode 100644 src/main/kotlin/simplerag/ragback/domain/document/dto/DataFileResponseDTO.kt create mode 100644 src/main/kotlin/simplerag/ragback/domain/document/repository/DataFileRepository.kt create mode 100644 src/main/kotlin/simplerag/ragback/domain/document/repository/DataFileTagRepository.kt create mode 100644 src/main/kotlin/simplerag/ragback/domain/document/repository/TagRepository.kt create mode 100644 src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt diff --git a/src/main/kotlin/simplerag/ragback/domain/document/controller/DataFileController.kt b/src/main/kotlin/simplerag/ragback/domain/document/controller/DataFileController.kt new file mode 100644 index 0000000..f26300c --- /dev/null +++ b/src/main/kotlin/simplerag/ragback/domain/document/controller/DataFileController.kt @@ -0,0 +1,27 @@ +package simplerag.ragback.domain.document.controller + +import jakarta.validation.Valid +import org.springframework.http.MediaType +import org.springframework.web.bind.annotation.* +import org.springframework.web.multipart.MultipartFile +import simplerag.ragback.domain.document.dto.DataFileBulkCreateRequest +import simplerag.ragback.domain.document.dto.DataFileResponseList +import simplerag.ragback.domain.document.service.DataFileService +import simplerag.ragback.global.response.ApiResponse + +@RestController +@RequestMapping("/api/v1/data-files") +class DataFileController( + private val service: DataFileService +) { + + @PostMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE]) + fun upload( + @RequestPart("files") files: List, + @Valid @RequestPart("request") req: DataFileBulkCreateRequest + ): ApiResponse { + val saved = service.upload(files, req) + return ApiResponse.ok(saved, "업로드 완료") + } + +} diff --git a/src/main/kotlin/simplerag/ragback/domain/document/dto/DataFileRequestDTO.kt b/src/main/kotlin/simplerag/ragback/domain/document/dto/DataFileRequestDTO.kt new file mode 100644 index 0000000..0212044 --- /dev/null +++ b/src/main/kotlin/simplerag/ragback/domain/document/dto/DataFileRequestDTO.kt @@ -0,0 +1,17 @@ +package simplerag.ragback.domain.document.dto + +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.Size + +data class DataFileBulkCreateRequest( + @field:Size(min = 1, message = "최소 하나 이상 업로드해야 합니다") + val items: List +) + +data class DataFileCreateItem( + @field:NotBlank(message = "title은 비어있을 수 없습니다") + val title: String, + + @field:Size(max = 10, message = "태그는 최대 10개까지 가능합니다") + val tags: List = emptyList() +) diff --git a/src/main/kotlin/simplerag/ragback/domain/document/dto/DataFileResponseDTO.kt b/src/main/kotlin/simplerag/ragback/domain/document/dto/DataFileResponseDTO.kt new file mode 100644 index 0000000..6f3a59f --- /dev/null +++ b/src/main/kotlin/simplerag/ragback/domain/document/dto/DataFileResponseDTO.kt @@ -0,0 +1,10 @@ +package simplerag.ragback.domain.document.dto + +data class DataFileResponseList( + val dataFileResponseList : List, +) + +data class DataFileResponse( + val id: Long, + val sha256: String, +) \ No newline at end of file diff --git a/src/main/kotlin/simplerag/ragback/domain/document/repository/DataFileRepository.kt b/src/main/kotlin/simplerag/ragback/domain/document/repository/DataFileRepository.kt new file mode 100644 index 0000000..11590fc --- /dev/null +++ b/src/main/kotlin/simplerag/ragback/domain/document/repository/DataFileRepository.kt @@ -0,0 +1,8 @@ +package simplerag.ragback.domain.document.repository + +import org.springframework.data.jpa.repository.JpaRepository +import simplerag.ragback.domain.document.entity.DataFile + +interface DataFileRepository : JpaRepository { + fun existsBySha256(sha256: String): Boolean +} \ No newline at end of file diff --git a/src/main/kotlin/simplerag/ragback/domain/document/repository/DataFileTagRepository.kt b/src/main/kotlin/simplerag/ragback/domain/document/repository/DataFileTagRepository.kt new file mode 100644 index 0000000..69509a4 --- /dev/null +++ b/src/main/kotlin/simplerag/ragback/domain/document/repository/DataFileTagRepository.kt @@ -0,0 +1,8 @@ +package simplerag.ragback.domain.document.repository + +import org.springframework.data.jpa.repository.JpaRepository +import simplerag.ragback.domain.document.entity.DataFileTag + +interface DataFileTagRepository : JpaRepository { + fun existsByDataFileIdAndTagId(dataFileId: Long, tagId: Long): Boolean +} \ No newline at end of file diff --git a/src/main/kotlin/simplerag/ragback/domain/document/repository/TagRepository.kt b/src/main/kotlin/simplerag/ragback/domain/document/repository/TagRepository.kt new file mode 100644 index 0000000..fcfcd6b --- /dev/null +++ b/src/main/kotlin/simplerag/ragback/domain/document/repository/TagRepository.kt @@ -0,0 +1,10 @@ +package simplerag.ragback.domain.document.repository + +import org.springframework.data.jpa.repository.JpaRepository +import simplerag.ragback.domain.document.entity.Tag +import java.util.* + +interface TagRepository : JpaRepository { + + fun findByName(name: String): Optional +} diff --git a/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt new file mode 100644 index 0000000..94e393d --- /dev/null +++ b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt @@ -0,0 +1,96 @@ +package simplerag.ragback.domain.document.service + +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.multipart.MultipartFile +import simplerag.ragback.domain.document.dto.DataFileBulkCreateRequest +import simplerag.ragback.domain.document.dto.DataFileResponse +import simplerag.ragback.domain.document.dto.DataFileResponseList +import simplerag.ragback.domain.document.entity.DataFile +import simplerag.ragback.domain.document.entity.DataFileTag +import simplerag.ragback.domain.document.entity.Tag +import simplerag.ragback.domain.document.repository.DataFileRepository +import simplerag.ragback.domain.document.repository.DataFileTagRepository +import simplerag.ragback.domain.document.repository.TagRepository +import simplerag.ragback.global.error.CustomException +import simplerag.ragback.global.error.ErrorCode +import simplerag.ragback.global.error.FileException +import simplerag.ragback.global.util.S3Type +import simplerag.ragback.global.util.S3Util +import java.security.MessageDigest +import java.time.LocalDateTime + +@Service +class DataFileService( + private val dataFileRepository: DataFileRepository, + private val tagRepository: TagRepository, + private val dataFileTagRepository: DataFileTagRepository, + private val s3Util: S3Util, +) { + + @Transactional + fun upload( + files: List, + req: DataFileBulkCreateRequest + ): DataFileResponseList { + if (files.isEmpty() || files.size != req.items.size) { + throw CustomException(ErrorCode.INVALID_INPUT) + } + + val now = LocalDateTime.now() + + val responses = files.mapIndexed { idx, file -> + val meta = req.items[idx] + val bytes = file.bytes + val sha256 = sha256Hex(bytes) + + val sizeMb = kotlin.math.round((bytes.size / (1024.0 * 1024.0)) * 1000) / 1000.0 + val type = file.contentType + ?: file.originalFilename?.substringAfterLast('.', "application/octet-stream") + ?: "application/octet-stream" + + if (dataFileRepository.existsBySha256(sha256)) { + throw FileException(ErrorCode.ALREADY_FILE, meta.title) + } + + val fileUrl = s3Util.upload(file, S3Type.ORIGINAL_FILE) + + val dataFile = dataFileRepository.save( + DataFile(meta.title, type, sizeMb, sha256, fileUrl, now, now) + ) + + val tags = getOrCreateTags(meta.tags) + attachTagsIfMissing(dataFile, tags) + + DataFileResponse(requireNotNull(dataFile.id), dataFile.sha256) + } + + return DataFileResponseList(responses) + } + + private fun getOrCreateTags(names: List): List = + names.mapNotNull { it.trim() } + .filter { it.isNotEmpty() } + .distinct() + .map { name -> + tagRepository.findByName(name).orElseGet { tagRepository.save(Tag(name = name)) } + } + + private fun attachTagsIfMissing(dataFile: DataFile, tags: List) { + val fileId = dataFile.id ?: return + tags.forEach { tag -> + val tagId = tag.id ?: return@forEach + val exists = dataFileTagRepository.existsByDataFileIdAndTagId(fileId, tagId) + if (!exists) { + dataFileTagRepository.save(DataFileTag(tag = tag, dataFile = dataFile)) + } + } + } + + private fun sha256Hex(bytes: ByteArray): String { + val md = MessageDigest.getInstance("SHA-256") + val digest = md.digest(bytes) + return digest.joinToString("") { "%02x".format(it) } + } + +} From e2c24057e59349774686e3535533ab2f5f147d7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 16:24:45 +0900 Subject: [PATCH 07/53] =?UTF-8?q?:test=5Ftube:=20test:=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=97=85=EB=A1=9C=EB=93=9C=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 + .../ragback/global/config/S3Config.kt | 2 + .../ragback/global/storage/FakeS3Util.kt | 43 +++++ .../simplerag/ragback/global/util/S3Util.kt | 99 +---------- .../ragback/global/util/S3UtilImpl.kt | 93 ++++++++++ .../document/service/DataFileServiceTest.kt | 162 ++++++++++++++++++ src/test/resources/application-test.yml | 10 ++ 7 files changed, 319 insertions(+), 92 deletions(-) create mode 100644 src/main/kotlin/simplerag/ragback/global/storage/FakeS3Util.kt create mode 100644 src/main/kotlin/simplerag/ragback/global/util/S3UtilImpl.kt create mode 100644 src/test/kotlin/simplerag/ragback/domain/document/service/DataFileServiceTest.kt create mode 100644 src/test/resources/application-test.yml diff --git a/build.gradle b/build.gradle index 0b19d1e..64f30b3 100644 --- a/build.gradle +++ b/build.gradle @@ -41,8 +41,10 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5' + testImplementation("org.mockito.kotlin:mockito-kotlin:5.3.1") testRuntimeOnly 'org.junit.platform:junit-platform-launcher' runtimeOnly 'org.postgresql:postgresql' + testRuntimeOnly("com.h2database:h2") // swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0' diff --git a/src/main/kotlin/simplerag/ragback/global/config/S3Config.kt b/src/main/kotlin/simplerag/ragback/global/config/S3Config.kt index ea218d3..c71b40c 100644 --- a/src/main/kotlin/simplerag/ragback/global/config/S3Config.kt +++ b/src/main/kotlin/simplerag/ragback/global/config/S3Config.kt @@ -3,6 +3,7 @@ package simplerag.ragback.global.config import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Profile import software.amazon.awssdk.auth.credentials.AwsBasicCredentials import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider @@ -13,6 +14,7 @@ import software.amazon.awssdk.services.s3.S3Configuration import software.amazon.awssdk.services.s3.presigner.S3Presigner @Configuration +@Profile("!test") class S3Config( @Value("\${cloud.aws.region.static}") diff --git a/src/main/kotlin/simplerag/ragback/global/storage/FakeS3Util.kt b/src/main/kotlin/simplerag/ragback/global/storage/FakeS3Util.kt new file mode 100644 index 0000000..760bdb0 --- /dev/null +++ b/src/main/kotlin/simplerag/ragback/global/storage/FakeS3Util.kt @@ -0,0 +1,43 @@ +package simplerag.ragback.global.storage + +import org.springframework.context.annotation.Primary +import org.springframework.context.annotation.Profile +import org.springframework.stereotype.Component +import org.springframework.web.multipart.MultipartFile +import simplerag.ragback.global.util.S3Type +import simplerag.ragback.global.util.S3Util +import java.security.MessageDigest +import java.util.concurrent.ConcurrentHashMap + +@Component +@Primary +@Profile("test") +class FakeS3Util : S3Util { + + private val store = ConcurrentHashMap() + + override fun upload(file: MultipartFile, dir: S3Type): String { + val clean = (file.originalFilename ?: "file") + .substringAfterLast('/').substringAfterLast('\\').ifBlank { "file" } + + val hash = sha256(file.bytes).take(12) + val key = "${dir.label}/${hash}_$clean" + + store[key] = file.bytes + return urlFromKey(key) + } + + override fun urlFromKey(key: String): String = "fake://$key" + + override fun deleteByUrl(url: String) { keyFromUrl(url)?.let { store.remove(it) } } + + override fun delete(key: String) { store.remove(key) } + + override fun keyFromUrl(url: String): String? = + url.removePrefix("fake://").ifBlank { null } + + private fun sha256(bytes: ByteArray): String { + val md = MessageDigest.getInstance("SHA-256") + return md.digest(bytes).joinToString("") { "%02x".format(it) } + } +} diff --git a/src/main/kotlin/simplerag/ragback/global/util/S3Util.kt b/src/main/kotlin/simplerag/ragback/global/util/S3Util.kt index d1ac37a..e1aba27 100644 --- a/src/main/kotlin/simplerag/ragback/global/util/S3Util.kt +++ b/src/main/kotlin/simplerag/ragback/global/util/S3Util.kt @@ -1,96 +1,11 @@ package simplerag.ragback.global.util -import org.springframework.beans.factory.annotation.Value -import org.springframework.stereotype.Component import org.springframework.web.multipart.MultipartFile -import simplerag.ragback.global.config.S3Config -import simplerag.ragback.global.error.ErrorCode -import simplerag.ragback.global.error.S3Exception -import software.amazon.awssdk.core.sync.RequestBody -import software.amazon.awssdk.services.s3.S3Client -import software.amazon.awssdk.services.s3.model.DeleteObjectRequest -import software.amazon.awssdk.services.s3.model.ObjectCannedACL -import software.amazon.awssdk.services.s3.model.PutObjectRequest -import software.amazon.awssdk.services.s3.presigner.S3Presigner -import java.net.URI -import java.util.UUID -@Component -class S3Util( - private val s3: S3Client, - private val presigner: S3Presigner, - private val s3Config: S3Config, - @Value("\${cloud.aws.region.static}") private val region: String -) { - private val bucket get() = s3Config.bucket - - fun upload(file: MultipartFile, dir: S3Type): String { - if (file.isEmpty) throw S3Exception(ErrorCode.S3_EMPTY_FILE) - - val key = buildKey(dir.label, file.originalFilename) - - try { - file.inputStream.use { input -> - val putReq = PutObjectRequest.builder() - .bucket(bucket) - .key(key) - .contentType(file.contentType ?: "application/octet-stream") - .build() - - val body = RequestBody.fromInputStream(input, file.size) - s3.putObject(putReq, body) - } - - return urlFromKey(key) - } catch (e: software.amazon.awssdk.services.s3.model.S3Exception) { - throw S3Exception(ErrorCode.S3_UPLOAD_FAIL) - } catch (e: Exception) { - throw S3Exception(ErrorCode.S3_UPLOAD_FAIL) - } - } - - fun urlFromKey(key: String): String = - s3.utilities() - .getUrl { it.bucket(bucket).key(key) } - .toExternalForm() - - fun deleteByUrl(url: String) { - val key = keyFromUrl(url) ?: throw S3Exception(ErrorCode.S3_INVALID_URL) - delete(key) - } - - fun delete(key: String) { - try { - val req = DeleteObjectRequest.builder() - .bucket(bucket) - .key(key) - .build() - s3.deleteObject(req) - } catch (e: software.amazon.awssdk.services.s3.model.S3Exception) { - // NoSuchKey 등 - throw S3Exception(ErrorCode.S3_OBJECT_NOT_FOUND) - } catch (e: Exception) { - throw S3Exception(ErrorCode.S3_DELETE_FAIL) - } - } - - private fun buildKey(dir: String, originalFilename: String?): String { - val cleanName = (originalFilename ?: "file") - .substringAfterLast('/') - .substringAfterLast('\\') - .ifBlank { "file" } - - val prefix = dir.trim('/') - - return "$prefix/${UUID.randomUUID()}_$cleanName" - } - - fun keyFromUrl(url: String): String? { - val path = try { - URI(url).path // e.g. "/market/menu/uuid_name.jpg" - } catch (_: Exception) { - return null - } - return path.removePrefix("/").ifBlank { null } - } -} +interface S3Util { + fun upload(file: MultipartFile, dir: S3Type): String + fun urlFromKey(key: String): String + fun deleteByUrl(url: String) + fun delete(key: String) + fun keyFromUrl(url: String): String? +} \ No newline at end of file diff --git a/src/main/kotlin/simplerag/ragback/global/util/S3UtilImpl.kt b/src/main/kotlin/simplerag/ragback/global/util/S3UtilImpl.kt new file mode 100644 index 0000000..3b64e7b --- /dev/null +++ b/src/main/kotlin/simplerag/ragback/global/util/S3UtilImpl.kt @@ -0,0 +1,93 @@ +package simplerag.ragback.global.util + +import org.springframework.context.annotation.Profile +import org.springframework.stereotype.Component +import org.springframework.web.multipart.MultipartFile +import simplerag.ragback.global.config.S3Config +import simplerag.ragback.global.error.ErrorCode +import simplerag.ragback.global.error.S3Exception +import software.amazon.awssdk.core.sync.RequestBody +import software.amazon.awssdk.services.s3.S3Client +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest +import software.amazon.awssdk.services.s3.model.PutObjectRequest +import java.net.URI +import java.util.UUID + +@Component +@Profile("!test") +class S3UtilImpl( + private val s3: S3Client, + private val s3Config: S3Config, +): S3Util { + private val bucket get() = s3Config.bucket + + override fun upload(file: MultipartFile, dir: S3Type): String { + if (file.isEmpty) throw S3Exception(ErrorCode.S3_EMPTY_FILE) + + val key = buildKey(dir.label, file.originalFilename) + + try { + file.inputStream.use { input -> + val putReq = PutObjectRequest.builder() + .bucket(bucket) + .key(key) + .contentType(file.contentType ?: "application/octet-stream") + .build() + + val body = RequestBody.fromInputStream(input, file.size) + s3.putObject(putReq, body) + } + + return urlFromKey(key) + } catch (e: software.amazon.awssdk.services.s3.model.S3Exception) { + throw S3Exception(ErrorCode.S3_UPLOAD_FAIL) + } catch (e: Exception) { + throw S3Exception(ErrorCode.S3_UPLOAD_FAIL) + } + } + + override fun urlFromKey(key: String): String = + s3.utilities() + .getUrl { it.bucket(bucket).key(key) } + .toExternalForm() + + override fun deleteByUrl(url: String) { + val key = keyFromUrl(url) ?: throw S3Exception(ErrorCode.S3_INVALID_URL) + delete(key) + } + + override fun delete(key: String) { + try { + val req = DeleteObjectRequest.builder() + .bucket(bucket) + .key(key) + .build() + s3.deleteObject(req) + } catch (e: software.amazon.awssdk.services.s3.model.S3Exception) { + // NoSuchKey 등 + throw S3Exception(ErrorCode.S3_OBJECT_NOT_FOUND) + } catch (e: Exception) { + throw S3Exception(ErrorCode.S3_DELETE_FAIL) + } + } + + private fun buildKey(dir: String, originalFilename: String?): String { + val cleanName = (originalFilename ?: "file") + .substringAfterLast('/') + .substringAfterLast('\\') + .ifBlank { "file" } + + val prefix = dir.trim('/') + + return "$prefix/${UUID.randomUUID()}_$cleanName" + } + + override fun keyFromUrl(url: String): String? { + val path = try { + URI(url).path // e.g. "/market/menu/uuid_name.jpg" + } catch (_: Exception) { + return null + } + return path.removePrefix("/").ifBlank { null } + } +} diff --git a/src/test/kotlin/simplerag/ragback/domain/document/service/DataFileServiceTest.kt b/src/test/kotlin/simplerag/ragback/domain/document/service/DataFileServiceTest.kt new file mode 100644 index 0000000..7a310c9 --- /dev/null +++ b/src/test/kotlin/simplerag/ragback/domain/document/service/DataFileServiceTest.kt @@ -0,0 +1,162 @@ +package simplerag.ragback.domain.document.service + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.mock.web.MockMultipartFile +import org.springframework.test.context.ActiveProfiles +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.multipart.MultipartFile +import simplerag.ragback.domain.document.dto.DataFileBulkCreateRequest +import simplerag.ragback.domain.document.dto.DataFileCreateItem +import simplerag.ragback.domain.document.entity.DataFile +import simplerag.ragback.domain.document.repository.DataFileRepository +import simplerag.ragback.domain.document.repository.DataFileTagRepository +import simplerag.ragback.domain.document.repository.TagRepository +import simplerag.ragback.global.error.CustomException +import simplerag.ragback.global.error.ErrorCode +import simplerag.ragback.global.error.FileException +import java.security.MessageDigest +import java.time.LocalDateTime + +@SpringBootTest +@ActiveProfiles("test") +class DataFileServiceTest( + @Autowired private val service: DataFileService, + @Autowired private val dataFileRepository: DataFileRepository, + @Autowired private val tagRepository: TagRepository, + @Autowired private val dataFileTagRepository: DataFileTagRepository, +) { + + @Test + @Transactional + @DisplayName("업로드 시 잘 저장이 된다.") + fun uploadOk() { + // given + val bytes = "hello world".toByteArray() + val req = DataFileBulkCreateRequest( + listOf(DataFileCreateItem(title = "greeting", tags = listOf(" ai ", "rag", "ai"))) + ) + val f = file("greet.txt", bytes, contentType = "text/plain") + + // when + val res = service.upload(listOf(f), req) + + // then + assertEquals(1, res.dataFileResponseList.size) + val r0 = res.dataFileResponseList.first() + assertTrue(r0.id > 0) + assertEquals(sha256Hex(bytes), r0.sha256) + + val saved = dataFileRepository.findById(r0.id).orElseThrow() + assertEquals("greeting", saved.title) + assertEquals("text/plain", saved.type) + assertEquals(sha256Hex(bytes), saved.sha256) + assertFalse(saved.fileUrl.isNullOrBlank()) + + val ai = tagRepository.findByName("ai").orElse(null) + val rag = tagRepository.findByName("rag").orElse(null) + assertNotNull(ai); assertNotNull(rag) + assertTrue(dataFileTagRepository.existsByDataFileIdAndTagId(saved.id!!, ai!!.id!!)) + assertTrue(dataFileTagRepository.existsByDataFileIdAndTagId(saved.id!!, rag!!.id!!)) + } + + @Test + @DisplayName("files와 items 개수 불일치 시 예외를 던진다.") + fun invalidInputCountMismatch() { + // given + val req = DataFileBulkCreateRequest(items = emptyList()) + val f = file("a.txt", "a".toByteArray()) + + // when + val ex = assertThrows(CustomException::class.java) { service.upload(listOf(f), req) } + + // then + assertEquals(ErrorCode.INVALID_INPUT, ex.errorCode) + } + + @Test + @Transactional + @DisplayName("동일 sha256이 이미 있으면 업로드 거부한다.") + fun rejectDuplicateSha() { + // given + val bytes = "same".toByteArray() + val sha = sha256Hex(bytes) + val now = LocalDateTime.now() + dataFileRepository.save( + DataFile( + title = "exists", + type = "text/plain", + sizeMb = 0.0, + sha256 = sha, + fileUrl = "fake://original/exists.txt", + updatedAt = now, + createdAt = now + ) + ) + val req = DataFileBulkCreateRequest(listOf(DataFileCreateItem("dup", listOf("tag")))) + val f = file("dup.txt", bytes) + + // when + val ex = assertThrows(FileException::class.java) { service.upload(listOf(f), req) } + + // then + assertEquals(ErrorCode.ALREADY_FILE, ex.errorCode) + } + + @Test + @Transactional + @DisplayName("컨텐츠 타입 null이거나 확장자 없을 시 application/octet-stream 저장이 된다") + fun unknownTypeOctetStream() { + // given + val bytes = "x".toByteArray() + val req = DataFileBulkCreateRequest(listOf(DataFileCreateItem("noext", emptyList()))) + val f = file(name = "noext", content = bytes, contentType = null) // no extension + + // when + val res = service.upload(listOf(f), req) + + // then + val saved = dataFileRepository.findById(res.dataFileResponseList.first().id).orElseThrow() + assertEquals("application/octet-stream", saved.type) + } + + @Test + @Transactional + @DisplayName("같은 파일 두 번 업로드 시 에러가 난다.") + fun secondUploadAlreadyFile() { + // given + val bytes = "dupcontent".toByteArray() + val req = DataFileBulkCreateRequest(listOf(DataFileCreateItem("t", emptyList()))) + val f1 = file("x.txt", bytes) + val f2 = file("y.txt", bytes) + + // when + service.upload(listOf(f1), req) + val ex = assertThrows(FileException::class.java) { service.upload(listOf(f2), req) } + + // then + assertEquals(ErrorCode.ALREADY_FILE, ex.errorCode) + } + + + // ----------------------- + // helpers + // ----------------------- + + private fun file( + name: String, + content: ByteArray, + contentType: String? = "text/plain", + paramName: String = "files" + ): MultipartFile = + MockMultipartFile(paramName, name, contentType, content) + + private fun sha256Hex(bytes: ByteArray): String { + val md = MessageDigest.getInstance("SHA-256") + val digest = md.digest(bytes) + return digest.joinToString("") { "%02x".format(it) } + } +} diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml new file mode 100644 index 0000000..5885995 --- /dev/null +++ b/src/test/resources/application-test.yml @@ -0,0 +1,10 @@ +spring: + datasource: + url: jdbc:h2:mem:simplerag;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE + driver-class-name: org.h2.Driver + username: sa + password: + jpa: + database-platform: org.hibernate.dialect.H2Dialect + hibernate: + ddl-auto: create-drop From c58b67748efb7c1a69d7c8deaa7f39bbef5334b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 16:27:28 +0900 Subject: [PATCH 08/53] =?UTF-8?q?:recycle:=20refactor:=20DataFileService?= =?UTF-8?q?=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../document/service/DataFileService.kt | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt index 94e393d..b247cd4 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt @@ -19,6 +19,7 @@ import simplerag.ragback.global.util.S3Type import simplerag.ragback.global.util.S3Util import java.security.MessageDigest import java.time.LocalDateTime +import kotlin.math.round @Service class DataFileService( @@ -44,10 +45,8 @@ class DataFileService( val bytes = file.bytes val sha256 = sha256Hex(bytes) - val sizeMb = kotlin.math.round((bytes.size / (1024.0 * 1024.0)) * 1000) / 1000.0 - val type = file.contentType - ?: file.originalFilename?.substringAfterLast('.', "application/octet-stream") - ?: "application/octet-stream" + val sizeMb = byteToMegaByte(bytes) + val type = file.resolveContentType() if (dataFileRepository.existsBySha256(sha256)) { throw FileException(ErrorCode.ALREADY_FILE, meta.title) @@ -55,9 +54,7 @@ class DataFileService( val fileUrl = s3Util.upload(file, S3Type.ORIGINAL_FILE) - val dataFile = dataFileRepository.save( - DataFile(meta.title, type, sizeMb, sha256, fileUrl, now, now) - ) + val dataFile = dataFileRepository.save(DataFile(meta.title, type, sizeMb, sha256, fileUrl, now, now)) val tags = getOrCreateTags(meta.tags) attachTagsIfMissing(dataFile, tags) @@ -68,8 +65,10 @@ class DataFileService( return DataFileResponseList(responses) } + private fun byteToMegaByte(bytes: ByteArray) = round((bytes.size / (1024.0 * 1024.0)) * 1000) / 1000.0 + private fun getOrCreateTags(names: List): List = - names.mapNotNull { it.trim() } + names.map { it.trim() } .filter { it.isNotEmpty() } .distinct() .map { name -> @@ -87,6 +86,12 @@ class DataFileService( } } + fun MultipartFile.resolveContentType(): String { + return this.contentType + ?: this.originalFilename?.substringAfterLast('.', "application/octet-stream") + ?: "application/octet-stream" + } + private fun sha256Hex(bytes: ByteArray): String { val md = MessageDigest.getInstance("SHA-256") val digest = md.digest(bytes) From 9f09266c0d58d45eb91aa90a6903abe482a8e521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 16:33:59 +0900 Subject: [PATCH 09/53] =?UTF-8?q?:recycle:=20refactor:=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EA=B4=80=EB=A0=A8=20=EC=9C=A0=ED=8B=B8=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../document/service/DataFileService.kt | 18 +----------- .../ragback/global/util/FileConvertUtil.kt | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+), 17 deletions(-) create mode 100644 src/main/kotlin/simplerag/ragback/global/util/FileConvertUtil.kt diff --git a/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt index b247cd4..ddb970c 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt @@ -15,11 +15,8 @@ import simplerag.ragback.domain.document.repository.TagRepository import simplerag.ragback.global.error.CustomException import simplerag.ragback.global.error.ErrorCode import simplerag.ragback.global.error.FileException -import simplerag.ragback.global.util.S3Type -import simplerag.ragback.global.util.S3Util -import java.security.MessageDigest +import simplerag.ragback.global.util.* import java.time.LocalDateTime -import kotlin.math.round @Service class DataFileService( @@ -65,7 +62,6 @@ class DataFileService( return DataFileResponseList(responses) } - private fun byteToMegaByte(bytes: ByteArray) = round((bytes.size / (1024.0 * 1024.0)) * 1000) / 1000.0 private fun getOrCreateTags(names: List): List = names.map { it.trim() } @@ -86,16 +82,4 @@ class DataFileService( } } - fun MultipartFile.resolveContentType(): String { - return this.contentType - ?: this.originalFilename?.substringAfterLast('.', "application/octet-stream") - ?: "application/octet-stream" - } - - private fun sha256Hex(bytes: ByteArray): String { - val md = MessageDigest.getInstance("SHA-256") - val digest = md.digest(bytes) - return digest.joinToString("") { "%02x".format(it) } - } - } diff --git a/src/main/kotlin/simplerag/ragback/global/util/FileConvertUtil.kt b/src/main/kotlin/simplerag/ragback/global/util/FileConvertUtil.kt new file mode 100644 index 0000000..217ec71 --- /dev/null +++ b/src/main/kotlin/simplerag/ragback/global/util/FileConvertUtil.kt @@ -0,0 +1,29 @@ +package simplerag.ragback.global.util + +import org.springframework.web.multipart.MultipartFile +import java.math.BigDecimal +import java.math.RoundingMode +import java.security.MessageDigest +import kotlin.math.round + +fun byteToMegaByte(bytes: ByteArray): Double = + BigDecimal(bytes.size.toDouble() / (1024.0 * 1024.0)) + .setScale(3, RoundingMode.HALF_UP) + .toDouble() + +fun sha256Hex(bytes: ByteArray): String = + MessageDigest.getInstance("SHA-256") + .digest(bytes) + .joinToString("") { "%02x".format(it) } + +fun MultipartFile.resolveContentType(): String { + this.contentType?.let { return it } + val ext = this.originalFilename?.substringAfterLast('.', "")?.lowercase() + return when (ext) { + "png" -> "image/png" + "jpg", "jpeg" -> "image/jpeg" + "pdf" -> "application/pdf" + "txt" -> "text/plain" + else -> "application/octet-stream" + } +} \ No newline at end of file From fab22d9520df88f6fb96c6ca9c90e06b026c2e3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 16:35:14 +0900 Subject: [PATCH 10/53] =?UTF-8?q?:recycle:=20refactor:=20DTO=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EB=B3=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ragback/domain/document/dto/DataFileResponseDTO.kt | 4 ++-- .../ragback/domain/document/service/DataFileService.kt | 4 ++-- .../ragback/domain/document/service/DataFileServiceTest.kt | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/simplerag/ragback/domain/document/dto/DataFileResponseDTO.kt b/src/main/kotlin/simplerag/ragback/domain/document/dto/DataFileResponseDTO.kt index 6f3a59f..fd689e7 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/dto/DataFileResponseDTO.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/dto/DataFileResponseDTO.kt @@ -1,10 +1,10 @@ package simplerag.ragback.domain.document.dto data class DataFileResponseList( - val dataFileResponseList : List, + val dataFilePreviewResponseList : List, ) -data class DataFileResponse( +data class DataFilePreviewResponse( val id: Long, val sha256: String, ) \ No newline at end of file diff --git a/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt index ddb970c..a799ae5 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt @@ -4,7 +4,7 @@ import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import org.springframework.web.multipart.MultipartFile import simplerag.ragback.domain.document.dto.DataFileBulkCreateRequest -import simplerag.ragback.domain.document.dto.DataFileResponse +import simplerag.ragback.domain.document.dto.DataFilePreviewResponse import simplerag.ragback.domain.document.dto.DataFileResponseList import simplerag.ragback.domain.document.entity.DataFile import simplerag.ragback.domain.document.entity.DataFileTag @@ -56,7 +56,7 @@ class DataFileService( val tags = getOrCreateTags(meta.tags) attachTagsIfMissing(dataFile, tags) - DataFileResponse(requireNotNull(dataFile.id), dataFile.sha256) + DataFilePreviewResponse(requireNotNull(dataFile.id), dataFile.sha256) } return DataFileResponseList(responses) diff --git a/src/test/kotlin/simplerag/ragback/domain/document/service/DataFileServiceTest.kt b/src/test/kotlin/simplerag/ragback/domain/document/service/DataFileServiceTest.kt index 7a310c9..d581cf6 100644 --- a/src/test/kotlin/simplerag/ragback/domain/document/service/DataFileServiceTest.kt +++ b/src/test/kotlin/simplerag/ragback/domain/document/service/DataFileServiceTest.kt @@ -45,8 +45,8 @@ class DataFileServiceTest( val res = service.upload(listOf(f), req) // then - assertEquals(1, res.dataFileResponseList.size) - val r0 = res.dataFileResponseList.first() + assertEquals(1, res.dataFilePreviewResponseList.size) + val r0 = res.dataFilePreviewResponseList.first() assertTrue(r0.id > 0) assertEquals(sha256Hex(bytes), r0.sha256) @@ -119,7 +119,7 @@ class DataFileServiceTest( val res = service.upload(listOf(f), req) // then - val saved = dataFileRepository.findById(res.dataFileResponseList.first().id).orElseThrow() + val saved = dataFileRepository.findById(res.dataFilePreviewResponseList.first().id).orElseThrow() assertEquals("application/octet-stream", saved.type) } From 40fc893f42ff691174e3088619c264ef2237e1a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 18:27:33 +0900 Subject: [PATCH 11/53] =?UTF-8?q?:rocket:=20chore:=20AWS=20build=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 64f30b3..3aad617 100644 --- a/build.gradle +++ b/build.gradle @@ -50,7 +50,8 @@ dependencies { implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0' // s3 - implementation("software.amazon.awssdk:s3:2.25.70") + implementation(platform("software.amazon.awssdk:bom:2.25.70")) + implementation("software.amazon.awssdk:s3") } dependencyManagement { From 63a10637e11a9bd65d04818683b850a56afa87e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 18:33:09 +0900 Subject: [PATCH 12/53] =?UTF-8?q?:recycle:=20refactor:=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EA=B0=92=20=EB=AA=85=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/document/controller/DataFileController.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/simplerag/ragback/domain/document/controller/DataFileController.kt b/src/main/kotlin/simplerag/ragback/domain/document/controller/DataFileController.kt index f26300c..07d73ab 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/controller/DataFileController.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/controller/DataFileController.kt @@ -1,7 +1,9 @@ package simplerag.ragback.domain.document.controller import jakarta.validation.Valid +import org.springframework.http.HttpStatus import org.springframework.http.MediaType +import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.* import org.springframework.web.multipart.MultipartFile import simplerag.ragback.domain.document.dto.DataFileBulkCreateRequest @@ -11,11 +13,16 @@ import simplerag.ragback.global.response.ApiResponse @RestController @RequestMapping("/api/v1/data-files") +@Validated class DataFileController( private val service: DataFileService ) { - @PostMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE]) + @PostMapping(consumes = [ + MediaType.MULTIPART_FORM_DATA_VALUE, + MediaType.APPLICATION_JSON_VALUE + ]) + @ResponseStatus(HttpStatus.CREATED) fun upload( @RequestPart("files") files: List, @Valid @RequestPart("request") req: DataFileBulkCreateRequest From 28f0acde101dc4a4be8d0dbdcd8e1fecdf4dadf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 18:35:04 +0900 Subject: [PATCH 13/53] =?UTF-8?q?:bug:=20fix:=20Valid=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../simplerag/ragback/domain/document/dto/DataFileRequestDTO.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/kotlin/simplerag/ragback/domain/document/dto/DataFileRequestDTO.kt b/src/main/kotlin/simplerag/ragback/domain/document/dto/DataFileRequestDTO.kt index 0212044..5062667 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/dto/DataFileRequestDTO.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/dto/DataFileRequestDTO.kt @@ -1,10 +1,12 @@ package simplerag.ragback.domain.document.dto +import jakarta.validation.Valid import jakarta.validation.constraints.NotBlank import jakarta.validation.constraints.Size data class DataFileBulkCreateRequest( @field:Size(min = 1, message = "최소 하나 이상 업로드해야 합니다") + @Valid val items: List ) From 3c348834986e66cea00b363d45a8ec82160e2e0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 18:36:11 +0900 Subject: [PATCH 14/53] =?UTF-8?q?:recycle:=20refactor:=20title=20=EA=B8=B8?= =?UTF-8?q?=EC=9D=B4=20=EC=A0=9C=ED=95=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../simplerag/ragback/domain/document/dto/DataFileRequestDTO.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/simplerag/ragback/domain/document/dto/DataFileRequestDTO.kt b/src/main/kotlin/simplerag/ragback/domain/document/dto/DataFileRequestDTO.kt index 5062667..985c912 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/dto/DataFileRequestDTO.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/dto/DataFileRequestDTO.kt @@ -12,6 +12,7 @@ data class DataFileBulkCreateRequest( data class DataFileCreateItem( @field:NotBlank(message = "title은 비어있을 수 없습니다") + @field:Size(max = 100) val title: String, @field:Size(max = 10, message = "태그는 최대 10개까지 가능합니다") From d74cbf91b7a34d1c720ff30950843e6ed92ca9d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 18:37:21 +0900 Subject: [PATCH 15/53] =?UTF-8?q?:bug:=20fix:=20data=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/simplerag/ragback/domain/document/entity/DataFile.kt | 2 +- src/main/kotlin/simplerag/ragback/domain/document/entity/Tag.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/simplerag/ragback/domain/document/entity/DataFile.kt b/src/main/kotlin/simplerag/ragback/domain/document/entity/DataFile.kt index fe48378..7cd05e2 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/entity/DataFile.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/entity/DataFile.kt @@ -8,7 +8,7 @@ import java.time.LocalDateTime name = "data_file", uniqueConstraints = [UniqueConstraint(columnNames = ["sha256"])] ) -data class DataFile( +class DataFile( @Column(nullable = false, length = 255) val title: String, diff --git a/src/main/kotlin/simplerag/ragback/domain/document/entity/Tag.kt b/src/main/kotlin/simplerag/ragback/domain/document/entity/Tag.kt index d817108..7669439 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/entity/Tag.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/entity/Tag.kt @@ -7,7 +7,7 @@ import jakarta.persistence.* name = "tags", uniqueConstraints = [UniqueConstraint(columnNames = ["name"])] ) -data class Tag( +class Tag( @Column(nullable = false, length = 60) val name: String, From 39d3b6f3e6c8c99a435890daf17d712bb928bf20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 18:44:30 +0900 Subject: [PATCH 16/53] =?UTF-8?q?:bug:=20fix:=20MB=20=EA=B8=B0=EC=A4=80?= =?UTF-8?q?=EC=9D=84=20byte=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20(Double=20->?= =?UTF-8?q?=20Long)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ragback/domain/document/entity/DataFile.kt | 2 +- .../ragback/domain/document/service/DataFileService.kt | 4 ++-- .../simplerag/ragback/global/util/FileConvertUtil.kt | 10 +++------- .../domain/document/service/DataFileServiceTest.kt | 2 +- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/simplerag/ragback/domain/document/entity/DataFile.kt b/src/main/kotlin/simplerag/ragback/domain/document/entity/DataFile.kt index 7cd05e2..109f8e2 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/entity/DataFile.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/entity/DataFile.kt @@ -17,7 +17,7 @@ class DataFile( val type: String, @Column(name = "size_mb", nullable = false) - val sizeMb: Double, + val sizeBytes: Long, @Column(nullable = false, length = 128) val sha256: String, diff --git a/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt index a799ae5..19c5c1d 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt @@ -42,7 +42,7 @@ class DataFileService( val bytes = file.bytes val sha256 = sha256Hex(bytes) - val sizeMb = byteToMegaByte(bytes) + val sizeByte = byteToLong(bytes) val type = file.resolveContentType() if (dataFileRepository.existsBySha256(sha256)) { @@ -51,7 +51,7 @@ class DataFileService( val fileUrl = s3Util.upload(file, S3Type.ORIGINAL_FILE) - val dataFile = dataFileRepository.save(DataFile(meta.title, type, sizeMb, sha256, fileUrl, now, now)) + val dataFile = dataFileRepository.save(DataFile(meta.title, type, sizeByte, sha256, fileUrl, now, now)) val tags = getOrCreateTags(meta.tags) attachTagsIfMissing(dataFile, tags) diff --git a/src/main/kotlin/simplerag/ragback/global/util/FileConvertUtil.kt b/src/main/kotlin/simplerag/ragback/global/util/FileConvertUtil.kt index 217ec71..6e3cb7c 100644 --- a/src/main/kotlin/simplerag/ragback/global/util/FileConvertUtil.kt +++ b/src/main/kotlin/simplerag/ragback/global/util/FileConvertUtil.kt @@ -1,15 +1,11 @@ package simplerag.ragback.global.util import org.springframework.web.multipart.MultipartFile -import java.math.BigDecimal -import java.math.RoundingMode import java.security.MessageDigest -import kotlin.math.round +import kotlin.math.roundToLong -fun byteToMegaByte(bytes: ByteArray): Double = - BigDecimal(bytes.size.toDouble() / (1024.0 * 1024.0)) - .setScale(3, RoundingMode.HALF_UP) - .toDouble() +fun byteToLong(bytes: ByteArray): Long = + bytes.size.toLong() fun sha256Hex(bytes: ByteArray): String = MessageDigest.getInstance("SHA-256") diff --git a/src/test/kotlin/simplerag/ragback/domain/document/service/DataFileServiceTest.kt b/src/test/kotlin/simplerag/ragback/domain/document/service/DataFileServiceTest.kt index d581cf6..c87ca77 100644 --- a/src/test/kotlin/simplerag/ragback/domain/document/service/DataFileServiceTest.kt +++ b/src/test/kotlin/simplerag/ragback/domain/document/service/DataFileServiceTest.kt @@ -89,7 +89,7 @@ class DataFileServiceTest( DataFile( title = "exists", type = "text/plain", - sizeMb = 0.0, + sizeBytes = 0, sha256 = sha, fileUrl = "fake://original/exists.txt", updatedAt = now, From 51f580a30fd480d46737ceee33b68458176fd152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 18:49:10 +0900 Subject: [PATCH 17/53] =?UTF-8?q?:recycle:=20refactor:=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20=EA=B2=80=EC=A6=9D=20=EA=B0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../simplerag/ragback/domain/document/entity/DataFile.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/simplerag/ragback/domain/document/entity/DataFile.kt b/src/main/kotlin/simplerag/ragback/domain/document/entity/DataFile.kt index 109f8e2..1687f17 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/entity/DataFile.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/entity/DataFile.kt @@ -13,19 +13,22 @@ class DataFile( @Column(nullable = false, length = 255) val title: String, - @Column(nullable = false, length = 120) + @Column(name = "file_type", nullable = false, length = 120) val type: String, @Column(name = "size_mb", nullable = false) val sizeBytes: Long, - @Column(nullable = false, length = 128) + @Column(nullable = false, length = 64) val sha256: String, + @Column(nullable = false, length = 2048) val fileUrl: String, + @Column(nullable = false) val updatedAt: LocalDateTime, + @Column(nullable = false) val createdAt: LocalDateTime, @Id @GeneratedValue(strategy = GenerationType.IDENTITY) From b725fb4f9648fcb670bead4dd0fcfc7dd4ea316c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 18:53:12 +0900 Subject: [PATCH 18/53] =?UTF-8?q?:recycle:=20refactor:=20Elvis=20=EC=97=B0?= =?UTF-8?q?=EC=82=B0=EC=9E=90=20=EC=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ragback/domain/document/repository/TagRepository.kt | 2 +- .../ragback/domain/document/service/DataFileService.kt | 2 +- .../ragback/domain/document/service/DataFileServiceTest.kt | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/simplerag/ragback/domain/document/repository/TagRepository.kt b/src/main/kotlin/simplerag/ragback/domain/document/repository/TagRepository.kt index fcfcd6b..03f0f8c 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/repository/TagRepository.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/repository/TagRepository.kt @@ -6,5 +6,5 @@ import java.util.* interface TagRepository : JpaRepository { - fun findByName(name: String): Optional + fun findByName(name: String): Tag? } diff --git a/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt index 19c5c1d..4e3b0f4 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt @@ -68,7 +68,7 @@ class DataFileService( .filter { it.isNotEmpty() } .distinct() .map { name -> - tagRepository.findByName(name).orElseGet { tagRepository.save(Tag(name = name)) } + tagRepository.findByName(name) ?: tagRepository.save(Tag(name = name)) } private fun attachTagsIfMissing(dataFile: DataFile, tags: List) { diff --git a/src/test/kotlin/simplerag/ragback/domain/document/service/DataFileServiceTest.kt b/src/test/kotlin/simplerag/ragback/domain/document/service/DataFileServiceTest.kt index c87ca77..3160613 100644 --- a/src/test/kotlin/simplerag/ragback/domain/document/service/DataFileServiceTest.kt +++ b/src/test/kotlin/simplerag/ragback/domain/document/service/DataFileServiceTest.kt @@ -56,8 +56,8 @@ class DataFileServiceTest( assertEquals(sha256Hex(bytes), saved.sha256) assertFalse(saved.fileUrl.isNullOrBlank()) - val ai = tagRepository.findByName("ai").orElse(null) - val rag = tagRepository.findByName("rag").orElse(null) + val ai = tagRepository.findByName("ai") + val rag = tagRepository.findByName("rag") assertNotNull(ai); assertNotNull(rag) assertTrue(dataFileTagRepository.existsByDataFileIdAndTagId(saved.id!!, ai!!.id!!)) assertTrue(dataFileTagRepository.existsByDataFileIdAndTagId(saved.id!!, rag!!.id!!)) From 6ef1473130212d24fdb74d6ca4de72aa966c9a4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 20:16:24 +0900 Subject: [PATCH 19/53] =?UTF-8?q?:bug:=20fix:=20S3=20=ED=8A=B8=EB=9E=9C?= =?UTF-8?q?=EC=9E=AD=EC=85=98=20=EB=A1=A4=EB=B0=B1=20=EB=8F=99=EA=B8=B0?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../document/service/DataFileService.kt | 14 ++++ .../ragback/global/storage/FakeS3Util.kt | 10 +-- .../ragback/global/util/FileConvertUtil.kt | 1 - .../document/service/DataFileServiceTest.kt | 75 +++++++++++++++++-- 4 files changed, 88 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt index 4e3b0f4..ddb8e16 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt @@ -2,6 +2,8 @@ package simplerag.ragback.domain.document.service import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional +import org.springframework.transaction.support.TransactionSynchronization +import org.springframework.transaction.support.TransactionSynchronizationManager import org.springframework.web.multipart.MultipartFile import simplerag.ragback.domain.document.dto.DataFileBulkCreateRequest import simplerag.ragback.domain.document.dto.DataFilePreviewResponse @@ -36,6 +38,17 @@ class DataFileService( } val now = LocalDateTime.now() + val uploadedUrls = mutableListOf() + + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.registerSynchronization(object : TransactionSynchronization { + override fun afterCompletion(status: Int) { + if (status == TransactionSynchronization.STATUS_ROLLED_BACK) { + uploadedUrls.forEach { runCatching { s3Util.deleteByUrl(it) } } + } + } + }) + } val responses = files.mapIndexed { idx, file -> val meta = req.items[idx] @@ -50,6 +63,7 @@ class DataFileService( } val fileUrl = s3Util.upload(file, S3Type.ORIGINAL_FILE) + uploadedUrls += fileUrl val dataFile = dataFileRepository.save(DataFile(meta.title, type, sizeByte, sha256, fileUrl, now, now)) diff --git a/src/main/kotlin/simplerag/ragback/global/storage/FakeS3Util.kt b/src/main/kotlin/simplerag/ragback/global/storage/FakeS3Util.kt index 760bdb0..88ab713 100644 --- a/src/main/kotlin/simplerag/ragback/global/storage/FakeS3Util.kt +++ b/src/main/kotlin/simplerag/ragback/global/storage/FakeS3Util.kt @@ -6,6 +6,7 @@ import org.springframework.stereotype.Component import org.springframework.web.multipart.MultipartFile import simplerag.ragback.global.util.S3Type import simplerag.ragback.global.util.S3Util +import simplerag.ragback.global.util.sha256Hex import java.security.MessageDigest import java.util.concurrent.ConcurrentHashMap @@ -20,7 +21,7 @@ class FakeS3Util : S3Util { val clean = (file.originalFilename ?: "file") .substringAfterLast('/').substringAfterLast('\\').ifBlank { "file" } - val hash = sha256(file.bytes).take(12) + val hash = sha256Hex(file.bytes).take(12) val key = "${dir.label}/${hash}_$clean" store[key] = file.bytes @@ -36,8 +37,7 @@ class FakeS3Util : S3Util { override fun keyFromUrl(url: String): String? = url.removePrefix("fake://").ifBlank { null } - private fun sha256(bytes: ByteArray): String { - val md = MessageDigest.getInstance("SHA-256") - return md.digest(bytes).joinToString("") { "%02x".format(it) } - } + // 테스트 용 함수 + fun exists(url: String): Boolean = keyFromUrl(url)?.let { store.containsKey(it) } == true + fun count(): Int = store.size } diff --git a/src/main/kotlin/simplerag/ragback/global/util/FileConvertUtil.kt b/src/main/kotlin/simplerag/ragback/global/util/FileConvertUtil.kt index 6e3cb7c..5f991a8 100644 --- a/src/main/kotlin/simplerag/ragback/global/util/FileConvertUtil.kt +++ b/src/main/kotlin/simplerag/ragback/global/util/FileConvertUtil.kt @@ -2,7 +2,6 @@ package simplerag.ragback.global.util import org.springframework.web.multipart.MultipartFile import java.security.MessageDigest -import kotlin.math.roundToLong fun byteToLong(bytes: ByteArray): Long = bytes.size.toLong() diff --git a/src/test/kotlin/simplerag/ragback/domain/document/service/DataFileServiceTest.kt b/src/test/kotlin/simplerag/ragback/domain/document/service/DataFileServiceTest.kt index 3160613..2ca08f4 100644 --- a/src/test/kotlin/simplerag/ragback/domain/document/service/DataFileServiceTest.kt +++ b/src/test/kotlin/simplerag/ragback/domain/document/service/DataFileServiceTest.kt @@ -8,6 +8,7 @@ import org.springframework.boot.test.context.SpringBootTest import org.springframework.mock.web.MockMultipartFile import org.springframework.test.context.ActiveProfiles import org.springframework.transaction.annotation.Transactional +import org.springframework.transaction.support.TransactionTemplate import org.springframework.web.multipart.MultipartFile import simplerag.ragback.domain.document.dto.DataFileBulkCreateRequest import simplerag.ragback.domain.document.dto.DataFileCreateItem @@ -18,6 +19,10 @@ import simplerag.ragback.domain.document.repository.TagRepository import simplerag.ragback.global.error.CustomException import simplerag.ragback.global.error.ErrorCode import simplerag.ragback.global.error.FileException +import simplerag.ragback.global.storage.FakeS3Util +import simplerag.ragback.global.util.S3Type +import simplerag.ragback.global.util.S3Util +import simplerag.ragback.global.util.sha256Hex import java.security.MessageDigest import java.time.LocalDateTime @@ -28,8 +33,14 @@ class DataFileServiceTest( @Autowired private val dataFileRepository: DataFileRepository, @Autowired private val tagRepository: TagRepository, @Autowired private val dataFileTagRepository: DataFileTagRepository, + @Autowired private val s3Util: FakeS3Util ) { + @Autowired + lateinit var txManager: org.springframework.transaction.PlatformTransactionManager + + private fun txTemplate() = TransactionTemplate(txManager) + @Test @Transactional @DisplayName("업로드 시 잘 저장이 된다.") @@ -141,6 +152,64 @@ class DataFileServiceTest( assertEquals(ErrorCode.ALREADY_FILE, ex.errorCode) } + @Test + @DisplayName("트랜잭션이 커밋되면 DB와 S3에 정상 저장된다") + fun uploadCommitPersist() { + // given + val bytes = "commit-case".toByteArray() + val req = DataFileBulkCreateRequest( + listOf(DataFileCreateItem(title = "commit-title", tags = listOf("t1"))) + ) + val f = file("c.txt", bytes) + + // when + val resultIds = txTemplate().execute { + val res = service.upload(listOf(f), req) + res.dataFilePreviewResponseList.map { it.id } + }!! + + // then (DB) + assertEquals(1, resultIds.size) + val saved = dataFileRepository.findById(resultIds.first()).orElseThrow() + assertEquals("commit-title", saved.title) + assertEquals(sha256Hex(bytes), saved.sha256) + assertFalse(saved.fileUrl.isNullOrBlank()) + + // then (S3 - FakeS3Util 기준) + assertTrue(s3Util.exists(saved.fileUrl!!), "커밋 시 S3에 파일이 존재해야 합니다") + } + + + @Test + @DisplayName("파일 업로드 중 트랜잭션이 롤백되면 DB와 S3에서 모두 정리된다") + fun uploadRollbackCleansDBandS3() { + // given + val bytes = "rollback-case".toByteArray() + val filename = "r.txt" + val req = DataFileBulkCreateRequest( + listOf(DataFileCreateItem(title = "rollback-title", tags = listOf("t2"))) + ) + val f = file(filename, bytes) + + val hash12 = MessageDigest.getInstance("SHA-256") + .digest(bytes).joinToString("") { "%02x".format(it) } + .take(12) + val expectedKey = "${S3Type.ORIGINAL_FILE.label}/${hash12}_$filename" + val expectedUrl = s3Util.urlFromKey(expectedKey) + + // when: 트랜잭션 내에서 업로드 후 강제 롤백 + txTemplate().execute { status -> + service.upload(listOf(f), req) + status!!.setRollbackOnly() + } + + // then + val sha = sha256Hex(bytes) + val existsInDb = dataFileRepository.findAll().any { it.sha256 == sha } + assertFalse(existsInDb, "롤백되었으므로 DB에 남으면 안 됩니다") + + assertFalse(s3Util.exists(expectedUrl), "롤백 시 S3도 보상 삭제되어야 합니다") + } // ----------------------- // helpers @@ -153,10 +222,4 @@ class DataFileServiceTest( paramName: String = "files" ): MultipartFile = MockMultipartFile(paramName, name, contentType, content) - - private fun sha256Hex(bytes: ByteArray): String { - val md = MessageDigest.getInstance("SHA-256") - val digest = md.digest(bytes) - return digest.joinToString("") { "%02x".format(it) } - } } From db60ddda763395435a2b194743673d68c03eab28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 20:17:33 +0900 Subject: [PATCH 20/53] =?UTF-8?q?:recycle:=20refactor:=20=EC=B6=94?= =?UTF-8?q?=EC=83=81=ED=99=94=20=EB=8B=A8=EA=B3=84=20=EB=A7=9E=EC=B6=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../document/service/DataFileService.kt | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt index ddb8e16..f73ee47 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt @@ -40,15 +40,7 @@ class DataFileService( val now = LocalDateTime.now() val uploadedUrls = mutableListOf() - if (TransactionSynchronizationManager.isSynchronizationActive()) { - TransactionSynchronizationManager.registerSynchronization(object : TransactionSynchronization { - override fun afterCompletion(status: Int) { - if (status == TransactionSynchronization.STATUS_ROLLED_BACK) { - uploadedUrls.forEach { runCatching { s3Util.deleteByUrl(it) } } - } - } - }) - } + registerRollbackCleanup(uploadedUrls) val responses = files.mapIndexed { idx, file -> val meta = req.items[idx] @@ -76,6 +68,18 @@ class DataFileService( return DataFileResponseList(responses) } + private fun registerRollbackCleanup(uploadedUrls: MutableList) { + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.registerSynchronization(object : TransactionSynchronization { + override fun afterCompletion(status: Int) { + if (status == TransactionSynchronization.STATUS_ROLLED_BACK) { + uploadedUrls.forEach { runCatching { s3Util.deleteByUrl(it) } } + } + } + }) + } + } + private fun getOrCreateTags(names: List): List = names.map { it.trim() } From 6ff7de9182996367182151f00d66d995451aa540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 20:24:33 +0900 Subject: [PATCH 21/53] =?UTF-8?q?:bug:=20fix:=20byte=20size=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EC=8B=9C=20stream=EC=9D=84=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=97=AC=20=EB=A9=94=EB=AA=A8=EB=A6=AC=20=EC=A0=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../document/service/DataFileService.kt | 6 ++--- .../ragback/global/util/FileConvertUtil.kt | 27 +++++++++++++++++-- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt index f73ee47..b391aee 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt @@ -44,10 +44,10 @@ class DataFileService( val responses = files.mapIndexed { idx, file -> val meta = req.items[idx] - val bytes = file.bytes - val sha256 = sha256Hex(bytes) - val sizeByte = byteToLong(bytes) + val metrics = file.computeMetricsStreaming() + val sha256 = metrics.sha256 + val sizeByte = metrics.sizeByte val type = file.resolveContentType() if (dataFileRepository.existsBySha256(sha256)) { diff --git a/src/main/kotlin/simplerag/ragback/global/util/FileConvertUtil.kt b/src/main/kotlin/simplerag/ragback/global/util/FileConvertUtil.kt index 5f991a8..21a7ca1 100644 --- a/src/main/kotlin/simplerag/ragback/global/util/FileConvertUtil.kt +++ b/src/main/kotlin/simplerag/ragback/global/util/FileConvertUtil.kt @@ -1,10 +1,14 @@ package simplerag.ragback.global.util import org.springframework.web.multipart.MultipartFile +import java.io.BufferedInputStream +import java.security.DigestInputStream import java.security.MessageDigest -fun byteToLong(bytes: ByteArray): Long = - bytes.size.toLong() +data class FileMetrics( + val sha256: String, + val sizeByte: Long +) fun sha256Hex(bytes: ByteArray): String = MessageDigest.getInstance("SHA-256") @@ -21,4 +25,23 @@ fun MultipartFile.resolveContentType(): String { "txt" -> "text/plain" else -> "application/octet-stream" } +} + +fun MultipartFile.computeMetricsStreaming(): FileMetrics { + val digest = MessageDigest.getInstance("SHA-256") + var totalBytes = 0L + + inputStream.use { input -> + DigestInputStream(BufferedInputStream(input), digest).use { digestStream -> + val buffer = ByteArray(8192) // 8KB buffer + var bytesRead: Int + + while (digestStream.read(buffer).also { bytesRead = it } != -1) { + totalBytes += bytesRead + } + } + } + + val sha256 = digest.digest().joinToString("") { "%02x".format(it) } + return FileMetrics(sha256, totalBytes) } \ No newline at end of file From eaead1c1239e3d5f2e86192b958b69bdd087a846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 20:25:22 +0900 Subject: [PATCH 22/53] =?UTF-8?q?:recycle:=20refactor:=20swagger=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/simplerag/ragback/global/config/SwaggerConfig.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/simplerag/ragback/global/config/SwaggerConfig.kt b/src/main/kotlin/simplerag/ragback/global/config/SwaggerConfig.kt index 2347dfd..5077a30 100644 --- a/src/main/kotlin/simplerag/ragback/global/config/SwaggerConfig.kt +++ b/src/main/kotlin/simplerag/ragback/global/config/SwaggerConfig.kt @@ -12,7 +12,7 @@ import org.springframework.context.annotation.Configuration @Configuration class SwaggerConfig { @Bean - fun SimpleRAG(): OpenAPI { + fun openAPI(): OpenAPI { val info = Info().title("SimpleRAG API").description("SimpleRAG API 명세").version("0.0.1") val jwtSchemeName = "JWT TOKEN" From 0b66cded73f951f7ce99291c5ac965055f3d2bbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 20:25:55 +0900 Subject: [PATCH 23/53] =?UTF-8?q?:recycle:=20refactor:=20swagger=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=ED=8F=AC=EB=A9=A7=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/simplerag/ragback/global/config/SwaggerConfig.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/simplerag/ragback/global/config/SwaggerConfig.kt b/src/main/kotlin/simplerag/ragback/global/config/SwaggerConfig.kt index 5077a30..2effd47 100644 --- a/src/main/kotlin/simplerag/ragback/global/config/SwaggerConfig.kt +++ b/src/main/kotlin/simplerag/ragback/global/config/SwaggerConfig.kt @@ -25,7 +25,7 @@ class SwaggerConfig { SecurityScheme() .name(jwtSchemeName) .type(SecurityScheme.Type.HTTP) - .scheme("Bearer") + .scheme("bearer") .bearerFormat("JWT") ) From 59b55797cda49634c5a2f6db62e65eef0df0701a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 20:28:20 +0900 Subject: [PATCH 24/53] =?UTF-8?q?:recycle:=20refactor:=20error=20=EC=9B=90?= =?UTF-8?q?=EC=9D=B8=20=ED=8C=8C=EC=95=85=EC=9A=A9=20cause=20=EC=B6=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../simplerag/ragback/global/error/CustomException.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/simplerag/ragback/global/error/CustomException.kt b/src/main/kotlin/simplerag/ragback/global/error/CustomException.kt index 9605746..723a62c 100644 --- a/src/main/kotlin/simplerag/ragback/global/error/CustomException.kt +++ b/src/main/kotlin/simplerag/ragback/global/error/CustomException.kt @@ -3,13 +3,16 @@ package simplerag.ragback.global.error open class CustomException( open val errorCode: ErrorCode, override val message: String = errorCode.message, -) : RuntimeException(message) + override val cause: Throwable? = null, +) : RuntimeException(message, cause) class S3Exception( override val errorCode: ErrorCode, -) : CustomException(errorCode) + override val cause: Throwable? = null, +) : CustomException(errorCode, errorCode.message, cause) class FileException( override val errorCode: ErrorCode, override val message: String, -) : CustomException(errorCode, message) \ No newline at end of file + override val cause: Throwable? = null, +) : CustomException(errorCode, message, cause) \ No newline at end of file From fb305fbc78818de5045813bb0aee3deedf2fc79f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 20:29:05 +0900 Subject: [PATCH 25/53] =?UTF-8?q?:recycle:=20refactor:=20ErrorCode=20?= =?UTF-8?q?=EA=B3=B5=EB=B0=B1=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/simplerag/ragback/global/error/ErrorCode.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/simplerag/ragback/global/error/ErrorCode.kt b/src/main/kotlin/simplerag/ragback/global/error/ErrorCode.kt index 4bf7ffc..3f0b2df 100644 --- a/src/main/kotlin/simplerag/ragback/global/error/ErrorCode.kt +++ b/src/main/kotlin/simplerag/ragback/global/error/ErrorCode.kt @@ -10,7 +10,7 @@ enum class ErrorCode( INVALID_INPUT(HttpStatus.BAD_REQUEST, "INVALID_INPUT", "잘못된 요청입니다."), NOT_FOUND(HttpStatus.NOT_FOUND, "NOT_FOUND", "리소스를 찾을 수 없습니다."), INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", "서버 오류가 발생했습니다."), - ALREADY_FILE (HttpStatus.BAD_REQUEST, "ALREADY_FILE", "같은 내용의 파일이 이미 존재합니다."), + ALREADY_FILE(HttpStatus.BAD_REQUEST, "ALREADY_FILE", "같은 내용의 파일이 이미 존재합니다."), // S3 S3_OBJECT_NOT_FOUND(HttpStatus.NOT_FOUND, "S3_001", "S3 오브젝트를 찾을 수 없습니다."), From 6a564ab38fdfa21b66ef6faf6f33e6d406039c4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 20:31:31 +0900 Subject: [PATCH 26/53] =?UTF-8?q?:recycle:=20refactor:=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EB=A1=9C=EA=B7=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../simplerag/ragback/global/error/GlobalExceptionHandler.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/simplerag/ragback/global/error/GlobalExceptionHandler.kt b/src/main/kotlin/simplerag/ragback/global/error/GlobalExceptionHandler.kt index d057a42..c2cf063 100644 --- a/src/main/kotlin/simplerag/ragback/global/error/GlobalExceptionHandler.kt +++ b/src/main/kotlin/simplerag/ragback/global/error/GlobalExceptionHandler.kt @@ -6,10 +6,13 @@ import org.springframework.web.bind.MethodArgumentNotValidException import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.RestControllerAdvice import simplerag.ragback.global.response.ApiResponse +import org.slf4j.LoggerFactory @RestControllerAdvice class GlobalExceptionHandler { + private val log = LoggerFactory.getLogger(GlobalExceptionHandler::class.java) + @ExceptionHandler(MethodArgumentNotValidException::class) fun handleValidationException(ex: MethodArgumentNotValidException): ResponseEntity> { val message = ex.bindingResult.allErrors.first().defaultMessage ?: "잘못된 요청" @@ -44,7 +47,7 @@ class GlobalExceptionHandler { @ExceptionHandler(Exception::class) fun handleGeneralException(ex: Exception): ResponseEntity> { - ex.printStackTrace() + log.error("Unhandled exception", ex) return ResponseEntity .status(ErrorCode.INTERNAL_ERROR.status) From 3b5948faa4a134c166d9ed0853a3b6095c3668da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 20:33:13 +0900 Subject: [PATCH 27/53] =?UTF-8?q?:recycle:=20refactor:=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20=EA=B0=92=20=EC=8B=9D=EB=B3=84=EC=9E=90=20sha256?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ragback/domain/document/service/DataFileService.kt | 2 +- .../simplerag/ragback/global/error/GlobalExceptionHandler.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt index b391aee..3708b5c 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt @@ -51,7 +51,7 @@ class DataFileService( val type = file.resolveContentType() if (dataFileRepository.existsBySha256(sha256)) { - throw FileException(ErrorCode.ALREADY_FILE, meta.title) + throw FileException(ErrorCode.ALREADY_FILE, sha256) } val fileUrl = s3Util.upload(file, S3Type.ORIGINAL_FILE) diff --git a/src/main/kotlin/simplerag/ragback/global/error/GlobalExceptionHandler.kt b/src/main/kotlin/simplerag/ragback/global/error/GlobalExceptionHandler.kt index c2cf063..bf1ae95 100644 --- a/src/main/kotlin/simplerag/ragback/global/error/GlobalExceptionHandler.kt +++ b/src/main/kotlin/simplerag/ragback/global/error/GlobalExceptionHandler.kt @@ -34,7 +34,7 @@ class GlobalExceptionHandler { val errorCode = ex.errorCode return ResponseEntity .status(errorCode.status) - .body(ApiResponse.fail(errorCode.code, "${errorCode.message} 파일명: ${ex.message}")) + .body(ApiResponse.fail(errorCode.code, "${errorCode.message} sha256: ${ex.message}")) } @ExceptionHandler(CustomException::class) From 094cecd37013c0ef08d2a95cf34e65badc7a66e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 20:35:17 +0900 Subject: [PATCH 28/53] =?UTF-8?q?:recycle:=20refactor:=20fake=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EB=8B=A8=EC=9D=BC=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/simplerag/ragback/global/storage/FakeS3Util.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/simplerag/ragback/global/storage/FakeS3Util.kt b/src/main/kotlin/simplerag/ragback/global/storage/FakeS3Util.kt index 88ab713..579caaa 100644 --- a/src/main/kotlin/simplerag/ragback/global/storage/FakeS3Util.kt +++ b/src/main/kotlin/simplerag/ragback/global/storage/FakeS3Util.kt @@ -7,7 +7,6 @@ import org.springframework.web.multipart.MultipartFile import simplerag.ragback.global.util.S3Type import simplerag.ragback.global.util.S3Util import simplerag.ragback.global.util.sha256Hex -import java.security.MessageDigest import java.util.concurrent.ConcurrentHashMap @Component @@ -19,10 +18,13 @@ class FakeS3Util : S3Util { override fun upload(file: MultipartFile, dir: S3Type): String { val clean = (file.originalFilename ?: "file") - .substringAfterLast('/').substringAfterLast('\\').ifBlank { "file" } + .substringAfterLast('/') + .substringAfterLast('\\') + .ifBlank { "file" } val hash = sha256Hex(file.bytes).take(12) - val key = "${dir.label}/${hash}_$clean" + val prefix = dir.label.trim('/') + val key = "$prefix/${hash}_$clean" store[key] = file.bytes return urlFromKey(key) From 475236a4e7a8afc6c201c426b417348a258e368a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 20:36:17 +0900 Subject: [PATCH 29/53] =?UTF-8?q?:recycle:=20refactor:=20keyFromUrl=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/simplerag/ragback/global/storage/FakeS3Util.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/simplerag/ragback/global/storage/FakeS3Util.kt b/src/main/kotlin/simplerag/ragback/global/storage/FakeS3Util.kt index 579caaa..d6e102f 100644 --- a/src/main/kotlin/simplerag/ragback/global/storage/FakeS3Util.kt +++ b/src/main/kotlin/simplerag/ragback/global/storage/FakeS3Util.kt @@ -36,8 +36,9 @@ class FakeS3Util : S3Util { override fun delete(key: String) { store.remove(key) } - override fun keyFromUrl(url: String): String? = - url.removePrefix("fake://").ifBlank { null } + override fun keyFromUrl(url: String): String? = url.removePrefix("fake://") + .removePrefix("/") + .ifBlank { null } // 테스트 용 함수 fun exists(url: String): Boolean = keyFromUrl(url)?.let { store.containsKey(it) } == true From b0fd4a870a232994b6b004003d2bd969c14e5db3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 20:38:12 +0900 Subject: [PATCH 30/53] =?UTF-8?q?:recycle:=20refactor:=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=ED=8F=AC=EB=A9=A7=20=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../simplerag/ragback/global/util/FileConvertUtil.kt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/simplerag/ragback/global/util/FileConvertUtil.kt b/src/main/kotlin/simplerag/ragback/global/util/FileConvertUtil.kt index 21a7ca1..ca50c62 100644 --- a/src/main/kotlin/simplerag/ragback/global/util/FileConvertUtil.kt +++ b/src/main/kotlin/simplerag/ragback/global/util/FileConvertUtil.kt @@ -16,13 +16,23 @@ fun sha256Hex(bytes: ByteArray): String = .joinToString("") { "%02x".format(it) } fun MultipartFile.resolveContentType(): String { - this.contentType?.let { return it } + if (!this.contentType.isNullOrBlank()) return this.contentType!! val ext = this.originalFilename?.substringAfterLast('.', "")?.lowercase() return when (ext) { "png" -> "image/png" "jpg", "jpeg" -> "image/jpeg" "pdf" -> "application/pdf" "txt" -> "text/plain" + "csv" -> "text/csv" + "md" -> "text/markdown" + "json" -> "application/json" + "zip" -> "application/zip" + "doc" -> "application/msword" + "docx" -> "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + "xls" -> "application/vnd.ms-excel" + "xlsx" -> "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + "ppt" -> "application/vnd.ms-powerpoint" + "pptx" -> "application/vnd.openxmlformats-officedocument.presentationml.presentation" else -> "application/octet-stream" } } From f5c04e381f861e4115bdff38b3d02cf68346edc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 20:39:11 +0900 Subject: [PATCH 31/53] =?UTF-8?q?:recycle:=20refactor:=20!!=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/util/MultipartJackson2HttpMessageConverter.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/simplerag/ragback/global/util/MultipartJackson2HttpMessageConverter.kt b/src/main/kotlin/simplerag/ragback/global/util/MultipartJackson2HttpMessageConverter.kt index b5f84a3..9e276dd 100644 --- a/src/main/kotlin/simplerag/ragback/global/util/MultipartJackson2HttpMessageConverter.kt +++ b/src/main/kotlin/simplerag/ragback/global/util/MultipartJackson2HttpMessageConverter.kt @@ -8,8 +8,8 @@ import java.lang.reflect.Type @Component class MultipartJackson2HttpMessageConverter - (objectMapper: ObjectMapper?) : - AbstractJackson2HttpMessageConverter(objectMapper!!, MediaType.APPLICATION_OCTET_STREAM) { + (objectMapper: ObjectMapper) : + AbstractJackson2HttpMessageConverter(objectMapper, MediaType.APPLICATION_OCTET_STREAM) { override fun canWrite(clazz: Class<*>, mediaType: MediaType?): Boolean { return false From 913a2b1b501160ec1dfbfff1244a2d461c33ac86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 20:39:43 +0900 Subject: [PATCH 32/53] =?UTF-8?q?:recycle:=20refactor:=20=EC=84=B8?= =?UTF-8?q?=EB=AF=B8=EC=BD=9C=EB=A1=A0=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/simplerag/ragback/global/util/S3Type.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/simplerag/ragback/global/util/S3Type.kt b/src/main/kotlin/simplerag/ragback/global/util/S3Type.kt index a408188..f7136a3 100644 --- a/src/main/kotlin/simplerag/ragback/global/util/S3Type.kt +++ b/src/main/kotlin/simplerag/ragback/global/util/S3Type.kt @@ -4,5 +4,5 @@ package simplerag.ragback.global.util enum class S3Type( val label: String, ) { - ORIGINAL_FILE("/ORIGINAL/"),; + ORIGINAL_FILE("/ORIGINAL/"), } \ No newline at end of file From fe87736313af5a04c8b505a2861d691270c03f60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 20:45:20 +0900 Subject: [PATCH 33/53] =?UTF-8?q?:rocket:=20chore:=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EB=82=98=EB=88=84=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-local.yml | 26 +++++++++++++++++++++ src/main/resources/application.yml | 29 +++--------------------- 2 files changed, 29 insertions(+), 26 deletions(-) create mode 100644 src/main/resources/application-local.yml diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml new file mode 100644 index 0000000..5019101 --- /dev/null +++ b/src/main/resources/application-local.yml @@ -0,0 +1,26 @@ +spring: + datasource: + url: ${DB_URL_LOCAL} + username: ${DB_USERNAME_LOCAL} + password: ${DB_PASSWORD_LOCAL} + driver-class-name: org.postgresql.Driver + jpa: + hibernate: + ddl-auto: update + show-sql: true + +logging: + level: + root: INFO + org.hibernate.SQL: DEBUG + org.hibernate.type.descriptor.sql.BasicBinder: TRACE + +cloud: + aws: + region: + static: ${REGION_LOCAL} + s3: + bucket: ${BUCKET_LOCAL} + credentials: + access-key: ${AWS_ACCESS_KEY_ID_LOCAL} + secret-key: ${AWS_SECRET_ACCESS_KEY_LOCAL} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 872fc96..a403076 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -8,34 +8,11 @@ spring: multipart: max-file-size: 50MB max-request-size: 200MB - datasource: - url: ${DB_URL} - username: ${DB_USERNAME} - password: - driver-class-name: org.postgresql.Driver + jackson: + time-zone: Asia/Seoul + jpa: - hibernate: - ddl-auto: update properties: hibernate: dialect: org.hibernate.dialect.PostgreSQLDialect format_sql: true - show_sql: true - jackson: - time-zone: Asia/Seoul - -logging: - level: - root: INFO - org.hibernate.SQL: DEBUG - org.hibernate.type.descriptor.sql.BasicBinder: TRACE - -cloud: - aws: - region: - static: ${REGION} - s3: - bucket: ${BUCKET} - credentials: - access-key: ${AWS_ACCESS_KEY_ID} - secret-key: ${AWS_SECRET_ACCESS_KEY} \ No newline at end of file From 72de563005fd3db919d694c02d8666b874cd106d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 20:48:55 +0900 Subject: [PATCH 34/53] =?UTF-8?q?:recycle:=20recycle:=20=ED=83=9C=EA=B7=B8?= =?UTF-8?q?=20=EB=8C=80=EB=AC=B8=EC=9E=90=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ragback/domain/document/service/DataFileService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt index 3708b5c..8a358b0 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt @@ -82,7 +82,7 @@ class DataFileService( private fun getOrCreateTags(names: List): List = - names.map { it.trim() } + names.map { it.trim().uppercase() } .filter { it.isNotEmpty() } .distinct() .map { name -> From 2e3978eba4cbd924ac89ebadc7991955fc04eb72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 20:54:57 +0900 Subject: [PATCH 35/53] =?UTF-8?q?:bug:=20fix:=20test=20=EB=B3=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ragback/domain/document/service/DataFileServiceTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/kotlin/simplerag/ragback/domain/document/service/DataFileServiceTest.kt b/src/test/kotlin/simplerag/ragback/domain/document/service/DataFileServiceTest.kt index 2ca08f4..6deff48 100644 --- a/src/test/kotlin/simplerag/ragback/domain/document/service/DataFileServiceTest.kt +++ b/src/test/kotlin/simplerag/ragback/domain/document/service/DataFileServiceTest.kt @@ -48,7 +48,7 @@ class DataFileServiceTest( // given val bytes = "hello world".toByteArray() val req = DataFileBulkCreateRequest( - listOf(DataFileCreateItem(title = "greeting", tags = listOf(" ai ", "rag", "ai"))) + listOf(DataFileCreateItem(title = "greeting", tags = listOf(" ai ", "RAG", "ai"))) ) val f = file("greet.txt", bytes, contentType = "text/plain") @@ -67,8 +67,8 @@ class DataFileServiceTest( assertEquals(sha256Hex(bytes), saved.sha256) assertFalse(saved.fileUrl.isNullOrBlank()) - val ai = tagRepository.findByName("ai") - val rag = tagRepository.findByName("rag") + val ai = tagRepository.findByName("AI") + val rag = tagRepository.findByName("RAG") assertNotNull(ai); assertNotNull(rag) assertTrue(dataFileTagRepository.existsByDataFileIdAndTagId(saved.id!!, ai!!.id!!)) assertTrue(dataFileTagRepository.existsByDataFileIdAndTagId(saved.id!!, rag!!.id!!)) From 62f99de07e767244b5516bef8c8d842b39604eba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 20:55:30 +0900 Subject: [PATCH 36/53] =?UTF-8?q?:bug:=20fix:=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=A4=91=EB=B3=B5=EC=8B=9C=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=9E=A1=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ragback/domain/document/service/DataFileService.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt index 8a358b0..90ba780 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt @@ -1,5 +1,6 @@ package simplerag.ragback.domain.document.service +import org.springframework.dao.DataIntegrityViolationException import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.support.TransactionSynchronization @@ -57,7 +58,11 @@ class DataFileService( val fileUrl = s3Util.upload(file, S3Type.ORIGINAL_FILE) uploadedUrls += fileUrl - val dataFile = dataFileRepository.save(DataFile(meta.title, type, sizeByte, sha256, fileUrl, now, now)) + val dataFile = try { + dataFileRepository.save(DataFile(meta.title, type, sizeByte, sha256, fileUrl, now, now)) + } catch (ex: DataIntegrityViolationException) { + throw FileException(ErrorCode.ALREADY_FILE, sha256) + } val tags = getOrCreateTags(meta.tags) attachTagsIfMissing(dataFile, tags) From 729826a039459864ba5371bf6752b590c2b57468 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 21:02:06 +0900 Subject: [PATCH 37/53] =?UTF-8?q?:recycle:=20refactor:=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EB=A1=9C=EA=B7=B8=20=EB=82=A8=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/simplerag/ragback/global/util/S3UtilImpl.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/kotlin/simplerag/ragback/global/util/S3UtilImpl.kt b/src/main/kotlin/simplerag/ragback/global/util/S3UtilImpl.kt index 3b64e7b..96e097d 100644 --- a/src/main/kotlin/simplerag/ragback/global/util/S3UtilImpl.kt +++ b/src/main/kotlin/simplerag/ragback/global/util/S3UtilImpl.kt @@ -1,10 +1,12 @@ package simplerag.ragback.global.util +import org.slf4j.LoggerFactory import org.springframework.context.annotation.Profile import org.springframework.stereotype.Component import org.springframework.web.multipart.MultipartFile import simplerag.ragback.global.config.S3Config import simplerag.ragback.global.error.ErrorCode +import simplerag.ragback.global.error.GlobalExceptionHandler import simplerag.ragback.global.error.S3Exception import software.amazon.awssdk.core.sync.RequestBody import software.amazon.awssdk.services.s3.S3Client @@ -19,7 +21,9 @@ class S3UtilImpl( private val s3: S3Client, private val s3Config: S3Config, ): S3Util { + private val bucket get() = s3Config.bucket + private val log = LoggerFactory.getLogger(GlobalExceptionHandler::class.java) override fun upload(file: MultipartFile, dir: S3Type): String { if (file.isEmpty) throw S3Exception(ErrorCode.S3_EMPTY_FILE) @@ -40,8 +44,13 @@ class S3UtilImpl( return urlFromKey(key) } catch (e: software.amazon.awssdk.services.s3.model.S3Exception) { + log.error("S3 putObject fail bucket={}, key={}, status={}, awsCode={}, reqId={}, msg={}", + bucket, key, e.statusCode(), e.awsErrorDetails()?.errorCode(), e.requestId(), + e.awsErrorDetails()?.errorMessage(), e + ) throw S3Exception(ErrorCode.S3_UPLOAD_FAIL) } catch (e: Exception) { + log.error(e.message, e) throw S3Exception(ErrorCode.S3_UPLOAD_FAIL) } } From 392fa4acbffb8df7fb635f0a06973726a25fd2e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 22:00:50 +0900 Subject: [PATCH 38/53] =?UTF-8?q?:recycle:=20refactor:=20deleteByUrl=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/simplerag/ragback/global/storage/FakeS3Util.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/simplerag/ragback/global/storage/FakeS3Util.kt b/src/main/kotlin/simplerag/ragback/global/storage/FakeS3Util.kt index d6e102f..a101b56 100644 --- a/src/main/kotlin/simplerag/ragback/global/storage/FakeS3Util.kt +++ b/src/main/kotlin/simplerag/ragback/global/storage/FakeS3Util.kt @@ -32,7 +32,12 @@ class FakeS3Util : S3Util { override fun urlFromKey(key: String): String = "fake://$key" - override fun deleteByUrl(url: String) { keyFromUrl(url)?.let { store.remove(it) } } + override fun deleteByUrl(url: String) { + val key = keyFromUrl(url) ?: throw simplerag.ragback.global.error.S3Exception( + simplerag.ragback.global.error.ErrorCode.S3_INVALID_URL + ) + store.remove(key) + } override fun delete(key: String) { store.remove(key) } From 8594135707b1851cbfbd75fb136702a5b97599dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 22:21:18 +0900 Subject: [PATCH 39/53] =?UTF-8?q?:recycle:=20refactor:=20multipart=20?= =?UTF-8?q?=EC=A0=84=EC=9A=A9=20consumes,=20JSON=20produces=20=EB=AA=85?= =?UTF-8?q?=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/document/controller/DataFileController.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/simplerag/ragback/domain/document/controller/DataFileController.kt b/src/main/kotlin/simplerag/ragback/domain/document/controller/DataFileController.kt index 07d73ab..0796646 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/controller/DataFileController.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/controller/DataFileController.kt @@ -18,10 +18,14 @@ class DataFileController( private val service: DataFileService ) { - @PostMapping(consumes = [ - MediaType.MULTIPART_FORM_DATA_VALUE, - MediaType.APPLICATION_JSON_VALUE - ]) + @PostMapping( + consumes = [ + MediaType.MULTIPART_FORM_DATA_VALUE, + ], + produces = [ + MediaType.APPLICATION_JSON_VALUE + ] + ) @ResponseStatus(HttpStatus.CREATED) fun upload( @RequestPart("files") files: List, From b8ab8dbab4581bfa4d32fb6793aac04d20a10c14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 22:23:36 +0900 Subject: [PATCH 40/53] =?UTF-8?q?:recycle:=20refactor:=20byte=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/simplerag/ragback/domain/document/entity/DataFile.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/simplerag/ragback/domain/document/entity/DataFile.kt b/src/main/kotlin/simplerag/ragback/domain/document/entity/DataFile.kt index 1687f17..6a2cc6b 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/entity/DataFile.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/entity/DataFile.kt @@ -16,7 +16,7 @@ class DataFile( @Column(name = "file_type", nullable = false, length = 120) val type: String, - @Column(name = "size_mb", nullable = false) + @Column(name = "size_bytes", nullable = false) val sizeBytes: Long, @Column(nullable = false, length = 64) From 61cdb852f6d79d29f29c52b66cde8b7c3fd3784f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 22:27:24 +0900 Subject: [PATCH 41/53] =?UTF-8?q?:recycle:=20refactor:=20contentType=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/simplerag/ragback/global/util/S3UtilImpl.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/simplerag/ragback/global/util/S3UtilImpl.kt b/src/main/kotlin/simplerag/ragback/global/util/S3UtilImpl.kt index 96e097d..6769422 100644 --- a/src/main/kotlin/simplerag/ragback/global/util/S3UtilImpl.kt +++ b/src/main/kotlin/simplerag/ragback/global/util/S3UtilImpl.kt @@ -29,13 +29,14 @@ class S3UtilImpl( if (file.isEmpty) throw S3Exception(ErrorCode.S3_EMPTY_FILE) val key = buildKey(dir.label, file.originalFilename) + val contentType = file.contentType ?: "application/octet-stream" try { file.inputStream.use { input -> val putReq = PutObjectRequest.builder() .bucket(bucket) .key(key) - .contentType(file.contentType ?: "application/octet-stream") + .contentType(contentType) .build() val body = RequestBody.fromInputStream(input, file.size) From de5b382c277ddb1ace5c04ccb868cd38dfdda3fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 22:35:27 +0900 Subject: [PATCH 42/53] =?UTF-8?q?:recycle:=20refactor:=20service=20?= =?UTF-8?q?=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../document/controller/DataFileController.kt | 4 ++-- .../document/service/DataFileServiceTest.kt | 19 +++++++++---------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/simplerag/ragback/domain/document/controller/DataFileController.kt b/src/main/kotlin/simplerag/ragback/domain/document/controller/DataFileController.kt index 0796646..dacf857 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/controller/DataFileController.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/controller/DataFileController.kt @@ -15,7 +15,7 @@ import simplerag.ragback.global.response.ApiResponse @RequestMapping("/api/v1/data-files") @Validated class DataFileController( - private val service: DataFileService + private val dataFileService: DataFileService ) { @PostMapping( @@ -31,7 +31,7 @@ class DataFileController( @RequestPart("files") files: List, @Valid @RequestPart("request") req: DataFileBulkCreateRequest ): ApiResponse { - val saved = service.upload(files, req) + val saved = dataFileService.upload(files, req) return ApiResponse.ok(saved, "업로드 완료") } diff --git a/src/test/kotlin/simplerag/ragback/domain/document/service/DataFileServiceTest.kt b/src/test/kotlin/simplerag/ragback/domain/document/service/DataFileServiceTest.kt index 6deff48..2d58831 100644 --- a/src/test/kotlin/simplerag/ragback/domain/document/service/DataFileServiceTest.kt +++ b/src/test/kotlin/simplerag/ragback/domain/document/service/DataFileServiceTest.kt @@ -21,7 +21,6 @@ import simplerag.ragback.global.error.ErrorCode import simplerag.ragback.global.error.FileException import simplerag.ragback.global.storage.FakeS3Util import simplerag.ragback.global.util.S3Type -import simplerag.ragback.global.util.S3Util import simplerag.ragback.global.util.sha256Hex import java.security.MessageDigest import java.time.LocalDateTime @@ -29,7 +28,7 @@ import java.time.LocalDateTime @SpringBootTest @ActiveProfiles("test") class DataFileServiceTest( - @Autowired private val service: DataFileService, + @Autowired private val dataFileService: DataFileService, @Autowired private val dataFileRepository: DataFileRepository, @Autowired private val tagRepository: TagRepository, @Autowired private val dataFileTagRepository: DataFileTagRepository, @@ -53,7 +52,7 @@ class DataFileServiceTest( val f = file("greet.txt", bytes, contentType = "text/plain") // when - val res = service.upload(listOf(f), req) + val res = dataFileService.upload(listOf(f), req) // then assertEquals(1, res.dataFilePreviewResponseList.size) @@ -82,7 +81,7 @@ class DataFileServiceTest( val f = file("a.txt", "a".toByteArray()) // when - val ex = assertThrows(CustomException::class.java) { service.upload(listOf(f), req) } + val ex = assertThrows(CustomException::class.java) { dataFileService.upload(listOf(f), req) } // then assertEquals(ErrorCode.INVALID_INPUT, ex.errorCode) @@ -111,7 +110,7 @@ class DataFileServiceTest( val f = file("dup.txt", bytes) // when - val ex = assertThrows(FileException::class.java) { service.upload(listOf(f), req) } + val ex = assertThrows(FileException::class.java) { dataFileService.upload(listOf(f), req) } // then assertEquals(ErrorCode.ALREADY_FILE, ex.errorCode) @@ -127,7 +126,7 @@ class DataFileServiceTest( val f = file(name = "noext", content = bytes, contentType = null) // no extension // when - val res = service.upload(listOf(f), req) + val res = dataFileService.upload(listOf(f), req) // then val saved = dataFileRepository.findById(res.dataFilePreviewResponseList.first().id).orElseThrow() @@ -145,8 +144,8 @@ class DataFileServiceTest( val f2 = file("y.txt", bytes) // when - service.upload(listOf(f1), req) - val ex = assertThrows(FileException::class.java) { service.upload(listOf(f2), req) } + dataFileService.upload(listOf(f1), req) + val ex = assertThrows(FileException::class.java) { dataFileService.upload(listOf(f2), req) } // then assertEquals(ErrorCode.ALREADY_FILE, ex.errorCode) @@ -164,7 +163,7 @@ class DataFileServiceTest( // when val resultIds = txTemplate().execute { - val res = service.upload(listOf(f), req) + val res = dataFileService.upload(listOf(f), req) res.dataFilePreviewResponseList.map { it.id } }!! @@ -199,7 +198,7 @@ class DataFileServiceTest( // when: 트랜잭션 내에서 업로드 후 강제 롤백 txTemplate().execute { status -> - service.upload(listOf(f), req) + dataFileService.upload(listOf(f), req) status!!.setRollbackOnly() } From 6f38323013d6c01ca2f6037e9377385470ac016a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 22:58:54 +0900 Subject: [PATCH 43/53] =?UTF-8?q?:recycle:=20refactor:=20request=20dto=20n?= =?UTF-8?q?ull=20=ED=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + .../document/controller/DataFileController.kt | 16 ++++++--- .../global/error/GlobalExceptionHandler.kt | 34 +++++++++++++++++++ 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 3aad617..fe060a4 100644 --- a/build.gradle +++ b/build.gradle @@ -31,6 +31,7 @@ ext { } dependencies { + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' diff --git a/src/main/kotlin/simplerag/ragback/domain/document/controller/DataFileController.kt b/src/main/kotlin/simplerag/ragback/domain/document/controller/DataFileController.kt index dacf857..64693ba 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/controller/DataFileController.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/controller/DataFileController.kt @@ -1,6 +1,9 @@ package simplerag.ragback.domain.document.controller +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.media.Content import jakarta.validation.Valid +import jakarta.validation.constraints.Size import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.validation.annotation.Validated @@ -21,15 +24,18 @@ class DataFileController( @PostMapping( consumes = [ MediaType.MULTIPART_FORM_DATA_VALUE, - ], - produces = [ - MediaType.APPLICATION_JSON_VALUE ] ) @ResponseStatus(HttpStatus.CREATED) fun upload( - @RequestPart("files") files: List, - @Valid @RequestPart("request") req: DataFileBulkCreateRequest + @RequestPart("files") + @Size(min = 1, message = "최소 하나 이상 업로드해야 합니다") + files: List, + + @Parameter(content = [Content(mediaType = MediaType.APPLICATION_JSON_VALUE)]) + @RequestPart("request") + @Valid + req: DataFileBulkCreateRequest ): ApiResponse { val saved = dataFileService.upload(files, req) return ApiResponse.ok(saved, "업로드 완료") diff --git a/src/main/kotlin/simplerag/ragback/global/error/GlobalExceptionHandler.kt b/src/main/kotlin/simplerag/ragback/global/error/GlobalExceptionHandler.kt index bf1ae95..27b08c6 100644 --- a/src/main/kotlin/simplerag/ragback/global/error/GlobalExceptionHandler.kt +++ b/src/main/kotlin/simplerag/ragback/global/error/GlobalExceptionHandler.kt @@ -1,5 +1,6 @@ package simplerag.ragback.global.error +import com.fasterxml.jackson.databind.exc.InvalidFormatException import jakarta.validation.ConstraintViolationException import org.springframework.http.ResponseEntity import org.springframework.web.bind.MethodArgumentNotValidException @@ -7,6 +8,8 @@ import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.RestControllerAdvice import simplerag.ragback.global.response.ApiResponse import org.slf4j.LoggerFactory +import org.springframework.http.converter.HttpMessageNotReadableException +import org.springframework.web.multipart.support.MissingServletRequestPartException @RestControllerAdvice class GlobalExceptionHandler { @@ -29,6 +32,37 @@ class GlobalExceptionHandler { .body(ApiResponse.fail(ErrorCode.INVALID_INPUT.code, message)) } + @ExceptionHandler(MissingServletRequestPartException::class) + fun handleMissingPart(e: MissingServletRequestPartException): ResponseEntity> { + val msg = "필수 '${e.requestPartName}' 가 없습니다." + return ResponseEntity + .badRequest() + .body(ApiResponse.fail(code = "FILE_PART_MISSING", message = msg)) + } + + @ExceptionHandler(HttpMessageNotReadableException::class) + fun handleUnreadable(e: HttpMessageNotReadableException): ResponseEntity> { + val cause = e.cause + val msg = when (cause) { + is com.fasterxml.jackson.databind.exc.MismatchedInputException -> { + val field = cause.path.lastOrNull()?.fieldName ?: "unknown" + if (cause.message?.contains("Null value for creator property") == true) { + "'$field' 값이 비어있습니다." + } else { + "'$field' 값 타입이 올바르지 않습니다." + } + } + is InvalidFormatException -> { + val field = cause.path.lastOrNull()?.fieldName ?: "unknown" + "'$field' 값 형식이 올바르지 않습니다." + } + else -> "유효하지 않은 요청입니다." + } + + return ResponseEntity.badRequest() + .body(ApiResponse.fail(code = "INVALID_JSON", message = msg)) + } + @ExceptionHandler(FileException::class) fun handleCustomException(ex: FileException): ResponseEntity> { val errorCode = ex.errorCode From 9f069b230f0de2551854e111ab5e159db2855466 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 23:01:31 +0900 Subject: [PATCH 44/53] =?UTF-8?q?:recycle:=20refactor:=20exception=20handl?= =?UTF-8?q?er=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/error/GlobalExceptionHandler.kt | 62 +++++++++---------- 1 file changed, 28 insertions(+), 34 deletions(-) diff --git a/src/main/kotlin/simplerag/ragback/global/error/GlobalExceptionHandler.kt b/src/main/kotlin/simplerag/ragback/global/error/GlobalExceptionHandler.kt index 27b08c6..64ae263 100644 --- a/src/main/kotlin/simplerag/ragback/global/error/GlobalExceptionHandler.kt +++ b/src/main/kotlin/simplerag/ragback/global/error/GlobalExceptionHandler.kt @@ -1,8 +1,10 @@ package simplerag.ragback.global.error +import com.fasterxml.jackson.core.JsonParseException import com.fasterxml.jackson.databind.exc.InvalidFormatException +import com.fasterxml.jackson.databind.exc.InvalidNullException +import com.fasterxml.jackson.databind.exc.MismatchedInputException import jakarta.validation.ConstraintViolationException -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 @@ -17,74 +19,66 @@ class GlobalExceptionHandler { private val log = LoggerFactory.getLogger(GlobalExceptionHandler::class.java) @ExceptionHandler(MethodArgumentNotValidException::class) - fun handleValidationException(ex: MethodArgumentNotValidException): ResponseEntity> { + fun handleValidationException(ex: MethodArgumentNotValidException): ApiResponse { val message = ex.bindingResult.allErrors.first().defaultMessage ?: "잘못된 요청" - return ResponseEntity - .badRequest() - .body(ApiResponse.fail(ErrorCode.INVALID_INPUT.code, message)) + return ApiResponse.fail(ErrorCode.INVALID_INPUT.code, message) } @ExceptionHandler(ConstraintViolationException::class) - fun handleConstraintViolation(ex: ConstraintViolationException): ResponseEntity> { + fun handleConstraintViolation(ex: ConstraintViolationException): ApiResponse { val message = ex.constraintViolations.firstOrNull()?.message ?: "잘못된 요청" - return ResponseEntity - .badRequest() - .body(ApiResponse.fail(ErrorCode.INVALID_INPUT.code, message)) + return ApiResponse.fail(ErrorCode.INVALID_INPUT.code, message) } @ExceptionHandler(MissingServletRequestPartException::class) - fun handleMissingPart(e: MissingServletRequestPartException): ResponseEntity> { + fun handleMissingPart(e: MissingServletRequestPartException): ApiResponse { val msg = "필수 '${e.requestPartName}' 가 없습니다." - return ResponseEntity - .badRequest() - .body(ApiResponse.fail(code = "FILE_PART_MISSING", message = msg)) + return ApiResponse.fail(code = "FILE_PART_MISSING", message = msg) } @ExceptionHandler(HttpMessageNotReadableException::class) - fun handleUnreadable(e: HttpMessageNotReadableException): ResponseEntity> { + fun handleUnreadable(e: HttpMessageNotReadableException): ApiResponse { val cause = e.cause + val msg = when (cause) { - is com.fasterxml.jackson.databind.exc.MismatchedInputException -> { + is InvalidNullException -> { val field = cause.path.lastOrNull()?.fieldName ?: "unknown" - if (cause.message?.contains("Null value for creator property") == true) { - "'$field' 값이 비어있습니다." - } else { - "'$field' 값 타입이 올바르지 않습니다." - } + "'$field' 값이 비어있습니다." } is InvalidFormatException -> { val field = cause.path.lastOrNull()?.fieldName ?: "unknown" "'$field' 값 형식이 올바르지 않습니다." } + is MismatchedInputException -> { + val field = cause.path.lastOrNull()?.fieldName ?: "unknown" + "'$field' 값 타입이 올바르지 않습니다." + } + is JsonParseException -> { + // JSON 문법 오류 (콤마, 따옴표 누락 등) + "JSON 문법이 올바르지 않습니다." + } else -> "유효하지 않은 요청입니다." } - return ResponseEntity.badRequest() - .body(ApiResponse.fail(code = "INVALID_JSON", message = msg)) + return ApiResponse.fail(code = "INVALID_JSON", message = msg) } @ExceptionHandler(FileException::class) - fun handleCustomException(ex: FileException): ResponseEntity> { + fun handleCustomException(ex: FileException): ApiResponse { val errorCode = ex.errorCode - return ResponseEntity - .status(errorCode.status) - .body(ApiResponse.fail(errorCode.code, "${errorCode.message} sha256: ${ex.message}")) + return ApiResponse.fail(errorCode.code, "${errorCode.message} sha256: ${ex.message}") } @ExceptionHandler(CustomException::class) - fun handleCustomException(ex: CustomException): ResponseEntity> { + fun handleCustomException(ex: CustomException): ApiResponse { val errorCode = ex.errorCode - return ResponseEntity - .status(errorCode.status) - .body(ApiResponse.fail(errorCode.code, errorCode.message)) + return ApiResponse.fail(errorCode.code, errorCode.message) } @ExceptionHandler(Exception::class) - fun handleGeneralException(ex: Exception): ResponseEntity> { + fun handleGeneralException(ex: Exception): ApiResponse { log.error("Unhandled exception", ex) - return ResponseEntity - .status(ErrorCode.INTERNAL_ERROR.status) - .body(ApiResponse.fail(ErrorCode.INTERNAL_ERROR.code, ErrorCode.INTERNAL_ERROR.message)) + return ApiResponse.fail(ErrorCode.INTERNAL_ERROR.code, ErrorCode.INTERNAL_ERROR.message) } } From 484fdf6fa496e485af94210c61a1a68d554455a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 23:05:27 +0900 Subject: [PATCH 45/53] =?UTF-8?q?:recycle:=20refactor:=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=BD=94=EB=93=9C=20=EC=9C=84=EC=B9=98=20=ED=86=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/simplerag/ragback/global/error/ErrorCode.kt | 2 ++ .../simplerag/ragback/global/error/GlobalExceptionHandler.kt | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/simplerag/ragback/global/error/ErrorCode.kt b/src/main/kotlin/simplerag/ragback/global/error/ErrorCode.kt index 3f0b2df..f1a2810 100644 --- a/src/main/kotlin/simplerag/ragback/global/error/ErrorCode.kt +++ b/src/main/kotlin/simplerag/ragback/global/error/ErrorCode.kt @@ -11,6 +11,8 @@ enum class ErrorCode( NOT_FOUND(HttpStatus.NOT_FOUND, "NOT_FOUND", "리소스를 찾을 수 없습니다."), INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", "서버 오류가 발생했습니다."), ALREADY_FILE(HttpStatus.BAD_REQUEST, "ALREADY_FILE", "같은 내용의 파일이 이미 존재합니다."), + FILE_PART_MISSING(HttpStatus.BAD_REQUEST, "FILE_PART_MISSING", "필수 파트가 존재하지 않습니다."), + INVALID_JSON(HttpStatus.BAD_REQUEST, "INVALID_JSON", "JSON이 유효하지 않습니다."), // S3 S3_OBJECT_NOT_FOUND(HttpStatus.NOT_FOUND, "S3_001", "S3 오브젝트를 찾을 수 없습니다."), diff --git a/src/main/kotlin/simplerag/ragback/global/error/GlobalExceptionHandler.kt b/src/main/kotlin/simplerag/ragback/global/error/GlobalExceptionHandler.kt index 64ae263..efd8fed 100644 --- a/src/main/kotlin/simplerag/ragback/global/error/GlobalExceptionHandler.kt +++ b/src/main/kotlin/simplerag/ragback/global/error/GlobalExceptionHandler.kt @@ -33,7 +33,7 @@ class GlobalExceptionHandler { @ExceptionHandler(MissingServletRequestPartException::class) fun handleMissingPart(e: MissingServletRequestPartException): ApiResponse { val msg = "필수 '${e.requestPartName}' 가 없습니다." - return ApiResponse.fail(code = "FILE_PART_MISSING", message = msg) + return ApiResponse.fail(ErrorCode.FILE_PART_MISSING.code, message = msg) } @ExceptionHandler(HttpMessageNotReadableException::class) @@ -60,7 +60,7 @@ class GlobalExceptionHandler { else -> "유효하지 않은 요청입니다." } - return ApiResponse.fail(code = "INVALID_JSON", message = msg) + return ApiResponse.fail(ErrorCode.INVALID_JSON.code, message = msg) } @ExceptionHandler(FileException::class) From 1c992c4f5c96c1527de24df8f3479fcb937bddcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 23:10:18 +0900 Subject: [PATCH 46/53] =?UTF-8?q?:recycle:=20refactor:=20=EC=86=8C?= =?UTF-8?q?=EB=82=98=20=ED=81=90=EB=B8=8C=20=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ragback/domain/document/dto/DataFileResponseDTO.kt | 2 +- .../ragback/domain/document/repository/TagRepository.kt | 1 - .../ragback/domain/document/service/DataFileService.kt | 5 ++++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/simplerag/ragback/domain/document/dto/DataFileResponseDTO.kt b/src/main/kotlin/simplerag/ragback/domain/document/dto/DataFileResponseDTO.kt index fd689e7..04244f3 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/dto/DataFileResponseDTO.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/dto/DataFileResponseDTO.kt @@ -1,7 +1,7 @@ package simplerag.ragback.domain.document.dto data class DataFileResponseList( - val dataFilePreviewResponseList : List, + val dataFilePreviewResponseList: List, ) data class DataFilePreviewResponse( diff --git a/src/main/kotlin/simplerag/ragback/domain/document/repository/TagRepository.kt b/src/main/kotlin/simplerag/ragback/domain/document/repository/TagRepository.kt index 03f0f8c..6e3c35e 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/repository/TagRepository.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/repository/TagRepository.kt @@ -2,7 +2,6 @@ package simplerag.ragback.domain.document.repository import org.springframework.data.jpa.repository.JpaRepository import simplerag.ragback.domain.document.entity.Tag -import java.util.* interface TagRepository : JpaRepository { diff --git a/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt index 90ba780..2fdd439 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt @@ -18,7 +18,10 @@ import simplerag.ragback.domain.document.repository.TagRepository import simplerag.ragback.global.error.CustomException import simplerag.ragback.global.error.ErrorCode import simplerag.ragback.global.error.FileException -import simplerag.ragback.global.util.* +import simplerag.ragback.global.util.S3Type +import simplerag.ragback.global.util.S3Util +import simplerag.ragback.global.util.computeMetricsStreaming +import simplerag.ragback.global.util.resolveContentType import java.time.LocalDateTime @Service From f4ee29bd3c9abc82ce58b1149d651bf185f13d43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 23:10:54 +0900 Subject: [PATCH 47/53] =?UTF-8?q?:recycle:=20refactor:=20=EC=86=8C?= =?UTF-8?q?=EB=82=98=20=ED=81=90=EB=B8=8C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ragback/global/error/GlobalExceptionHandler.kt | 10 +++++++--- .../simplerag/ragback/global/storage/FakeS3Util.kt | 4 +++- .../kotlin/simplerag/ragback/global/util/S3UtilImpl.kt | 7 ++++--- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/simplerag/ragback/global/error/GlobalExceptionHandler.kt b/src/main/kotlin/simplerag/ragback/global/error/GlobalExceptionHandler.kt index efd8fed..0642cd5 100644 --- a/src/main/kotlin/simplerag/ragback/global/error/GlobalExceptionHandler.kt +++ b/src/main/kotlin/simplerag/ragback/global/error/GlobalExceptionHandler.kt @@ -5,13 +5,13 @@ import com.fasterxml.jackson.databind.exc.InvalidFormatException import com.fasterxml.jackson.databind.exc.InvalidNullException import com.fasterxml.jackson.databind.exc.MismatchedInputException import jakarta.validation.ConstraintViolationException +import org.slf4j.LoggerFactory +import org.springframework.http.converter.HttpMessageNotReadableException import org.springframework.web.bind.MethodArgumentNotValidException import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.RestControllerAdvice -import simplerag.ragback.global.response.ApiResponse -import org.slf4j.LoggerFactory -import org.springframework.http.converter.HttpMessageNotReadableException import org.springframework.web.multipart.support.MissingServletRequestPartException +import simplerag.ragback.global.response.ApiResponse @RestControllerAdvice class GlobalExceptionHandler { @@ -45,18 +45,22 @@ class GlobalExceptionHandler { val field = cause.path.lastOrNull()?.fieldName ?: "unknown" "'$field' 값이 비어있습니다." } + is InvalidFormatException -> { val field = cause.path.lastOrNull()?.fieldName ?: "unknown" "'$field' 값 형식이 올바르지 않습니다." } + is MismatchedInputException -> { val field = cause.path.lastOrNull()?.fieldName ?: "unknown" "'$field' 값 타입이 올바르지 않습니다." } + is JsonParseException -> { // JSON 문법 오류 (콤마, 따옴표 누락 등) "JSON 문법이 올바르지 않습니다." } + else -> "유효하지 않은 요청입니다." } diff --git a/src/main/kotlin/simplerag/ragback/global/storage/FakeS3Util.kt b/src/main/kotlin/simplerag/ragback/global/storage/FakeS3Util.kt index a101b56..c701405 100644 --- a/src/main/kotlin/simplerag/ragback/global/storage/FakeS3Util.kt +++ b/src/main/kotlin/simplerag/ragback/global/storage/FakeS3Util.kt @@ -39,7 +39,9 @@ class FakeS3Util : S3Util { store.remove(key) } - override fun delete(key: String) { store.remove(key) } + override fun delete(key: String) { + store.remove(key) + } override fun keyFromUrl(url: String): String? = url.removePrefix("fake://") .removePrefix("/") diff --git a/src/main/kotlin/simplerag/ragback/global/util/S3UtilImpl.kt b/src/main/kotlin/simplerag/ragback/global/util/S3UtilImpl.kt index 6769422..b64fcf3 100644 --- a/src/main/kotlin/simplerag/ragback/global/util/S3UtilImpl.kt +++ b/src/main/kotlin/simplerag/ragback/global/util/S3UtilImpl.kt @@ -13,14 +13,14 @@ import software.amazon.awssdk.services.s3.S3Client import software.amazon.awssdk.services.s3.model.DeleteObjectRequest import software.amazon.awssdk.services.s3.model.PutObjectRequest import java.net.URI -import java.util.UUID +import java.util.* @Component @Profile("!test") class S3UtilImpl( private val s3: S3Client, private val s3Config: S3Config, -): S3Util { +) : S3Util { private val bucket get() = s3Config.bucket private val log = LoggerFactory.getLogger(GlobalExceptionHandler::class.java) @@ -45,7 +45,8 @@ class S3UtilImpl( return urlFromKey(key) } catch (e: software.amazon.awssdk.services.s3.model.S3Exception) { - log.error("S3 putObject fail bucket={}, key={}, status={}, awsCode={}, reqId={}, msg={}", + log.error( + "S3 putObject fail bucket={}, key={}, status={}, awsCode={}, reqId={}, msg={}", bucket, key, e.statusCode(), e.awsErrorDetails()?.errorCode(), e.requestId(), e.awsErrorDetails()?.errorMessage(), e ) From d2d784755dd340c154390d5db0ed8441cce36e2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 23:20:33 +0900 Subject: [PATCH 48/53] =?UTF-8?q?:bug:=20fix:=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=A1=A4=EB=B0=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/error/GlobalExceptionHandler.kt | 53 +++++++++++-------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/src/main/kotlin/simplerag/ragback/global/error/GlobalExceptionHandler.kt b/src/main/kotlin/simplerag/ragback/global/error/GlobalExceptionHandler.kt index 0642cd5..22cc0b4 100644 --- a/src/main/kotlin/simplerag/ragback/global/error/GlobalExceptionHandler.kt +++ b/src/main/kotlin/simplerag/ragback/global/error/GlobalExceptionHandler.kt @@ -1,11 +1,11 @@ package simplerag.ragback.global.error -import com.fasterxml.jackson.core.JsonParseException import com.fasterxml.jackson.databind.exc.InvalidFormatException import com.fasterxml.jackson.databind.exc.InvalidNullException import com.fasterxml.jackson.databind.exc.MismatchedInputException import jakarta.validation.ConstraintViolationException import org.slf4j.LoggerFactory +import org.springframework.http.ResponseEntity import org.springframework.http.converter.HttpMessageNotReadableException import org.springframework.web.bind.MethodArgumentNotValidException import org.springframework.web.bind.annotation.ExceptionHandler @@ -19,27 +19,33 @@ class GlobalExceptionHandler { private val log = LoggerFactory.getLogger(GlobalExceptionHandler::class.java) @ExceptionHandler(MethodArgumentNotValidException::class) - fun handleValidationException(ex: MethodArgumentNotValidException): ApiResponse { + fun handleValidationException(ex: MethodArgumentNotValidException): ResponseEntity> { val message = ex.bindingResult.allErrors.first().defaultMessage ?: "잘못된 요청" - return ApiResponse.fail(ErrorCode.INVALID_INPUT.code, message) + return ResponseEntity + .status(ErrorCode.INVALID_INPUT.status) + .body(ApiResponse.fail(ErrorCode.INVALID_INPUT.code, message)) } @ExceptionHandler(ConstraintViolationException::class) - fun handleConstraintViolation(ex: ConstraintViolationException): ApiResponse { + fun handleConstraintViolation(ex: ConstraintViolationException): ResponseEntity> { val message = ex.constraintViolations.firstOrNull()?.message ?: "잘못된 요청" - return ApiResponse.fail(ErrorCode.INVALID_INPUT.code, message) + return ResponseEntity + .status(ErrorCode.INVALID_INPUT.status) + .body(ApiResponse.fail(ErrorCode.INVALID_INPUT.code, message)) } @ExceptionHandler(MissingServletRequestPartException::class) - fun handleMissingPart(e: MissingServletRequestPartException): ApiResponse { + fun handleMissingPart(e: MissingServletRequestPartException): ResponseEntity> { val msg = "필수 '${e.requestPartName}' 가 없습니다." - return ApiResponse.fail(ErrorCode.FILE_PART_MISSING.code, message = msg) + return ResponseEntity + .status(ErrorCode.FILE_PART_MISSING.status) + .body(ApiResponse.fail(ErrorCode.FILE_PART_MISSING.code, message = msg)) } + @ExceptionHandler(HttpMessageNotReadableException::class) - fun handleUnreadable(e: HttpMessageNotReadableException): ApiResponse { + fun handleUnreadable(e: HttpMessageNotReadableException): ResponseEntity> { val cause = e.cause - val msg = when (cause) { is InvalidNullException -> { val field = cause.path.lastOrNull()?.fieldName ?: "unknown" @@ -56,33 +62,34 @@ class GlobalExceptionHandler { "'$field' 값 타입이 올바르지 않습니다." } - is JsonParseException -> { - // JSON 문법 오류 (콤마, 따옴표 누락 등) - "JSON 문법이 올바르지 않습니다." - } - else -> "유효하지 않은 요청입니다." } - - return ApiResponse.fail(ErrorCode.INVALID_JSON.code, message = msg) + return ResponseEntity + .status(ErrorCode.INVALID_JSON.status) + .body(ApiResponse.fail(ErrorCode.INVALID_JSON.code, message = msg)) } @ExceptionHandler(FileException::class) - fun handleCustomException(ex: FileException): ApiResponse { + fun handleFileException(ex: FileException): ResponseEntity> { val errorCode = ex.errorCode - return ApiResponse.fail(errorCode.code, "${errorCode.message} sha256: ${ex.message}") + return ResponseEntity + .status(errorCode.status) + .body(ApiResponse.fail(errorCode.code, "${errorCode.message} sha256: ${ex.message}")) } @ExceptionHandler(CustomException::class) - fun handleCustomException(ex: CustomException): ApiResponse { + fun handleCustomException(ex: CustomException): ResponseEntity> { val errorCode = ex.errorCode - return ApiResponse.fail(errorCode.code, errorCode.message) + return ResponseEntity + .status(errorCode.status) + .body(ApiResponse.fail(errorCode.code, errorCode.message)) } @ExceptionHandler(Exception::class) - fun handleGeneralException(ex: Exception): ApiResponse { + fun handleGeneralException(ex: Exception): ResponseEntity> { log.error("Unhandled exception", ex) - - return ApiResponse.fail(ErrorCode.INTERNAL_ERROR.code, ErrorCode.INTERNAL_ERROR.message) + return ResponseEntity + .status(ErrorCode.INTERNAL_ERROR.status) + .body(ApiResponse.fail(ErrorCode.INTERNAL_ERROR.code, ErrorCode.INTERNAL_ERROR.message)) } } From efe93b19778525cb9183ce341e3cca4a8b8d44e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 23:22:47 +0900 Subject: [PATCH 49/53] =?UTF-8?q?:bug:=20fix:=20=ED=82=A4=20null=20?= =?UTF-8?q?=EA=B0=92=20=ED=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/simplerag/ragback/global/util/S3UtilImpl.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/simplerag/ragback/global/util/S3UtilImpl.kt b/src/main/kotlin/simplerag/ragback/global/util/S3UtilImpl.kt index b64fcf3..c657152 100644 --- a/src/main/kotlin/simplerag/ragback/global/util/S3UtilImpl.kt +++ b/src/main/kotlin/simplerag/ragback/global/util/S3UtilImpl.kt @@ -90,7 +90,13 @@ class S3UtilImpl( val prefix = dir.trim('/') - return "$prefix/${UUID.randomUUID()}_$cleanName" + val key = if (prefix.isBlank()) { + "${UUID.randomUUID()}_$cleanName" + } else { + "$prefix/${UUID.randomUUID()}_$cleanName" + } + + return key } override fun keyFromUrl(url: String): String? { From 57bc8ab07e72e7c7914afa6cb98e0bbcb934fc64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 23:23:57 +0900 Subject: [PATCH 50/53] =?UTF-8?q?:recycle:=20refactor:=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=EB=AA=85=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ragback/domain/document/service/DataFileService.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt index 2fdd439..84c77c1 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt @@ -51,7 +51,7 @@ class DataFileService( val metrics = file.computeMetricsStreaming() val sha256 = metrics.sha256 - val sizeByte = metrics.sizeByte + val sizeBytes = metrics.sizeByte val type = file.resolveContentType() if (dataFileRepository.existsBySha256(sha256)) { @@ -62,7 +62,7 @@ class DataFileService( uploadedUrls += fileUrl val dataFile = try { - dataFileRepository.save(DataFile(meta.title, type, sizeByte, sha256, fileUrl, now, now)) + dataFileRepository.save(DataFile(meta.title, type, sizeBytes, sha256, fileUrl, now, now)) } catch (ex: DataIntegrityViolationException) { throw FileException(ErrorCode.ALREADY_FILE, sha256) } From c2c0508d126b656b0bbd2d462979303230de6299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 23:29:02 +0900 Subject: [PATCH 51/53] =?UTF-8?q?:recycle:=20refactor:=20N=20+=201=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../document/repository/TagRepository.kt | 2 ++ .../domain/document/service/DataFileService.kt | 18 +++++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/simplerag/ragback/domain/document/repository/TagRepository.kt b/src/main/kotlin/simplerag/ragback/domain/document/repository/TagRepository.kt index 6e3c35e..fd15dc6 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/repository/TagRepository.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/repository/TagRepository.kt @@ -6,4 +6,6 @@ import simplerag.ragback.domain.document.entity.Tag interface TagRepository : JpaRepository { fun findByName(name: String): Tag? + + fun findByNameIn(names: Collection): List } diff --git a/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt index 84c77c1..390a58a 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt @@ -89,13 +89,21 @@ class DataFileService( } - private fun getOrCreateTags(names: List): List = - names.map { it.trim().uppercase() } + private fun getOrCreateTags(names: List): List { + val normalized = names.map { it.trim().uppercase() } .filter { it.isNotEmpty() } .distinct() - .map { name -> - tagRepository.findByName(name) ?: tagRepository.save(Tag(name = name)) - } + + val existing = tagRepository.findByNameIn(normalized) + val existingMap = existing.associateBy { it.name } + + val toCreate = normalized.filterNot { existingMap.containsKey(it) } + .map { Tag(name = it) } + + val saved = if (toCreate.isNotEmpty()) tagRepository.saveAll(toCreate) else emptyList() + + return existing + saved + } private fun attachTagsIfMissing(dataFile: DataFile, tags: List) { val fileId = dataFile.id ?: return From c90c81ddc1815e707dff27b9647d5a29807b20f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 23:41:21 +0900 Subject: [PATCH 52/53] =?UTF-8?q?:recycle:=20refactor:=20uppercase=20?= =?UTF-8?q?=ED=99=95=EC=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ragback/domain/document/service/DataFileService.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt index 390a58a..ffe3028 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt @@ -23,6 +23,7 @@ import simplerag.ragback.global.util.S3Util import simplerag.ragback.global.util.computeMetricsStreaming import simplerag.ragback.global.util.resolveContentType import java.time.LocalDateTime +import java.util.* @Service class DataFileService( @@ -90,7 +91,7 @@ class DataFileService( private fun getOrCreateTags(names: List): List { - val normalized = names.map { it.trim().uppercase() } + val normalized = names.map { it.trim().uppercase(Locale.ROOT) } .filter { it.isNotEmpty() } .distinct() From 639ffdb495d478171ee91c1658c1dbcd04b69219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A4=80=ED=99=98?= Date: Sat, 16 Aug 2025 23:43:21 +0900 Subject: [PATCH 53/53] =?UTF-8?q?:sparkles:=20feature:=20=EB=8F=99?= =?UTF-8?q?=EC=8B=9C=EC=84=B1=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../document/service/DataFileService.kt | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt index ffe3028..bcde47d 100644 --- a/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt +++ b/src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt @@ -91,21 +91,34 @@ class DataFileService( private fun getOrCreateTags(names: List): List { - val normalized = names.map { it.trim().uppercase(Locale.ROOT) } + val normalized = names + .map { it.trim().uppercase(Locale.ROOT) } .filter { it.isNotEmpty() } .distinct() + if (normalized.isEmpty()) return emptyList() + val existing = tagRepository.findByNameIn(normalized) - val existingMap = existing.associateBy { it.name } + val existingByName = existing.associateBy { it.name } - val toCreate = normalized.filterNot { existingMap.containsKey(it) } + val toCreate = normalized + .asSequence() + .filter { it !in existingByName } .map { Tag(name = it) } + .toList() - val saved = if (toCreate.isNotEmpty()) tagRepository.saveAll(toCreate) else emptyList() + val created = if (toCreate.isNotEmpty()) { + try { + tagRepository.saveAllAndFlush(toCreate) + } catch (ex: DataIntegrityViolationException) { + tagRepository.findByNameIn(toCreate.map { it.name }) + } + } else emptyList() - return existing + saved + return existing + created } + private fun attachTagsIfMissing(dataFile: DataFile, tags: List) { val fileId = dataFile.id ?: return tags.forEach { tag ->