-
Notifications
You must be signed in to change notification settings - Fork 0
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
base: develop
Are you sure you want to change the base?
feat: 이미지 첨부 기능 구현 #709
Conversation
Note Reviews pausedUse the following commands to manage reviews:
""" Walkthrough이 PR은 서재(피드) 이미지 첨부 기능의 전체적인 도입 및 확장과 관련된 대규모 변경을 포함합니다. 이미지 업로드, 다운로드, 압축, 선택 제한, UI 표시, 상세보기, API 연동, 데이터 모델 확장 등 이미지와 관련된 전반적인 기능이 추가 및 개선되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant CreateFeedActivity
participant CreateFeedViewModel
participant DynamicLimitPhotoPicker
participant ImageCompressor
participant FeedRepository
participant MultiPartMapper
participant FeedApi
User->>CreateFeedActivity: 이미지 첨부 버튼 클릭
CreateFeedActivity->>DynamicLimitPhotoPicker: 이미지 선택 런처 실행 (최대 개수 전달)
DynamicLimitPhotoPicker-->>CreateFeedActivity: 선택된 이미지 URI 리스트 반환
CreateFeedActivity->>CreateFeedViewModel: addImages(URI 리스트)
CreateFeedViewModel->>ImageCompressor: compressUris(URI 리스트)
ImageCompressor-->>CreateFeedViewModel: 압축된 URI 리스트 반환
CreateFeedViewModel->>FeedRepository: saveFeed(..., images = URI 리스트)
FeedRepository->>MultiPartMapper: formatToMultipart(FeedRequestDto, 이미지 URI 리스트)
MultiPartMapper-->>FeedRepository: MultipartBody.Part 리스트 반환
FeedRepository->>FeedApi: postFeed(멀티파트 데이터)
FeedApi-->>FeedRepository: 응답
sequenceDiagram
participant User
participant FeedDetailActivity
participant ExpandedFeedImageActivity
User->>FeedDetailActivity: 피드 이미지 클릭
FeedDetailActivity->>ExpandedFeedImageActivity: 인텐트로 index, imageUrls 전달 및 실행
ExpandedFeedImageActivity->>ExpandedFeedImageScreen: 이미지 리스트, 인덱스로 전체보기 표시
User->>ExpandedFeedImageActivity: 스와이프/닫기
ExpandedFeedImageActivity->>FeedDetailActivity: 결과 반환 및 종료
Assessment against linked issues
Possibly related PRs
Suggested reviewers
Poem
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
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.
Actionable comments posted: 4
🔭 Outside diff range comments (1)
app/src/main/java/com/into/websoso/ui/createFeed/component/CreateFeedImageContainer.kt (1)
51-73
: 🛠️ Refactor suggestion이미지 로딩 상태 처리 개선 필요
rememberAsyncImagePainter
를 사용할 때 로딩 상태, 오류 상태에 대한 처리가 없습니다.다음과 같이 상태 처리를 추가하는 것이 좋습니다:
@Composable private fun CreateFeedImageBox( image: Uri, onRemoveClick: () -> Unit, ) { Box( modifier = Modifier .size(100.dp) .aspectRatio(1f) .clip(RoundedCornerShape(8.dp)), ) { - Image( - painter = rememberAsyncImagePainter(image), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize(), - ) + val painter = rememberAsyncImagePainter( + model = image, + onError = { + // 오류 로깅 가능 + } + ) + + when (painter.state) { + is AsyncImagePainter.State.Loading -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + strokeWidth = 2.dp + ) + } + } + is AsyncImagePainter.State.Error -> { + Box( + modifier = Modifier.fillMaxSize().background(Color.LightGray), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Default.BrokenImage, + contentDescription = null, + tint = Color.Gray + ) + } + } + else -> { + Image( + painter = painter, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize(), + ) + } + } ImageRemoveButton( modifier = Modifier.align(Alignment.TopEnd), onRemoveClick = onRemoveClick, ) } }상단에 다음 import를 추가해야 합니다:
import androidx.compose.foundation.background import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.BrokenImage import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Icon import androidx.compose.ui.graphics.Color import coil.compose.AsyncImagePainter
🧹 Nitpick comments (9)
app/src/main/res/layout/activity_create_feed.xml (1)
157-166
: 이미지 첨부 버튼 구현이 적절합니다.텍스트 영역 내에 이미지 버튼을 배치한 위치는 UI/UX 관점에서 적절합니다. 사용자가 콘텐츠 입력 중에 쉽게 이미지를 첨부할 수 있는 직관적인 위치입니다.
접근성을 위해
contentDescription
속성을 추가하는 것이 좋습니다:<ImageView android:id="@+id/iv_create_feed_image" android:layout_width="44dp" android:layout_height="48dp" android:layout_marginStart="10dp" android:paddingHorizontal="10dp" android:paddingVertical="12dp" android:src="@drawable/ic_create_feed_image" + android:contentDescription="@string/content_description_attach_image" app:layout_constraintBottom_toBottomOf="@id/et_create_feed_content" app:layout_constraintStart_toStartOf="@id/et_create_feed_content" />
app/src/main/java/com/into/websoso/core/common/util/MutableSingleLiveData.kt (1)
1-15
: MutableSingleLiveData 클래스가 잘 구현되었습니다.이 클래스는 Android의
MutableLiveData
/LiveData
패턴을 따라 구현되어 일관성 있는 API를 제공합니다.SingleLiveData
의 protected 메서드를 public으로 오버라이드하여 외부 클래스에서 값을 설정할 수 있게 해줍니다.한 가지 제안사항으로, 클래스의 목적과 사용법을 설명하는 KDoc 주석을 추가하는 것이 좋을 것 같습니다. 특히 이 클래스가 이벤트를 한 번만 소비하는 패턴에 사용된다면 그 특성을 문서화하면 도움이 될 것입니다.
package com.into.websoso.core.common.util +/** + * 단일 이벤트 LiveData를 위한 뮤터블 구현체입니다. + * 이 클래스는 외부에서 값을 설정할 수 있는 기능을 추가하여 SingleLiveData를 확장합니다. + * 이벤트는 한 번만 소비되므로 구성 변경 후 이벤트가 다시 트리거되는 것을 방지합니다. + */ class MutableSingleLiveData<T> : SingleLiveData<T> { constructor() : super() constructor(value: T) : super(value) public override fun postValue(value: T) { super.postValue(value) } public override fun setValue(value: T) { super.setValue(value) } }app/src/main/java/com/into/websoso/core/common/util/ImageCompressor.kt (1)
61-63
: 상수 이름 및 주석 개선MAX_IMAGE_SIZE 상수에 대한 설명이 부족하며, 상수 이름이 의미를 완전히 전달하지 않습니다.
다음과 같이 개선할 수 있습니다:
companion object { - private const val MAX_IMAGE_SIZE = 4_800_000 + /** + * 최대 이미지 크기 (4.8MB, 실제 목표 5MB보다 안전 마진 적용) + */ + private const val MAX_IMAGE_SIZE_BYTES = 4_800_000 }그리고 45줄에서도 상수 이름을 MAX_IMAGE_SIZE_BYTES로 변경하세요.
app/src/main/java/com/into/websoso/ui/createFeed/component/CreateFeedImageContainer.kt (1)
29-49
: 이미지 로딩 실패 처리 추가 필요Uri 타입으로 변경은 잘 되었지만, 이미지 로딩 실패 시 처리가 없습니다.
Coil의 상태 처리를 추가하여 로딩 실패 시 대체 이미지를 보여주는 것이 좋습니다:
@Composable fun CreateFeedImageContainer( images: List<Uri>, onRemoveClick: (index: Int) -> Unit, ) { LazyRow( modifier = Modifier .fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp), contentPadding = PaddingValues(horizontal = 20.dp), ) { items(images.size) { index -> CreateFeedImageBox( image = images.elementAt(index), onRemoveClick = { onRemoveClick(index) }, ) } } }app/src/main/java/com/into/websoso/ui/createFeed/CreateFeedViewModel.kt (3)
86-86
: 이미지 URI 문자열 변환 처리 개선 필요이미지 URI를 문자열로 변환할 때 null 처리가 개선될 수 있습니다.
- imageUris = attachedImages.value?.map { it.toString() } ?: emptyList(), + imageUris = attachedImages.value?.filterNotNull()?.map { it.toString() } ?: emptyList(),
104-104
: 이미지 URI 문자열 변환 처리 개선 필요이미지 URI를 문자열로 변환할 때 null 처리가 개선될 수 있습니다.
- imageUris = attachedImages.value?.map { it.toString() } ?: emptyList(), + imageUris = attachedImages.value?.filterNotNull()?.map { it.toString() } ?: emptyList(),
230-233
: 이미지 제거 로직 최적화 필요현재 이미지 제거 로직은 필터링을 사용하여 전체 리스트를 순회하며 비효율적입니다. 인덱스를 직접 사용하여 제거하는 방식이 더 효율적입니다.
다음과 같이 개선할 수 있습니다:
fun removeImage(index: Int) { - val imageToRemove: Uri = attachedImages.value?.getOrNull(index) ?: return - _attachedImages.value = attachedImages.value?.filter { eachImage -> eachImage != imageToRemove } + val currentImages = _attachedImages.value?.toMutableList() ?: return + if (index < 0 || index >= currentImages.size) return + + currentImages.removeAt(index) + _attachedImages.value = currentImages }app/src/main/java/com/into/websoso/core/common/util/SingleLiveData.kt (1)
32-37
: observePeek 는 구성 변경마다 중복 호출될 수 있습니다
observePeek
는LiveData.observe
로 등록된 직후 즉시peekContent()
를 전달하므로, 화면 회전·다크모드 전환 등으로LifecycleOwner
가 재생성될 때마다 동일 이벤트가 반복 전달됩니다.
순수 ‘상태(State)’ 목적이 아니라면 불필요한 재호출이 발생할 수 있으니,
- 실제 필요 여부를 검증하거나
distinctUntilChanged()
-식 우회(예:MediatorLiveData
활용)로 중복 방지를 고려해 보세요.app/src/main/java/com/into/websoso/core/common/util/DynamicLimitPhotoPicker.kt (1)
64-81
: maxSelectable 제한이 API 32 이하에서 강제되지 않습니다시스템 픽커가 아닌 경우 사용자가
maxSelectable
을 초과해 선택할 수 있으므로,parseResult
에서 리스트 크기를 잘라 주거나 ViewModel 단에서 다시 한 번 필터링해야 합니다.return uris + .take(input?.maxSelectable ?: Int.MAX_VALUE)
(또는 ViewModel의
addImages()
내부에서 동일 로직을 보강해도 무방합니다.)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (27)
app/src/main/java/com/into/websoso/core/common/util/BindingAdapter.kt
(1 hunks)app/src/main/java/com/into/websoso/core/common/util/DynamicLimitPhotoPicker.kt
(1 hunks)app/src/main/java/com/into/websoso/core/common/util/Event.kt
(1 hunks)app/src/main/java/com/into/websoso/core/common/util/ImageCompressor.kt
(1 hunks)app/src/main/java/com/into/websoso/core/common/util/MutableSingleLiveData.kt
(1 hunks)app/src/main/java/com/into/websoso/core/common/util/SingleLiveData.kt
(1 hunks)app/src/main/java/com/into/websoso/data/mapper/FeedMapper.kt
(2 hunks)app/src/main/java/com/into/websoso/data/model/FeedEntity.kt
(1 hunks)app/src/main/java/com/into/websoso/data/remote/request/FeedRequestDto.kt
(1 hunks)app/src/main/java/com/into/websoso/data/remote/response/FeedDetailResponseDto.kt
(1 hunks)app/src/main/java/com/into/websoso/data/remote/response/FeedResponseDto.kt
(1 hunks)app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt
(4 hunks)app/src/main/java/com/into/websoso/domain/mapper/FeedMapper.kt
(1 hunks)app/src/main/java/com/into/websoso/domain/model/Feed.kt
(1 hunks)app/src/main/java/com/into/websoso/ui/createFeed/CreateFeedActivity.kt
(7 hunks)app/src/main/java/com/into/websoso/ui/createFeed/CreateFeedViewModel.kt
(5 hunks)app/src/main/java/com/into/websoso/ui/createFeed/component/CreateFeedImageContainer.kt
(6 hunks)app/src/main/java/com/into/websoso/ui/feedDetail/adapter/FeedDetailContentViewHolder.kt
(1 hunks)app/src/main/java/com/into/websoso/ui/feedDetail/component/AdaptationFeedImageContainer.kt
(2 hunks)app/src/main/java/com/into/websoso/ui/feedDetail/model/ImageContainerType.kt
(1 hunks)app/src/main/java/com/into/websoso/ui/main/feed/model/FeedModel.kt
(1 hunks)app/src/main/java/com/into/websoso/ui/mapper/FeedMapper.kt
(2 hunks)app/src/main/res/layout/activity_create_feed.xml
(1 hunks)app/src/main/res/layout/item_feed.xml
(2 hunks)app/src/main/res/layout/item_feed_detail_header.xml
(1 hunks)core/resource/src/main/res/drawable/ic_create_feed_image.xml
(1 hunks)core/resource/src/main/res/values/strings.xml
(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
app/src/main/java/com/into/websoso/ui/feedDetail/adapter/FeedDetailContentViewHolder.kt (1)
app/src/main/java/com/into/websoso/ui/feedDetail/component/AdaptationFeedImageContainer.kt (1)
AdaptationFeedImageContainer
(11-23)
app/src/main/java/com/into/websoso/ui/createFeed/CreateFeedActivity.kt (2)
app/src/main/java/com/into/websoso/core/common/util/ExtentionFuction.kt (1)
showWebsosoSnackBar
(80-90)app/src/main/java/com/into/websoso/ui/createFeed/component/CreateFeedImageContainer.kt (1)
CreateFeedImageContainer
(29-49)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: build
🔇 Additional comments (27)
core/resource/src/main/res/values/strings.xml (1)
404-404
: 이미지 업로드 제한 문자열 추가 확인.이미지 첨부 기능 구현을 위한 적절한 문자열 리소스가 추가되었습니다. 동적으로 최대 이미지 수를 표시할 수 있도록 %d 포맷 지정자를 사용한 것이 좋습니다.
app/src/main/java/com/into/websoso/core/common/util/Event.kt (1)
1-27
: 이벤트 래퍼 클래스 구현이 잘 되었습니다.LiveData를 통해 노출되는 일회성 이벤트를 처리하기 위한 Event 클래스가 적절하게 구현되었습니다. 이 패턴은 화면 회전과 같은 구성 변경 시 이벤트가 다시 처리되는 것을 방지하는 데 유용합니다.
getContentIfNotHandled()
와peekContent()
메서드를 통해 이벤트 처리 상태에 따라 콘텐츠에 접근할 수 있도록 한 설계가 좋습니다.app/src/main/java/com/into/websoso/domain/model/Feed.kt (1)
16-17
: 피드에 이미지 관련 속성 추가가 적절합니다.피드 모델에 이미지 URL 목록(
imageUrls
)과 이미지 개수(imageCount
)를 추가한 것이 적절합니다. 이는 이미지 첨부 기능 구현에 필요한 데이터를 모델에 반영한 것입니다.
imageCount
는imageUrls.size
로 계산할 수도 있지만, 별도의 필드로 관리하면 UI에서 이미지 개수 표시 시 리스트 크기를 매번 계산하지 않아도 되는 장점이 있습니다.app/src/main/java/com/into/websoso/ui/feedDetail/model/ImageContainerType.kt (1)
6-6
: 이미지가 없는 경우 처리를 위한 EMPTY 타입 추가가 적절합니다.이미지 컨테이너 타입에
EMPTY(0)
값을 추가하여 이미지가 없는 경우를 명시적으로 처리할 수 있게 된 것이 좋습니다. 이렇게 하면 이미지가 없는 피드의 경우 UI에서 보다 명확하게 처리할 수 있습니다.app/src/main/java/com/into/websoso/ui/main/feed/model/FeedModel.kt (1)
16-17
: 이미지 관련 속성 추가 적절합니다.피드 모델에 이미지 URL 목록과 이미지 개수를 저장하는 속성이 추가되었습니다. 이는 이미지 첨부 기능을 구현하기 위한 적절한 확장입니다.
app/src/main/java/com/into/websoso/data/remote/response/FeedDetailResponseDto.kt (1)
44-45
: API 응답 모델에 이미지 필드 추가 적절합니다.
FeedDetailResponseDto
에 이미지 URL 목록을 저장하는 속성이 추가되었습니다.@SerialName("images")
어노테이션을 통해 JSON 직렬화 시 올바르게 매핑되도록 처리되었습니다.app/src/main/res/layout/item_feed_detail_header.xml (1)
111-111
: 이미지 컨테이너 조건부 표시 처리 좋습니다.이미지 URL 목록이 비어있지 않을 때만
ComposeView
를 표시하도록 하는 가시성 바인딩이 추가되었습니다. 이를 통해 이미지가 없는 피드에서는 불필요한 공간을 차지하지 않게 됩니다.app/src/main/java/com/into/websoso/ui/feedDetail/component/AdaptationFeedImageContainer.kt (1)
6-6
: 빈 이미지 케이스 처리 추가 적절합니다.
EMPTY
타입을 import하고 when 표현식에서 이미지가 없는 경우를 명시적으로 처리하도록 추가했습니다. 이는 이미지가 없을 때 아무것도 렌더링하지 않는 적절한 처리입니다.Also applies to: 17-17
app/src/main/java/com/into/websoso/data/remote/request/FeedRequestDto.kt (1)
18-19
: images 필드 추가는 적절합니다.Nullable 타입으로 선언한 것은 모든 피드가 이미지를 가질 필요가 없다는 점에서 적절합니다. SerialName 어노테이션을 사용하여 백엔드 API와의 일관성을 유지하고 있습니다.
app/src/main/java/com/into/websoso/domain/mapper/FeedMapper.kt (1)
33-34
: 도메인 모델 매핑 구현이 적절합니다.데이터 계층의
images
및imageCount
속성이 도메인 계층의imageUrls
및imageCount
로 올바르게 매핑되어 있습니다. 이름 변경(images → imageUrls)은 도메인 계층에서 더 명확한 의미를 제공합니다.app/src/main/java/com/into/websoso/data/model/FeedEntity.kt (1)
17-18
: 이미지 관련 필드 추가가 적절합니다.두 필드 모두 non-nullable로 선언되어 있어 항상 값이 존재해야 합니다. 이는 이미지가 없는 경우 빈 리스트와 0을 사용한다는 가정이 있음을 의미합니다. 이 설계는 일관성 있는 데이터 처리를 가능하게 합니다.
추후 필요에 따라 이미지가 없는 경우를 특별하게 처리해야 한다면 nullable로 변경을 고려할 수 있지만, 현재 구현도 적절합니다.
app/src/main/java/com/into/websoso/ui/feedDetail/adapter/FeedDetailContentViewHolder.kt (1)
26-27
: 실제 이미지 URL 데이터를 사용하도록 업데이트된 것을 확인했습니다.기존 하드코딩된 이미지 URL 대신 피드 모델의 실제
imageUrls
데이터를 사용하고, 이미지 클릭 시 올바른 URL 리스트와 인덱스를 전달하도록 수정되었습니다. 이는 이미지 첨부 기능의 자연스러운 통합을 위한 적절한 변경입니다.app/src/main/java/com/into/websoso/data/remote/response/FeedResponseDto.kt (1)
44-47
: 이미지 관련 데이터 필드 추가가 적절하게 이루어졌습니다.이미지 첨부 기능을 지원하기 위해 두 가지 필드가 추가되었습니다:
thumbnailUrl
: 피드의 대표 이미지 URL (nullable)imageCount
: 피드에 첨부된 이미지 수이 구조는 피드에 이미지가 없는 경우도 적절히 처리할 수 있으며, 백엔드 API 응답과 일관성 있게 매핑되어 있습니다.
app/src/main/java/com/into/websoso/core/common/util/BindingAdapter.kt (1)
27-30
: 이미지 URL이 null이거나 비어있을 때의 처리가 추가되었습니다.이미지 URL이 유효하지 않은 경우 기본 로딩 썸네일을 표시하고 함수를 조기 종료하는 방어적 코드가 추가되었습니다. 이는 앱의 안정성과 사용자 경험을 향상시키는 좋은 개선점입니다.
app/src/main/res/layout/item_feed.xml (3)
132-133
: 이미지 표시 로직의 개선이 이루어졌습니다.이미지에 둥근 모서리(8dp)가 적용되었고, 이미지 목록이 비어있을 때 이미지 뷰를 숨기는 가시성 제어가 추가되었습니다. 이는 UI의 일관성과 사용자 경험을 향상시킵니다.
139-139
: 이미지 로딩 로직이 개선되었습니다.이미지 URL 목록이 비어있지 않은 경우에만 첫 번째 이미지를 로드하고, 그렇지 않으면 null을 전달하도록 설정되었습니다. 이는 앞서
BindingAdapter.kt
에 추가된 null 처리 로직과 잘 맞물리는 구현입니다.
150-153
: 이미지 개수 표시 기능이 구현되었습니다.피드의 이미지 개수를 표시하고, 이미지가 없는 경우 이 표시도 숨기는 가시성 제어가 추가되었습니다. 이 기능은 사용자에게 피드에 첨부된 이미지 수를 직관적으로 보여주는 좋은 UX 요소입니다.
app/src/main/java/com/into/websoso/ui/mapper/FeedMapper.kt (2)
29-30
: 이미지 관련 필드가 적절히 추가되었습니다!
Feed
도메인 모델에서 UI 모델로의 매핑 과정에서 이미지 URL과 이미지 개수가 정확히 전달되도록 구현되었습니다. 이는 UI 계층에서 이미지를 표시하는데 필요한 데이터를 제공합니다.
57-58
: 데이터 모델에서 UI 모델로 이미지 데이터가 올바르게 매핑되었습니다.
FeedEntity
의images
필드를FeedModel
의imageUrls
로 매핑하여 변수명을 더 명확하게 만든 부분이 좋습니다. 이렇게 하면 UI 레이어에서 필드의 용도가 더 명확해집니다.app/src/main/java/com/into/websoso/data/mapper/FeedMapper.kt (2)
44-45
: 썸네일 URL을 이미지 리스트로 적절히 변환했습니다.
thumbnailUrl
이 있는 경우 단일 아이템 리스트로, 없는 경우 빈 리스트로 설정하는 방식이 적절합니다. 이는 리스트 API 응답에서 썸네일만 제공하는 경우의 일반적인 패턴입니다.
95-96
:❌ Incorrect review comment
이미지 카운트 계산 방식 검토가 필요할 수 있습니다.
현재
imageCount
를images.size
로 계산하고 있는데, API 응답에 이미imageCount
필드가 있다면 해당 값을 사용하는 것이 더 좋을 수 있습니다. 서버에서 계산된 값이 보다 정확할 수 있으며, 추가 계산도 필요 없습니다.
🏁 Script executed:
#!/bin/bash # FeedDetailResponseDto의 구조를 확인하여 imageCount 필드 존재 여부 체크 rg -A 2 -B 2 "class FeedDetailResponseDto" --type kotlinLength of output: 560
FeedDetailResponseDto에
imageCount
필드가 실제로 정의되어 있는지 추가로 확인합니다. 아래 스크립트를 실행해주세요.
🏁 Script executed:
#!/bin/bash # FeedDetailResponseDto에서 imageCount 필드 존재 여부 확인 rg "val imageCount" -n app/src/main/java/com/into/websoso/data/remote/response/FeedDetailResponseDto.ktLength of output: 105
이미지 관련 필드 전체 확인을 위해 추가 스크립트를 실행해주세요.
🏁 Script executed:
#!/bin/bash # FeedDetailResponseDto에서 image 관련 모든 속성 조회 rg -n "image" app/src/main/java/com/into/websoso/data/remote/response/FeedDetailResponseDto.ktLength of output: 156
API 응답에
imageCount
필드가 없습니다
현재FeedDetailResponseDto
에는imageCount
가 정의되어 있지 않으므로,images.size
로 계산하는 현 방식을 유지하는 것이 적절합니다.Likely an incorrect or invalid review comment.
core/resource/src/main/res/drawable/ic_create_feed_image.xml (1)
1-26
: 이미지 아이콘이 적절하게 구현되었습니다.벡터 드로어블 아이콘이 표준 24dp 크기와 일관된 색상(#949399)으로 잘 구현되었습니다. 이미지 첨부 기능을 직관적으로 표현하는 아이콘으로, 사용자가 쉽게 인식할 수 있는 디자인입니다.
app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt (2)
46-56
: 이미지 URI 처리 코드가 잘 추가되었습니다!이미지 URI 리스트를 피드 요청에 포함시키는 변경이 올바르게 구현되었습니다.
67-78
: 이미지 URI 처리 코드가 잘 추가되었습니다!편집된 피드에 이미지 URI 리스트를 포함시키는 변경이 올바르게 구현되었습니다.
app/src/main/java/com/into/websoso/ui/createFeed/CreateFeedViewModel.kt (1)
38-41
: 이미지 처리를 위한 LiveData 추가가 잘 되었습니다이미지 목록을 관리하기 위한 LiveData 구현이 적절합니다.
app/src/main/java/com/into/websoso/core/common/util/SingleLiveData.kt (1)
23-24
: peekContent 사용 시 이미 소비된 값이 그대로 노출될 수 있습니다
getValue()
가peekContent()
를 그대로 반환하면, 이미getContentIfNotHandled()
로 소비된 이벤트도 다시 노출됩니다.
뷰모델 등에서 “현재 값”으로 오해해 로직이 꼬일 가능성이 있으니, 사용처를 다시 한 번 점검하거나Event
에hasBeenHandled
값을 함께 반환하는 별도 API(예:getLatestIfNotHandled()
)를 추가하는 방안을 고려해 주세요.app/src/main/java/com/into/websoso/ui/createFeed/CreateFeedActivity.kt (1)
219-223
: 압축 작업은 메인 스레드를 차단할 위험이 있습니다
lifecycleScope.launch { … }
는 기본적으로 Main 디스패처를 사용합니다.ImageCompressor.compressUris()
가 내부에서 IO-Dispatcher로 넘기지 않으면 UI 프리징이 발생할 수 있습니다.lifecycleScope.launch(Dispatchers.IO) { val compressedUris = imageCompressor.compressUris( this@CreateFeedActivity, uris, contentResolver ) withContext(Dispatchers.Main) { createFeedViewModel.addImages(compressedUris) } }
compressUris
구현이 이미withContext(Dispatchers.IO)
를 포함한다면 스킵해도 무방하지만, 명시적 분리가 안전합니다.
suspend fun compressUris( | ||
context: Context, | ||
uris: List<Uri>, | ||
contentResolver: ContentResolver, | ||
): List<Uri> = | ||
withContext(Dispatchers.IO) { | ||
val compressedUris = mutableListOf<Uri>() | ||
|
||
for (uri in uris) { | ||
val inputStream = contentResolver.openInputStream(uri) | ||
val bitmap = BitmapFactory.decodeStream(inputStream) | ||
inputStream?.close() | ||
|
||
var quality = 100 | ||
val outputStream = ByteArrayOutputStream() | ||
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream) | ||
|
||
while (outputStream.size() > MAX_IMAGE_SIZE && quality > 10) { | ||
quality -= 10 | ||
outputStream.reset() | ||
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream) | ||
} | ||
|
||
val compressedFile = File.createTempFile("compressed_", ".jpg", context.cacheDir) | ||
FileOutputStream(compressedFile).use { it.write(outputStream.toByteArray()) } | ||
|
||
compressedUris.add(Uri.fromFile(compressedFile)) | ||
outputStream.close() | ||
} | ||
|
||
return@withContext compressedUris | ||
} |
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.
🛠️ Refactor suggestion
이미지 압축 로직 개선 및 자원 관리 필요
이미지 압축 기능은 잘 구현되었지만, 몇 가지 개선이 필요합니다:
- 비트맵 자원이 명시적으로 해제되지 않아 메모리 누수 가능성이 있습니다
- 임시 파일이 생성된 후 앱 종료 시 정리되는 매커니즘이 없습니다
- 이미지 디코딩 실패나 파일 작업 오류에 대한 예외 처리가 부족합니다
다음과 같이 수정하는 것을 권장합니다:
suspend fun compressUris(
context: Context,
uris: List<Uri>,
contentResolver: ContentResolver,
): List<Uri> =
withContext(Dispatchers.IO) {
val compressedUris = mutableListOf<Uri>()
for (uri in uris) {
- val inputStream = contentResolver.openInputStream(uri)
- val bitmap = BitmapFactory.decodeStream(inputStream)
- inputStream?.close()
+ var bitmap: Bitmap? = null
+ try {
+ contentResolver.openInputStream(uri)?.use { inputStream ->
+ bitmap = BitmapFactory.decodeStream(inputStream)
+ } ?: throw IOException("이미지를 열 수 없습니다")
+ } catch (e: Exception) {
+ continue
+ }
+ if (bitmap == null) continue
var quality = 100
val outputStream = ByteArrayOutputStream()
- bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
+ bitmap!!.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
while (outputStream.size() > MAX_IMAGE_SIZE && quality > 10) {
quality -= 10
outputStream.reset()
- bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
+ bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
}
- val compressedFile = File.createTempFile("compressed_", ".jpg", context.cacheDir)
- FileOutputStream(compressedFile).use { it.write(outputStream.toByteArray()) }
+ try {
+ val compressedFile = File.createTempFile("compressed_", ".jpg", context.cacheDir)
+ FileOutputStream(compressedFile).use { it.write(outputStream.toByteArray()) }
+ compressedUris.add(Uri.fromFile(compressedFile))
+ } catch (e: IOException) {
+ // 파일 작성 실패 로깅
+ }
- compressedUris.add(Uri.fromFile(compressedFile))
outputStream.close()
+ bitmap.recycle()
}
return@withContext compressedUris
}
또한 파일 상단에 java.io.IOException
import를 추가해야 합니다.
🤖 Prompt for AI Agents
In app/src/main/java/com/into/websoso/core/common/util/ImageCompressor.kt
between lines 28 and 59, the image compression function lacks proper resource
management and error handling. To fix this, explicitly call bitmap.recycle()
after compression to free bitmap memory, add try-catch blocks around image
decoding and file operations to handle exceptions gracefully, and implement a
mechanism to delete the temporary compressed files when they are no longer
needed, such as scheduling cleanup on app exit or using a cache management
strategy. Also, add the import for java.io.IOException at the top of the file to
support exception handling.
fun addImages(newUris: List<Uri>) { | ||
val current = _attachedImages.value.orEmpty().toMutableList() | ||
val remaining = MAX_IMAGE_COUNT - current.size | ||
|
||
if (remaining >= newUris.size) { | ||
current.addAll(newUris) | ||
_attachedImages.value = current | ||
} else { | ||
_exceedingImageCountEvent.postValue(Unit) | ||
} | ||
} |
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.
🛠️ Refactor suggestion
이미지 추가 로직 개선 필요
현재 구현은 이미지 추가 시 전체 개수가 제한을 초과하면 모든 이미지를 거부합니다. 가능한 만큼 추가하고 사용자에게 알리는 방식이 더 사용자 친화적일 수 있습니다.
다음과 같이 개선할 수 있습니다:
fun addImages(newUris: List<Uri>) {
val current = _attachedImages.value.orEmpty().toMutableList()
val remaining = MAX_IMAGE_COUNT - current.size
- if (remaining >= newUris.size) {
- current.addAll(newUris)
- _attachedImages.value = current
- } else {
+ if (remaining <= 0) {
_exceedingImageCountEvent.postValue(Unit)
+ return
+ }
+
+ if (remaining >= newUris.size) {
+ current.addAll(newUris)
+ } else {
+ // 가능한 만큼만 추가하고 이벤트 발생
+ current.addAll(newUris.take(remaining))
+ _exceedingImageCountEvent.postValue(Unit)
}
+
+ _attachedImages.value = current
}
🤖 Prompt for AI Agents
In app/src/main/java/com/into/websoso/ui/createFeed/CreateFeedViewModel.kt
around lines 218 to 228, the addImages function currently rejects all new images
if adding them exceeds the max limit. Modify the logic to add as many images as
possible up to the MAX_IMAGE_COUNT, then update _attachedImages with the partial
list and post _exceedingImageCountEvent to notify the user that not all images
were added.
app/src/main/java/com/into/websoso/core/common/util/DynamicLimitPhotoPicker.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/com/into/websoso/ui/createFeed/CreateFeedActivity.kt
Outdated
Show resolved
Hide resolved
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.
Actionable comments posted: 4
♻️ Duplicate comments (1)
app/src/main/java/com/into/websoso/ui/createFeed/CreateFeedViewModel.kt (1)
220-230
: 이미지 추가 로직 개선 필요현재 구현은 이미지 추가 시 전체 개수가 제한을 초과하면 모든 이미지를 거부합니다. 가능한 만큼 추가하고 사용자에게 알리는 방식이 더 사용자 친화적일 수 있습니다.
다음과 같이 개선할 수 있습니다:
fun addImages(newImages: List<Uri>) { val current = _attachedImages.value.toMutableList() val remaining = MAX_IMAGE_COUNT - current.size - if (remaining >= newImages.size) { - current.addAll(newImages) - _attachedImages.value = current - } else { + if (remaining <= 0) { _exceedingImageCountEvent.postValue(Unit) + return + } + + if (remaining >= newImages.size) { + current.addAll(newImages) + } else { + // 가능한 만큼만 추가하고 이벤트 발생 + current.addAll(newImages.take(remaining)) + _exceedingImageCountEvent.postValue(Unit) } + + _attachedImages.value = current }
🧹 Nitpick comments (2)
app/src/main/java/com/into/websoso/ui/expandedFeedImage/ExpandedFeedImageScreen.kt (1)
72-88
: 접근성 개선이 필요합니다.닫기 버튼의
contentDescription
이null
로 설정되어 있어 접근성에 문제가 있을 수 있습니다.접근성을 위해 적절한 설명을 추가해주세요:
Icon( painter = painterResource(id = ic_expanded_feed_image_close), - contentDescription = null, + contentDescription = "닫기", tint = White, // ... )app/src/main/java/com/into/websoso/ui/createFeed/component/CreateFeedImageContainer.kt (1)
91-102
: 프리뷰 컴포저블을 활성화하는 것을 권장합니다.프리뷰가 주석처리되어 개발 생산성이 저하될 수 있습니다. Mock ViewModel을 사용하여 프리뷰를 활성화하세요.
다음과 같이 개선할 수 있습니다:
-/* @Preview @Composable private fun CreateFeedImageContainerPreview() { + val mockViewModel = remember { + CreateFeedViewModel().apply { + // Mock URI 데이터 추가 (테스트용) + } + } WebsosoTheme { CreateFeedImageContainer( - viewModel = CreateFeedViewModel(), + viewModel = mockViewModel, onRemoveClick = {}, ) } } -*/
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (16)
app/src/main/AndroidManifest.xml
(1 hunks)app/src/main/java/com/into/websoso/data/di/RepositoryModule.kt
(2 hunks)app/src/main/java/com/into/websoso/data/mapper/MultipartMapper.kt
(1 hunks)app/src/main/java/com/into/websoso/data/remote/api/FeedApi.kt
(2 hunks)app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt
(4 hunks)app/src/main/java/com/into/websoso/ui/createFeed/CreateFeedActivity.kt
(7 hunks)app/src/main/java/com/into/websoso/ui/createFeed/CreateFeedViewModel.kt
(6 hunks)app/src/main/java/com/into/websoso/ui/createFeed/component/CreateFeedImageContainer.kt
(5 hunks)app/src/main/java/com/into/websoso/ui/expandedFeedImage/ExpandedFeedImage.kt
(1 hunks)app/src/main/java/com/into/websoso/ui/expandedFeedImage/ExpandedFeedImageActivity.kt
(1 hunks)app/src/main/java/com/into/websoso/ui/expandedFeedImage/ExpandedFeedImageScreen.kt
(1 hunks)app/src/main/java/com/into/websoso/ui/feedDetail/FeedDetailActivity.kt
(3 hunks)app/src/main/java/com/into/websoso/ui/feedDetail/FeedDetailClickListener.kt
(1 hunks)app/src/main/java/com/into/websoso/ui/feedDetail/adapter/FeedDetailContentViewHolder.kt
(1 hunks)core/designsystem/src/main/java/com/into/websoso/core/designsystem/theme/Color.kt
(1 hunks)core/resource/src/main/res/drawable/ic_expanded_feed_image_close.xml
(1 hunks)
✅ Files skipped from review due to trivial changes (5)
- core/designsystem/src/main/java/com/into/websoso/core/designsystem/theme/Color.kt
- app/src/main/java/com/into/websoso/ui/expandedFeedImage/ExpandedFeedImage.kt
- app/src/main/AndroidManifest.xml
- core/resource/src/main/res/drawable/ic_expanded_feed_image_close.xml
- app/src/main/java/com/into/websoso/ui/expandedFeedImage/ExpandedFeedImageActivity.kt
🚧 Files skipped from review as they are similar to previous changes (1)
- app/src/main/java/com/into/websoso/ui/feedDetail/adapter/FeedDetailContentViewHolder.kt
🧰 Additional context used
🧬 Code Graph Analysis (2)
app/src/main/java/com/into/websoso/ui/expandedFeedImage/ExpandedFeedImageScreen.kt (1)
app/src/main/java/com/into/websoso/ui/expandedFeedImage/ExpandedFeedImage.kt (1)
ExpandedFeedImage
(11-23)
app/src/main/java/com/into/websoso/ui/createFeed/CreateFeedActivity.kt (2)
app/src/main/java/com/into/websoso/core/common/util/ExtentionFuction.kt (1)
showWebsosoSnackBar
(80-90)app/src/main/java/com/into/websoso/ui/createFeed/component/CreateFeedImageContainer.kt (1)
CreateFeedImageContainer
(28-50)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: build
🔇 Additional comments (20)
app/src/main/java/com/into/websoso/ui/feedDetail/FeedDetailClickListener.kt (1)
20-23
: 파라미터 순서 변경이 올바르게 적용되었습니다.인터페이스의 파라미터 순서가
(index: Int, imageUrls: List<String>)
로 변경되어 더 직관적입니다. 일반적으로 선택된 인덱스를 먼저 전달하는 것이 의미상 자연스럽습니다.app/src/main/java/com/into/websoso/ui/feedDetail/FeedDetailActivity.kt (3)
52-52
: 적절한 import 추가입니다.새로 도입된
ExpandedFeedImageActivity
를 사용하기 위한 필수적인 import입니다.
144-149
: 인터페이스 변경에 맞춰 올바르게 업데이트되었습니다.
FeedDetailClickListener
인터페이스의 파라미터 순서 변경에 맞춰 구현이 일관되게 업데이트되었습니다.
188-194
: 이미지 확장 보기 네비게이션이 올바르게 구현되었습니다.
ExpandedFeedImageActivity.getIntent()
를 사용하여 액티비티를 시작하는 구현이 깔끔하고 명확합니다. 파라미터도 올바른 순서로 전달되고 있습니다.app/src/main/java/com/into/websoso/ui/expandedFeedImage/ExpandedFeedImageScreen.kt (3)
40-50
: HorizontalPager 구현이 우수합니다.
HorizontalPager
를 사용한 이미지 스와이프 기능이 적절하게 구현되었습니다.pageCount
람다가imageUrls.size
를 반환하므로 페이지 범위와 이미지 배열 인덱스가 일치합니다.
90-104
: 페이지 번호 표시 기능이 잘 구현되었습니다.
pagerState.currentPage + 1
을 사용하여 사용자 친화적인 1-based 인덱스로 페이지 번호를 표시하고 있으며, 전체 이미지 수와 함께 명확하게 표시됩니다.
106-119
: Preview가 적절하게 구현되었습니다.샘플 데이터를 사용한 Preview가 개발 시 UI 확인에 도움이 됩니다. WebsosoTheme으로 감싸져 있어 일관된 테마가 적용됩니다.
app/src/main/java/com/into/websoso/data/di/RepositoryModule.kt (1)
5-5
: 의존성 주입 설정이 올바르게 구현되었습니다.MultipartMapper가 FeedRepository에 적절히 주입되도록 설정되어 이미지 업로드 기능을 지원합니다.
Also applies to: 28-31
app/src/main/java/com/into/websoso/data/remote/api/FeedApi.kt (1)
9-9
: 멀티파트 API 설계가 적절합니다.피드 데이터와 이미지를 별도의 파트로 분리하여 효율적인 업로드를 지원하고, 이미지 리스트를 nullable로 설정하여 선택적 업로드가 가능합니다.
Also applies to: 13-13, 16-16, 28-33, 35-41
app/src/main/java/com/into/websoso/ui/createFeed/component/CreateFeedImageContainer.kt (2)
30-33
: 상태 관리가 올바르게 개선되었습니다.
collectAsStateWithLifecycle
을 사용하여 라이프사이클 안전성을 확보하고, ViewModel에서 직접 상태를 관찰하는 것이 좋은 패턴입니다.
63-68
: 이미지 로딩 방식이 효율적으로 개선되었습니다.네트워크 이미지에서 로컬 URI 기반 로딩으로 변경하여 성능이 향상되었고, Coil의
rememberAsyncImagePainter
사용이 적절합니다.app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt (3)
3-4
: 이미지 업로드를 위한 의존성 추가가 적절합니다.Uri와 MultipartMapper import 및 의존성 주입이 올바르게 구현되었습니다.
Also applies to: 20-20
43-63
: 이미지 업로드 기능이 잘 구현되었습니다.multipartMapper를 사용한 DTO와 이미지 변환 로직이 적절하며, 기존 API 구조를 유지하면서 새로운 기능을 추가했습니다.
65-87
: 일관성 있는 구현으로 잘 작성되었습니다.saveEditedFeed 메소드도 saveFeed와 동일한 패턴으로 구현되어 코드 일관성이 좋습니다.
app/src/main/java/com/into/websoso/ui/createFeed/CreateFeedViewModel.kt (2)
3-3
: StateFlow 사용으로 좋은 개선이 이루어졌습니다.LiveData 대신 StateFlow를 사용한 이미지 상태 관리와 이벤트 처리를 위한 SingleLiveData 도입이 적절합니다.
Also applies to: 10-11, 20-22, 40-43
77-110
: Repository 호출에 이미지 파라미터가 올바르게 추가되었습니다.createFeed와 editFeed 메소드에서 첨부된 이미지들을 repository에 전달하는 로직이 적절히 구현되었습니다.
app/src/main/java/com/into/websoso/ui/createFeed/CreateFeedActivity.kt (4)
10-10
: 이미지 기능에 필요한 의존성들이 적절히 추가되었습니다.DynamicLimitPhotoPicker, ImageCompressor 등 필요한 클래스들의 import와 필드 선언이 올바르게 구현되었습니다.
Also applies to: 12-12, 23-25, 27-27, 30-32, 35-36, 40-40, 50-51
145-163
: 이미지 선택 로직이 잘 구현되었습니다.현재 이미지 개수를 확인하여 제한 내에서만 picker를 실행하고, 제한 초과 시 적절한 피드백을 제공하는 로직이 좋습니다.
170-188
: 이벤트 관찰자가 적절히 추가되었습니다.exceedingImageCountEvent에 대한 관찰자가 추가되어 사용자에게 적절한 피드백을 제공합니다.
208-216
: ComposeView 성능 이슈가 잘 해결되었습니다.이전에 지적된 setContent 반복 호출 문제가 해결되어, 이제 한 번만 호출하고 Compose 내부에서 상태를 관찰하도록 개선되었습니다.
@Composable | ||
fun ExpandedFeedImageScreen( | ||
index: Int = 0, | ||
imageUrls: List<String> = emptyList(), | ||
onBackButtonClick: () -> Unit, | ||
) { | ||
val pagerState = rememberPagerState(initialPage = index, pageCount = { imageUrls.size }) | ||
|
||
BackHandler { | ||
onBackButtonClick() | ||
} |
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.
🛠️ Refactor suggestion
초기 파라미터 검증 추가를 권장합니다.
현재 index
파라미터가 imageUrls
범위를 벗어나는 경우에 대한 검증이 없습니다. 이는 rememberPagerState
에서 initialPage
로 전달될 때 문제를 일으킬 수 있습니다.
다음과 같이 파라미터 검증을 추가하는 것을 권장합니다:
@Composable
fun ExpandedFeedImageScreen(
index: Int = 0,
imageUrls: List<String> = emptyList(),
onBackButtonClick: () -> Unit,
) {
+ val safeIndex = index.coerceIn(0, (imageUrls.size - 1).coerceAtLeast(0))
- val pagerState = rememberPagerState(initialPage = index, pageCount = { imageUrls.size })
+ val pagerState = rememberPagerState(initialPage = safeIndex, pageCount = { imageUrls.size })
🤖 Prompt for AI Agents
In
app/src/main/java/com/into/websoso/ui/expandedFeedImage/ExpandedFeedImageScreen.kt
around lines 28 to 38, the index parameter is used as the initialPage for
rememberPagerState without validation, which can cause issues if index is out of
bounds of imageUrls. Add validation to ensure index is within the valid range (0
to imageUrls.size - 1) before passing it to rememberPagerState, for example by
clamping the index value or providing a default fallback within range.
fun formatToMultipart(uri: Uri): MultipartBody.Part { | ||
val inputStream: InputStream = requireNotNull(context.contentResolver.openInputStream(uri)) | ||
|
||
val bytes = inputStream.readBytes() | ||
val fileName = uri.lastPathSegment ?: "image.jpg" | ||
|
||
val requestBody = bytes.toRequestBody("image/*".toMediaType()) | ||
return MultipartBody.Part.createFormData("images", fileName, requestBody) | ||
} |
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.
🛠️ Refactor suggestion
메모리 관리와 에러 처리를 개선해야 합니다.
현재 구현에서 다음 문제점들이 있습니다:
requireNotNull
로 인한 크래시 위험readBytes()
로 전체 이미지를 메모리에 로드하여 대용량 이미지 시 OOM 발생 가능- InputStream 리소스가 적절히 해제되지 않음
다음과 같이 개선하세요:
fun formatToMultipart(uri: Uri): MultipartBody.Part {
- val inputStream: InputStream = requireNotNull(context.contentResolver.openInputStream(uri))
-
- val bytes = inputStream.readBytes()
+ val inputStream: InputStream = context.contentResolver.openInputStream(uri)
+ ?: throw IllegalArgumentException("Invalid 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)
+ }
- val fileName = uri.lastPathSegment ?: "image.jpg"
-
- val requestBody = bytes.toRequestBody("image/*".toMediaType())
- return MultipartBody.Part.createFormData("images", fileName, requestBody)
}
또한 대용량 이미지 처리를 위해 스트리밍 방식을 고려하거나 이미지 압축을 먼저 적용하는 것을 권장합니다.
📝 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.
fun formatToMultipart(uri: Uri): MultipartBody.Part { | |
val inputStream: InputStream = requireNotNull(context.contentResolver.openInputStream(uri)) | |
val bytes = inputStream.readBytes() | |
val fileName = uri.lastPathSegment ?: "image.jpg" | |
val requestBody = bytes.toRequestBody("image/*".toMediaType()) | |
return MultipartBody.Part.createFormData("images", fileName, requestBody) | |
} | |
fun formatToMultipart(uri: Uri): MultipartBody.Part { | |
val inputStream: InputStream = context.contentResolver.openInputStream(uri) | |
?: throw IllegalArgumentException("Invalid 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) | |
} | |
} |
🤖 Prompt for AI Agents
In app/src/main/java/com/into/websoso/data/mapper/MultipartMapper.kt around
lines 26 to 34, the current formatToMultipart function risks crashing due to
requireNotNull, loads the entire image into memory causing potential OOM, and
does not properly close the InputStream. To fix this, replace requireNotNull
with safe null checks and handle the null case gracefully, avoid reading the
entire InputStream into a byte array by using streaming or chunked reading, and
ensure the InputStream is closed properly using a try-with-resources or Kotlin's
use block. Additionally, consider applying image compression before creating the
multipart to handle large images efficiently.
private fun createFeedImagePickerLauncher() = | ||
registerForActivityResult(DynamicLimitPhotoPicker()) { uris -> | ||
if (uris.isNullOrEmpty()) return@registerForActivityResult | ||
lifecycleScope.launch { | ||
val compressedUris = imageCompressor.compressUris(this@CreateFeedActivity, uris, contentResolver) | ||
createFeedViewModel.addImages(compressedUris) | ||
} | ||
} |
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.
🛠️ Refactor suggestion
이미지 압축 과정에 에러 처리 추가가 필요합니다.
이미지 압축 중 예외가 발생할 수 있으므로 적절한 에러 처리를 추가하는 것이 좋겠습니다.
다음과 같이 개선할 수 있습니다:
private fun createFeedImagePickerLauncher() =
registerForActivityResult(DynamicLimitPhotoPicker()) { uris ->
if (uris.isNullOrEmpty()) return@registerForActivityResult
lifecycleScope.launch {
- val compressedUris = imageCompressor.compressUris(this@CreateFeedActivity, uris, contentResolver)
- createFeedViewModel.addImages(compressedUris)
+ runCatching {
+ val compressedUris = imageCompressor.compressUris(this@CreateFeedActivity, uris, contentResolver)
+ createFeedViewModel.addImages(compressedUris)
+ }.onFailure {
+ // 이미지 압축 실패 시 사용자에게 알림
+ showWebsosoSnackBar(binding.root, "이미지 처리 중 오류가 발생했습니다.", ic_blocked_user_snack_bar)
+ }
}
}
📝 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.
private fun createFeedImagePickerLauncher() = | |
registerForActivityResult(DynamicLimitPhotoPicker()) { uris -> | |
if (uris.isNullOrEmpty()) return@registerForActivityResult | |
lifecycleScope.launch { | |
val compressedUris = imageCompressor.compressUris(this@CreateFeedActivity, uris, contentResolver) | |
createFeedViewModel.addImages(compressedUris) | |
} | |
} | |
private fun createFeedImagePickerLauncher() = | |
registerForActivityResult(DynamicLimitPhotoPicker()) { uris -> | |
if (uris.isNullOrEmpty()) return@registerForActivityResult | |
lifecycleScope.launch { | |
runCatching { | |
val compressedUris = imageCompressor.compressUris( | |
this@CreateFeedActivity, | |
uris, | |
contentResolver | |
) | |
createFeedViewModel.addImages(compressedUris) | |
}.onFailure { | |
// 이미지 압축 실패 시 사용자에게 알림 | |
showWebsosoSnackBar( | |
binding.root, | |
"이미지 처리 중 오류가 발생했습니다.", | |
ic_blocked_user_snack_bar | |
) | |
} | |
} | |
} |
🤖 Prompt for AI Agents
In app/src/main/java/com/into/websoso/ui/createFeed/CreateFeedActivity.kt around
lines 218 to 225, the image compression process lacks error handling which may
cause crashes if exceptions occur. Wrap the compression and subsequent calls
inside a try-catch block within the coroutine to catch any exceptions during
compression. In the catch block, handle the error appropriately, such as logging
the error or showing a user-friendly message, to ensure the app remains stable.
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.
Actionable comments posted: 5
🧹 Nitpick comments (1)
app/src/main/java/com/into/websoso/core/common/util/ImageDownloader.kt (1)
21-22
: 불필요한 connect() 호출을 제거하세요.
connection.connect()
는getInputStream()
을 호출할 때 자동으로 실행되므로 중복입니다.- val connection = URL(url).openConnection() - connection.connect() - val inputStream = connection.getInputStream() + val connection = URL(url).openConnection() + val inputStream = connection.getInputStream()
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
app/src/main/java/com/into/websoso/core/common/util/ImageCompressor.kt
(1 hunks)app/src/main/java/com/into/websoso/core/common/util/ImageDownloader.kt
(1 hunks)app/src/main/java/com/into/websoso/core/common/util/MultiPartConvertor.kt
(1 hunks)app/src/main/java/com/into/websoso/data/di/RepositoryModule.kt
(0 hunks)app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt
(5 hunks)app/src/main/java/com/into/websoso/ui/createFeed/CreateFeedActivity.kt
(7 hunks)app/src/main/java/com/into/websoso/ui/createFeed/CreateFeedViewModel.kt
(7 hunks)app/src/main/java/com/into/websoso/ui/feedDetail/FeedDetailActivity.kt
(4 hunks)app/src/main/java/com/into/websoso/ui/feedDetail/model/EditFeedModel.kt
(1 hunks)app/src/main/java/com/into/websoso/ui/main/feed/FeedFragment.kt
(1 hunks)app/src/main/java/com/into/websoso/ui/novelDetail/NovelDetailActivity.kt
(1 hunks)app/src/main/java/com/into/websoso/ui/novelFeed/NovelFeedFragment.kt
(1 hunks)app/src/main/res/layout/item_feed.xml
(2 hunks)
💤 Files with no reviewable changes (1)
- app/src/main/java/com/into/websoso/data/di/RepositoryModule.kt
🚧 Files skipped from review as they are similar to previous changes (6)
- app/src/main/res/layout/item_feed.xml
- app/src/main/java/com/into/websoso/core/common/util/ImageCompressor.kt
- app/src/main/java/com/into/websoso/ui/feedDetail/FeedDetailActivity.kt
- app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt
- app/src/main/java/com/into/websoso/ui/createFeed/CreateFeedActivity.kt
- app/src/main/java/com/into/websoso/ui/createFeed/CreateFeedViewModel.kt
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: build
🔇 Additional comments (8)
app/src/main/java/com/into/websoso/ui/feedDetail/model/EditFeedModel.kt (1)
15-15
: 이미지 URL 속성 추가가 적절하게 구현되었습니다.
imageUrls
속성이 기존 패턴과 일관되게 추가되었으며, 기본값으로 빈 리스트를 설정한 것이 적절합니다. Parcelable 데이터 클래스에서 새로운 필드 추가가 올바르게 처리되었습니다.app/src/main/java/com/into/websoso/ui/novelFeed/NovelFeedFragment.kt (1)
248-248
: 피드 편집 시 이미지 URL 전달이 올바르게 구현되었습니다.기존 피드의
imageUrls
를EditFeedModel
에 전달하여 편집 시 기존 이미지 정보를 유지하는 로직이 적절하게 구현되었습니다.app/src/main/java/com/into/websoso/ui/main/feed/FeedFragment.kt (1)
347-347
: 일관된 이미지 URL 전달 로직 구현
NovelFeedFragment
와 동일한 패턴으로 피드 편집 시imageUrls
를 전달하여 코드 일관성이 잘 유지되었습니다.app/src/main/java/com/into/websoso/ui/novelDetail/NovelDetailActivity.kt (1)
335-335
: 새 피드 작성 시 이미지 URL 초기화가 적절합니다.새로운 피드 작성 시
imageUrls
를 빈 리스트로 초기화하는 것이 논리적으로 올바르며, 다른 파일들의 구현과 일관성을 유지하고 있습니다.app/src/main/java/com/into/websoso/core/common/util/ImageDownloader.kt (1)
1-17
: 클래스 구조와 의존성 주입이 올바르게 구현되었습니다.Hilt를 사용한 의존성 주입과 패키지 구조가 적절합니다.
app/src/main/java/com/into/websoso/core/common/util/MultiPartConvertor.kt (3)
1-19
: 클래스 구조와 의존성이 올바르게 설정되었습니다.필요한 라이브러리 imports와 Hilt 의존성 주입이 적절합니다.
20-24
: FeedRequestDto 멀티파트 변환이 올바르게 구현되었습니다.JSON 직렬화와 멀티파트 변환 로직이 적절합니다.
20-24
: JSON 멀티파트 변환 로직이 올바르게 구현되었습니다.FeedRequestDto를 JSON으로 직렬화하여 멀티파트로 변환하는 로직이 적절합니다.
suspend fun formatImageToUri(url: String): Uri? = | ||
withContext(Dispatchers.IO) { | ||
runCatching { | ||
val connection = URL(url).openConnection() | ||
connection.connect() | ||
val inputStream = connection.getInputStream() | ||
val file = File.createTempFile("image_", ".jpg", context.cacheDir) | ||
FileOutputStream(file).use { output -> inputStream.copyTo(output) } | ||
Uri.fromFile(file) | ||
}.getOrNull() | ||
} |
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.
보안 및 성능 개선이 필요합니다.
현재 구현에 다음과 같은 보안 및 성능 문제가 있습니다:
- URL 검증 없음 - 악의적인 URL에 대한 방어 부족
- 연결 타임아웃 미설정 - 무한 대기 가능성
- 파일 크기 제한 없음 - 메모리 부족 위험
- Content-Type 검증 없음 - 이미지가 아닌 파일 다운로드 가능
다음과 같이 개선하는 것을 권장합니다:
suspend fun formatImageToUri(url: String): Uri? =
withContext(Dispatchers.IO) {
runCatching {
+ // URL 검증
+ if (!url.startsWith("http://") && !url.startsWith("https://")) {
+ throw IllegalArgumentException("Invalid URL scheme")
+ }
+
val connection = URL(url).openConnection()
+ connection.connectTimeout = 10000 // 10초
+ connection.readTimeout = 30000 // 30초
+
+ // Content-Type 검증
+ val contentType = connection.contentType
+ if (contentType?.startsWith("image/") != true) {
+ throw IllegalArgumentException("Content is not an image")
+ }
+
+ // 파일 크기 제한 (예: 10MB)
+ val contentLength = connection.contentLength
+ if (contentLength > 10 * 1024 * 1024) {
+ throw IllegalArgumentException("File too large")
+ }
+
connection.connect()
- val inputStream = connection.getInputStream()
+ connection.getInputStream().use { inputStream ->
val file = File.createTempFile("image_", ".jpg", context.cacheDir)
FileOutputStream(file).use { output -> inputStream.copyTo(output) }
Uri.fromFile(file)
+ }
- Uri.fromFile(file)
}.getOrNull()
}
🤖 Prompt for AI Agents
In app/src/main/java/com/into/websoso/core/common/util/ImageDownloader.kt around
lines 18 to 28, the current image download method lacks URL validation,
connection timeout settings, file size limits, and content-type checks, which
pose security and performance risks. To fix this, add validation to ensure the
URL is well-formed and points to a trusted domain, set connection and read
timeouts on the URL connection to prevent hanging, check the Content-Type header
to confirm the resource is an image before downloading, and impose a maximum
file size limit by monitoring the input stream to avoid excessive memory use.
Implement these checks before saving the file and returning the Uri.
🛠️ Refactor suggestion
보안 및 성능 개선이 필요합니다.
현재 구현에서 다음과 같은 문제점들이 있습니다:
- 연결 타임아웃 미설정: 네트워크 지연 시 무한 대기 가능성
- 파일 크기 제한 없음: 대용량 파일 다운로드로 인한 메모리/저장공간 부족 위험
- 리소스 정리 부족: InputStream이 명시적으로 닫히지 않음
- 보안 검증 없음: 임의의 URL 다운로드 및 이미지 형식 검증 부재
다음과 같이 개선하는 것을 권장합니다:
suspend fun formatImageToUri(url: String): Uri? =
withContext(Dispatchers.IO) {
runCatching {
- val connection = URL(url).openConnection()
- connection.connect()
- val inputStream = connection.getInputStream()
+ val connection = URL(url).openConnection().apply {
+ connectTimeout = 10_000
+ readTimeout = 30_000
+ setRequestProperty("User-Agent", "WSS-Android")
+ }
+ val contentType = connection.contentType
+ if (!contentType?.startsWith("image/") == true) {
+ return@runCatching null
+ }
+ val contentLength = connection.contentLengthLong
+ if (contentLength > 10 * 1024 * 1024) { // 10MB 제한
+ return@runCatching null
+ }
+ val inputStream = connection.getInputStream()
val file = File.createTempFile("image_", ".jpg", context.cacheDir)
- FileOutputStream(file).use { output -> inputStream.copyTo(output) }
+ inputStream.use { input ->
+ FileOutputStream(file).use { output ->
+ input.copyTo(output)
+ }
+ }
Uri.fromFile(file)
}.getOrNull()
}
📝 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.
suspend fun formatImageToUri(url: String): Uri? = | |
withContext(Dispatchers.IO) { | |
runCatching { | |
val connection = URL(url).openConnection() | |
connection.connect() | |
val inputStream = connection.getInputStream() | |
val file = File.createTempFile("image_", ".jpg", context.cacheDir) | |
FileOutputStream(file).use { output -> inputStream.copyTo(output) } | |
Uri.fromFile(file) | |
}.getOrNull() | |
} | |
suspend fun formatImageToUri(url: String): Uri? = | |
withContext(Dispatchers.IO) { | |
runCatching { | |
val connection = URL(url).openConnection().apply { | |
connectTimeout = 10_000 | |
readTimeout = 30_000 | |
setRequestProperty("User-Agent", "WSS-Android") | |
} | |
val contentType = connection.contentType | |
if (!contentType?.startsWith("image/") == true) { | |
return@runCatching null | |
} | |
val contentLength = connection.contentLengthLong | |
if (contentLength > 10 * 1024 * 1024) { // 10MB 제한 | |
return@runCatching null | |
} | |
val inputStream = connection.getInputStream() | |
val file = File.createTempFile("image_", ".jpg", context.cacheDir) | |
inputStream.use { input -> | |
FileOutputStream(file).use { output -> | |
input.copyTo(output) | |
} | |
} | |
Uri.fromFile(file) | |
}.getOrNull() | |
} |
🤖 Prompt for AI Agents
In app/src/main/java/com/into/websoso/core/common/util/ImageDownloader.kt around
lines 18 to 28, the current image download implementation lacks connection
timeout settings, file size limits, proper resource cleanup, and security
checks. To fix this, set appropriate connection and read timeouts on the URL
connection, limit the maximum size of the downloaded file to prevent excessive
memory or storage use, ensure the InputStream is closed explicitly using a
try-with-resources or Kotlin's use block, and add validation to verify the
downloaded content is a valid image format before returning the Uri.
fun formatToMultipart(uri: Uri): MultipartBody.Part { | ||
val inputStream: InputStream = requireNotNull(context.contentResolver.openInputStream(uri)) | ||
|
||
val bytes = inputStream.readBytes() | ||
val fileName = uri.lastPathSegment ?: "image.jpg" | ||
|
||
val requestBody = bytes.toRequestBody("image/*".toMediaType()) | ||
return MultipartBody.Part.createFormData("images", fileName, requestBody) | ||
} |
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.
🛠️ Refactor suggestion
리소스 관리와 메모리 효율성 개선이 필요합니다.
현재 구현에서 다음 사항들을 개선할 수 있습니다:
- InputStream 리소스 정리가 명시적이지 않음
- 전체 파일을 메모리에 로드하여 큰 파일에 대한 메모리 문제 가능성
- 예외 메시지 부족
다음과 같이 개선하는 것을 권장합니다:
fun formatToMultipart(uri: Uri): MultipartBody.Part {
- val inputStream: InputStream = requireNotNull(context.contentResolver.openInputStream(uri))
-
- val bytes = inputStream.readBytes()
+ val inputStream: InputStream = requireNotNull(
+ context.contentResolver.openInputStream(uri)
+ ) { "Unable to open input stream for URI: $uri" }
+
+ val bytes = inputStream.use { it.readBytes() }
val fileName = uri.lastPathSegment ?: "image.jpg"
+ // 파일 크기 제한 (예: 10MB)
+ if (bytes.size > 10 * 1024 * 1024) {
+ throw IllegalArgumentException("File too large: ${bytes.size} bytes")
+ }
+
val requestBody = bytes.toRequestBody("image/*".toMediaType())
return MultipartBody.Part.createFormData("images", fileName, requestBody)
}
📝 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.
fun formatToMultipart(uri: Uri): MultipartBody.Part { | |
val inputStream: InputStream = requireNotNull(context.contentResolver.openInputStream(uri)) | |
val bytes = inputStream.readBytes() | |
val fileName = uri.lastPathSegment ?: "image.jpg" | |
val requestBody = bytes.toRequestBody("image/*".toMediaType()) | |
return MultipartBody.Part.createFormData("images", fileName, requestBody) | |
} | |
fun formatToMultipart(uri: Uri): MultipartBody.Part { | |
val inputStream: InputStream = requireNotNull( | |
context.contentResolver.openInputStream(uri) | |
) { "Unable to open input stream for URI: $uri" } | |
val bytes = inputStream.use { it.readBytes() } | |
val fileName = uri.lastPathSegment ?: "image.jpg" | |
// 파일 크기 제한 (예: 10MB) | |
if (bytes.size > 10 * 1024 * 1024) { | |
throw IllegalArgumentException("File too large: ${bytes.size} bytes") | |
} | |
val requestBody = bytes.toRequestBody("image/*".toMediaType()) | |
return MultipartBody.Part.createFormData("images", fileName, requestBody) | |
} |
🤖 Prompt for AI Agents
In app/src/main/java/com/into/websoso/core/common/util/MultiPartConvertor.kt
around lines 26 to 34, the formatToMultipart function does not explicitly close
the InputStream, reads the entire file into memory which can cause issues with
large files, and lacks detailed exception handling. To fix this, use a
try-with-resources or Kotlin's use function to ensure the InputStream is closed
properly, avoid reading the entire file into a byte array by streaming the data
directly into the request body if possible, and add proper exception handling
that includes informative error messages for better debugging.
🛠️ Refactor suggestion
메모리 및 리소스 관리 개선이 필요합니다.
현재 구현에서 다음과 같은 문제점들이 있습니다:
- InputStream 리소스 누수:
use
블록으로 자동 닫기 처리되지 않음 - 메모리 부족 위험:
readBytes()
로 전체 파일을 메모리에 로드 - 파일 크기 제한 없음: 대용량 이미지 처리 시 OOM 발생 가능성
- 에러 처리 부족:
requireNotNull
로 인한 예상치 못한 크래시 위험
다음과 같이 개선하는 것을 권장합니다:
fun formatToMultipart(uri: Uri): MultipartBody.Part {
- val inputStream: InputStream = requireNotNull(context.contentResolver.openInputStream(uri))
-
- val bytes = inputStream.readBytes()
+ val inputStream = context.contentResolver.openInputStream(uri)
+ ?: throw IllegalArgumentException("Cannot open input stream for URI: $uri")
+
+ val bytes = inputStream.use { stream ->
+ // 파일 크기 체크 (예: 10MB 제한)
+ val availableBytes = stream.available()
+ if (availableBytes > 10 * 1024 * 1024) {
+ throw IllegalArgumentException("File size exceeds limit: $availableBytes bytes")
+ }
+ stream.readBytes()
+ }
val fileName = uri.lastPathSegment ?: "image.jpg"
val requestBody = bytes.toRequestBody("image/*".toMediaType())
return MultipartBody.Part.createFormData("images", fileName, requestBody)
}
📝 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.
fun formatToMultipart(uri: Uri): MultipartBody.Part { | |
val inputStream: InputStream = requireNotNull(context.contentResolver.openInputStream(uri)) | |
val bytes = inputStream.readBytes() | |
val fileName = uri.lastPathSegment ?: "image.jpg" | |
val requestBody = bytes.toRequestBody("image/*".toMediaType()) | |
return MultipartBody.Part.createFormData("images", fileName, requestBody) | |
} | |
fun formatToMultipart(uri: Uri): MultipartBody.Part { | |
val inputStream = context.contentResolver.openInputStream(uri) | |
?: throw IllegalArgumentException("Cannot open input stream for URI: $uri") | |
val bytes = inputStream.use { stream -> | |
// 파일 크기 체크 (예: 10MB 제한) | |
val availableBytes = stream.available() | |
if (availableBytes > 10 * 1024 * 1024) { | |
throw IllegalArgumentException("File size exceeds limit: $availableBytes bytes") | |
} | |
stream.readBytes() | |
} | |
val fileName = uri.lastPathSegment ?: "image.jpg" | |
val requestBody = bytes.toRequestBody("image/*".toMediaType()) | |
return MultipartBody.Part.createFormData("images", fileName, requestBody) | |
} |
🤖 Prompt for AI Agents
In app/src/main/java/com/into/websoso/core/common/util/MultiPartConvertor.kt
lines 26 to 34, improve resource and memory management by wrapping the
InputStream usage in a 'use' block to ensure it is closed properly, avoid
loading the entire file into memory by streaming the data instead of using
readBytes(), add a file size check to prevent processing overly large files that
could cause OOM, and replace requireNotNull with proper error handling to avoid
unexpected crashes when the InputStream is null.
} | ||
|
||
fun formatToMultipart(uri: Uri): MultipartBody.Part { | ||
val inputStream: InputStream = requireNotNull(context.contentResolver.openInputStream(uri)) |
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.
🛠️ Refactor suggestion
requireNotNull 사용으로 인한 크래시 위험이 있습니다.
requireNotNull
은 예기치 않은 크래시를 발생시킬 수 있습니다. 더 명확한 에러 메시지와 함께 적절한 예외 처리를 사용하는 것이 좋습니다.
- val inputStream: InputStream = requireNotNull(context.contentResolver.openInputStream(uri))
+ val inputStream = context.contentResolver.openInputStream(uri)
+ ?: throw IllegalArgumentException("Cannot open input stream for URI: $uri")
📝 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.
val inputStream: InputStream = requireNotNull(context.contentResolver.openInputStream(uri)) | |
val inputStream = context.contentResolver.openInputStream(uri) | |
?: throw IllegalArgumentException("Cannot open input stream for URI: $uri") |
🤖 Prompt for AI Agents
In app/src/main/java/com/into/websoso/core/common/util/MultiPartConvertor.kt at
line 27, replace the use of requireNotNull when opening the InputStream from the
content resolver with a safer approach that checks if the InputStream is null.
If it is null, throw a clear and descriptive exception or handle the error
gracefully instead of allowing a crash. This will prevent unexpected crashes and
provide better error diagnostics.
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.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
app/src/main/java/com/into/websoso/data/mapper/MultiPartMapper.kt
(1 hunks)app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt
(5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: build
🔇 Additional comments (2)
app/src/main/java/com/into/websoso/data/mapper/MultiPartMapper.kt (2)
15-19
: 클래스 구조가 잘 설계되었습니다.Dagger Hilt를 사용한 의존성 주입 패턴과 ApplicationContext 사용이 적절합니다. 멀티파트 변환을 위한 유틸리티 클래스로서 역할이 명확합니다.
20-24
: FeedRequestDto 변환 로직이 올바릅니다.JSON 직렬화와 멀티파트 폼 데이터 생성이 적절하게 구현되었습니다. "feed" 파트명과 "application/json" 미디어 타입 설정이 올바릅니다.
app/src/main/java/com/into/websoso/data/mapper/MultiPartMapper.kt
Outdated
Show resolved
Hide resolved
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.
Actionable comments posted: 1
♻️ Duplicate comments (2)
app/src/main/java/com/into/websoso/core/common/util/ImageCompressor.kt (1)
37-57
: 자원 관리 및 예외 처리 개선이 여전히 필요합니다이전 리뷰에서 지적된 문제들이 아직 해결되지 않았습니다:
- 비트맵이 null일 경우에 대한 처리가 없어 NPE 발생 가능성이 있습니다
bitmap.recycle()
이 호출되지 않아 메모리 누수가 발생할 수 있습니다- 파일 작업 및 이미지 디코딩 실패에 대한 예외 처리가 부족합니다
app/src/main/java/com/into/websoso/ui/createFeed/CreateFeedViewModel.kt (1)
246-255
: 이미지 추가 로직이 여전히 사용자 친화적이지 않습니다이전 리뷰에서 제안된 개선사항이 반영되지 않았습니다. 현재 구현은 전체 개수가 제한을 초과하면 모든 이미지를 거부합니다. 가능한 만큼 추가하고 사용자에게 알리는 방식이 더 나은 UX를 제공할 수 있습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
app/src/main/java/com/into/websoso/core/common/util/ImageCompressor.kt
(1 hunks)app/src/main/java/com/into/websoso/ui/createFeed/CreateFeedActivity.kt
(7 hunks)app/src/main/java/com/into/websoso/ui/createFeed/CreateFeedViewModel.kt
(7 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- app/src/main/java/com/into/websoso/ui/createFeed/CreateFeedActivity.kt
🧰 Additional context used
🧬 Code Graph Analysis (1)
app/src/main/java/com/into/websoso/ui/createFeed/CreateFeedViewModel.kt (1)
app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt (1)
downloadImage
(161-161)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: build
🔇 Additional comments (3)
app/src/main/java/com/into/websoso/ui/createFeed/CreateFeedViewModel.kt (3)
40-45
: 이미지 상태 관리 구현이 잘 되어 있습니다
StateFlow
를 사용한 반응형 이미지 상태 관리와 단일 이벤트 처리가 적절히 구현되었습니다. 이미지 첨부 기능의 핵심 상태 관리가 깔끔하게 설계되었습니다.
75-92
: 재시도 로직이 포함된 피드 이미지 로딩이 잘 구현되었습니다네트워크 오류에 대비한 재시도 로직(최대 3회)과 예외 처리가 적절히 구현되어 있어 안정성이 높습니다.
110-110
: 피드 생성/수정 시 이미지 포함 로직이 올바르게 구현되었습니다첨부된 이미지들이 피드 저장 시 적절히 전달되고 있으며, 성공 이벤트 처리도 잘 되어 있습니다.
Also applies to: 130-130
fun removeImage(index: Int) { | ||
val currentImages = attachedImages.value.toMutableList().apply { | ||
this.getOrNull(index).let { removeAt(index) } | ||
} | ||
_attachedImages.value = currentImages | ||
} |
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.
removeImage 함수의 논리적 오류 수정 필요
getOrNull(index)
를 호출하지만 그 결과를 사용하지 않고 있습니다. 인덱스가 유효하지 않을 경우 예외가 발생할 수 있습니다.
다음과 같이 수정하세요:
fun removeImage(index: Int) {
val currentImages = attachedImages.value.toMutableList()
- currentImages.getOrNull(index).let { removeAt(index) }
+ if (index in currentImages.indices) {
+ currentImages.removeAt(index)
+ }
_attachedImages.value = currentImages
}
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In app/src/main/java/com/into/websoso/ui/createFeed/CreateFeedViewModel.kt
around lines 278 to 283, the removeImage function calls getOrNull(index) but
does not check its result before calling removeAt(index), which can cause an
exception if the index is invalid. Fix this by first checking if
getOrNull(index) returns a non-null value before attempting to remove the item
at that index, ensuring safe removal only when the index is valid.
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.
c: 이건 코드래빗 리뷰 반영해도 좋겠어요
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.
@s9hn 네, 좋은 제안이네요! 해당 리뷰를 반영해서 removeImage
함수의 안전성을 개선하겠습니다.
flowchart TB
A[Fix removeImage function] --> B[Check index bounds before removal]
B --> C[Update removeImage implementation]
C --> D[Remove unsafe getOrNull pattern]
D --> E[Add proper index validation]
⏳ Preparing changes...
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 summary of the edits CodeRabbit can apply:
Fix the removeImage function to validate the index before removal in CreateFeedViewModel.kt
A plan of each step that CodeRabbit will take in order to execute the edits for each file (with inline rough draft snippets):
✏️ app/src/main/java/com/into/websoso/ui/createFeed/CreateFeedViewModel.kt
Replace the existing removeImage implementation (lines 300–305) with a safe version that checks the index before removing. Change it to:
fun removeImage(index: Int) {
val currentImages = attachedImages.value.toMutableList()
if (index in currentImages.indices) {
currentImages.removeAt(index)
}
_attachedImages.value = currentImages
}
Make sure the indentation (8 spaces before fun
, 12 spaces before inner lines) matches the surrounding code.
- ✅ Create PR with these edits
- 📌 Create commit in current branch
- 📋 Get copyable edits
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.
c: 이건 코드래빗 리뷰 반영해도 좋겠어요
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.
Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!
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.
어려운 작업인데 고생하셨어유! stream 작업이 워낙 리소스를 많이먹고 복잡해서 코드래빗 리뷰를 참고해도 좋겠구만유
추가적으로 이미지 리사이징하는 작업은 딱히 없어보이는데, 리사이징 로직도 추가되면 좋을 것 같아유
캐싱까지? ㅎ
data class Input( | ||
val maxSelectable: Int, | ||
) |
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가 필요한건가요?
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가 필요한건가요?
커스텀된 객체임을 감안하면 남겨두는게 좋을 것 같아요 👍
app/src/main/java/com/into/websoso/core/common/util/DynamicLimitPhotoPicker.kt
Outdated
Show resolved
Hide resolved
private val multiPartMapper: MultiPartMapper, | ||
private val imageDownloader: ImageDownloader, | ||
private val imageCompressor: ImageCompressor, |
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: 멀티파트 부분은 나중에 RemoteDataSource에서 관리해도 좋겠어요
app/src/main/java/com/into/websoso/core/common/util/ImageCompressor.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/com/into/websoso/core/common/util/ImageDownloader.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/com/into/websoso/core/common/util/ImageDownloader.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/com/into/websoso/core/common/util/ImageDownloader.kt
Outdated
Show resolved
Hide resolved
constructor( | ||
@ApplicationContext private val context: Context, | ||
) { | ||
suspend fun formatImageToUri(url: String): Uri? = |
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: 함수가 많은 책임을 가지고 있는 것 같아요! 현재는 네트워크 에러와 IO작업 에러를 구분할 수 없어요!
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: 함수가 많은 책임을 가지고 있는 것 같아요! 현재는 네트워크 에러와 IO작업 에러를 구분할 수 없어요!
거기까지 에러를 분기할 여유는 없어 보이는...ㅠ 우선은 Result로 감싸서 반환하고 추후 리팩터링 진행하죠
@coderabbitai pause |
✅ Actions performedReviews paused. |
app/src/main/java/com/into/websoso/core/common/util/MutableSingleStateFlow.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/com/into/websoso/data/mapper/MultiPartMapper.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/com/into/websoso/data/util/ImageDownloader.kt
Outdated
Show resolved
Hide resolved
lifecycleScope.launch { | ||
repeatOnLifecycle(Lifecycle.State.STARTED) { | ||
createFeedViewModel.exceedingImageCountEvent.collect { event -> |
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.
c: 연진이가 만들어둔 확장함수 있읍니다
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.
c: 연진이가 만들어둔 확장함수 있읍니다
깔끔해지고 좋네요!
문득 든 생각인데 변수 타입별로 확장함수 파일을 슬슬 분리해도 좋을 것 같다는 생각
알아서 있나 없나 찾기 쉽게요 🤔
ExtensionFunction
은 너무 추상적이라는 생각이 드네요
b6ee10b
app/src/main/java/com/into/websoso/ui/createFeed/CreateFeedViewModel.kt
Outdated
Show resolved
Hide resolved
fun removeImage(index: Int) { | ||
val currentImages = attachedImages.value.toMutableList().apply { | ||
this.getOrNull(index).let { removeAt(index) } | ||
} | ||
_attachedImages.value = currentImages | ||
} |
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.
c: 이건 코드래빗 리뷰 반영해도 좋겠어요
fun ExpandedFeedImageScreen( | ||
index: Int = 0, | ||
imageUrls: List<String> = emptyList(), | ||
onBackButtonClick: () -> Unit, |
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: Screen함수를 람다형식으로 작성하고 싶어서 맨 아래 둔건가요? 아니라면 파라미터 권장 순서대로 조정해도 좋겠어요
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: Screen함수를 람다형식으로 작성하고 싶어서 맨 아래 둔건가요? 아니라면 파라미터 권장 순서대로 조정해도 좋겠어요
람다식이 아래로 내려가는게 파라미터 권장 순서가 아니었나요? Modifier보다 아래로 오는걸로 알고 있었는데
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.
필수생성자가 위, 기본생성자중에서 모디파이어가 제일 위!
람다 맨 아래쓰는건 해당 함수에 람다 넘길 때 선언형으로 작성하고 싶으면 그렇게 두는데 일반적으론 위처럼 !
core/designsystem/src/main/java/com/into/websoso/core/designsystem/theme/Color.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/com/into/websoso/ui/createFeed/CreateFeedViewModel.kt
Outdated
Show resolved
Hide resolved
리뷰 반영하느라 고생했씁니다~ |
📌𝘐𝘴𝘴𝘶𝘦𝘴
📎𝘞𝘰𝘳𝘬 𝘋𝘦𝘴𝘤𝘳𝘪𝘱𝘵𝘪𝘰𝘯
코드래빗의 멋진 요약
📷𝘚𝘤𝘳𝘦𝘦𝘯𝘴𝘩𝘰𝘵
피드 작성 이미지 첨부
Screen_recording_20250524_120636.mp4
피드 상세 이미지 확대
Screen_recording_20250524_120703.mp4
💬𝘛𝘰 𝘙𝘦𝘷𝘪𝘦𝘸𝘦𝘳𝘴
Summary by CodeRabbit
신규 기능
사용성 개선
UI 변경
기타