Skip to content

[Feat/#145] 이미지 최적화 및 이미지 전송 구조 개편#146

Open
doyeon0307 wants to merge 10 commits intodevelopfrom
feat/#145-image
Open

[Feat/#145] 이미지 최적화 및 이미지 전송 구조 개편#146
doyeon0307 wants to merge 10 commits intodevelopfrom
feat/#145-image

Conversation

@doyeon0307
Copy link
Copy Markdown
Contributor

@doyeon0307 doyeon0307 commented Feb 5, 2026

Related issue 🛠️

Work Description ✏️

파일 양이 엄청 많은 건 아니지만 image 관련 data layer에 대폭 변경사항이 있었고 쉽게 이해할 수 있는 로직은 아니어서 노션에 아티클 작성했습니다! 참고해주세욤

이미지 최적화

Screenshot 📸

Uncompleted Tasks 😅

  • 뷰모델 연동

To Reviewers 📢

data~domain 레이어까지만 작성했습니다! presentation에는 변경된 이미지 로직을 적용하지 않았어요. 본 PR과 등록 뷰모델 리팩토링 건이 모두 병합된 후에 이슈 따로 파서 진행하겠습니다. (이 때 S3Repository, ImageUtil, UploadImagesUseCase도 삭제할게용)

Summary by CodeRabbit

  • 새로운 기능
    • 여러 이미지 선택 후 원격 서버에 일괄 업로드 지원(사전 서명 URL 사용).
    • 업로드 전 이미지 자동 압축 및 최대 1024×1024 픽셀로 리사이즈.
    • JPEG 형식으로 변환해 저장 공간 절약.
    • 업로드 과정에서 임시 파일 생성 및 완료 후 자동 정리.

- S3 to File : S3 버킷은 서버 단에서 활용하는 도구이므로, 본 프로젝트에서 해당 이름을 사용하지 않습니다
- 원본 사이즈 읽어 비율 유지해 리사이징
- 생성된 파일 객체 제거
- file 기반 이미지 업로드
- uri 기반 이미지 생성
- 생성된 이미지 제거
- 다음 과정 순차 수행 : presigned-url 발급 > 리사이징된 파일 객체 생성 > s3 업로드 > 파일 객체 메모리에서 삭제
- FileUploadDataSource to FileUploadRemoteDataSource
@doyeon0307 doyeon0307 requested a review from a team February 5, 2026 09:12
@doyeon0307 doyeon0307 self-assigned this Feb 5, 2026
@doyeon0307 doyeon0307 added 🌟 Feat 새로운 기능 구현 🐣 도연 labels Feb 5, 2026
@doyeon0307 doyeon0307 linked an issue Feb 5, 2026 that may be closed by this pull request
2 tasks
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 5, 2026

📝 Walkthrough

Walkthrough

이미지 변환(URI→파일), 압축, 임시 저장, 프리사인드 URL 기반 업로드 파이프라인과 DI 바인딩(한정자 리네임, Repository 바인딩)이 추가되었습니다.

Changes

Cohort / File(s) Summary
Domain: Repository & Use Case
app/src/main/java/com/poti/android/domain/repository/FileUploadRepository.kt, app/src/main/java/com/poti/android/domain/usecase/image/UploadImagesUseCaseV2.kt
파일 업로드 계약 인터페이스와 이미지 업로드 오케스트레이션 Use Case 추가(프리사인드 URL 조회 → 로컬 파일 생성 → 업로드 → 정리).
Data: Repository & Sources
app/src/main/java/com/poti/android/data/repository/FileUploadRepositoryImpl.kt, app/src/main/java/com/poti/android/data/local/datasource/FileLocalDataSource.kt, app/src/main/java/com/poti/android/data/remote/datasource/FileUploadRemoteDataSource.kt
로컬 이미지 디코딩·리사이징·JPEG 압축 후 임시파일 생성 구현, OkHttp 기반 PUT 업로드 구현, Repository 구현체로 조율.
DI & Qualifier
app/src/main/java/com/poti/android/data/di/FileUploadClient.kt, app/src/main/java/com/poti/android/data/di/NetworkModule.kt, app/src/main/java/com/poti/android/data/di/RepositoryModule.kt
한정자 이름 S3UploadClientFileUploadClient로 변경 및 해당 바인딩 적용, RepositoryModule에 FileUploadRepository 바인딩 추가.
Constants
app/src/main/java/com/poti/android/core/common/constant/ImageConstants.kt
이미지 확장자(jpg)와 MIME 타입(image/jpeg) 상수 추가.
Existing Update
app/src/main/java/com/poti/android/data/repository/S3RepositoryImpl.kt
한정자 임포트/주입 어노테이션을 FileUploadClient로 교체.

Sequence Diagram(s)

sequenceDiagram
    participant UC as UploadImagesUseCaseV2
    participant IR as ImageRepository
    participant FR as FileUploadRepository
    participant FL as FileLocalDataSource
    participant RU as FileUploadRemoteDataSource

    UC->>IR: getPresignedUrls(uploadType, extensions)
    IR-->>UC: List<String>

    loop for each uri
        UC->>FR: createImage(uriString)
        FR->>FL: createImage(uriString)
        FL->>FL: decode, resize, compress -> temp File
        FL-->>FR: File
    end

    loop for each file & url
        UC->>FR: uploadImage(uploadUrl, file)
        FR->>RU: uploadImage(uploadUrl, file)
        RU->>RU: build PUT request (image/jpeg)
        RU-->>FR: success/failure
    end

    UC->>FR: clearDirectory()
    FR->>FL: clearDirectory()
    FL-->>FR: Result
    UC-->>UC: Result<List<String>>
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • [Feat/#60] 등록 뷰 API 구현 #70: 프리사인드 이미지 업로드 흐름(이미지 서비스/DTOs/RemoteDataSource/Repository 관련)과 코드 수준으로 밀접하게 연관됨.

Suggested reviewers

  • cmj7271
  • jyvnee
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 이미지 최적화 및 전송 구조 개편이라는 실제 변경사항을 명확하게 반영하고 있습니다.
Description check ✅ Passed PR 설명이 주요 내용(데이터/도메인 레이어 변경), 참고 자료(Notion 문서), 미완료 작업을 포함하고 있습니다.
Linked Issues check ✅ Passed 이슈 #145의 요구사항인 이미지 리사이징(FileLocalDataSource), 파일 생성 및 업로드 구조(FileUploadRepository, UploadImagesUseCaseV2)가 모두 구현되었습니다.
Out of Scope Changes check ✅ Passed S3UploadClient를 FileUploadClient로 변경한 것은 이미지 업로드 구조 개편의 일부로 관련성이 있으며, 모든 변경사항이 이미지 처리 최적화 범위 내입니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#145-image

No actionable comments were generated in the recent review. 🎉


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.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Feb 5, 2026

Ktlint check passed.

Run: https://github.com/team-poti/POTI-ANDROID/actions/runs/21852143764

Copy link
Copy Markdown

@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: 5

🤖 Fix all issues with AI agents
In
`@app/src/main/java/com/poti/android/data/repository/FileUploadRepositoryImpl.kt`:
- Around line 10-13: The constructor parameter name fileUplaodRemoteDataSource
in class FileUploadRepositoryImpl is misspelled; rename it to
fileUploadRemoteDataSource everywhere (constructor, property declaration, usages
inside FileUploadRepositoryImpl) and update any places that inject or reference
this dependency (e.g., constructor injection sites and calls to methods on
fileUplaodRemoteDataSource) to use the corrected identifier so compilation and
searches work correctly.

In
`@app/src/main/java/com/poti/android/domain/repository/FileUplaodRepository.kt`:
- Around line 1-14: Rename the source file from "FileUplaodRepository.kt" to the
correctly spelled "FileUploadRepository.kt" and update any references or imports
that refer to the misspelled filename; ensure the interface declaration
FileUploadRepository remains unchanged and that build files, tests, or other
modules that import this interface are adjusted to reference the new filename.

In
`@app/src/main/java/com/poti/android/domain/usecase/image/UploadImagesUseCaseV2.kt`:
- Around line 19-25: If createImages(...) succeeds but an exception occurs
afterwards (e.g., during uploadImages(...)), clearDirectory() may never run;
wrap the sequence that calls getUploadUrls(...), createImages(...), and
uploadImages(...) in a try/finally so clearDirectory() is invoked in the finally
block as a best-effort cleanup, while preserving and rethrowing any exceptions
from uploadImages; specifically modify the flow around getUploadUrls,
createImages, uploadImages, and clearDirectory to ensure clearDirectory() always
runs even on error.
- Around line 18-29: The catch-all Throwable in UploadImagesUseCaseV2
(surrounding getUploadUrls, createImages, uploadImages, clearDirectory) swallows
coroutine cancellation; change the error handling to either catch Exception
instead of Throwable or explicitly rethrow CancellationException
(kotlinx.coroutines.CancellationException) when caught, then return
Result.failure for other exceptions—ensure CancellationException from coroutines
is not swallowed by rethrowing it immediately.
- Around line 46-51: The uploadImages function fails to compile because zip's
transform lambda isn't suspend; replace the zip+lambda with an explicit
suspend-safe loop: first validate urls.size == files.size and throw or return an
error if they differ, then iterate indices (for (i in urls.indices)) and call
the suspend function fileUploadRepository.uploadImage(urls[i], files[i]) to
collect results into a List to return; also ensure temporary cleanup by invoking
clearDirectory() in a finally block (or using try/catch/finally) so
clearDirectory() runs regardless of success or failure. Reference: uploadImages,
fileUploadRepository.uploadImage, clearDirectory.
🧹 Nitpick comments (4)
app/src/main/java/com/poti/android/data/remote/datasource/FileUploadRemoteDataSource.kt (1)

14-20: 테스트 용이성을 위해 Dispatcher 주입을 권장합니다.

현재 Dispatchers.IO가 하드코딩되어 있어 단위 테스트 시 Dispatcher를 교체하기 어렵습니다. 코딩 가이드라인에 따라 생성자를 통해 Dispatcher를 주입받는 것이 좋습니다.

♻️ Dispatcher 주입 제안
+import kotlinx.coroutines.CoroutineDispatcher
+
 class FileUploadRemoteDataSource `@Inject` constructor(
     `@param`:FileUploadClient private val okHttpClient: OkHttpClient,
+    private val ioDispatcher: CoroutineDispatcher,
 ) {
     suspend fun uploadImage(
         uploadUrl: String,
         file: File,
-    ) = withContext(Dispatchers.IO) {
+    ) = withContext(ioDispatcher) {

DI 모듈에서 @IoDispatcher qualifier와 함께 Dispatchers.IO를 제공하시면 됩니다.

app/src/main/java/com/poti/android/data/local/datasource/FileLocalDataSource.kt (2)

20-27: 블로킹 I/O 작업에 대한 스레드 안전성을 고려해 주세요.

createImageFile() 함수는 이미지 디코딩, 압축, 파일 쓰기 등 상당한 I/O 작업을 수행하지만 suspend 함수가 아닙니다. 메인 스레드에서 호출될 경우 ANR이 발생할 수 있습니다.

suspend 함수로 변경하고 withContext(Dispatchers.IO)를 사용하거나, 호출부에서 백그라운드 스레드를 보장해야 합니다.

♻️ suspend 함수로 변경 제안
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
-    fun createImageFile(uriString: String): File {
+    suspend fun createImageFile(uriString: String): File = withContext(Dispatchers.IO) {
         val uri = uriString.toUri()
 
         val directory = getDirectory()
         val compressedImage = compressImage(uri, directory)
 
-        return compressedImage
+        compressedImage
     }

29-32: clearDirectory()도 동일하게 suspend 함수 변환을 권장합니다.

파일 삭제 작업도 I/O 작업이므로 createImageFile()과 동일한 패턴을 적용하시면 좋겠습니다.

app/src/main/java/com/poti/android/data/repository/FileUploadRepositoryImpl.kt (1)

22-37: runCatching으로 보일러플레이트 축소 제안

try/catch + Result 패턴이 반복됩니다. runCatching으로 간결하게 표현할 수 있습니다.

♻️ 제안 수정
     override fun createImage(uriString: String): Result<File> {
-        try {
-            val file = fileLocalDataSource.createImageFile(uriString)
-            return Result.success(file)
-        } catch (exception: Throwable) {
-            return Result.failure(exception)
-        }
+        return runCatching {
+            fileLocalDataSource.createImageFile(uriString)
+        }
     }

     override fun clearDirectory(): Result<Unit> {
-        try {
-            fileLocalDataSource.clearDirectory()
-            return Result.success(Unit)
-        } catch (exception: Throwable) {
-            return Result.failure(exception)
-        }
+        return runCatching {
+            fileLocalDataSource.clearDirectory()
+        }
     }

- 캔슬 throw 로직 추가
- zip 내에서 suspend 호출하지 않도록 변경
- uri/files 크기 다를 때 예외처리
- 최종 실패 시에도 이미지 디렉토리 비우도록
Copy link
Copy Markdown
Contributor

@cmj7271 cmj7271 left a comment

Choose a reason for hiding this comment

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

문서가 유익하고, 이해하기 좋아요

- as-is : finally 블록에서 에러를 throw로 전파해 위험
- to-be : throw 제거, runCatching으로 에러 시 로그만 찍음
Copy link
Copy Markdown

@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: 1

🤖 Fix all issues with AI agents
In
`@app/src/main/java/com/poti/android/domain/usecase/image/UploadImagesUseCaseV2.kt`:
- Line 7: UploadImagesUseCaseV2 currently imports and uses Android-specific
Timber; remove the direct Timber dependency from the domain layer and replace it
with a pure-Kotlin logging abstraction or move logging out of the use case.
Create or use an injected Logger interface (e.g., Logger.log/d debug/warn/error)
and update UploadImagesUseCaseV2 to accept the Logger in its constructor (or
remove all logging calls here and let the caller in the data/presentation layer
handle logs) so the domain module remains Android-free; ensure all usages of
Timber in UploadImagesUseCaseV2 are replaced by the Logger methods or removed.
🧹 Nitpick comments (2)
app/src/main/java/com/poti/android/domain/repository/FileUploadRepository.kt (1)

5-14: createImageclearDirectory가 I/O 작업임에도 suspend가 아닌 점이 아쉽습니다.

createImage는 URI로부터 파일을 생성하는 I/O 작업이고, clearDirectory 역시 파일 시스템 작업입니다. 현재 non-suspend로 선언되어 있어, 호출하는 코루틴의 스레드(예: Main)를 블로킹할 수 있습니다.

구현체(Impl)에서 withContext(Dispatchers.IO)를 사용하여 처리하시는 구조라면 suspend로 선언하시는 것이 더 안전하고, Dispatcher 주입을 통한 테스트 용이성도 확보할 수 있습니다.

🛠️ 제안 수정
 interface FileUploadRepository {
     suspend fun uploadImage(
         uploadUrl: String,
         file: File,
     ): Result<Unit>

-    fun createImage(uriString: String): Result<File>
+    suspend fun createImage(uriString: String): Result<File>

-    fun clearDirectory(): Result<Unit>
+    suspend fun clearDirectory(): Result<Unit>
 }

As per coding guidelines, "Dispatcher 주입: Dispatchers.IO 등을 하드코딩하지 말고, 테스트 용이성을 위해 생성자를 통해 주입(Injection)받도록 제안해줘."

app/src/main/java/com/poti/android/domain/usecase/image/UploadImagesUseCaseV2.kt (1)

63-68: clearDirectory()에서 runCatchingResult가 이중으로 감싸지고 있습니다.

fileUploadRepository.clearDirectory()가 이미 Result<Unit>을 반환하므로 예외를 던지지 않습니다. 바깥의 runCatching은 불필요한 이중 래핑이 됩니다. 간결하게 정리하시면 좋겠습니다.

♻️ 제안 수정
-    private fun clearDirectory() = runCatching {
-        fileUploadRepository.clearDirectory()
-            .onFailure {
-                Timber.e(it, "fail on clearDirectory")
-            }
-    }
+    private fun clearDirectory() {
+        fileUploadRepository.clearDirectory()
+            .onFailure {
+                Timber.e(it, "fail on clearDirectory")
+            }
+    }

Comment thread app/src/main/java/com/poti/android/domain/usecase/image/UploadImagesUseCaseV2.kt Outdated
- Timber 의존성 제거
- Repository에 기본 에러처리가 되어있어 중복처리 제거
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🌟 Feat 새로운 기능 구현 🐣 도연

Projects

Status: To-do

Development

Successfully merging this pull request may close these issues.

[Feat] 이미지 최적화

2 participants