-
Notifications
You must be signed in to change notification settings - Fork 1
feat: 이미지 첨부 기능 구현 #709
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
feat: 이미지 첨부 기능 구현 #709
Changes from 28 commits
Commits
Show all changes
39 commits
Select commit
Hold shift + click to select a range
1a2c913
feat: 피드 이미지 API 명세 구현
junseo511 3e4acd0
feat: 피드 목록 아이템 이미지 추가
junseo511 4984321
feat: 피드 상세 이미지 추가
junseo511 fc09e57
feat: 사진 갯수 제한을 위한 커스텀 ActivityResultContract 구현
junseo511 0a3caa0
feat: 사진 압축을 위한 ImageCompressor 구현
junseo511 e86a397
feat: 싱글 이벤트 처리를 위한 유틸 클래스 구현
junseo511 2a0c62d
feat: 이미지 첨부 기능 구현
junseo511 e189a2b
feat: 멀티파트 API 전송 적용
junseo511 23d112e
feat: 이미지 확대창 구현
junseo511 951396e
refactor: 컴포즈 뷰 성능 개선
junseo511 05e169a
refactor: 최대 업로드 이미지 수 변경
junseo511 9fece35
feat: 피드 수정 구현
junseo511 9107fe9
refactor: 의존성 주입 방식 변경
junseo511 98d31fa
refactor: 피드 목록 이미지 여백 수정
junseo511 1a3d12b
refactor: 멀티파트 변환 객체 이동
junseo511 8435a8a
refactor: 이미지 삭제 방식 변경
junseo511 7d6ab44
refactor: 이미지 최대 사이즈 변경
junseo511 f818cc9
refactor: stream 안정성 개선
junseo511 f1696b0
refactor: PhotoPicker 안정성 개선
junseo511 fc288b6
refactor: 불필요한 임포트 변수 제거
junseo511 b8145ec
refactor: 유틸 객체 싱글톤으로 변경
junseo511 5919fb6
refactor: 이벤트 핸들러 사용 방식 변경
junseo511 adeddc6
refactor: 기존 이미지 불러오는 순서 보장
junseo511 4e10589
refactor: 압축 예외 처리 추가
junseo511 ca130c2
refactor: 이미지 최대 장수 변경
junseo511 df3d358
refactor: 하드코딩된 변수 상수화
junseo511 844ca3a
refactor: 이미지 압축시 사용하는 Dispatchers 변경
junseo511 dea9e73
refactor: 이미지 압축시 사용하는 Dispatchers 롤백
junseo511 44e910b
refactor: SharedFlow를 활용하도록 이전
junseo511 f76797a
refactor: .asSharedFlow() 함수 활용
junseo511 6f4cbe0
refactor: 멀티파트 변환 함수 제네릭 적용
junseo511 b6ee10b
refactor: Flow collect 확장함수 적용
junseo511 f7dc4de
refactor: 사용하지 않는 함수 제거
junseo511 afdcb2f
refactor: 이미지 다운로드 병렬처리 추가
junseo511 1b406cc
refactor: connection 연결 차단 함수 추가
junseo511 eea4a8c
refactor: ExpandedFeedImageBackground 위치 이동
junseo511 ad6467d
refactor: 인덱스 안정성 개선
junseo511 5f0cd64
refactor: 피드 이미지 불러오기 로직 개선
junseo511 d57f570
feat: 피드 이미지 개수 표시
junseo511 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
82 changes: 82 additions & 0 deletions
82
app/src/main/java/com/into/websoso/core/common/util/DynamicLimitPhotoPicker.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| package com.into.websoso.core.common.util | ||
|
|
||
| import android.app.Activity | ||
| import android.content.Context | ||
| import android.content.Intent | ||
| import android.net.Uri | ||
| import android.os.Build | ||
| import android.provider.MediaStore | ||
| import androidx.activity.result.contract.ActivityResultContract | ||
| import com.into.websoso.core.common.util.DynamicLimitPhotoPicker.Input | ||
|
|
||
| /** | ||
| * Websoso 프로젝트에서 사용하는 커스텀 이미지 선택기입니다. | ||
| * | ||
| * 이 클래스는 이미지 여러 장을 선택할 수 있으며, | ||
| * 선택 가능한 최대 개수(maxSelectable)를 런타임에서 동적으로 지정할 수 있습니다. | ||
| * | ||
| * Android 13(API 33) 이상에서는 시스템의 PhotoPicker([MediaStore.ACTION_PICK_IMAGES])를 사용하며, | ||
| * `MediaStore.EXTRA_PICK_IMAGES_MAX`를 통해 선택 가능 개수를 제한할 수 있습니다. | ||
| * | ||
| * Android 13 미만에서는 [Intent.ACTION_OPEN_DOCUMENT]를 사용하여 | ||
| * 여러 장 선택이 가능하지만 선택 개수 제한은 직접 처리해주셔야 합니다. | ||
| * | ||
| * 사용 예시: | ||
| * ``` | ||
| * val pickerLauncher = registerForActivityResult(DynamicLimitPhotoPicker()) { uris -> | ||
| * // 선택된 이미지 URI 목록을 처리합니다 | ||
| * } | ||
| * | ||
| * pickerLauncher.launch(DynamicLimitPhotoPicker.Input(maxSelectable = 3)) | ||
| * ``` | ||
| * | ||
| * @return 선택된 이미지들의 [Uri] 목록을 반환합니다. | ||
| * @see Input 선택 가능한 최대 개수를 설정할 수 있는 입력 파라미터입니다. | ||
| */ | ||
| class DynamicLimitPhotoPicker : ActivityResultContract<Input, List<Uri>>() { | ||
| data class Input( | ||
| val maxSelectable: Int, | ||
| ) | ||
|
|
||
| override fun createIntent( | ||
| context: Context, | ||
| input: Input, | ||
| ): Intent = | ||
| if (Build.VERSION.SDK_INT >= 33) { | ||
| Intent(MediaStore.ACTION_PICK_IMAGES).apply { | ||
| type = MINE_TYPE | ||
| putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, input.maxSelectable) | ||
| } | ||
| } else { | ||
| Intent(Intent.ACTION_OPEN_DOCUMENT).apply { | ||
| type = MINE_TYPE | ||
| addCategory(Intent.CATEGORY_OPENABLE) | ||
| addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) | ||
| addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) | ||
| putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) | ||
| } | ||
| } | ||
|
|
||
| override fun parseResult( | ||
| resultCode: Int, | ||
| intent: Intent?, | ||
| ): List<Uri> { | ||
| if (resultCode != Activity.RESULT_OK || intent == null) return emptyList() | ||
|
|
||
| val uris = mutableListOf<Uri>() | ||
|
|
||
| intent.clipData?.let { clip -> | ||
| for (i in 0 until clip.itemCount) { | ||
| clip.getItemAt(i).uri?.let { uris.add(it) } | ||
| } | ||
| } | ||
|
|
||
| intent.data?.let { uris.add(it) } | ||
|
|
||
| return uris | ||
| } | ||
|
|
||
| companion object { | ||
| private const val MINE_TYPE: String = "image/*" | ||
| } | ||
| } | ||
17 changes: 17 additions & 0 deletions
17
app/src/main/java/com/into/websoso/core/common/util/Event.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package com.into.websoso.core.common.util | ||
|
|
||
| open class Event<out T>( | ||
|
junseo511 marked this conversation as resolved.
Outdated
|
||
| private val content: T, | ||
| ) { | ||
| private var hasBeenHandled = false | ||
|
|
||
| fun getContentIfNotHandled(): T? = | ||
| if (hasBeenHandled) { | ||
| null | ||
| } else { | ||
| hasBeenHandled = true | ||
| content | ||
| } | ||
|
|
||
| fun peekContent(): T = content | ||
| } | ||
61 changes: 61 additions & 0 deletions
61
app/src/main/java/com/into/websoso/core/common/util/ImageCompressor.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| package com.into.websoso.core.common.util | ||
|
|
||
| import android.content.Context | ||
| import android.graphics.Bitmap | ||
| import android.graphics.BitmapFactory | ||
| import android.net.Uri | ||
| import dagger.hilt.android.qualifiers.ApplicationContext | ||
| import kotlinx.coroutines.Dispatchers | ||
| import kotlinx.coroutines.withContext | ||
| import java.io.ByteArrayOutputStream | ||
| import java.io.File | ||
| import java.io.FileOutputStream | ||
| import javax.inject.Inject | ||
| import javax.inject.Singleton | ||
|
|
||
| @Singleton | ||
| class ImageCompressor | ||
|
junseo511 marked this conversation as resolved.
|
||
| @Inject | ||
| constructor( | ||
| @ApplicationContext private val context: Context, | ||
| ) { | ||
| suspend fun compressUris( | ||
| uris: List<Uri>, | ||
| size: Double = DEFAULT_MAX_IMAGE_SIZE, | ||
| ): List<Uri> = | ||
| withContext(Dispatchers.IO) { | ||
| uris.mapNotNull { uri -> | ||
| runCatching { | ||
| val inputStream = context.contentResolver.openInputStream(uri) | ||
| val bitmap = inputStream?.use { BitmapFactory.decodeStream(it) } ?: return@runCatching null | ||
|
|
||
| var quality = INITIAL_QUALITY | ||
| val outputStream = ByteArrayOutputStream() | ||
| bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream) | ||
|
|
||
| while (outputStream.size() > (size * MB) && quality > QUALITY_DECREMENT_STEP) { | ||
| quality -= QUALITY_DECREMENT_STEP | ||
| outputStream.reset() | ||
| bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream) | ||
| } | ||
|
|
||
| val compressedFile = File.createTempFile("compressed_", ".jpg", context.cacheDir) | ||
| FileOutputStream(compressedFile).use { | ||
| it.write(outputStream.toByteArray()) | ||
| } | ||
|
|
||
| outputStream.close() | ||
| Uri.fromFile(compressedFile) | ||
| }.onFailure { | ||
| it.printStackTrace() | ||
| }.getOrNull() | ||
| } | ||
| } | ||
|
|
||
| companion object { | ||
| private const val INITIAL_QUALITY: Int = 100 | ||
| private const val QUALITY_DECREMENT_STEP: Int = 5 | ||
| private const val DEFAULT_MAX_IMAGE_SIZE: Double = 0.25 | ||
| private const val MB: Double = (1024 * 1024).toDouble() | ||
| } | ||
| } | ||
15 changes: 15 additions & 0 deletions
15
app/src/main/java/com/into/websoso/core/common/util/MutableSingleStateFlow.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package com.into.websoso.core.common.util | ||
|
|
||
| import kotlinx.coroutines.flow.MutableStateFlow | ||
|
|
||
| class MutableSingleStateFlow<T>( | ||
|
junseo511 marked this conversation as resolved.
Outdated
|
||
| private val _stateFlow: MutableStateFlow<Event<T>?> = MutableStateFlow(null), | ||
| ) : SingleStateFlow<T> by _stateFlow { | ||
| fun emit(value: T) { | ||
| _stateFlow.value = Event(value) | ||
| } | ||
|
|
||
| fun clear() { | ||
| _stateFlow.value = null | ||
| } | ||
| } | ||
5 changes: 5 additions & 0 deletions
5
app/src/main/java/com/into/websoso/core/common/util/SingleStateFlow.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| package com.into.websoso.core.common.util | ||
|
|
||
| import kotlinx.coroutines.flow.StateFlow | ||
|
|
||
| typealias SingleStateFlow<T> = StateFlow<Event<T>?> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
37 changes: 37 additions & 0 deletions
37
app/src/main/java/com/into/websoso/data/mapper/MultiPartMapper.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| package com.into.websoso.data.mapper | ||
|
|
||
| import android.content.Context | ||
| import android.net.Uri | ||
| import com.into.websoso.data.remote.request.FeedRequestDto | ||
| import dagger.hilt.android.qualifiers.ApplicationContext | ||
| import kotlinx.serialization.encodeToString | ||
| import kotlinx.serialization.json.Json | ||
| import okhttp3.MediaType.Companion.toMediaType | ||
| import okhttp3.MultipartBody | ||
| import okhttp3.RequestBody.Companion.toRequestBody | ||
| import java.io.InputStream | ||
| import javax.inject.Inject | ||
|
|
||
| class MultiPartMapper | ||
| @Inject | ||
| constructor( | ||
| @ApplicationContext private val context: Context, | ||
| ) { | ||
| fun formatToMultipart(feedRequestDto: FeedRequestDto): MultipartBody.Part { | ||
|
junseo511 marked this conversation as resolved.
Outdated
|
||
| val json = Json.encodeToString(feedRequestDto) | ||
| val requestBody = json.toRequestBody("application/json".toMediaType()) | ||
| return MultipartBody.Part.createFormData("feed", "feed.json", requestBody) | ||
| } | ||
|
|
||
| fun formatToMultipart(uri: Uri): MultipartBody.Part { | ||
| val inputStream: InputStream = context.contentResolver.openInputStream(uri) | ||
| ?: throw IllegalArgumentException("유효하지 않은 URI: $uri") | ||
|
|
||
| return inputStream.use { stream -> | ||
| val bytes = stream.readBytes() | ||
| val fileName = uri.lastPathSegment ?: "image.jpg" | ||
| val requestBody = bytes.toRequestBody("image/*".toMediaType()) | ||
| MultipartBody.Part.createFormData("images", fileName, requestBody) | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a: 추후에 프로퍼티가 추가될 수 있어서 data class가 필요한건가요?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
커스텀된 객체임을 감안하면 남겨두는게 좋을 것 같아요 👍