Skip to content

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

Open
wants to merge 39 commits into
base: develop
Choose a base branch
from
Open

feat: 이미지 첨부 기능 구현 #709

wants to merge 39 commits into from

Conversation

junseo511
Copy link
Member

@junseo511 junseo511 commented May 23, 2025

📌𝘐𝘴𝘴𝘶𝘦𝘴

📎𝘞𝘰𝘳𝘬 𝘋𝘦𝘴𝘤𝘳𝘪𝘱𝘵𝘪𝘰𝘯

코드래빗의 멋진 요약

  • 신규 기능
    • 피드 작성 시 이미지를 최대 20장까지 첨부할 수 있는 기능이 추가되었습니다.
    • 이미지 선택 시 갤러리에서 여러 이미지를 동적으로 선택할 수 있습니다.
    • 피드 상세 화면에서 이미지를 전체 화면으로 확대하여 볼 수 있는 갤러리 뷰가 제공됩니다.
  • UI/UX 개선
    • 피드 목록 및 상세 화면에서 이미지가 있을 경우 썸네일과 이미지 개수가 표시됩니다.
    • 이미지 첨부 한도를 초과하면 안내 메시지가 스낵바로 노출됩니다.
    • 이미지가 없는 경우 관련 UI가 숨겨집니다.
  • 버그 수정
    • 이미지 URL이 비어있거나 잘못된 경우 기본 썸네일로 대체되어 오류가 방지됩니다.
  • 리팩터/구조 개선
    • 이미지 첨부, 다운로드, 압축 등 이미지 관련 데이터 흐름이 개선되었습니다.
  • 리소스 추가
    • 피드 이미지 추가 아이콘 및 전체 이미지 닫기 아이콘이 추가되었습니다.
    • 최대 업로드 가능 이미지 수 안내 문자열이 추가되었습니다.

📷𝘚𝘤𝘳𝘦𝘦𝘯𝘴𝘩𝘰𝘵

피드 작성 이미지 첨부

Screen_recording_20250524_120636.mp4

피드 상세 이미지 확대

Screen_recording_20250524_120703.mp4

💬𝘛𝘰 𝘙𝘦𝘷𝘪𝘦𝘸𝘦𝘳𝘴

Summary by CodeRabbit

  • 신규 기능

    • 피드 작성 및 수정 시 최대 20장의 이미지를 첨부하고, 이미지를 선택·삭제할 수 있는 기능이 추가되었습니다.
    • 피드 상세, 목록, 편집 화면 등에서 피드에 첨부된 이미지와 이미지 개수가 표시됩니다.
    • 피드 이미지를 클릭하면 전체 화면 갤러리로 확대하여 이미지를 넘겨볼 수 있습니다.
    • 이미지 선택 시 최대 선택 개수를 동적으로 설정할 수 있는 이미지 피커가 도입되었습니다.
  • 사용성 개선

    • 이미지 첨부 개수 제한 초과 시 안내 메시지가 표시됩니다.
    • 이미지 첨부 시 용량이 자동으로 압축되어 업로드됩니다.
    • 이미지 다운로드 및 압축 과정에 재시도 로직이 추가되어 안정성이 향상되었습니다.
    • 이미지 URL이 없을 때 기본 썸네일 이미지가 표시되도록 개선되었습니다.
  • UI 변경

    • 피드 작성 화면에 이미지 추가 버튼 및 이미지 미리보기 컨테이너가 추가되었습니다.
    • 피드 목록 및 상세 화면에서 이미지가 있을 경우 썸네일과 개수 뱃지가 표시됩니다.
    • 전체 화면 이미지 확대 뷰에 페이지 인디케이터와 닫기 버튼이 추가되었습니다.
  • 기타

    • 피드 API 요청이 멀티파트 형식으로 변경되어 이미지 파일과 함께 전송할 수 있게 되었습니다.
    • 새로 추가된 이미지 관련 데이터 모델과 매핑 로직이 반영되었습니다.

@junseo511 junseo511 self-assigned this May 23, 2025
@junseo511 junseo511 added 🍯 [FEAT] 새로운 기능을 개발합니다. 🔮 법사 준서 아카데미의 천재 마법사 ❌[DO NOT MERGE] 머지를 하면 안됩니다. labels May 23, 2025
Copy link

coderabbitai bot commented May 23, 2025

Note

Reviews paused

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

"""

Walkthrough

이 PR은 서재(피드) 이미지 첨부 기능의 전체적인 도입 및 확장과 관련된 대규모 변경을 포함합니다. 이미지 업로드, 다운로드, 압축, 선택 제한, UI 표시, 상세보기, API 연동, 데이터 모델 확장 등 이미지와 관련된 전반적인 기능이 추가 및 개선되었습니다.

Changes

파일/그룹 변경 요약
.../core/common/util/BindingAdapter.kt 이미지 URL이 없을 때 기본 썸네일 이미지를 표시하도록 수정
.../core/common/util/DynamicLimitPhotoPicker.kt, ImageCompressor.kt, ImageDownloader.kt, Event.kt, MutableSingleLiveData.kt, SingleLiveData.kt 이미지 선택, 압축, 다운로드, 이벤트 전달 등 유틸리티 클래스 및 LiveData 확장 추가
.../data/mapper/FeedMapper.kt, .../data/model/FeedEntity.kt, .../data/remote/response/FeedDetailResponseDto.kt, FeedResponseDto.kt, .../data/repository/FeedRepository.kt, .../data/mapper/MultiPartMapper.kt 피드/피드상세 DTO, Entity, Repository에 이미지 리스트 및 개수 속성 추가, 멀티파트 변환 및 이미지 관련 API 연동 구현
.../domain/mapper/FeedMapper.kt, .../domain/model/Feed.kt 도메인 모델에 이미지 리스트 및 개수 속성 추가
.../ui/createFeed/CreateFeedActivity.kt, CreateFeedViewModel.kt, component/CreateFeedImageContainer.kt 이미지 첨부, 선택 제한, UI 표시, 제거, 스낵바 알림 등 피드 생성 화면에 이미지 관련 기능 추가 및 Compose 연동
.../ui/feedDetail/adapter/FeedDetailContentViewHolder.kt, FeedDetailActivity.kt, FeedDetailClickListener.kt, component/AdaptationFeedImageContainer.kt, model/ImageContainerType.kt, model/EditFeedModel.kt 상세보기에서 이미지 표시, 클릭 시 확장 뷰 이동, EMPTY 타입 처리, 모델에 이미지 리스트 추가, 콜백 파라미터 순서 수정
.../ui/expandedFeedImage/ExpandedFeedImageActivity.kt, ExpandedFeedImageScreen.kt, ExpandedFeedImage.kt 이미지 전체보기 전용 Activity 및 Compose 화면 추가, 인텐트 전달 및 페이지네이션 구현
.../ui/main/feed/model/FeedModel.kt, .../ui/mapper/FeedMapper.kt, FeedFragment.kt, .../ui/novelDetail/NovelDetailActivity.kt, .../ui/novelFeed/NovelFeedFragment.kt 메인/소설 피드 모델 및 매퍼에 이미지 속성 추가, EditFeedModel에 이미지 리스트 전달
.../data/di/RepositoryModule.kt FeedRepository Provider 메서드 제거
.../data/remote/api/FeedApi.kt 피드 생성/수정 API를 멀티파트 방식으로 변경, 이미지 파일 파트 추가
app/src/main/res/layout/activity_create_feed.xml, item_feed.xml, item_feed_detail_header.xml 피드 생성/리스트/상세 레이아웃에 이미지 뷰 및 조건부 표시, 개수 뱃지, ComposeView 바인딩 추가
core/resource/src/main/res/drawable/ic_create_feed_image.xml, ic_expanded_feed_image_close.xml 이미지 첨부/닫기 아이콘 벡터 리소스 추가
core/resource/src/main/res/values/strings.xml 이미지 첨부 개수 제한 안내 문자열 추가
core/designsystem/src/main/java/.../theme/Color.kt 확장 이미지 배경색 컬러 추가

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: 응답
Loading
sequenceDiagram
    participant User
    participant FeedDetailActivity
    participant ExpandedFeedImageActivity

    User->>FeedDetailActivity: 피드 이미지 클릭
    FeedDetailActivity->>ExpandedFeedImageActivity: 인텐트로 index, imageUrls 전달 및 실행
    ExpandedFeedImageActivity->>ExpandedFeedImageScreen: 이미지 리스트, 인덱스로 전체보기 표시
    User->>ExpandedFeedImageActivity: 스와이프/닫기
    ExpandedFeedImageActivity->>FeedDetailActivity: 결과 반환 및 종료
Loading

Assessment against linked issues

Objective Addressed Explanation
서재 이미지 첨부 API 구현 (#708)

Possibly related PRs

Suggested reviewers

  • yeonjeen
  • s9hn

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need 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)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions github-actions bot requested review from m6z1, s9hn and yeonjeen May 23, 2025 13:04
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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 는 구성 변경마다 중복 호출될 수 있습니다

observePeekLiveData.observe로 등록된 직후 즉시 peekContent()를 전달하므로, 화면 회전·다크모드 전환 등으로 LifecycleOwner가 재생성될 때마다 동일 이벤트가 반복 전달됩니다.
순수 ‘상태(State)’ 목적이 아니라면 불필요한 재호출이 발생할 수 있으니,

  1. 실제 필요 여부를 검증하거나
  2. 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

📥 Commits

Reviewing files that changed from the base of the PR and between cd7bc94 and 2a0c62d.

📒 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)를 추가한 것이 적절합니다. 이는 이미지 첨부 기능 구현에 필요한 데이터를 모델에 반영한 것입니다.

imageCountimageUrls.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: 도메인 모델 매핑 구현이 적절합니다.

데이터 계층의 imagesimageCount 속성이 도메인 계층의 imageUrlsimageCount로 올바르게 매핑되어 있습니다. 이름 변경(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 모델로 이미지 데이터가 올바르게 매핑되었습니다.

FeedEntityimages 필드를 FeedModelimageUrls로 매핑하여 변수명을 더 명확하게 만든 부분이 좋습니다. 이렇게 하면 UI 레이어에서 필드의 용도가 더 명확해집니다.

app/src/main/java/com/into/websoso/data/mapper/FeedMapper.kt (2)

44-45: 썸네일 URL을 이미지 리스트로 적절히 변환했습니다.

thumbnailUrl이 있는 경우 단일 아이템 리스트로, 없는 경우 빈 리스트로 설정하는 방식이 적절합니다. 이는 리스트 API 응답에서 썸네일만 제공하는 경우의 일반적인 패턴입니다.


95-96:

❌ Incorrect review comment

이미지 카운트 계산 방식 검토가 필요할 수 있습니다.

현재 imageCountimages.size로 계산하고 있는데, API 응답에 이미 imageCount 필드가 있다면 해당 값을 사용하는 것이 더 좋을 수 있습니다. 서버에서 계산된 값이 보다 정확할 수 있으며, 추가 계산도 필요 없습니다.


🏁 Script executed:

#!/bin/bash
# FeedDetailResponseDto의 구조를 확인하여 imageCount 필드 존재 여부 체크
rg -A 2 -B 2 "class FeedDetailResponseDto" --type kotlin

Length 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.kt

Length of output: 105


이미지 관련 필드 전체 확인을 위해 추가 스크립트를 실행해주세요.


🏁 Script executed:

#!/bin/bash
# FeedDetailResponseDto에서 image 관련 모든 속성 조회
rg -n "image" app/src/main/java/com/into/websoso/data/remote/response/FeedDetailResponseDto.kt

Length 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()로 소비된 이벤트도 다시 노출됩니다.
뷰모델 등에서 “현재 값”으로 오해해 로직이 꼬일 가능성이 있으니, 사용처를 다시 한 번 점검하거나 EventhasBeenHandled 값을 함께 반환하는 별도 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)를 포함한다면 스킵해도 무방하지만, 명시적 분리가 안전합니다.

Comment on lines 28 to 59
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
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

이미지 압축 로직 개선 및 자원 관리 필요

이미지 압축 기능은 잘 구현되었지만, 몇 가지 개선이 필요합니다:

  1. 비트맵 자원이 명시적으로 해제되지 않아 메모리 누수 가능성이 있습니다
  2. 임시 파일이 생성된 후 앱 종료 시 정리되는 매커니즘이 없습니다
  3. 이미지 디코딩 실패나 파일 작업 오류에 대한 예외 처리가 부족합니다

다음과 같이 수정하는 것을 권장합니다:

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.

Comment on lines 218 to 228
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)
}
}
Copy link

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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: 접근성 개선이 필요합니다.

닫기 버튼의 contentDescriptionnull로 설정되어 있어 접근성에 문제가 있을 수 있습니다.

접근성을 위해 적절한 설명을 추가해주세요:

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

📥 Commits

Reviewing files that changed from the base of the PR and between 2a0c62d and 05e169a.

📒 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 내부에서 상태를 관찰하도록 개선되었습니다.

Comment on lines +28 to +38
@Composable
fun ExpandedFeedImageScreen(
index: Int = 0,
imageUrls: List<String> = emptyList(),
onBackButtonClick: () -> Unit,
) {
val pagerState = rememberPagerState(initialPage = index, pageCount = { imageUrls.size })

BackHandler {
onBackButtonClick()
}
Copy link

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.

Comment on lines 26 to 34
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)
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

메모리 관리와 에러 처리를 개선해야 합니다.

현재 구현에서 다음 문제점들이 있습니다:

  1. requireNotNull로 인한 크래시 위험
  2. readBytes()로 전체 이미지를 메모리에 로드하여 대용량 이미지 시 OOM 발생 가능
  3. 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.

Suggested change
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.

Comment on lines 218 to 225
private fun createFeedImagePickerLauncher() =
registerForActivityResult(DynamicLimitPhotoPicker()) { uris ->
if (uris.isNullOrEmpty()) return@registerForActivityResult
lifecycleScope.launch {
val compressedUris = imageCompressor.compressUris(this@CreateFeedActivity, uris, contentResolver)
createFeedViewModel.addImages(compressedUris)
}
}
Copy link

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.

Suggested change
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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 05e169a and 98d31fa.

📒 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 전달이 올바르게 구현되었습니다.

기존 피드의 imageUrlsEditFeedModel에 전달하여 편집 시 기존 이미지 정보를 유지하는 로직이 적절하게 구현되었습니다.

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으로 직렬화하여 멀티파트로 변환하는 로직이 적절합니다.

Comment on lines 18 to 28
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()
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

보안 및 성능 개선이 필요합니다.

현재 구현에 다음과 같은 보안 및 성능 문제가 있습니다:

  1. URL 검증 없음 - 악의적인 URL에 대한 방어 부족
  2. 연결 타임아웃 미설정 - 무한 대기 가능성
  3. 파일 크기 제한 없음 - 메모리 부족 위험
  4. 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

보안 및 성능 개선이 필요합니다.

현재 구현에서 다음과 같은 문제점들이 있습니다:

  1. 연결 타임아웃 미설정: 네트워크 지연 시 무한 대기 가능성
  2. 파일 크기 제한 없음: 대용량 파일 다운로드로 인한 메모리/저장공간 부족 위험
  3. 리소스 정리 부족: InputStream이 명시적으로 닫히지 않음
  4. 보안 검증 없음: 임의의 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.

Suggested change
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.

Comment on lines 26 to 34
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)
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

리소스 관리와 메모리 효율성 개선이 필요합니다.

현재 구현에서 다음 사항들을 개선할 수 있습니다:

  1. InputStream 리소스 정리가 명시적이지 않음
  2. 전체 파일을 메모리에 로드하여 큰 파일에 대한 메모리 문제 가능성
  3. 예외 메시지 부족

다음과 같이 개선하는 것을 권장합니다:

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.

Suggested change
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

메모리 및 리소스 관리 개선이 필요합니다.

현재 구현에서 다음과 같은 문제점들이 있습니다:

  1. InputStream 리소스 누수: use 블록으로 자동 닫기 처리되지 않음
  2. 메모리 부족 위험: readBytes()로 전체 파일을 메모리에 로드
  3. 파일 크기 제한 없음: 대용량 이미지 처리 시 OOM 발생 가능성
  4. 에러 처리 부족: 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.

Suggested change
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))
Copy link

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.

Suggested change
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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 98d31fa and 1a3d12b.

📒 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" 미디어 타입 설정이 올바릅니다.

@Team-WSS Team-WSS deleted a comment from coderabbitai bot May 24, 2025
@junseo511 junseo511 removed the ❌[DO NOT MERGE] 머지를 하면 안됩니다. label May 24, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
app/src/main/java/com/into/websoso/core/common/util/ImageCompressor.kt (1)

37-57: 자원 관리 및 예외 처리 개선이 여전히 필요합니다

이전 리뷰에서 지적된 문제들이 아직 해결되지 않았습니다:

  1. 비트맵이 null일 경우에 대한 처리가 없어 NPE 발생 가능성이 있습니다
  2. bitmap.recycle()이 호출되지 않아 메모리 누수가 발생할 수 있습니다
  3. 파일 작업 및 이미지 디코딩 실패에 대한 예외 처리가 부족합니다
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

📥 Commits

Reviewing files that changed from the base of the PR and between 8435a8a and 7d6ab44.

📒 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

Comment on lines 278 to 283
fun removeImage(index: Int) {
val currentImages = attachedImages.value.toMutableList().apply {
this.getOrNull(index).let { removeAt(index) }
}
_attachedImages.value = currentImages
}
Copy link

@coderabbitai coderabbitai bot May 25, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Copy link
Member

@s9hn s9hn Jun 2, 2025

Choose a reason for hiding this comment

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

c: 이건 코드래빗 리뷰 반영해도 좋겠어요

Copy link

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]
Loading

⏳ Preparing changes...

Copy link

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

Copy link
Member Author

Choose a reason for hiding this comment

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

c: 이건 코드래빗 리뷰 반영해도 좋겠어요

ad6467d

Copy link

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!

Copy link
Member

@s9hn s9hn left a comment

Choose a reason for hiding this comment

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

어려운 작업인데 고생하셨어유! stream 작업이 워낙 리소스를 많이먹고 복잡해서 코드래빗 리뷰를 참고해도 좋겠구만유
추가적으로 이미지 리사이징하는 작업은 딱히 없어보이는데, 리사이징 로직도 추가되면 좋을 것 같아유
캐싱까지? ㅎ

Comment on lines +37 to +39
data class Input(
val maxSelectable: Int,
)
Copy link
Member

Choose a reason for hiding this comment

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

a: 추후에 프로퍼티가 추가될 수 있어서 data class가 필요한건가요?

Copy link
Member Author

@junseo511 junseo511 May 31, 2025

Choose a reason for hiding this comment

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

a: 추후에 프로퍼티가 추가될 수 있어서 data class가 필요한건가요?

커스텀된 객체임을 감안하면 남겨두는게 좋을 것 같아요 👍

Comment on lines +24 to +26
private val multiPartMapper: MultiPartMapper,
private val imageDownloader: ImageDownloader,
private val imageCompressor: ImageCompressor,
Copy link
Member

@s9hn s9hn May 25, 2025

Choose a reason for hiding this comment

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

a: 멀티파트 부분은 나중에 RemoteDataSource에서 관리해도 좋겠어요

constructor(
@ApplicationContext private val context: Context,
) {
suspend fun formatImageToUri(url: String): Uri? =
Copy link
Member

Choose a reason for hiding this comment

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

a: 함수가 많은 책임을 가지고 있는 것 같아요! 현재는 네트워크 에러와 IO작업 에러를 구분할 수 없어요!

Copy link
Member Author

Choose a reason for hiding this comment

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

a: 함수가 많은 책임을 가지고 있는 것 같아요! 현재는 네트워크 에러와 IO작업 에러를 구분할 수 없어요!

거기까지 에러를 분기할 여유는 없어 보이는...ㅠ 우선은 Result로 감싸서 반환하고 추후 리팩터링 진행하죠

@junseo511
Copy link
Member Author

@coderabbitai pause

Copy link

coderabbitai bot commented May 31, 2025

✅ Actions performed

Reviews paused.

Comment on lines 205 to 207
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
createFeedViewModel.exceedingImageCountEvent.collect { event ->
Copy link
Member

Choose a reason for hiding this comment

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

c: 연진이가 만들어둔 확장함수 있읍니다

Copy link
Member Author

Choose a reason for hiding this comment

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

c: 연진이가 만들어둔 확장함수 있읍니다

깔끔해지고 좋네요!
문득 든 생각인데 변수 타입별로 확장함수 파일을 슬슬 분리해도 좋을 것 같다는 생각
알아서 있나 없나 찾기 쉽게요 🤔
ExtensionFunction 은 너무 추상적이라는 생각이 드네요
b6ee10b

Comment on lines 278 to 283
fun removeImage(index: Int) {
val currentImages = attachedImages.value.toMutableList().apply {
this.getOrNull(index).let { removeAt(index) }
}
_attachedImages.value = currentImages
}
Copy link
Member

@s9hn s9hn Jun 2, 2025

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,
Copy link
Member

Choose a reason for hiding this comment

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

a: Screen함수를 람다형식으로 작성하고 싶어서 맨 아래 둔건가요? 아니라면 파라미터 권장 순서대로 조정해도 좋겠어요

Copy link
Member Author

Choose a reason for hiding this comment

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

a: Screen함수를 람다형식으로 작성하고 싶어서 맨 아래 둔건가요? 아니라면 파라미터 권장 순서대로 조정해도 좋겠어요

람다식이 아래로 내려가는게 파라미터 권장 순서가 아니었나요? Modifier보다 아래로 오는걸로 알고 있었는데

Copy link
Member

Choose a reason for hiding this comment

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

필수생성자가 위, 기본생성자중에서 모디파이어가 제일 위!
람다 맨 아래쓰는건 해당 함수에 람다 넘길 때 선언형으로 작성하고 싶으면 그렇게 두는데 일반적으론 위처럼 !

@s9hn
Copy link
Member

s9hn commented Jun 2, 2025

리뷰 반영하느라 고생했씁니다~
충분히 개설시킬 여지가 많아보이니 시간나면 딥다이브 후 포스팅..? ㅎ

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🍯 [FEAT] 새로운 기능을 개발합니다. 🔮 법사 준서 아카데미의 천재 마법사
Projects
None yet
Development

Successfully merging this pull request may close these issues.

feat: 서재 이미지 첨부 API 구현
3 participants