Skip to content

Conversation

@coehgns
Copy link
Contributor

@coehgns coehgns commented Sep 26, 2025

Summary by CodeRabbit

  • New Features

    • 사진 업로드 API 추가(POST /photo): 이미지 업로드 시 photo_url 반환
    • 지원 확장자: JPG, JPEG, PNG, HEIC
    • 지원자 상세 응답에 사진 경로(주소) 포함
  • Error Handling

    • 유효하지 않은 확장자 업로드 시 400 오류 및 안내 메시지 반환
  • Infrastructure

    • AWS S3 연동으로 이미지 저장 및 접근 URL 생성
    • 보안 컨텍스트 기반 사용자별 사진 관리
  • Chores

    • S3 SDK 의존성 추가 및 클라우드 설정 구성

@coehgns coehgns self-assigned this Sep 26, 2025
@coderabbitai
Copy link

coderabbitai bot commented Sep 26, 2025

Important

Review skipped

Auto reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

사진 업로드 및 조회를 위한 파일 처리 기능을 도메인/인프라 전반에 추가. S3 연동(설정/어댑터), 업로드/URL 생성 포트 정의, 컨트롤러 및 유스케이스 구현, 이미지 확장자 검증/변환 유틸리티, 파일 관련 예외 계층 도입, DTO/엔티티/리포지토리 확장 및 의존성 추가.

Changes

Cohort / File(s) Summary of Changes
도메인 경로 상수
casper-application-domain/.../domain/file/object/PathList.kt
PathList 객체 추가, PHOTO = "entry_photo/" 상수 정의
파일 SPI 포트
.../domain/file/spi/GenerateFileUrlPort.kt, .../domain/file/spi/UploadFilePort.kt
URL 생성 및 업로드 포트 인터페이스 신설
글로벌 예외 베이스
casper-application-domain/.../global/exception/WebException.kt
HTTP 스타일 상태/메시지를 담는 WebException 추상 클래스 추가
빌드 설정
casper-application-infrastructure/build.gradle.kts
AWS S3 SDK 의존성 추가 (com.amazonaws:aws-java-sdk-s3:1.12.767)
JPA 엔티티/리포지토리
.../entity/ApplicationJpaEntity.kt, .../entity/PhotoJpaEntity.kt, .../repository/PhotoJpaRepository.kt
ApplicationJpaEntity.photoPathvar로 변경; PhotoJpaEntityPhotoJpaRepository 추가
프레젠테이션 계층
.../presentation/FileController.kt, .../presentation/dto/response/ApplicationDetailResponse.kt
POST /photo 업로드 엔드포인트 추가; 응답 DTO에 photoPath 필드 추가
유스케이스
.../usecase/ApplicationQueryUseCase.kt, .../usecase/FileUploadUseCase.kt
조회 시 사용자 사진 URL 생성/주입 로직 추가; 업로드 유스케이스 신설(보안 컨텍스트 기반 저장/갱신)
파일 변환/검증 유틸
.../file/presentation/converter/FileConverter.kt, .../converter/FileExtensions.kt, .../converter/ImageFileConverter.kt
이미지 확장자(JPG/JPEG/PNG/HEIC) 검증, 멀티파트 → File 변환 로직 추가
파일 예외
.../file/presentation/exception/WebFileExceptions.kt
InvalidExtension(400) 등 파일 전용 WebException 파생 예외 추가
AWS 설정/프로퍼티
.../global/config/AwsS3Config.kt, .../global/storage/AwsProperties.kt, .../global/storage/AwsCredentialsProperties.kt
S3 클라이언트 빈 구성 및 버킷/리전/자격증명 프로퍼티 바인딩 추가
AWS 어댑터
.../global/storage/AwsS3Adapter.kt
UploadFilePort, GenerateFileUrlPort 구현: S3 업로드, 공개 URL/프리사인드 URL 생성

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant C as FileController
  participant Conv as ImageFileConverter
  participant UC as FileUploadUseCase
  participant Sec as SecurityAdapter
  participant Repo as PhotoJpaRepository
  participant S3 as AwsS3Adapter

  U->>C: POST /photo (Multipart image)
  C->>Conv: transferTo(multipartFile)
  alt 유효 확장자
    Conv-->>C: File
    C->>UC: execute(file)
    UC->>Sec: currentUserId()
    Sec-->>UC: userId
    UC->>S3: upload(file, PathList.PHOTO)
    S3-->>UC: s3Url
    UC->>Repo: findByUserId(userId)
    Repo-->>UC: PhotoJpaEntity?
    UC->>Repo: save(new/updated PhotoJpaEntity)
    UC-->>C: s3Url
    C-->>U: 200 {"photo_url": s3Url}
  else 잘못된 확장자
    Conv-->>C: throws InvalidExtension(400)
    C-->>U: 400 Error
  end
Loading
sequenceDiagram
  autonumber
  actor U as User
  participant AQ as ApplicationQueryUseCase
  participant Sec as SecurityAdapter
  participant Repo as PhotoJpaRepository
  participant URL as GenerateFileUrlPort

  U->>AQ: getApplicationById(...)
  AQ->>Sec: currentUser()
  Sec-->>AQ: user
  AQ->>Repo: findByUserId(user.id)
  Repo-->>AQ: PhotoJpaEntity?
  alt 사진 있음
    AQ->>URL: generateFileUrl(fileName=photo, path=PathList.PHOTO)
    URL-->>AQ: presignedUrl
    AQ-->>U: ApplicationDetail(photoPath=presignedUrl)
  else 없음
    AQ-->>U: ApplicationDetail(photoPath=null)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

귀가 쫑긋, 코가 납작, 깡총 점프 두 번 반!
사진은 S3로, URL은 토닥-토닥 출발한다.
JPG든 PNG든, 검증 OK면 통과🐰
새 길 PathList 따라, 프리사인드 반짝!
오늘도 깃발 펄럭, 배포 들판을 달린다.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title Check ❓ Inconclusive 제목 “upload file”은 PR에서 구현된 파일 업로드 기능을 지칭하나 이미지 업로드, URL 생성, AWS S3 연동 등 주요 변경 사항의 구체적인 범위를 설명하지 않아 PR의 목적과 내용을 한눈에 파악하기 어렵습니다. 또한 소문자로만 구성되어 가독성과 일관성 측면에서 아쉬움이 있습니다. PR 목록을 빠르게 훑는 동료 개발자가 해당 변경 사항의 전체 맥락을 이해하기에는 정보가 부족합니다. PR의 주요 기능을 반영하는 보다 구체적이고 명확한 제목으로 수정해 주세요. 예를 들어 “AWS S3 기반 이미지 업로드 및 URL 생성 기능 추가” 또는 “파일 업로드 및 presigned URL 생성 구현”과 같이 변경하면 PR 검토 시 이해도가 크게 향상될 것입니다.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coehgns
Copy link
Contributor Author

coehgns commented Sep 26, 2025

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Sep 26, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (11)
casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/file/spi/UploadFilePort.kt (1)

3-7: 도메인 포트에서 java.io.File 의존 — 스트리밍 기반 서명으로 전환 권장

대용량 처리/메모리 효율/테스트 용이성을 위해 File 대신 InputStream(+메타데이터)로 받는 것이 바람직합니다. 도메인 계층의 로컬 파일시스템 의존도도 줄일 수 있습니다.

다음과 같이 서명 변경을 제안합니다:

-import java.io.File
+import java.io.InputStream

 interface UploadFilePort {
-    fun upload(file: File, path: String): String
+    /**
+     * 파일 스트림과 메타데이터를 업로드하고 '객체 키'를 반환합니다.
+     */
+    fun upload(
+        input: InputStream,
+        path: String,
+        filename: String,
+        contentType: String? = null,
+    ): String
 }
casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/usecase/FileUploadUseCase.kt (3)

18-22: 외부 I/O(S3 업로드)를 트랜잭션 안에서 수행 — 트랜잭션 경계 분리 권장

업로드는 네트워크 I/O이며 지연/재시도/실패 가능성이 큽니다. DB 트랜잭션을 불필요하게 오래 잡아두지 않도록 업로드를 트랜잭션 밖에서 수행하고, DB 갱신만 트랜잭션으로 묶는 구조가 안전합니다.

아래처럼 @transactional을 제거하고, DB 갱신 전용 메서드를 분리하는 방식을 권장합니다:

-    @Transactional
     fun execute(file: File): String {
         val userId = securityAdapter.getCurrentUserId()
-        val photoUrl = uploadFilePort.upload(file, PathList.PHOTO)
+        val photoKey = uploadFilePort.upload(file, PathList.PHOTO)

클래스 내부에 다음 메서드를 추가:

import org.springframework.transaction.annotation.Transactional
import java.util.UUID

@Transactional
private fun upsertPhoto(userId: UUID, photoKey: String) {
    photoJpaRepository.findByUserId(userId)?.apply {
        photo = photoKey // 더티 체킹
    } ?: photoJpaRepository.save(
        PhotoJpaEntity(
            userId = userId,
            photo = photoKey
        )
    )
}

그리고 execute 내에서 업로드 후 upsertPhoto 호출로 마무리:

upsertPhoto(userId, photoKey)
return photoKey

21-31: 저장값/변수명 불일치 가능성(photoUrl) — ‘키’ 저장으로 정렬 및 불필요한 save 제거

photoUrl이라는 이름은 URL 저장으로 오해될 수 있습니다. 엔티티 컬럼명이 photo_path라면 ‘키’를 저장하는 것이 자연스럽습니다. 또한 JPA 트랜잭션 내에서는 필드 변경만으로 더티 체킹이 적용되므로 기존 엔티티에 대해 save 호출은 불필요합니다.

아래처럼 변수명을 photoKey로, 더티 체킹을 활용하도록 제안합니다:

-        val photoUrl = uploadFilePort.upload(file, PathList.PHOTO)
+        val photoKey = uploadFilePort.upload(file, PathList.PHOTO)

-        photoJpaRepository.findByUserId(userId)?.apply {
-            photo = photoUrl
-            photoJpaRepository.save(this)
-        } ?: photoJpaRepository.save(
+        photoJpaRepository.findByUserId(userId)?.apply {
+            photo = photoKey
+        } ?: photoJpaRepository.save(
             PhotoJpaEntity(
                 userId = userId,
-                photo = photoUrl
+                photo = photoKey
             )
         )

-        return photoUrl
+        return photoKey

추가 확인 요청:

  • UploadFilePort.upload가 실제로 ‘URL’이 아닌 ‘키’를 반환하는지 확인해 주세요. URL을 반환한다면, 키 저장으로 전환해야 이후 GenerateFileUrlPort를 통한 URL 생성 흐름과 합치됩니다.

Also applies to: 33-34


23-31: 교체 업로드 시 기존 S3 객체 정리 누락 — 스토리지 누수 방지

사용자 사진 교체 시 기존 객체 삭제가 없으면 스토리지 누수가 발생합니다. DeleteFilePort(또는 어댑터의 삭제 기능)를 추가해, 키가 변경될 때 이전 키를 제거하는 처리를 권장합니다.

원칙:

  • 새 키 업로드 성공 → DB 갱신 성공 시 이전 키 삭제(또는 반대로: DB 실패 시 새 키 롤백 삭제) 처리를 포함한 보상 로직 고려.
casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/storage/AwsCredentialsProperties.kt (1)

5-9: 정적 액세스 키 사용 지양 + 값 검증 추가

운영환경에서는 IAM Role/Default Credentials Provider 사용을 우선하세요. 불가피하게 정적 자격증명을 쓰는 경우 유효성 검증을 추가하고, 로깅/노출에 각별히 주의해야 합니다.

아래처럼 @validated와 제약을 추가하세요:

 import org.springframework.boot.context.properties.ConfigurationProperties
+import org.springframework.validation.annotation.Validated
+import jakarta.validation.constraints.NotBlank
 
-@ConfigurationProperties("cloud.aws.credentials")
-class AwsCredentialsProperties(
-    val accessKey: String,
-    val secretKey: String,
-)
+@ConfigurationProperties("cloud.aws.credentials")
+@Validated
+class AwsCredentialsProperties(
+    @field:NotBlank val accessKey: String,
+    @field:NotBlank val secretKey: String,
+)

추가 조언:

  • SDK 기본 자격증명 체인(IAM Role, 환경변수, 프로파일 등) 우선 적용을 검토하고, 장기적으로는 AWS SDK v2로의 이전을 고려하세요. Based on learnings
casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/domain/entity/PhotoJpaEntity.kt (1)

11-17: photo_path 컬럼에 length 지정 추가
URL/키 길이를 고려해 @column에 length=2048 지정 권장

-    @Column(name = "photo_path", nullable = false)
+    @Column(name = "photo_path", nullable = false, length = 2048)
casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/presentation/FileController.kt (1)

3-11: multipart 업로드 컨트롤 협의: consumes 지정 및 빈 파일 가드 추가 권장

  • consumes 미지정 시 일부 클라이언트/프록시에서 컨텐츠 협상 문제가 날 수 있습니다.
  • 업로드 파일이 비어있는 경우 즉시 400 처리하는 가드가 필요합니다.

아래처럼 개선을 제안합니다.

 import hs.kr.entrydsm.application.domain.application.usecase.FileUploadUseCase
 import hs.kr.entrydsm.application.domain.file.presentation.converter.ImageFileConverter
+import org.springframework.http.MediaType
 import org.springframework.http.ResponseEntity
 import org.springframework.web.bind.annotation.PostMapping
 import org.springframework.web.bind.annotation.RequestMapping
 import org.springframework.web.bind.annotation.RequestPart
 import org.springframework.web.bind.annotation.RestController
 import org.springframework.web.multipart.MultipartFile

 @RequestMapping("/photo")
 @RestController
 class FileController(
     private val fileUploadUseCase: FileUploadUseCase
 ) {
-    @PostMapping
+    @PostMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
     fun uploadPhoto(@RequestPart(name = "image") file: MultipartFile): ResponseEntity<Map<String, String>> {
+        require(!file.isEmpty) { "빈 파일은 업로드할 수 없습니다." }
         val photoUrl = fileUploadUseCase.execute(
             file.let(ImageFileConverter::transferTo)
         )
         return ResponseEntity.ok(mapOf("photo_url" to photoUrl))
     }
 }

Also applies to: 17-23

casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/storage/AwsProperties.kt (1)

5-9: 프로퍼티 유효성 추가 권장

bucket/region은 비어있으면 안 됩니다. 프로퍼티 검증을 추가해 잘못된 설정을 조기에 차단하세요. 예: @field:NotBlank 사용(+ Boot 3이면 jakarta.validation).

예시:

casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/config/AwsS3Config.kt (1)

5-7: 구현 클래스 캐스팅 제거: AmazonS3 인터페이스로 노출

AmazonS3ClientBuilder.build()는 AmazonS3를 반환합니다. 구현 클래스( AmazonS3Client )로 캐스팅하면 교체/업데이트 시 런타임 위험이 있습니다. 빈/주입 모두 AmazonS3 인터페이스를 사용하세요.

-import com.amazonaws.services.s3.AmazonS3Client
+import com.amazonaws.services.s3.AmazonS3
 import com.amazonaws.services.s3.AmazonS3ClientBuilder
 ...
     @Bean
-    fun amazonS3Client(): AmazonS3Client {
+    fun amazonS3Client(): AmazonS3 {
         val credentials = BasicAWSCredentials(awsCredentialsProperties.accessKey, awsCredentialsProperties.secretKey)

         return AmazonS3ClientBuilder.standard()
             .withRegion(awsProperties.region)
             .withCredentials(AWSStaticCredentialsProvider(credentials))
-            .build() as AmazonS3Client
+            .build()
     }

Also applies to: 20-28

casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/file/presentation/exception/WebFileExceptions.kt (1)

9-13: HTTP 상태 코드 재검토(확장자 오류는 415 고려)

InvalidExtension를 400으로 매핑했는데, 미디어 타입/확장자 문제는 415 Unsupported Media Type도 옵션입니다. 에러 스펙에 맞춰 상수/코드를 재검토하세요.

casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/storage/AwsS3Adapter.kt (1)

59-69: 접근 제어 정책 불일치: PublicRead vs Presigned URL

객체를 PublicRead로 업로드하면서 동시에 presigned URL을 생성하고 있습니다. 정책을 하나로 통일하세요.

  • 사설 접근이 목표라면: 업로드 ACL 제거(기본 private), presign 사용.
  • 공개 접근이 목표라면: presign 제거, getUrl 반환만 사용.

현 로직과 API 사용처에 맞춰 한 가지 전략으로 정리하시길 권장합니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between b6da007 and 1087db0.

📒 Files selected for processing (20)
  • casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/file/object/PathList.kt (1 hunks)
  • casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/file/spi/GenerateFileUrlPort.kt (1 hunks)
  • casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/file/spi/UploadFilePort.kt (1 hunks)
  • casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/exception/WebException.kt (1 hunks)
  • casper-application-infrastructure/build.gradle.kts (1 hunks)
  • casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/domain/entity/ApplicationJpaEntity.kt (1 hunks)
  • casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/domain/entity/PhotoJpaEntity.kt (1 hunks)
  • casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/domain/repository/PhotoJpaRepository.kt (1 hunks)
  • casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/presentation/FileController.kt (1 hunks)
  • casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/presentation/dto/response/ApplicationDetailResponse.kt (1 hunks)
  • casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/usecase/ApplicationQueryUseCase.kt (3 hunks)
  • casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/usecase/FileUploadUseCase.kt (1 hunks)
  • casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/file/presentation/converter/FileConverter.kt (1 hunks)
  • casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/file/presentation/converter/FileExtensions.kt (1 hunks)
  • casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/file/presentation/converter/ImageFileConverter.kt (1 hunks)
  • casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/file/presentation/exception/WebFileExceptions.kt (1 hunks)
  • casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/config/AwsS3Config.kt (1 hunks)
  • casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/storage/AwsCredentialsProperties.kt (1 hunks)
  • casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/storage/AwsProperties.kt (1 hunks)
  • casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/storage/AwsS3Adapter.kt (1 hunks)
🧰 Additional context used
🪛 detekt (1.23.8)
casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/storage/AwsS3Adapter.kt

[warning] 50-50: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

🔇 Additional comments (9)
casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/domain/entity/ApplicationJpaEntity.kt (1)

55-55: 사진 경로 갱신을 위한 가변 필드 전환 확인 완료

사진 경로가 업로드 이후 갱신되어야 하므로 photoPathvar로 바꾼 선택이 타당합니다. 추가 우려 사항은 없습니다.

casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/exception/WebException.kt (1)

3-6: 기반 예외 타입 정의 적절

HTTP 상태 코드와 메시지를 묶어 전달하려는 의도가 명확하며, 구현상 문제 없습니다.

casper-application-infrastructure/build.gradle.kts (1)

138-139: AWS SDK 버전 고정 추가 확인

S3 연동을 위해 필요한 의존성이 명확히 추가되었습니다. 다른 충돌 요소는 보이지 않습니다.

casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/file/object/PathList.kt (1)

3-4: 경로 상수 추출 적절

사진 경로 prefix를 상수로 분리한 덕분에 재사용과 유지보수가 좋아졌습니다.

casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/file/spi/GenerateFileUrlPort.kt (1)

3-4: 포트 정의 명확

파일 URL 생성을 도메인 포트로 분리한 설계가 깔끔합니다.

casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/file/presentation/converter/FileExtensions.kt (1)

3-8: 확장자 상수화 문제 없음

이미지 확장자를 상수로 중앙화한 접근이 합리적이며, 다른 우려는 없습니다.

casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/presentation/dto/response/ApplicationDetailResponse.kt (1)

25-25: 응답 DTO 확장 확인

photoPath 필드 추가로 응답 스키마가 요구사항을 충족하게 되었으며 다른 영향은 없어 보입니다.

casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/file/spi/UploadFilePort.kt (1)

5-7: 반환값을 S3 객체 키로 명확히 명시
인터페이스 반환값이 URL인지 객체 키인지 모호하므로, URL 생성은 GenerateFileUrlPort에서 담당하고 이 포트는 “객체 키” 반환으로 고정해 KDoc으로 설명을 추가하세요.

 interface UploadFilePort {
-    fun upload(file: File, path: String): String
+    /**
+     * 파일을 업로드하고 S3 스토리지의 객체 키(object key)를 반환합니다.
+     * (접근 가능한 URL이 아닙니다.)
+     */
+    fun upload(file: File, path: String): String
 }
casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/storage/AwsS3Adapter.kt (1)

42-49: S3 오브젝트 키 구성 일관성 확인 필요

  • upload()path를 그대로 S3 키로 사용, generateFileUrl()${path}${fileName} 형태로 키 구성
  • DB에 저장되는 photo 필드가 파일명인지, 전체 키인지, URL인지 명확히 정의하고 키 생성 로직을 일관되게 정리

upload()/generateFileUrl() 호출부와 Photo 엔티티의 photo 필드 저장 형태를 코드베이스에서 직접 확인하세요.

Comment on lines 27 to 33
override fun upload(file: File, path: String): String {
runCatching { inputS3(file, path) }
.also { file.delete() }

return getS3Url(path)
}

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

업로드 예외가 삼켜져 성공으로 처리됨

runCatching 결과를 무시하고 항상 URL을 반환합니다. 업로드 실패 시 실패를 반환/전파해야 합니다. 또한 파일 삭제는 finally에서 보장하세요.

-    override fun upload(file: File, path: String): String {
-        runCatching { inputS3(file, path) }
-            .also { file.delete() }
-
-        return getS3Url(path)
-    }
+    override fun upload(file: File, path: String): String {
+        return try {
+            inputS3(file, path)
+            getS3Url(path)
+        } catch (e: Exception) {
+            throw IllegalStateException("S3 업로드 실패: ${e.message}", e)
+        } finally {
+            file.delete()
+        }
+    }
📝 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
override fun upload(file: File, path: String): String {
runCatching { inputS3(file, path) }
.also { file.delete() }
return getS3Url(path)
}
override fun upload(file: File, path: String): String {
return try {
inputS3(file, path)
getS3Url(path)
} catch (e: Exception) {
throw IllegalStateException("S3 업로드 실패: ${e.message}", e)
} finally {
file.delete()
}
}
🤖 Prompt for AI Agents
In
casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/storage/AwsS3Adapter.kt
around lines 27-33, the current implementation swallows upload exceptions by
ignoring the runCatching result and always returning the S3 URL while deleting
the file unconditionally; change it to attempt the upload and propagate any
failure (e.g., use runCatching(...).getOrThrow() or a try/catch that rethrows
the caught exception) so callers receive the error instead of a false success,
and move the file.delete() into a finally block (or use Kotlin's use/try/finally
pattern) so the file is always cleaned up regardless of success or failure.

Comment on lines +34 to +41
private fun inputS3(file: File, path: String) {
try {
val inputStream = file.inputStream()
val objectMetadata = ObjectMetadata().apply {
contentLength = file.length()
contentType = Mimetypes.getInstance().getMimetype(file)
}

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

InputStream 누수 가능성과 예외 타입 구체화

  • file.inputStream()을 use로 닫아주세요.
  • IOException만 포착하면 AWS SDK 예외(AmazonServiceException/SdkClientException)를 놓칩니다. 보다 넓게 포착 후 의미 있는 도메인 예외로 감싸세요.
-    private fun inputS3(file: File, path: String) {
-        try {
-            val inputStream = file.inputStream()
-            val objectMetadata = ObjectMetadata().apply {
-                contentLength = file.length()
-                contentType = Mimetypes.getInstance().getMimetype(file)
-            }
+    private fun inputS3(file: File, path: String) {
+        try {
+            file.inputStream().use { inputStream ->
+                val objectMetadata = ObjectMetadata().apply {
+                    contentLength = file.length()
+                    contentType = Mimetypes.getInstance().getMimetype(file)
+                }
 
-            amazonS3Client.putObject(
-                PutObjectRequest(
-                    awsProperties.bucket,
-                    path,
-                    inputStream,
-                    objectMetadata
-                ).withCannedAcl(CannedAccessControlList.PublicRead)
-            )
-        } catch (e: IOException) {
-            throw IllegalArgumentException("File Exception")
+                amazonS3Client.putObject(
+                    PutObjectRequest(
+                        awsProperties.bucket,
+                        path,
+                        inputStream,
+                        objectMetadata
+                    ).withCannedAcl(CannedAccessControlList.PublicRead)
+                )
+            }
+        } catch (e: Exception) {
+            throw IllegalStateException("S3 업로드 처리 중 예외 발생: ${e.message}", e)
         }
     }
📝 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
private fun inputS3(file: File, path: String) {
try {
val inputStream = file.inputStream()
val objectMetadata = ObjectMetadata().apply {
contentLength = file.length()
contentType = Mimetypes.getInstance().getMimetype(file)
}
private fun inputS3(file: File, path: String) {
try {
file.inputStream().use { inputStream ->
val objectMetadata = ObjectMetadata().apply {
contentLength = file.length()
contentType = Mimetypes.getInstance().getMimetype(file)
}
amazonS3Client.putObject(
PutObjectRequest(
awsProperties.bucket,
path,
inputStream,
objectMetadata
).withCannedAcl(CannedAccessControlList.PublicRead)
)
}
} catch (e: Exception) {
throw IllegalStateException("S3 업로드 처리 중 예외 발생: ${e.message}", e)
}
}
🤖 Prompt for AI Agents
In
casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/storage/AwsS3Adapter.kt
around lines 34 to 41, the File InputStream is opened without being closed and
only IOException is caught; change to use file.inputStream().use { inputStream
-> ... } so the stream is always closed, and broaden the catch to include AWS
SDK exceptions (AmazonServiceException, SdkClientException) or a general
Exception, then wrap the caught exception in a meaningful domain-specific
exception (or a clear runtime exception) with context about the S3 operation
before rethrowing.

@coehgns coehgns merged commit 0405a7d into main Sep 26, 2025
1 check passed
@coehgns coehgns deleted the feature/53-upload-image branch September 26, 2025 17:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants