Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
4b863c3
:rocket: chore: 엔티티 추가
catturtle123 Aug 15, 2025
4d6cffc
:rocket: chore: S3 세
catturtle123 Aug 16, 2025
c712b21
:rocket: chore: Swagger 세팅
catturtle123 Aug 16, 2025
9ab10be
:rocket: chore: 에러 추가
catturtle123 Aug 16, 2025
4fb07df
:rocket: chore: 엔티티 수정
catturtle123 Aug 16, 2025
3cc591c
:sparkles: feat: 파일 업로드 기능 추가
catturtle123 Aug 16, 2025
e2c2405
:test_tube: test: 파일 업로드 테스트 코드 작성
catturtle123 Aug 16, 2025
c58b677
:recycle: refactor: DataFileService 리팩토
catturtle123 Aug 16, 2025
9f09266
:recycle: refactor: 파일 관련 유틸 함수 리팩토
catturtle123 Aug 16, 2025
fab22d9
:recycle: refactor: DTO 이름 변
catturtle123 Aug 16, 2025
40fc893
:rocket: chore: AWS build 모듈화
catturtle123 Aug 16, 2025
63a1063
:recycle: refactor: 응답 값 명시
catturtle123 Aug 16, 2025
28f0acd
:bug: fix: Valid 추가
catturtle123 Aug 16, 2025
3c34883
:recycle: refactor: title 길이 제한
catturtle123 Aug 16, 2025
d74cbf9
:bug: fix: data 클래스 삭제
catturtle123 Aug 16, 2025
39d3b6f
:bug: fix: MB 기준을 byte로 변경 (Double -> Long)
catturtle123 Aug 16, 2025
51f580a
:recycle: refactor: 엔티티 검증 강화
catturtle123 Aug 16, 2025
b725fb4
:recycle: refactor: Elvis 연산자 적
catturtle123 Aug 16, 2025
6ef1473
:bug: fix: S3 트랜잭션 롤백 동기화
catturtle123 Aug 16, 2025
db60ddd
:recycle: refactor: 추상화 단계 맞추
catturtle123 Aug 16, 2025
6ff7de9
:bug: fix: byte size 저장 시 stream을 사용하여 메모리 절
catturtle123 Aug 16, 2025
eaead1c
:recycle: refactor: swagger 함수명 변경
catturtle123 Aug 16, 2025
0b66cde
:recycle: refactor: swagger 토큰 포멧 변경
catturtle123 Aug 16, 2025
59b5579
:recycle: refactor: error 원인 파악용 cause 추
catturtle123 Aug 16, 2025
fb305fb
:recycle: refactor: ErrorCode 공백 제거
catturtle123 Aug 16, 2025
6a564ab
:recycle: refactor: 에러 로그
catturtle123 Aug 16, 2025
3b5948f
:recycle: refactor: 중복 값 식별자 sha256으로 변경
catturtle123 Aug 16, 2025
094cecd
:recycle: refactor: fake 파일 단일화
catturtle123 Aug 16, 2025
475236a
:recycle: refactor: keyFromUrl 수정
catturtle123 Aug 16, 2025
b0fd4a8
:recycle: refactor: 파일 포멧 증
catturtle123 Aug 16, 2025
f5c04e3
:recycle: refactor: !! 제거
catturtle123 Aug 16, 2025
913a2b1
:recycle: refactor: 세미콜론 제거
catturtle123 Aug 16, 2025
fe87736
:rocket: chore: 프로파일 나누기
catturtle123 Aug 16, 2025
72de563
:recycle: recycle: 태그 대문자화
catturtle123 Aug 16, 2025
2e3978e
:bug: fix: test 변
catturtle123 Aug 16, 2025
62f99de
:bug: fix: 데이터 중복시 에러 잡기
catturtle123 Aug 16, 2025
729826a
:recycle: refactor: 에러 로그 남기
catturtle123 Aug 16, 2025
392fa4a
:recycle: refactor: deleteByUrl 개선
catturtle123 Aug 16, 2025
8594135
:recycle: refactor: multipart 전용 consumes, JSON produces 명시
catturtle123 Aug 16, 2025
b8ab8db
:recycle: refactor: byte 이름 변경
catturtle123 Aug 16, 2025
61cdb85
:recycle: refactor: contentType 분리
catturtle123 Aug 16, 2025
de5b382
:recycle: refactor: service 명 변경
catturtle123 Aug 16, 2025
6f38323
:recycle: refactor: request dto null 해
catturtle123 Aug 16, 2025
9f069b2
:recycle: refactor: exception handler 제거
catturtle123 Aug 16, 2025
484fdf6
:recycle: refactor: 에러 코드 위치 통
catturtle123 Aug 16, 2025
1c992c4
:recycle: refactor: 소나 큐브 정
catturtle123 Aug 16, 2025
f4ee29b
:recycle: refactor: 소나 큐브 정리
catturtle123 Aug 16, 2025
d2d7847
:bug: fix: 에러 코드 롤백
catturtle123 Aug 16, 2025
efe93b1
:bug: fix: 키 null 값 해
catturtle123 Aug 16, 2025
57bc8ab
:recycle: refactor: 변수명 통일
catturtle123 Aug 16, 2025
c2c0508
:recycle: refactor: N + 1 문제 해
catturtle123 Aug 16, 2025
c90c81d
:recycle: refactor: uppercase 확실
catturtle123 Aug 16, 2025
639ffdb
:sparkles: feature: 동시성 문제 해
catturtle123 Aug 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -41,8 +42,17 @@ 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'

// s3
implementation(platform("software.amazon.awssdk:bom:2.25.70"))
implementation("software.amazon.awssdk:s3")
}

dependencyManagement {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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
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")
@Validated
class DataFileController(
private val dataFileService: DataFileService
) {

@PostMapping(
consumes = [
MediaType.MULTIPART_FORM_DATA_VALUE,
]
)
@ResponseStatus(HttpStatus.CREATED)
fun upload(
@RequestPart("files")
@Size(min = 1, message = "최소 하나 이상 업로드해야 합니다")
files: List<MultipartFile>,

@Parameter(content = [Content(mediaType = MediaType.APPLICATION_JSON_VALUE)])
@RequestPart("request")
@Valid
req: DataFileBulkCreateRequest
): ApiResponse<DataFileResponseList> {
val saved = dataFileService.upload(files, req)
return ApiResponse.ok(saved, "업로드 완료")
Comment on lines +40 to +41
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

(선택) Location 헤더 설정 고려

다건 생성이지만 컬렉션 조회 엔드포인트를 Location으로 제공하면 REST 완결성이 올라갑니다. 현재는 @ResponseStatus로 201만 반환하므로, 원하시면 ResponseEntity.created(...)로 Location 헤더를 추가하는 형태로 바꾸는 스니펫을 드릴 수 있습니다.

🤖 Prompt for AI Agents
In
src/main/kotlin/simplerag/ragback/domain/document/controller/DataFileController.kt
around lines 40-41, the upload handler currently returns ApiResponse with
@ResponseStatus 201 but does not set a Location header; change the method to
return ResponseEntity<ApiResponse> and construct a Location URI pointing to the
collection retrieval endpoint (e.g., /api/data-files or the controller's list
path) using ServletUriComponentsBuilder or UriComponentsBuilder, then return
ResponseEntity.created(locationUri).body(ApiResponse.ok(saved, "업로드 완료")) so the
response includes the 201 status, Location header, and the body.

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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<DataFileCreateItem>
)

data class DataFileCreateItem(
@field:NotBlank(message = "title은 비어있을 수 없습니다")
@field:Size(max = 100)
val title: String,

@field:Size(max = 10, message = "태그는 최대 10개까지 가능합니다")
val tags: List<String> = emptyList()
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package simplerag.ragback.domain.document.dto

data class DataFileResponseList(
val dataFilePreviewResponseList: List<DataFilePreviewResponse>,
)
Comment on lines +3 to +5
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

응답 목록 필드명 간결화 제안: items로 축약

현재 dataFilePreviewResponseList는 중복된 의미가 반복되어 가독성이 떨어집니다. API 응답 스키마에서도 items 같은 관용적 이름이 더 읽기 좋습니다. 하위 영향 범위가 허용된다면 다음처럼 변경을 권장합니다.

-data class DataFileResponseList(
-    val dataFilePreviewResponseList: List<DataFilePreviewResponse>,
-)
+data class DataFileResponseList(
+    val items: List<DataFilePreviewResponse>,
+)

호출부 및 테스트의 필드 접근도 함께 정리해야 합니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
data class DataFileResponseList(
val dataFilePreviewResponseList: List<DataFilePreviewResponse>,
)
data class DataFileResponseList(
val items: List<DataFilePreviewResponse>,
)
🤖 Prompt for AI Agents
In src/main/kotlin/simplerag/ragback/domain/document/dto/DataFileResponseDTO.kt
around lines 3 to 5, rename the DTO property dataFilePreviewResponseList to a
concise items to improve readability and API ergonomics; update the data class
definition accordingly, then refactor all call sites and unit/integration tests
to use the new items property name; if your JSON mapper requires explicit names
(e.g., Jackson/Moshi/kotlinx.serialization), add the appropriate serialization
annotation to preserve the external field name or map to "items" to avoid
breaking clients.


data class DataFilePreviewResponse(
val id: Long,
val sha256: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package simplerag.ragback.domain.document.entity

import jakarta.persistence.*
import java.time.LocalDateTime

@Entity
@Table(
name = "data_file",
uniqueConstraints = [UniqueConstraint(columnNames = ["sha256"])]
)
class DataFile(

@Column(nullable = false, length = 255)
val title: String,

@Column(name = "file_type", nullable = false, length = 120)
val type: String,

@Column(name = "size_bytes", nullable = false)
val sizeBytes: Long,

@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)
val id: Long? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package simplerag.ragback.domain.document.entity

import jakarta.persistence.*

@Entity
@Table(
name = "data_file_tags",
uniqueConstraints = [UniqueConstraint(columnNames = ["data_file_id", "tag_id"])]
)
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,
)
17 changes: 17 additions & 0 deletions src/main/kotlin/simplerag/ragback/domain/document/entity/Tag.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package simplerag.ragback.domain.document.entity

import jakarta.persistence.*

@Entity
@Table(
name = "tags",
uniqueConstraints = [UniqueConstraint(columnNames = ["name"])]
)
class Tag(

@Column(nullable = false, length = 60)
val name: String,

@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
)
Original file line number Diff line number Diff line change
@@ -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<DataFile, Long> {
fun existsBySha256(sha256: String): Boolean
}
Comment on lines +6 to +8
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

대량 업로드 시 N+1 exists 호출 최소화를 위한 배치 조회 메서드 제안

여러 파일을 한 번에 업로드한다면 existsBySha256의 반복 호출보다 IN 조건으로 한 번에 조회하는 것이 효율적입니다. 아래 메서드를 추가해 서비스에서 중복 해시를 미리 한꺼번에 필터링하는 방법을 권장합니다.

 interface DataFileRepository : JpaRepository<DataFile, Long> {
     fun existsBySha256(sha256: String): Boolean
+    fun findAllBySha256In(hashes: Collection<String>): List<DataFile>
 }

서비스에서는 요청된 해시 집합을 기준으로 DB에 이미 존재하는 해시를 미리 회수해 중복을 제외한 뒤 저장하세요.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
interface DataFileRepository : JpaRepository<DataFile, Long> {
fun existsBySha256(sha256: String): Boolean
}
interface DataFileRepository : JpaRepository<DataFile, Long> {
fun existsBySha256(sha256: String): Boolean
fun findAllBySha256In(hashes: Collection<String>): List<DataFile>
}
🤖 Prompt for AI Agents
In
src/main/kotlin/simplerag/ragback/domain/document/repository/DataFileRepository.kt
around lines 6-8, add a batch lookup method (e.g., findAllBySha256In or
findSha256BySha256In) that accepts a collection of sha256 strings and returns
the matching DB entries or their sha256 values; in the service layer call this
once with the full set of incoming hashes, build a set of existing hashes from
the result, and filter out duplicates before saving to avoid N+1 exists calls
during bulk upload.

💡 Verification agent

🧩 Analysis chain

exists-then-insert 경쟁 상태 가능 — 유니크 제약 위반 예외를 서비스 레이어에서 매핑하세요

sha256 중복 방지를 existsBySha256로 선확인한 뒤 저장하면, 동시 업로드 상황에서 레이스로 인해 DB 유니크 제약 위반이 발생할 수 있습니다. 서비스에서 DataIntegrityViolationException 등을 캐치해 ALREADY_FILE로 매핑하는 처리가 필요합니다. 엔티티(또는 테이블)에 sha256 유니크 인덱스가 반드시 있어야 합니다.

원하시면 DataFileService에 try/catch 템플릿을 제안드리겠습니다.


🏁 Script executed:

#!/bin/bash
set -e
echo "Searching for DataFile entity..."
rg -nP "class\s+DataFile\b" -g "*.kt"
echo "Searching for `sha256` field in the entity (with or without unique constraint)..."
rg -nP "sha256\s*[:=]" -g "*.kt" -C2
echo "Searching for `DataFileService` class..."
rg -nP "class\s+DataFileService\b" -g "*.kt"
echo "Searching for exception handling of DataIntegrityViolationException in service layer..."
rg -nP "DataIntegrityViolationException" -g "*.kt" || echo "No DataIntegrityViolationException catch blocks found"

Length of output: 3691


🏁 Script executed:

#!/bin/bash
set -e
echo "Checking for @Table annotation on DataFile entity..."
rg -n "@Table" -g "src/main/kotlin/simplerag/ragback/domain/document/entity/DataFile.kt" -C2 || echo "No @Table annotation found"
echo "Checking for unique constraint on sha256 field..."
rg -n "unique" -g "src/main/kotlin/simplerag/ragback/domain/document/entity/DataFile.kt" -C2 || echo "No unique constraint found"

Length of output: 1360


DataIntegrityViolationException 캐치 및 ALREADY_FILE 매핑 필수

현재 DataFile 엔티티에 SHA-256에 대한 유니크 제약(@Table(uniqueConstraints = …))이 설정되어 있어, existsBySha256 사전 확인만으로는 동시 업로드 시 레이스로 유니크 제약 위반 예외가 발생할 수 있습니다. 서비스 계층에서 저장 로직을 try/catch로 감싸고, DataIntegrityViolationException을 캐치해 ALREADY_FILE로 매핑해주세요.

수정 대상:

  • 파일: src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt
  • 위치: dataFileRepository.save(dataFile) 호출부 근처

제안 코드 예시:

try {
    val saved = dataFileRepository.save(dataFile)
    // 필요에 따라 saved 사용
} catch (e: DataIntegrityViolationException) {
    // 이미 저장된 파일인 경우를 의미하는 비즈니스 예외로 매핑
    throw CustomException(ErrorCode.ALREADY_FILE, "이미 등록된 파일입니다.", e)
}
  • 위와 같이 예외를 명시적으로 처리하면, 유니크 제약 위반 시 서비스가 500 에러 대신 ALREADY_FILE로 응답합니다.
  • 필요하시면 DataFileService 전체 메서드에 적용할 수 있는 try/catch 템플릿을 추가로 제안드리겠습니다.
🤖 Prompt for AI Agents
In src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt
near the dataFileRepository.save(dataFile) call (where existsBySha256 is
currently used for pre-check), wrap the save call in a try/catch that catches
org.springframework.dao.DataIntegrityViolationException and rethrows a business
exception mapped to ErrorCode.ALREADY_FILE (e.g., throw
CustomException(ErrorCode.ALREADY_FILE, "이미 등록된 파일입니다.", e)). Ensure the catch
only maps unique-constraint violations to ALREADY_FILE and rethrow other
unexpected exceptions unchanged; keep the original flow when save succeeds.

Original file line number Diff line number Diff line change
@@ -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<DataFileTag, Long> {
fun existsByDataFileIdAndTagId(dataFileId: Long, tagId: Long): Boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package simplerag.ragback.domain.document.repository

import org.springframework.data.jpa.repository.JpaRepository
import simplerag.ragback.domain.document.entity.Tag

interface TagRepository : JpaRepository<Tag, Long> {

fun findByName(name: String): Tag?

fun findByNameIn(names: Collection<String>): List<Tag>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
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
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
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 simplerag.ragback.global.util.computeMetricsStreaming
import simplerag.ragback.global.util.resolveContentType
import java.time.LocalDateTime
import java.util.*

@Service
class DataFileService(
private val dataFileRepository: DataFileRepository,
private val tagRepository: TagRepository,
private val dataFileTagRepository: DataFileTagRepository,
private val s3Util: S3Util,
) {

@Transactional
fun upload(
files: List<MultipartFile>,
req: DataFileBulkCreateRequest
): DataFileResponseList {
if (files.isEmpty() || files.size != req.items.size) {
throw CustomException(ErrorCode.INVALID_INPUT)
}

val now = LocalDateTime.now()
val uploadedUrls = mutableListOf<String>()

registerRollbackCleanup(uploadedUrls)

val responses = files.mapIndexed { idx, file ->
val meta = req.items[idx]

val metrics = file.computeMetricsStreaming()
val sha256 = metrics.sha256
val sizeBytes = metrics.sizeByte
val type = file.resolveContentType()

if (dataFileRepository.existsBySha256(sha256)) {
throw FileException(ErrorCode.ALREADY_FILE, sha256)
}

val fileUrl = s3Util.upload(file, S3Type.ORIGINAL_FILE)
uploadedUrls += fileUrl

val dataFile = try {
dataFileRepository.save(DataFile(meta.title, type, sizeBytes, sha256, fileUrl, now, now))
} catch (ex: DataIntegrityViolationException) {
throw FileException(ErrorCode.ALREADY_FILE, sha256)
}

val tags = getOrCreateTags(meta.tags)
attachTagsIfMissing(dataFile, tags)

DataFilePreviewResponse(requireNotNull(dataFile.id), dataFile.sha256)
}

return DataFileResponseList(responses)
}

private fun registerRollbackCleanup(uploadedUrls: MutableList<String>) {
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(object : TransactionSynchronization {
override fun afterCompletion(status: Int) {
if (status == TransactionSynchronization.STATUS_ROLLED_BACK) {
uploadedUrls.forEach { runCatching { s3Util.deleteByUrl(it) } }
}
}
})
}
}
Comment on lines +80 to +90
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

롤백 보상삭제 시 예외 삼킴 — 최소한 경고 로그 남기기

보상 삭제에서 예외를 무시하면(삼키면) S3에 고아 오브젝트가 남아도 추적이 어렵습니다. 경고 로그를 추가해 가시성을 높이세요.

-                        uploadedUrls.forEach { runCatching { s3Util.deleteByUrl(it) } }
+                        uploadedUrls.forEach { url ->
+                            runCatching { s3Util.deleteByUrl(url) }
+                                .onFailure { e ->
+                                    // TODO: 클래스에 logger 추가 후 아래 라인 사용
+                                    // log.warn("Rollback cleanup failed for S3 object: {}", url, e)
+                                }
+                        }

필요 시 클래스 상단에 로거를 추가하세요:

private val log = org.slf4j.LoggerFactory.getLogger(DataFileService::class.java)
🤖 Prompt for AI Agents
In src/main/kotlin/simplerag/ragback/domain/document/service/DataFileService.kt
around lines 76 to 86, the rollback cleanup currently swallows exceptions when
deleting uploaded S3 URLs; change it to log a warning for any deletion failure
so orphaned objects are visible; add a private logger at the top of the class if
missing (private val log =
org.slf4j.LoggerFactory.getLogger(DataFileService::class.java)) and replace the
runCatching { s3Util.deleteByUrl(it) } with runCatching { s3Util.deleteByUrl(it)
}.onFailure { log.warn("Failed to delete rollback S3 object for url={}",
itMessageOrUrl, it) } (or equivalent try/catch that logs warn with the url and
exception).



private fun getOrCreateTags(names: List<String>): List<Tag> {
val normalized = names
.map { it.trim().uppercase(Locale.ROOT) }
.filter { it.isNotEmpty() }
.distinct()

if (normalized.isEmpty()) return emptyList()

val existing = tagRepository.findByNameIn(normalized)
val existingByName = existing.associateBy { it.name }

val toCreate = normalized
.asSequence()
.filter { it !in existingByName }
.map { Tag(name = it) }
.toList()

val created = if (toCreate.isNotEmpty()) {
try {
tagRepository.saveAllAndFlush(toCreate)
} catch (ex: DataIntegrityViolationException) {
tagRepository.findByNameIn(toCreate.map { it.name })
}
} else emptyList()

return existing + created
}


private fun attachTagsIfMissing(dataFile: DataFile, tags: List<Tag>) {
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))
}
}
}

}
Loading