Skip to content

[API] 피드 작성 구현#88

Merged
Nico1eKim merged 11 commits into
THIP-TextHip:developfrom
rbqks529:feat/#87_API_Feeds_Make
Aug 15, 2025
Merged

[API] 피드 작성 구현#88
Nico1eKim merged 11 commits into
THIP-TextHip:developfrom
rbqks529:feat/#87_API_Feeds_Make

Conversation

@rbqks529
Copy link
Copy Markdown
Collaborator

@rbqks529 rbqks529 commented Aug 15, 2025

➕ 이슈 링크


🔎 작업 내용

  • 피드 작성에 필요한 태그 불러오가
  • 피드 작성하기
  • 피드 작성완료 프로그래스 바 구현

📸 스크린샷

2025-08-15.7.15.11.mov

😢 해결하지 못한 과제

  • [] 피드 수정하기는 아직 피드를 볼 수 없어서 나중에 할듯 합니다


📢 리뷰어들에게

  • 영상에는 없긴한데 프로그래스 바도 구현했습니다.

Summary by CodeRabbit

  • New Features

    • 피드 작성 워크플로우 추가: 도서 선택/검색(디바운스), 내용 입력, 카테고리·태그(최대5개) 선택, 공개/비공개, 이미지 최대3장 업로드 및 작성 후 자동 복귀.
    • 작성 진행 표시(애니메이션 진행바) 및 완료 알림 추가.
  • Navigation

    • 피드 작성 화면 경로 및 전환 콜백(작성 화면 열기/결과 전달) 추가.
  • UI

    • 서브 장르 칩 가로 중앙 정렬 개선.
    • 사진/태그 개수 표기에 “개” 단위 추가.
  • Chores

    • 피드 작성 관련 문자열(진행/완료/오류) 추가.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Aug 15, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Caution

Review failed

The pull request is closed.

Walkthrough

피드 작성 기능을 추가합니다: FeedService 및 DI 바인딩, 요청/응답 모델과 FeedRepository(이미지 업로드 포함) 추가, FeedWriteViewModel/FeedWriteUiState 기반 UI 이전, FeedWriteScreen 및 네비게이션(FeedRoutes.Write, navigateToFeedWrite) 및 FeedScreen의 작성 흐름 연결과 관련 문자열/정렬 보완.

Changes

Cohort / File(s) Summary
DI & Service
app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt, app/src/main/java/com/texthip/thip/data/service/FeedService.kt
Retrofit FeedService 인터페이스 추가(GET feeds/write-info, POST feeds multipart) 및 Hilt 제공자(provideFeedService) 등록.
데이터 모델(요청/응답)
app/src/main/java/com/texthip/thip/data/model/feed/request/CreateFeedRequest.kt, app/src/main/java/com/texthip/thip/data/model/feed/response/CreateFeedResponse.kt, app/src/main/java/com/texthip/thip/data/model/feed/response/FeedWriteInfoResponse.kt
피드 작성 요청/응답 및 작성 정보(카테고리/태그) 모델(@serializable, @SerialName) 추가.
레포지토리
app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt
FeedRepository 추가: getFeedWriteInfo(), createFeed(...) 구현. 이미지 Uri → Multipart 변환, JSON 직렬화, 카테고리 우선순위 재정렬 포함.
피드 작성 화면 & 상태관리
app/src/main/java/com/texthip/thip/ui/feed/screen/FeedWriteScreen.kt, app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteUiState.kt, app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt
ViewModel 기반 리팩터링: FeedWriteUiState, FeedWriteViewModel 추가(카테고리 로드, 디바운스 검색, 이미지/태그 제약, 생성 로직). FeedWriteContent 컴포저블과 public 콜백(onFeedCreated 등) 추가.
피드 목록 화면 변경
app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt
FeedScreen 시그니처 변경(navController 제거), onNavigateToFeedWrite 콜백 추가, FAB 연동, 작성 결과(resultFeedId) 처리용 포스팅 진행 UI 추가.
네비게이션
app/src/main/java/com/texthip/thip/ui/navigator/routes/FeedRoutes.kt, app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt, app/src/main/java/com/texthip/thip/ui/navigator/extensions/FeedNavigationExtensions.kt
FeedRoutes.Write 추가, navigateToFeedWrite() 확장함수 추가, 네비게이션 그래프에 FeedWriteScreen 엔트리 및 작성 성공 시 savedStateHandle에 feedId 저장 로직 추가.
UI 사소 수정/리소스
app/src/main/java/com/texthip/thip/ui/common/buttons/SubGenreChipRow.kt, app/src/main/res/values/strings.xml
FlowRow 정렬 인자 보강(가로 중앙 정렬), photo_count/tag_count에 단위 "개" 추가, 게시 진행/완료 및 오류 문자열 추가.

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant FeedScreen
  participant NavController
  participant FeedWriteScreen
  participant FeedWriteViewModel
  participant FeedRepository
  participant FeedService
  participant BookRepository

  User->>FeedScreen: 작성 버튼 클릭
  FeedScreen->>NavController: navigateToFeedWrite()
  NavController->>FeedWriteScreen: Compose FeedWriteScreen
  FeedWriteScreen->>FeedWriteViewModel: collect uiState / 사용자 액션 전달
  FeedWriteViewModel->>FeedRepository: getFeedWriteInfo()
  FeedRepository->>FeedService: GET /feeds/write-info
  FeedService-->>FeedRepository: FeedWriteInfoResponse
  FeedRepository-->>FeedWriteViewModel: 카테고리 데이터

  User->>FeedWriteViewModel: 책 검색(query)
  FeedWriteViewModel->>BookRepository: searchBooks(query) (debounce)
  BookRepository-->>FeedWriteViewModel: 검색 결과

  User->>FeedWriteViewModel: 이미지/내용/태그 입력 및 작성 요청
  FeedWriteViewModel->>FeedRepository: createFeed(isbn, content, isPublic, tags, images)
  FeedRepository->>FeedService: POST /feeds (multipart request + images)
  FeedService-->>FeedRepository: CreateFeedResponse
  FeedRepository-->>FeedWriteViewModel: 생성 결과
  FeedWriteViewModel-->>FeedWriteScreen: onFeedCreated(feedId)
  FeedWriteScreen->>NavController: savedStateHandle.set("feedId", feedId) 및 popBackStack()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Assessment against linked issues

Objective Addressed Explanation
피드 작성 구현 (#87)
피드 수정 구현 (#87) 수정 관련 API 엔드포인트(예: PATCH/PUT) 또는 수정 UI/로직이 없음.

Out-of-scope changes

Code Change Explanation
FlowRow 정렬 인자 추가 (app/src/main/java/com/texthip/thip/ui/common/buttons/SubGenreChipRow.kt) 단순 UI 정렬 미세조정으로, 링크된 이슈의 핵심인 API 및 작성/수정 로직과 직접적 관련 없음.
문자열 단위 및 게시 상태 문자열 추가 (app/src/main/res/values/strings.xml) 사용자 메시지/로컬라이즈 보완으로 기능 구현 핵심 범위를 벗어나는 부수 변경임.

Possibly related PRs

Suggested reviewers

  • JJUYAAA

Poem

깡총깡총, 키보드 위를 걷는 토끼예요 🐇
책과 사진 셋, 태그 다섯, 마음 한 줄 담아,
서버에 살포시 속삭여요 — "업로드 해주세요" 📤
카테고리 춤추고 네비는 길을 내주네,
토끼가 쓴 피드, 작은 축제가 시작된다.

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4eae562 and 16247cb.

📒 Files selected for processing (1)
  • app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt (2 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.
    • 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.
  • 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 the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

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

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • 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.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Implements feed creation functionality for the Android app, allowing users to create new feeds with book selection, content writing, image uploads, and category/tag selection.

  • Adds feed creation UI with book search, content input, image upload (max 3), privacy toggle, and category/tag selection
  • Implements API integration for fetching feed categories/tags and creating feeds with multipart image upload
  • Integrates feed write screen into navigation flow with floating action button access from main feed screen

Reviewed Changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
strings.xml Updates photo and tag count display strings to include "개" suffix
FeedRoutes.kt Adds Write route for feed creation navigation
FeedNavigation.kt Integrates FeedWriteScreen into navigation with proper callbacks
FeedNavigationExtensions.kt Adds navigation extension for feed write screen
FeedWriteViewModel.kt Core view model handling feed creation logic, book search, and form validation
FeedWriteUiState.kt UI state data class with validation logic for feed creation form
FeedWriteScreen.kt Complete UI implementation for feed creation with image upload and tag selection
FeedScreen.kt Connects floating action button to navigate to feed write screen
SubGenreChipRow.kt Updates chip layout alignment for better visual presentation
FeedService.kt Retrofit service interface for feed creation API endpoints
FeedRepository.kt Repository implementation handling feed creation with image upload
Response/Request models Data models for feed creation API communication
ServiceModule.kt Dependency injection setup for FeedService

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
You can also share your feedback on Copilot code review for a chance to win a $100 gift card. Take the survey.

)

// JSON 요청 부분을 RequestBody로 변환
val requestJson = Json.encodeToString(request)
Copy link

Copilot AI Aug 15, 2025

Choose a reason for hiding this comment

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

Using Json.encodeToString without proper serializer configuration may fail. Consider using Json.encodeToString(CreateFeedRequest.serializer(), request) for explicit serialization.

Suggested change
val requestJson = Json.encodeToString(request)
val requestJson = Json.encodeToString(CreateFeedRequest.serializer(), request)

Copilot uses AI. Check for mistakes.

// 파일명 생성
val fileName = "feed_image_${System.currentTimeMillis()}.$extension"
val tempFile = File(context.cacheDir, fileName)
Copy link

Copilot AI Aug 15, 2025

Choose a reason for hiding this comment

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

Temporary files created in cache directory are not being cleaned up. Consider implementing cleanup logic or using try-with-resources pattern to ensure proper resource management.

Copilot uses AI. Check for mistakes.
categories = emptyList(),
isLoadingCategories = false,
errorMessage = stringResourceProvider.getString(R.string.error_network_error)
)
Copy link

Copilot AI Aug 15, 2025

Choose a reason for hiding this comment

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

This line references 'Json.encodeToString(request)' but this code appears to be in the wrong file - it should be in FeedRepository.kt, not FeedWriteViewModel.kt.

Copilot uses AI. Check for mistakes.
onSuccess = { feedId ->
onFeedCreated(feedId)
},
onError = { errorMessage ->
Copy link

Copilot AI Aug 15, 2025

Choose a reason for hiding this comment

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

The onError lambda is empty, which means error handling is incomplete. Consider adding proper error handling logic such as showing error messages to the user.

Suggested change
onError = { errorMessage ->
onError = { errorMessage ->
Toast.makeText(context, errorMessage, Toast.LENGTH_SHORT).show()

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (17)
app/src/main/res/values/strings.xml (1)

333-335: i18n/일관성 검토 제안

  • 동일 패턴의 문자열(예: Line 280 group_input_count)과 단위 표기 일관성 확보를 고려해 주세요.
  • 다국어 지원을 고려한다면 <plurals> 도입 또는 서버·클라이언트 공통 포맷 정의를 검토해 볼 수 있습니다. 한국어에서는 필수는 아니지만, 영/일 등 다른 언어에서는 복수 규칙이 달라집니다.
app/src/main/java/com/texthip/thip/data/model/feed/response/CreateFeedResponse.kt (1)

3-8: 서버 필드 케이스 스키마 양쪽을 허용하는 디코딩 보강 제안

백엔드의 JSON 키가 feedId(camelCase) 또는 feed_id(snake_case) 중 무엇인지에 따라 역직렬화가 실패할 수 있습니다. 디코딩 호환성을 높이기 위해 @JsonNames 사용을 고려해 주세요.

아래와 같이 보강하면 두 케이스 모두 디코딩 가능합니다(인코딩에는 영향 없음):

 package com.texthip.thip.data.model.feed.response

 import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.JsonNames

 @Serializable
 data class CreateFeedResponse(
-    val feedId: Int
+    @JsonNames("feedId", "feed_id")
+    val feedId: Int
 )
app/src/main/java/com/texthip/thip/data/model/feed/response/FeedWriteInfoResponse.kt (1)

3-14: 디코딩 내구성(키 케이스/필드 누락)에 대한 보강 제안

서버가 snake_case를 반환하거나 일부 리스트 필드를 생략할 경우를 대비해 다음 보강을 권장합니다.

 package com.texthip.thip.data.model.feed.response

 import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.JsonNames

 @Serializable
 data class FeedWriteInfoResponse(
-    val categoryList: List<FeedCategory>
+    @JsonNames("categoryList", "category_list")
+    val categoryList: List<FeedCategory> = emptyList()
 )

 @Serializable
 data class FeedCategory(
-    val category: String,
-    val tagList: List<String>
+    val category: String = "",
+    @JsonNames("tagList", "tag_list")
+    val tagList: List<String> = emptyList()
 )
  • 비즈니스적으로 빈 리스트/빈 문자열 허용이 문제 없다면 위 기본값은 런타임 안정성에 도움이 됩니다. 필수 필드라면 기본값 대신 검증 로직에서 명시적으로 에러를 처리하세요.
app/src/main/java/com/texthip/thip/data/model/feed/request/CreateFeedRequest.kt (1)

1-11: 직렬화 네이밍 전략의 전역 표준화 제안

여러 모델에서 케이스 매핑을 개별 주석으로 관리하기보다, 프로젝트 전역 Json 인스턴스에 네이밍 전략을 지정하는 편이 유지보수에 유리합니다(백엔드가 snake_case일 경우).

예시(사용 라이브러리 버전이 지원할 경우):

// kotlinx-serialization 1.6+ 필요
val json = Json {
    ignoreUnknownKeys = true
    explicitNulls = false
    encodeDefaults = true
    // 백엔드가 snake_case라면:
    namingStrategy = JsonNamingStrategy.SnakeCase
}

Retrofit에 동일한 json 인스턴스를 사용하는 ConverterFactory를 주입하여 요청/응답 모두 일관된 케이스 규칙을 적용하세요.

app/src/main/java/com/texthip/thip/ui/navigator/extensions/FeedNavigationExtensions.kt (1)

17-20: 피드 작성화면 네비게이션 확장 함수 추가 LGTM + 중복 네비 방지 제안

navigate(FeedRoutes.Write) 호출은 적절합니다. 다만 FAB 연타 시 동일 화면이 스택에 중첩될 수 있으니 singleTop 옵션(launchSingleTop)이나 자체 guard(예: 현재 목적지가 Write면 무시)를 고려해 주세요.

app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt (1)

264-266: FAB onClick 연결 적절함 + 연타 방지 니트픽

onClick에 콜백을 직접 전달하는 방식은 깔끔합니다. 다만 사용자가 빠르게 연타할 경우 동일 라우트가 중첩될 수 있으니 singleTop 네비게이션이나 클릭 딜레이/디스에이블 처리를 고려하면 UX가 좋아집니다.

app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt (1)

42-45: 생성 완료 시 피드 목록 갱신 신호 전달 권장 (SavedStateHandle 활용)

현재는 단순 popBackStack만 수행되어, 목록 화면이 새로고침되지 않을 수 있습니다. 이전 백스택 엔트리에 생성된 feedId나 갱신 플래그를 전달해 목록을 새로고침하도록 권장합니다.

아래와 같이 onFeedCreated 블록을 수정해 주세요:

-            onFeedCreated = { feedId ->
-                // 피드 생성 성공 시 피드 목록으로 돌아가기
-                navController.popBackStack()
-            }
+            onFeedCreated = { feedId ->
+                // 피드 생성 성공 시 피드 목록으로 돌아가며 목록 갱신 신호 전달
+                navController.previousBackStackEntry
+                    ?.savedStateHandle
+                    ?.apply {
+                        set("feed_created_id", feedId)
+                        set("refresh_feed", true)
+                    }
+                navController.popBackStack()
+            }

또는, 목록 화면에서 savedStateHandle.observe를 통해 이 신호를 수신해 재조회하도록 처리할 수 있습니다.

app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteUiState.kt (2)

26-46: 매직 넘버(2000, 3, 5) 상수화로 중복 제거 및 일관성 향상

UI와 ViewModel 전반에서 동일한 제한값(본문 2000자, 이미지 3장, 태그 5개)을 사용하고 있습니다. 상수로 정의해 재사용하면 유지보수성이 좋아집니다.

아래처럼 companion object에 상수를 도입하고 계산 프로퍼티에서 활용해 주세요:

 data class FeedWriteUiState(
@@
 ) {
+    companion object {
+        const val MAX_CONTENT_LENGTH = 2000
+        const val MAX_IMAGE_COUNT = 3
+        const val MAX_TAG_COUNT = 5
+    }
+
     // 유효성 검사 로직
     val isContentValid: Boolean
-        get() = feedContent.isNotBlank() && feedContent.length <= 2000
+        get() = feedContent.isNotBlank() && feedContent.length <= MAX_CONTENT_LENGTH
 
     val isImageCountValid: Boolean
-        get() = imageUris.size <= 3
+        get() = imageUris.size <= MAX_IMAGE_COUNT
 
     val isFormValid: Boolean
         get() = selectedBook != null &&
                 isContentValid &&
                 isImageCountValid &&
-                selectedTags.size <= 5  // 태그는 최대 5개까지만
+                selectedTags.size <= MAX_TAG_COUNT  // 태그는 최대 5개까지만
 
     // 태그 개수 제한 (최대 5개)
     val canAddMoreTags: Boolean
-        get() = selectedTags.size < 5
+        get() = selectedTags.size < MAX_TAG_COUNT
 
     // 이미지 개수 제한 (최대 3개)
     val canAddMoreImages: Boolean
-        get() = imageUris.size < 3
+        get() = imageUris.size < MAX_IMAGE_COUNT

추가로 동일 상수를 FeedWriteScreen/FeedWriteViewModel에서도 사용하도록 가져가면 더 좋습니다.


8-24: BookData가 mock 패키지에 위치

실제 피드 작성 기능에서 사용하는 모델이 mock 패키지에 있는 점은 혼동을 줄 수 있습니다. 공용 모델 패키지로 이동을 고려해 주세요.

모델을 ui/common/model 혹은 data/model/book 등 일관된 위치로 이전하고 import 의존성 정리하는 것을 제안합니다.

app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt (3)

100-105: 예외 삼킴: 최소한 로깅 추가 권장 (detekt 경고 대응)

loadBooks()에서 예외를 빈 처리로 삼키고 있습니다. 디버깅/관측성을 위해 로깅을 추가해 주세요.

아래처럼 Log를 추가하세요:

+import android.util.Log
@@
         } catch (e: Exception) {
+            Log.e("FeedWriteViewModel", "loadBooks() failed", e)
             updateState { it.copy(savedBooks = emptyList(), groupBooks = emptyList()) }
         } finally {

Timber를 사용 중이면 Timber.e로 대체해도 좋습니다.


159-166: 예외 삼킴: 검색 실패 시 로깅 추가 권장 (detekt 경고 대응)

searchBooks()의 catch 블록도 동일합니다. 원인 파악을 위해 로깅을 추가해 주세요.

-            } catch (e: Exception) {
+            } catch (e: Exception) {
+                Log.e("FeedWriteViewModel", "searchBooks() failed for query: $query", e)
                 updateState {
                     it.copy(
                         searchResults = emptyList(),
                         isSearching = false
                     )
                 }
             }

176-184: 이미지 최대 개수 상수 재사용 권장

이미지 3장의 비즈니스 룰이 여기와 UiState/화면에 분산되어 있습니다. 하나의 상수(예: FeedWriteUiState.MAX_IMAGE_COUNT)로 일원화하면 유지보수성이 올라갑니다.

아래처럼 적용해 보세요:

-        val availableSlots = 3 - currentState.imageUris.size
+        val availableSlots = FeedWriteUiState.MAX_IMAGE_COUNT - currentState.imageUris.size

UiState에 상수 추가를 병행해 주세요(별도 코멘트 참고).

app/src/main/java/com/texthip/thip/ui/feed/screen/FeedWriteScreen.kt (5)

68-69: 수명주기 인지된 수집 사용 권장: collectAsStateWithLifecycle

Compose에서 StateFlow 수집은 lifecycle-aware API를 사용하면 안전합니다.

아래처럼 변경해 주세요:

+import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@
-    val uiState by viewModel.uiState.collectAsState()
+    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

183-193: clickable(enabled)로 단순화

.let { if (...) it.clickable { ... } else it } 대신 clickable의 enabled 파라미터로 간결하게 표현 가능합니다.

-                                .let {
-                                    if (uiState.canAddMoreImages) it.clickable {
-                                        imagePickerLauncher.launch("image/*")
-                                    } else it // 클릭 비활성화
-                                },
+                                .clickable(enabled = uiState.canAddMoreImages) {
+                                    imagePickerLauncher.launch("image/*")
+                                },

267-271: 장르 칩 Row에 width(18.dp)는 의도치 않은 레이아웃 축소 가능성

Row 컨테이너 폭을 18dp로 제한해 칩 UI가 잘릴 수 있습니다. 전체 폭 사용을 권장합니다.

-                GenreChipRow(
-                    modifier = Modifier.width(18.dp),
+                GenreChipRow(
+                    modifier = Modifier.fillMaxWidth(),
                     genres = uiState.categories.map { it.category },
                     selectedIndex = uiState.selectedCategoryIndex,
                     onSelect = onSelectCategory
                 )

혹시 Modifier.width(18.dp)가 내부 컴포넌트의 패딩 의도였다면 적절한 padding/spacing API로 대체하는 것이 명확합니다.


73-81: 에러 콜백 처리 누락 — 사용자 피드백 제공 필요

onError에서 아무 처리도 하지 않아 실패 원인을 사용자에게 전달하지 못합니다. 스낵바/토스트/다이얼로그 등으로 피드백을 제공해 주세요.

예시(개념 코드):

  • 화면 상단에 SnackbarHost를 두고, uiState.errorMessage가 업데이트되면 LaunchedEffect로 스낵바 노출
  • 또는 LocalContext로 Toast 표시 후 viewModel.clearError() 호출

필요 시 샘플 구현 제공 가능합니다.

원하시면 스낵바 기반 에러 처리 예시 코드를 생성해 드릴게요.


170-176: 사진 개수 표시의 상수화

문자열 리소스에 3이 하드코딩되어 있습니다. UiState 상수(예: MAX_IMAGE_COUNT)로 일원화하면 유지보수성이 좋아집니다.

-                        text = stringResource(id = R.string.photo_count, uiState.imageUris.size, 3),
+                        text = stringResource(id = R.string.photo_count, uiState.imageUris.size, FeedWriteUiState.MAX_IMAGE_COUNT),
📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 01939f1 and 60c5a55.

📒 Files selected for processing (15)
  • app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt (2 hunks)
  • app/src/main/java/com/texthip/thip/data/model/feed/request/CreateFeedRequest.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/model/feed/response/CreateFeedResponse.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/model/feed/response/FeedWriteInfoResponse.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/service/FeedService.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/common/buttons/SubGenreChipRow.kt (2 hunks)
  • app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt (4 hunks)
  • app/src/main/java/com/texthip/thip/ui/feed/screen/FeedWriteScreen.kt (11 hunks)
  • app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteUiState.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/navigator/extensions/FeedNavigationExtensions.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt (2 hunks)
  • app/src/main/java/com/texthip/thip/ui/navigator/routes/FeedRoutes.kt (1 hunks)
  • app/src/main/res/values/strings.xml (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt (2)
app/src/main/java/com/texthip/thip/ui/feed/screen/MySubscriptionListScreen.kt (1)
  • MySubscriptionScreen (49-84)
app/src/main/java/com/texthip/thip/ui/feed/screen/FeedWriteScreen.kt (1)
  • FeedWriteScreen (61-94)
app/src/main/java/com/texthip/thip/ui/feed/screen/FeedWriteScreen.kt (6)
app/src/main/java/com/texthip/thip/ui/common/topappbar/InputTopAppBar.kt (1)
  • InputTopAppBar (25-69)
app/src/main/java/com/texthip/thip/ui/group/makeroom/component/SectionDivider.kt (1)
  • SectionDivider (13-23)
app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupInputField.kt (1)
  • GroupInputField (26-83)
app/src/main/java/com/texthip/thip/ui/common/buttons/SubGenreChipRow.kt (1)
  • SubGenreChipGrid (15-35)
app/src/main/java/com/texthip/thip/ui/common/buttons/GenreChipButton.kt (1)
  • GenreChipButton (29-74)
app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookSearchBottomSheet.kt (1)
  • GroupBookSearchBottomSheet (32-122)
🪛 detekt (1.23.8)
app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt

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

(detekt.exceptions.SwallowedException)


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

(detekt.exceptions.SwallowedException)

🔇 Additional comments (14)
app/src/main/java/com/texthip/thip/ui/common/buttons/SubGenreChipRow.kt (1)

23-25: FlowRow 가로 정렬 개선 좋습니다

간격 유지 + 중앙 정렬로 그리드가 더 균형 있게 보입니다. 선택형 칩 UI에 적합한 변경이에요.

app/src/main/res/values/strings.xml (1)

333-335: 수량 단위 '개' 추가로 가독성 향상

사진/태그 카운트에 단위를 명시해 사용자가 한눈에 인지하기 좋아졌습니다.

app/src/main/java/com/texthip/thip/data/model/feed/response/CreateFeedResponse.kt (2)

5-8: 단순·명확한 응답 모델 추가 👍

@Serializable + feedId: Int로 응답 페이로드가 명확합니다. 리포지토리/서비스 계층에서 활용하기에 충분합니다.


1-8: 서버 스키마(키 이름/타입) 정합성 확인 요청 — Json 네이밍 변환 미설정

NetworkModule.provideJson()를 확인했습니다. Json 설정(isLenient, prettyPrint, encodeDefaults, explicitNulls = false, ignoreUnknownKeys = true)에는 snake_case↔camelCase 자동 변환이 없습니다. 따라서 CreateFeedResponse의 필드명/타입은 서버 응답과 정확히 일치해야 합니다.

점검 위치

  • app/src/main/java/com/texthip/thip/data/di/NetworkModule.kt — Json 설정 확인됨 (네이밍 전략 없음).
  • app/src/main/java/com/texthip/thip/data/model/feed/response/CreateFeedResponse.kt — 현재: val feedId: Int (검증 필요).
  • app/src/main/java/com/texthip/thip/data/service/FeedService.kt — createFeed()가 BaseResponse 반환.
  • app/src/main/java/com/texthip/thip/data/model/base/BaseResponse.kt — 응답 래핑("data") 및 @SerialName 사용, ignoreUnknownKeys = true(미지정 키 무시).

권장 (서버 응답에 따라 적용)

  • 서버가 feed_id(스네이크) → @SerialName("feed_id") 추가
  • 서버가 숫자를 64-bit로 반환 → 타입을 Long으로 변경
    예시:
    import kotlinx.serialization.SerialName
    @serializable
    data class CreateFeedResponse(
    @SerialName("feed_id") val feedId: Int // 또는 Long으로 변경
    )

요청: 백엔드가 반환하는 키 이름(feedId vs feed_id)과 타입(Int vs Long)을 확인해 주세요. 확인 후 @SerialName 추가 또는 타입 변경(또는 nullable/default 값 처리)을 적용해 주세요.

app/src/main/java/com/texthip/thip/data/model/feed/response/FeedWriteInfoResponse.kt (1)

5-14: 피드 작성 정보 모델 정의 적절

FeedWriteInfoResponseFeedCategory로 태그/카테고리 구조가 명확히 표현되었습니다. UI 단의 정렬/분류에 활용하기 좋습니다.

app/src/main/java/com/texthip/thip/data/model/feed/request/CreateFeedRequest.kt (1)

5-11: 요청 모델 구성 적절

필요 필드가 최소한으로 정의되어 있고 tagList 기본값도 안전합니다. 멀티파트 전송에서 JSON 파트로 직렬화하기에 적합합니다.

app/src/main/java/com/texthip/thip/data/service/FeedService.kt (1)

15-18: 피드 작성 정보 API 시그니처 적절합니다

엔드포인트/응답 타입 모두 의도에 부합합니다. suspend + BaseResponse 제네릭 조합도 일관적이에요.

app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt (1)

47-50: FeedService DI 바인딩 추가 LGTM

Retrofit.create로의 바인딩과 스코프 지정(@singleton) 적절합니다.

app/src/main/java/com/texthip/thip/ui/navigator/routes/FeedRoutes.kt (1)

8-10: Write 라우트 추가 적절합니다

새로운 작성 화면 진입점으로 최소 단위 라우트 분리는 타당합니다. Routes 설계와도 일관적이에요.

app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt (3)

52-52: 콜백 주입으로 결합도 낮춘 점 좋습니다

기본값 {}를 둬서 호출부 호환성도 유지됩니다.


306-308: 프리뷰 파라미터 갱신 LGTM

새 콜백 인자 추가에 맞춰 프리뷰도 업데이트되어 컴파일 안정성이 유지됩니다.


327-329: 보조 프리뷰도 일관되게 갱신됨

두 프리뷰 모두 새 시그니처를 반영합니다.

app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt (1)

15-33: 피드 작성 네비게이션 연동 LGTM

FeedScreen에 onNavigateToFeedWrite 콜백을 추가하고, navigateToFeedWrite() 확장으로 라우팅하는 흐름이 명확합니다. MySubscription/Write 두 경로 모두 타입 세이프 라우트(composable<...>)를 활용한 점도 일관적입니다.

app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt (1)

67-79: 서버 필드명 확인 필요: Multipart 파트 이름 "images"

백엔드가 배열 형태(images[])나 다른 키를 요구할 수 있습니다. Retrofit 인터페이스와 서버 스펙을 재확인해 주세요.

서버 스펙에 맞는 파트 키가 "images"가 맞는지 확인 부탁드립니다. 필요 시 @Part("images") List<MultipartBody.Part> vs @Part images: List<MultipartBody.Part> 사용 방식도 점검 바랍니다.

Comment on lines +6 to +11
data class CreateFeedRequest(
val isbn: String,
val contentBody: String,
val isPublic: Boolean,
val tagList: List<String> = emptyList()
) No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

서버 요청 스키마(키 케이스/불리언 키) 정합성 확인

  • 서버가 snake_case를 요구한다면 현 구조(camelCase)로는 인코딩 시 키 불일치가 발생합니다. 요청 인코딩에는 @JsonNames 대신 @SerialName 또는 전역 네이밍 전략이 필요합니다.
  • 불리언 isPublic 키를 서버가 그대로 기대하는지(isPublic) 혹은 다른 키명(public, visibility 등)인지 확인해 주세요.

다음 스크립트로 직렬화기 설정/컨버터 팩토리 및 서비스 정의를 확인해 키 네이밍 전략을 점검하세요:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== kotlinx.serialization Json 설정(네이밍 전략 포함) 검색 =="
rg -n -C3 -P '\bJson\s*\{' --glob '!**/build/**' --glob '!**/.gradle/**'

echo
echo "== Retrofit + KotlinxSerialization 컨버터 사용 여부 확인 =="
rg -n -C3 -P 'KotlinxSerializationConverterFactory|asConverterFactory|application/json' --glob '!**/build/**' --glob '!**/.gradle/**'

echo
echo "== CreateFeedRequest 직렬화/전달 경로 추적 =="
rg -n -C3 -P '\bCreateFeedRequest\b' --glob '!**/build/**' --glob '!**/.gradle/**'

Length of output: 5783


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== show NetworkModule.kt =="
sed -n '1,240p' app/src/main/java/com/texthip/thip/data/di/NetworkModule.kt || true

echo
echo "== show FeedRepository.kt =="
sed -n '1,240p' app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt || true

echo
echo "== show CreateFeedRequest.kt =="
sed -n '1,240p' app/src/main/java/com/texthip/thip/data/model/feed/request/CreateFeedRequest.kt || true

echo
echo "== search for @SerialName / @JsonNames / namingStrategy /JsonNamingStrategy == "
rg -n --hidden -S -C2 '@SerialName|@JsonNames|namingStrategy|JsonNamingStrategy' --glob '!**/build/**' --glob '!**/.gradle/**' || true

echo
echo "== search for imports of kotlinx.serialization.json.Json or direct Json.encodeToString usage == "
rg -n --hidden -S -C3 'import kotlinx.serialization.json.Json|Json\.encodeToString\(' --glob '!**/build/**' --glob '!**/.gradle/**' || true

Length of output: 37486


직렬화 인스턴스 불일치 → 요청 키(특히 불리언) 직렬화 일관성 필요

간단 요약: NetworkModule에서 DI로 제공한 Json(json.asConverterFactory)을 Retrofit에 등록하고 있으나, FeedRepository에서는 전역 기본 Json(Json.encodeToString)을 사용해 직렬화를 수행하고 있어 DI 설정(encodeDefaults 등)과 네이밍 전략이 적용되지 않습니다. 또한 CreateFeedRequest에 @SerialName이 없어 불리언 키명이(예: isPublic vs public) 불명확합니다. 코드베이스 다른 요청/응답 모델들은 @SerialName("isPublic") 형태를 쓰는 사례가 있으므로 일관성 있게 처리하세요.

수정 권장 위치(핵심)

  • app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt
    • 생성자에 DI Json 주입 추가 후 Json.encodeToString(request) → json.encodeToString(request)로 변경
  • app/src/main/java/com/texthip/thip/data/model/feed/request/CreateFeedRequest.kt
    • 적어도 isPublic 필드에 @SerialName("isPublic") 추가(백엔드 키명이 다르면 그 값으로 조정)
  • (옵션) app/src/main/java/com/texthip/thip/data/di/NetworkModule.kt
    • 서버가 snake_case를 요구하면 전역 네이밍 전략(namingStrategy) 설정 추가 또는 모든 필드에 @SerialName 명시

권장 변경 예시(diff)

  • FeedRepository.kt (핵심 변경)

    • 생성자:
      • before: class FeedRepository @Inject constructor( private val feedService: FeedService, @param:ApplicationContext private val context: Context )
      • after: class FeedRepository @Inject constructor( private val feedService: FeedService, @param:ApplicationContext private val context: Context, private val json: Json )
    • 직렬화 호출:
      • before: val requestJson = Json.encodeToString(request)
      • after: val requestJson = json.encodeToString(request)
  • CreateFeedRequest.kt (간단 예)

    • before:
      @serializable
      data class CreateFeedRequest(
      val isbn: String,
      val contentBody: String,
      val isPublic: Boolean,
      val tagList: List = emptyList()
      )
    • after:
      import kotlinx.serialization.SerialName
      @serializable
      data class CreateFeedRequest(
      @SerialName("isbn") val isbn: String,
      @SerialName("contentBody") val contentBody: String,
      @SerialName("isPublic") val isPublic: Boolean,
      @SerialName("tagList") val tagList: List = emptyList()
      )

확인 포인트

  • FeedService.createFeed가 multipart로 JSON part를 받는 방식인지(@part("...") 등) 확인 — Retrofit의 converter를 활용할 수 있으면 Converter쪽으로 위임하는 편이 안전합니다.
  • 백엔드에 요청 키명(특히 불리언 필드)이 정확히 무엇인지(문서 또는 백엔드에 확인) 확인하세요.
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/data/model/feed/request/CreateFeedRequest.kt
around lines 6–11 and
app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt update the
serialization to use the DI-provided Json and make boolean key names explicit:
add @SerialName("isPublic") to the isPublic property in CreateFeedRequest (and
other @SerialName annotations as needed for consistency), inject
kotlinx.serialization.json.Json into FeedRepository via its constructor, and
replace any usages of Json.encodeToString(request) with the injected
json.encodeToString(request); optionally verify NetworkModule namingStrategy
(snake_case vs camelCase) and adjust global Json or add explicit @SerialName
values to match backend keys.

Comment thread app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt Outdated
Comment thread app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt Outdated
Comment on lines +19 to +25
/** 피드 생성 */
@Multipart
@POST("feeds")
suspend fun createFeed(
@Part("request") request: RequestBody,
@Part images: List<MultipartBody.Part>?
): BaseResponse<CreateFeedResponse>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

멀티파트 전송 시 JSON Part/이미지 Part 네이밍과 Content-Type을 확인해주세요

  • @part("request") RequestBody: Repository에서 반드시 "application/json; charset=UTF-8"로 생성되었는지 확인 필요합니다. 잘못된 미디어 타입이면 서버가 파싱하지 못할 수 있어요.
  • @part images: List<MultipartBody.Part>?: MultipartBody.Part 사용은 올바르며, 각 Part 생성 시 createFormData("images", filename, body)처럼 서버 스펙과 동일한 키를 써야 합니다. 서버가 images[] 형태를 요구하면 그에 맞춰 name을 설정해야 합니다.
  • 빈 이미지일 때: null을 넘겨 Part를 생략하는 현재 설계는 흔히 쓰이는 방식입니다. Repository에서 빈 리스트를 null로 변환해 주는지 확인해 주세요.

검증용 스크립트를 공유드립니다.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) JSON RequestBody가 올바른 미디어 타입으로 생성되는지 확인
rg -n -C3 --type=kt -P 'application/json' app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt || true
rg -n -C3 --type=kt -P '(toMediaType|MediaType\.parse|toRequestBody|RequestBody\.create)' app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt || true

# 2) 이미지 Part 네이밍 확인 (images 혹은 images[] 등)
rg -n -C2 --type=kt -P 'MultipartBody\.Part\.createFormData\(' app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt || true

# 3) 빈 이미지 목록 처리 (빈 리스트를 null로 변환하는지) 확인
rg -n -C3 --type=kt -P '(if\s*\(images\.isEmpty\(\)\)|takeIf|takeUnless)' app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt || true

Length of output: 706


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "PWD: $(pwd)"
echo "--- locating FeedRepository.kt ---"
find app -type f -iname 'FeedRepository.kt' -print || true

echo "--- searching repository for likely patterns ---"
rg -n -S -C3 'createFormData\(|MultipartBody\.Part|toRequestBody\(|application/json|toMediaType\(|MediaType\.parse|RequestBody\.create|feeds\b|createFeed\(' || true

echo "--- showing candidate repository files (top 200 lines each) ---"
# show top matches for files that look relevant
rg -n --files-with-matches -S 'createFormData\(|toRequestBody\(|application/json|toMediaType\(|MultipartBody\.Part|RequestBody\.create' || true \
| head -n 20 \
| xargs -r -I{} sh -c 'echo ""; echo "===== {} ====="; sed -n "1,200p" "{}"'

Length of output: 21725


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- FeedRepository.kt ----"
nl -ba app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt | sed -n '1,240p' || true

echo ""
echo "---- FeedService.kt ----"
nl -ba app/src/main/java/com/texthip/thip/data/service/FeedService.kt | sed -n '1,240p' || true

echo ""
echo "---- FeedWriteViewModel.kt ----"
nl -ba app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt | sed -n '1,420p' || true

echo ""
echo "---- NetworkModule.kt ----"
nl -ba app/src/main/java/com/texthip/thip/data/di/NetworkModule.kt | sed -n '1,200p' || true

Length of output: 1081


멀티파트 전송 점검 결과 — 조치 필요

확인 요약: 레포지토리에서 JSON RequestBody는 "application/json"으로 생성되며, 이미지 파트는 MultipartBody.Part.createFormData(paramName, ...)로 생성되고 빈 이미지 리스트는 null로 처리됩니다. 다음 항목을 확인/수정해주세요.

  • app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt

    • 63–66: requestJson.toRequestBody("application/json".toMediaType()) — 현재 Content-Type이 "application/json"입니다. 서버가 charset을 요구하면 "application/json; charset=UTF-8"로 변경하세요.
    • 68–80: imageUris -> imageParts 매핑; imageUris가 비어있으면 imageParts = null로 feedService.createFeed(requestBody, imageParts) 호출(빈 이미지 시 Part 생략) — 의도된 동작으로 보입니다.
    • 85–112: private fun uriToMultipartBodyPart(uri: Uri, paramName: String) { … MultipartBody.Part.createFormData(paramName, fileName, requestFile) } — createFormData에 전달되는 name은 paramName 변수입니다. 호출부에서 어떤 문자열(예: "images" 또는 "images[]")을 넘기는지 확인하세요.
  • app/src/main/java/com/texthip/thip/data/service/FeedService.kt

    • 19–25: API 시그니처는 @part("request") request: RequestBody, @part images: List<MultipartBody.Part>? — RequestBody 파트명이 "request"로 전송되는 점과 images 파트가 nullable인 점을 백엔드 스펙과 대조해 주세요.
  • app/src/main/java/com/texthip/thip/data/di/NetworkModule.kt

    • 66: json.asConverterFactory(requireNotNull("application/json".toMediaType())) — ConverterFactory의 미디어타입도 "application/json"입니다. RequestBody와 일관되게 필요 시 charset 추가 고려.

권장 작업(간단한 코드 예시)

  • RequestBody에 charset 추가(필요한 경우):
    requestJson.toRequestBody("application/json; charset=UTF-8".toMediaType())
  • 이미지 파트명이 서버 스펙과 일치하는지 확인(예: uriToMultipartBodyPart(uri, "images") 또는 "images[]")
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/data/service/FeedService.kt around lines
19 to 25, the multipart API signature currently uses @Part("request") request:
RequestBody and @Part images: List<MultipartBody.Part>?; verify and align these
names and nullability with the backend contract: if the backend expects the JSON
part under a different part name or requires charset, create the RequestBody
with "application/json; charset=UTF-8" and use that part name here; ensure the
image part name passed when creating MultipartBody.Part (e.g., "images" vs
"images[]") matches this @Part name, and keep the images parameter
nullable/omitted when no files are present so the feedService.createFeed call
omits empty parts; also ensure NetworkModule converter media type uses the same
media type (add charset if required) for consistency.

import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Compose 버전/의존성 호환성 확인 권장

Arrangement.spacedBy(…, Alignment.CenterHorizontally) 시그니처와 foundation.layout.FlowRow 사용은 특정 Compose 버전 이상에서만 지원됩니다. 프로젝트의 Compose BOM/버전을 한 번 점검해 주세요.

다음 스크립트로 Compose 관련 버전/의존성 선언을 확인할 수 있습니다:

Also applies to: 23-23


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Compose BOM / 버전 선언 검색 =="
rg -n -C2 -g '!**/build/**' -g '!**/.gradle/**' -P '(compose-bom|androidx\.compose|compose_version|composeOptions|kotlinCompilerExtensionVersion)'

echo
echo "== FlowRow 관련 아티팩트 확인 (foundation-layout vs accompanist-flowlayout) =="
rg -n -C2 -P 'foundation-layout|accompanist-flowlayout' -g '*gradle*' -g '!**/build/**' -g '!**/.gradle/**'

Length of output: 264338


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Showing gradle/libs.versions.toml =="
sed -n '1,240p' gradle/libs.versions.toml || true

echo
echo "== Searching for version refs used (composeBom, foundationVersion, composeVersion, kotlinCompilerExtensionVersion) =="
rg -n --hidden -S --no-ignore-vcs -g 'gradle/**' -P '^(composeBom|foundationVersion|compose_version|composeVersion|kotlinCompilerExtensionVersion)\s*=' || true

echo
echo "== Show settings/build files that may apply BOM usage =="
rg -n -C2 -g '!**/build/**' -g '!**/.gradle/**' -P '(platform\(|composeBom|androidx-compose-bom|bom\(platform' || true

Length of output: 5426


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Search for compose BOM usage (platform/enforcedPlatform/libs) in gradle files =="
rg -n --hidden -S -g '!**/build/**' -g '!**/.gradle/**' 'platform\(|enforcedPlatform\(|compose-bom|composeBom|libs\.[A-Za-z0-9._-]*compose' || true

echo
echo "== Show common gradle files if present (first 240 lines) =="
for f in settings.gradle settings.gradle.kts build.gradle build.gradle.kts app/build.gradle app/build.gradle.kts; do
  if [ -f "$f" ]; then
    echo "---- $f ----"
    sed -n '1,240p' "$f"
    echo
  fi
done

echo "== Search for explicit foundation dependency usages in build files =="
rg -n --hidden -S -g '!**/build/**' -g '!**/.gradle/**' '(androidx\.compose\.foundation|libs\.(foundation|androidx-foundation))' || true

Length of output: 182646


Compose 버전/의존성 정합성 확인 — 하드코딩된 foundation(1.5.0) 제거 권장

간단히: gradle/libs.versions.toml에서 composeBom = "2024.09.00", foundationVersion = "1.9.0-rc01"로 BOM 사용이 확인되었고 app/build.gradle.kts에는 platform(libs.androidx.compose.bom) 적용도 확인되었습니다. 다만 동일 파일에 implementation("androidx.compose.foundation:foundation:1.5.0")가 하드코딩되어 있어 BOM과 버전 혼재(충돌) 가능성이 있습니다. FlowRow / Arrangement.spacedBy(..., Alignment.CenterHorizontally)는 최신 Compose에서 지원되므로 버전 정리가 필요합니다.

조치 권장 위치:

  • gradle/libs.versions.toml
    • composeBom = "2024.09.00"
    • foundationVersion = "1.9.0-rc01"
  • app/build.gradle.kts
    • implementation(platform(libs.androidx.compose.bom)) — BOM 적용 확인
    • implementation("androidx.compose.foundation:foundation:1.5.0") — 제거 또는 정리 필요
  • UI 사용 파일 (예): app/src/main/java/com/texthip/thip/ui/common/buttons/SubGenreChipRow.kt (FlowRow/Arrangement 사용)

권장 조치:

  • app/build.gradle.kts에서 explicit한 implementation("androidx.compose.foundation:foundation:1.5.0") 제거하고 BOM(libs.foundation / libs.androidx.foundation)으로 버전 통일 또는 명시적 버전들을 BOM과 맞추기.
  • 필요시 Compose 컴파일러(kotlinCompilerExtensionVersion)와 Kotlin 플러그인 버전도 BOM과 호환되는지 함께 확인하세요.
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/common/buttons/SubGenreChipRow.kt
around line 9 the project is referencing Compose foundation APIs but the build
uses a hardcoded implementation("androidx.compose.foundation:foundation:1.5.0")
which conflicts with the BOM (composeBom = "2024.09.00" and foundationVersion =
"1.9.0-rc01") — remove the explicit foundation dependency from
app/build.gradle.kts and rely on the BOM (use
implementation(platform(libs.androidx.compose.bom)) and libs.foundation or
libs.androidx.foundation), or align the explicit version to the BOM; also verify
and, if necessary, update kotlinCompilerExtensionVersion and Kotlin plugin
versions to be compatible with the BOM so FlowRow/Arrangement APIs resolve
against the BOM-provided Compose versions.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt (1)

67-69: Serializer 명시 사용, 좋습니다

Json.encodeToString(CreateFeedRequest.serializer(), request)로 명시적 Serializer를 사용해 컴파일/런타임 이슈를 예방했습니다.

🧹 Nitpick comments (9)
app/src/main/res/values/strings.xml (2)

333-335: 카운트 문자열은 plurals로의 전환을 고려

현재 한국어에선 자연스럽지만, 다국어화(i18n) 시 언어별 복수형 규칙 처리가 필요할 수 있습니다. 추후 다국어 지원을 고려한다면 plurals 리소스로 교체하는 것을 권장합니다.

예시:

-    <string name="photo_count">%1$d / %2$d개</string>
-    <string name="tag_count">%1$d / %2$d개</string>
+    <plurals name="photo_count">
+        <item quantity="other">%1$d / %2$d개</item>
+    </plurals>
+    <plurals name="tag_count">
+        <item quantity="other">%1$d / %2$d개</item>
+    </plurals>

407-409: 사용자 노출 메시지에서 내부 용어(feedId) 노출 최소화 제안

"서버 feedId 반환 오류"는 개발자 지향 표현입니다. 사용자에게는 "피드 생성에 실패했습니다. 잠시 후 다시 시도해주세요." 등 중립적 문구가 더 적합합니다. 내부 로깅/토스트 구분이 가능하다면, 사용자용/로그용 메시지를 분리하는 것을 권장합니다.

다음과 같이 표현을 완화하는 것을 제안합니다:

-    <string name="error_feed_id_not_returned">서버 feedId 반환 오류</string>
+    <string name="error_feed_id_not_returned">피드 생성에 실패했습니다. 잠시 후 다시 시도해주세요.</string>
app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt (4)

31-49: 카테고리 정렬 기준 하드코딩 → 안정적 키/코드 기반 정렬로 변경 권장

서버 응답 문자열("문학", "과학·IT", …)에 직접 의존하면 레이블 변경/번역 시 정렬이 깨질 수 있습니다. 특히 strings.xml에는 "과학/IT"(슬래시) 상수도 존재해("과학·IT" 중점과 불일치) 오타/불일치에 취약합니다. 안전하게 코드/ID 기반 정렬(또는 맵 기반 우선순위)로 전환하는 것을 권장합니다.

다음처럼 불일치에도 견고하게 동작하도록 맵 기반 정렬로 변경을 제안합니다:

-        val orderedCategories = response?.categoryList?.sortedBy { category ->
-            when (category.category) {
-                "문학" -> 0
-                "과학·IT" -> 1
-                "사회과학" -> 2
-                "인문학" -> 3
-                "예술" -> 4
-                else -> 999
-            }
-        } ?: emptyList()
+        val priorityMap = mapOf(
+            "문학" to 0,
+            "과학·IT" to 1, // 중점
+            "과학/IT" to 1,  // 슬래시(문자열 상수 대비)
+            "사회과학" to 2,
+            "인문학" to 3,
+            "예술" to 4,
+        )
+        val orderedCategories = response?.categoryList?.sortedBy { category ->
+            priorityMap[category.category] ?: Int.MAX_VALUE
+        } ?: emptyList()

필요 시 서버 스키마에 카테고리 코드/정렬 우선순위 필드를 추가해 해당 필드로 정렬하는 것이 최선입니다.


76-84: 변환 중 예외가 삼켜져 원인 추적이 어려움 — 최소 로깅 추가 권장

mapNotNull 내부의 catch에서 null만 반환해(Detekt: SwallowedException) 문제 원인 파악이 어렵습니다. 경고 로그를 남겨 디버깅 가능성을 높여주세요.

아래처럼 TAG 로그를 추가하는 것을 제안합니다:

                 imageUris.mapNotNull { uri ->
                     try {
                         uriToMultipartBodyPart(uri, "images", tempFiles)
                     } catch (e: Exception) {
-                        null
+                        Log.w(TAG, "이미지 파트 생성 실패: $uri", e)
+                        null
                     }
                 }

추가(클래스 내부 어디든):

private companion object {
    const val TAG = "FeedRepository"
}

그리고 import:

import android.util.Log

92-95: 임시 파일 삭제는 IO 컨텍스트에서 수행

파일 삭제도 디스크 IO입니다. finally에서 IO 디스패처로 옮겨 미미하더라도 메인 스레드 작업을 피하는 것이 안전합니다.

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

-        } finally {
-            // 임시 파일들 정리
-            cleanupTempFiles(tempFiles)
-        }
+        } finally {
+            // 임시 파일들 정리
+            withContext(Dispatchers.IO) { cleanupTempFiles(tempFiles) }
+        }

98-130: MIME/확장자 범위 확장 및 로깅 개선 제안(HEIC/WEBP 등)

  • 최신 기기에서 HEIC/HEIF, WEBP 포맷이 흔합니다. 현재 매핑에 누락되어 JPG로 강제되며 서버 수용 포맷에 따라 실패 가능성이 있습니다.
  • e.printStackTrace() 대신 통일된 로깅 사용이 바람직합니다.

다음처럼 포맷 매핑/로깅을 보완하세요:

-            val mimeType = context.contentResolver.getType(uri) ?: "image/jpeg"
-            val extension = when (mimeType) {
-                "image/png" -> "png"
-                "image/gif" -> "gif"
-                "image/jpeg", "image/jpg" -> "jpg"
-                else -> "jpg" // 기본값
-            }
+            val mimeType = context.contentResolver.getType(uri) ?: "image/jpeg"
+            val extension = when (mimeType.lowercase()) {
+                "image/png" -> "png"
+                "image/gif" -> "gif"
+                "image/jpeg", "image/jpg" -> "jpg"
+                "image/webp" -> "webp"
+                "image/heic", "image/heif" -> "jpg" // 서버 미지원 시 JPEG로 업컨버트 고려
+                else -> "jpg" // 기본값
+            }
@@
-        } catch (e: Exception) {
-            e.printStackTrace()
-            null
-        }
+        } catch (e: Exception) {
+            Log.e(TAG, "uriToMultipartBodyPart 실패: $uri", e)
+            null
+        }

가능하다면 파일명도 ContentResolver의 DISPLAY_NAME를 우선 사용하여 확장자 추론 정확성을 높일 수 있습니다. 업로드 사이즈 제약이 있다면 해상도/품질 리샘플링도 고려하세요(옵션).

app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt (3)

77-106: 저장 책/참여 책 병렬 로딩으로 대기 시간 단축 가능

서로 독립된 두 API를 순차 호출 중입니다. async로 병렬화하면 시나리오에 따라 체감 대기시간이 절반 수준으로 줄어듭니다.

아래처럼 변경을 제안합니다:

-        viewModelScope.launch {
-            updateState { it.copy(isLoadingBooks = true) }
-            try {
-                val savedBooksResult = bookRepository.getBooks("SAVED")
-                savedBooksResult.onSuccess { response ->
-                    updateState {
-                        it.copy(savedBooks = response?.bookList?.map { dto -> dto.toBookData() }
-                            ?: emptyList())
-                    }
-                }.onFailure {
-                    updateState { it.copy(savedBooks = emptyList()) }
-                }
-
-                val groupBooksResult = bookRepository.getBooks("JOINING")
-                groupBooksResult.onSuccess { response ->
-                    updateState {
-                        it.copy(groupBooks = response?.bookList?.map { dto -> dto.toBookData() }
-                            ?: emptyList())
-                    }
-                }.onFailure {
-                    updateState { it.copy(groupBooks = emptyList()) }
-                }
-            } catch (e: Exception) {
-                updateState { it.copy(savedBooks = emptyList(), groupBooks = emptyList()) }
-            } finally {
-                updateState { it.copy(isLoadingBooks = false) }
-            }
-        }
+        viewModelScope.launch {
+            updateState { it.copy(isLoadingBooks = true) }
+            try {
+                kotlinx.coroutines.coroutineScope {
+                    val savedDeferred = async { bookRepository.getBooks("SAVED") }
+                    val groupDeferred = async { bookRepository.getBooks("JOINING") }
+
+                    savedDeferred.await()
+                        .onSuccess { response ->
+                            updateState {
+                                it.copy(savedBooks = response?.bookList?.map { dto -> dto.toBookData() } ?: emptyList())
+                            }
+                        }.onFailure {
+                            updateState { it.copy(savedBooks = emptyList()) }
+                        }
+
+                    groupDeferred.await()
+                        .onSuccess { response ->
+                            updateState {
+                                it.copy(groupBooks = response?.bookList?.map { dto -> dto.toBookData() } ?: emptyList())
+                            }
+                        }.onFailure {
+                            updateState { it.copy(groupBooks = emptyList()) }
+                        }
+                }
+            } catch (e: Exception) {
+                updateState { it.copy(savedBooks = emptyList(), groupBooks = emptyList()) }
+            } finally {
+                updateState { it.copy(isLoadingBooks = false) }
+            }
+        }

추가 import:

import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope

또한 "SAVED"/"JOINING" 매직 스트링은 enum/sealed class로 치환 시 타입 안정성이 좋아집니다.


126-168: 디바운스 검색 흐름 적절. 실패 시 로깅 추가 권장

디바운싱 및 이전 작업 취소가 잘 구현되어 있습니다. 다만 실패 분기에서 로그가 없어 원인 추적이 어렵습니다. 최소 warn 로그를 추가하면 운영 시 유용합니다.

-                }.onFailure {
+                }.onFailure {
+                    // TODO: 필요 시 로깅 도입 (예: Timber/Log)
                     updateState {
                         it.copy(
                             searchResults = emptyList(),
                             isSearching = false
                         )
                     }
                 }
@@
-            } catch (e: Exception) {
+            } catch (e: Exception) {
+                // TODO: 필요 시 로깅 도입 (예: Timber/Log)
                 updateState {
                     it.copy(
                         searchResults = emptyList(),
                         isSearching = false
                     )
                 }
             }

176-185: 이미지 추가: 개수 제한 로직 명확, LGTM

3장 제한 및 초과분 무시가 명확합니다. 필요 시 초과 시도에 대한 사용자 피드백(토스트 등)도 고려 가능합니다.

📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 60c5a55 and 3a24db5.

📒 Files selected for processing (6)
  • app/src/main/java/com/texthip/thip/data/model/feed/request/CreateFeedRequest.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/model/feed/response/CreateFeedResponse.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/model/feed/response/FeedWriteInfoResponse.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt (1 hunks)
  • app/src/main/res/values/strings.xml (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/src/main/java/com/texthip/thip/data/model/feed/request/CreateFeedRequest.kt
  • app/src/main/java/com/texthip/thip/data/model/feed/response/CreateFeedResponse.kt
  • app/src/main/java/com/texthip/thip/data/model/feed/response/FeedWriteInfoResponse.kt
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-13T05:42:13.580Z
Learnt from: rbqks529
PR: THIP-TextHip/THIP-Android#79
File: app/src/main/java/com/texthip/thip/ui/search/viewmodel/BookDetailViewModel.kt:26-27
Timestamp: 2025-08-13T05:42:13.580Z
Learning: ViewModelScope는 기본적으로 Dispatchers.Main을 사용하므로, Repository의 suspend 함수에서 네트워크 호출 시 명시적으로 withContext(Dispatchers.IO)로 전환해야 메인 스레드 블로킹을 방지할 수 있다. suspend 함수라고 해서 자동으로 적절한 디스패처가 사용되지는 않는다.

Applied to files:

  • app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt
🪛 detekt (1.23.8)
app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt

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

(detekt.exceptions.SwallowedException)


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

(detekt.exceptions.SwallowedException)

app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt

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

(detekt.exceptions.SwallowedException)

🔇 Additional comments (5)
app/src/main/res/values/strings.xml (1)

333-335: 사진/태그 카운트 표기 '개' 추가: 가독성 향상, LGTM

UI에서 사용자의 인지 부담을 줄이는 방향으로 좋습니다.

app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt (2)

75-85: 이미지 변환 IO 디스패처 오프로딩: 적절함

URI→파일 복사 작업을 withContext(Dispatchers.IO)로 이동해 UI 블로킹을 방지했습니다. 예외 시 null 필터링도 합리적입니다.


89-91: 확인: FeedService.createFeed는 이미지 파트 이름으로 'images'를 기대합니다

FeedService.kt에서 아래 시그니처를 확인했습니다:
@multipart
@post("feeds")
suspend fun createFeed(
@part("request") request: RequestBody,
@part images: List<MultipartBody.Part>?
): BaseResponse

따라서 클라이언트에서 MultipartBody.Part.createFormData("images", ...) 또는 uriToMultipartBodyPart(..., "images", ...)로 name="images"를 설정하면 Retrofit 인터페이스와 일치합니다.

  • 파일: app/src/main/java/com/texthip/thip/data/service/FeedService.kt
app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt (2)

42-64: 카테고리 로딩 흐름: 상태 전이 명확, LGTM

로딩/성공/실패 상태 전이가 깔끔합니다. 에러 메시지를 StringResourceProvider로 일원화한 점도 좋습니다.


228-278: feedId null 처리 개선, 사용자 메시지 리소스 사용: 좋습니다

서버가 feedId를 반환하지 않을 때 에러로 처리하고 error_feed_id_not_returned로 전달하는 흐름이 일관되고 안전합니다.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (5)
app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt (1)

16-19: SavedStateHandle의 "feedId" 키를 상수로 추출하세요 — 검증 필요

SavedStateHandle로 one-shot 결과 처리 패턴은 적절합니다. 다만 key 문자열("feedId")은 오타·추적 문제로 공용 상수화하시길 권장합니다.

자동 검색 결과: 제공해주신 스크립트 실행 시 저장소 내에서 해당 패턴을 찾지 못했습니다(검색 결과 없음). 추가 사용처 확인을 위해 로컬에서 재검증 부탁드립니다.

권장 변경(예)

// 예: app/src/main/java/com/texthip/thip/ui/navigator/keys/NavResultKeys.kt
package com.texthip.thip.ui.navigator.keys

object NavResultKeys {
    const val FEED_ID = "feedId"
}

적용 예시 diff:

-        val resultFeedId = backStackEntry.savedStateHandle.get<Int>("feedId")
+        val resultFeedId = backStackEntry.savedStateHandle.get<Int>(NavResultKeys.FEED_ID)
-                backStackEntry.savedStateHandle.remove<Int>("feedId")
+                backStackEntry.savedStateHandle.remove<Int>(NavResultKeys.FEED_ID)
-                    .set("feedId", feedId)
+                    .set(NavResultKeys.FEED_ID, feedId)

잠재적 변경 위치(리뷰 기준):

  • app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt — 라인 16–19, 26–29, 48–51

로컬에서 전체 사용처 확인용(권장) 명령:

rg -n -S '\bfeedId\b' app || true
app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt (4)

56-57: LaunchedEffect 내부에서 직접 suspend 호출로 단순화(rememberCoroutineScope/launch 불필요)

현재 LaunchedEffect 내부에서 scope.launch를 다시 사용하는 중복이 있습니다. Effect 스코프에서 바로 animateTo/delay를 호출하면 코드가 단순해지고 import도 줄어듭니다.

아래 diff처럼 정리 가능합니다.

-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
+import kotlinx.coroutines.delay
-    val scope = rememberCoroutineScope()
-    
-    var showProgressBar by remember { mutableStateOf(false) }
-    val progress = remember { Animatable(0f) }
-    
+    var showProgressBar by remember { mutableStateOf(false) }
+    val progress = remember { Animatable(0f) }
     LaunchedEffect(resultFeedId) {
         if (resultFeedId != null) {
             onResultConsumed()
             
             showProgressBar = true
             progress.snapTo(0f)
-            scope.launch {
-                progress.animateTo(
-                    targetValue = 1f,
-                    animationSpec = tween(durationMillis = 1000, easing = LinearEasing)
-                )
-                delay(500)
-                if (showProgressBar) {
-                    showProgressBar = false
-                }
-            }
+            // Effect 스코프에서 직접 수행
+            progress.animateTo(
+                targetValue = 1f,
+                animationSpec = tween(durationMillis = 1000, easing = LinearEasing)
+            )
+            delay(500)
+            showProgressBar = false
         }
     }

Also applies to: 80-84, 85-102


176-214: 진행 바 섹션은 현재 두 탭(피드/내 피드) 모두에 삽입됩니다 — 의도 확인 필요

코드는 LazyColumn 최상단에 item을 추가하므로 탭에 관계없이 나타납니다. AI 요약에는 “내 피드 탭 활성 시 첫 아이템으로 삽입”이라고 되어 있어 차이가 있습니다. 의도가 “피드 작성 완료 후 내 피드 상단에서만 보여주기”라면 아래처럼 가드해 주세요.

-                item {
-                    AnimatedVisibility(visible = showProgressBar) {
+                if (selectedIndex.value == 1) {
+                    item {
+                        AnimatedVisibility(visible = showProgressBar) {
                           // ... 기존 내용 동일 ...
-                    }
+                        }
+                    }
+                }

검증 메모:

  • “피드” 탭에서도 작성 완료 표시를 보여주려는 의도라면 현 구조가 맞습니다. 기획/디자인 의도를 한번만 확인 부탁드립니다.

279-286: Divider 렌더링 조건에 feeds.lastIndex 대신 feedStateList.lastIndex를 사용하세요

현재 비교 기준이 원본 파라미터 feeds라서, feedStateList와 크기가 달라질 경우(추가/삭제 등) 마지막 아이템 판단이 틀어질 수 있습니다.

-                            if (index != feeds.lastIndex) {
+                            if (index != feedStateList.lastIndex) {
                                 HorizontalDivider(
                                     color = colors.DarkGrey02,
                                     thickness = 10.dp
                                 )
                             }

327-333: 동일한 lastIndex 비교 버그 반복 — feedStateList 기준으로 통일

위와 동일한 이유로 리스트 기준을 feedStateList로 맞추는 것이 안전합니다.

-                        if (index != feeds.lastIndex) {
+                        if (index != feedStateList.lastIndex) {
                             HorizontalDivider(
                                 color = colors.DarkGrey02,
                                 thickness = 10.dp
                             )
                         }
📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3a24db5 and 4eae562.

📒 Files selected for processing (3)
  • app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt (7 hunks)
  • app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt (1 hunks)
  • app/src/main/res/values/strings.xml (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/main/res/values/strings.xml
🧰 Additional context used
🧬 Code Graph Analysis (1)
app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt (1)
app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt (1)
  • FeedScreen (60-342)
🔇 Additional comments (3)
app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt (1)

33-35: Feed 작성 라우팅(진입/복귀) 연결 방식 적절합니다

  • Write로 진입: 확장 함수 navigateToFeedWrite() 사용 깨끗합니다.
  • 작성 완료: SavedStateHandle에 feedId 저장 후 popBackStack으로 복귀하는 플로우는 Compose Navigation에서 흔히 쓰는 패턴입니다.

Also applies to: 41-54

app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt (2)

62-73: FeedScreen API 정리(Lifted navigation): navController 제거 + 콜백 주입 방식 적절

  • 네비게이션 의존성 제거하고 콜백(onNavigateToFeedWrite, onResultConsumed)만 노출한 점 좋습니다.
  • resultFeedId를 주입받아 1회성 처리하는 구조도 명확합니다.

338-340: FAB 클릭 콜백 교체(Lifted navigation) 적절

onClick = onNavigateToFeedWrite로 변경하여 화면이 네비게이션에 직접 의존하지 않도록 한 점 좋습니다.

Comment on lines +46 to +53
onFeedCreated = { feedId ->
// 피드 생성 성공 시 결과를 저장하고 피드 목록으로 돌아가기
navController.getBackStackEntry(MainTabRoutes.Feed)
.savedStateHandle
.set("feedId", feedId)
navController.popBackStack()
}
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

onFeedCreated에서 getBackStackEntry 직접 참조 대신 previousBackStackEntry 우선 사용 권장(예외/결과 유실 방지)

Feed 화면이 백스택에 없거나 다른 진입 경로일 때 getBackStackEntry(MainTabRoutes.Feed)가 예외를 던질 수 있습니다. previousBackStackEntry를 우선 사용하고, null일 때만 fallback으로 getBackStackEntry를 조회하면 안전합니다.

아래와 같이 간단히 보완해 주세요.

-            onFeedCreated = { feedId ->
-                // 피드 생성 성공 시 결과를 저장하고 피드 목록으로 돌아가기
-                navController.getBackStackEntry(MainTabRoutes.Feed)
-                    .savedStateHandle
-                    .set("feedId", feedId)
-                navController.popBackStack()
-            }
+            onFeedCreated = { feedId ->
+                // 피드 생성 성공 시 결과를 저장하고 피드 목록으로 돌아가기
+                val handle = navController.previousBackStackEntry?.savedStateHandle
+                    ?: navController.getBackStackEntry(MainTabRoutes.Feed).savedStateHandle
+                handle.set("feedId", feedId)
+                navController.popBackStack()
+            }
📝 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
onFeedCreated = { feedId ->
// 피드 생성 성공 시 결과를 저장하고 피드 목록으로 돌아가기
navController.getBackStackEntry(MainTabRoutes.Feed)
.savedStateHandle
.set("feedId", feedId)
navController.popBackStack()
}
)
onFeedCreated = { feedId ->
// 피드 생성 성공 시 결과를 저장하고 피드 목록으로 돌아가기
val handle = navController.previousBackStackEntry?.savedStateHandle
?: navController.getBackStackEntry(MainTabRoutes.Feed).savedStateHandle
handle.set("feedId", feedId)
navController.popBackStack()
}
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt
around lines 46 to 53, replace the direct call to
navController.getBackStackEntry(MainTabRoutes.Feed) with logic that first
attempts navController.previousBackStackEntry and only falls back to
getBackStackEntry(MainTabRoutes.Feed) if previousBackStackEntry is null; if you
obtain a non-null backStackEntry set its savedStateHandle "feedId" to the
created feedId, otherwise avoid calling getBackStackEntry (or handle the null
case gracefully, e.g., skip setting the result or log a warning), then call
navController.popBackStack() as before.

Copy link
Copy Markdown
Member

@Nico1eKim Nico1eKim left a comment

Choose a reason for hiding this comment

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

수고하셨습니둥 ~~

Comment on lines +67 to +68
val requestJson = json.encodeToString(CreateFeedRequest.serializer(), request)
val requestBody = requestJson.toRequestBody("application/json".toMediaType())
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

이거 FeedSerivce의 createFeed에서 CreateFeedRequest를 안쓰고 RequestBody를 써서 여기에서 처리한거같은데 그렇게 한 이유가 있을까요? 웬만하면 형식 통일해서 FeedService에서

suspend fun createFeed(
        @Part("request") request: CreateFeedRequest, ...

이런식으로 쓰는게 좋지 않을까 생각합니당

Copy link
Copy Markdown
Collaborator Author

@rbqks529 rbqks529 Aug 17, 2025

Choose a reason for hiding this comment

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

이게 제가 @MuitiPart를 사용하고 있어서 원시타입만 허용하기에 이렇게 해야된다고 합니다. RequestBody 같은거만 사용가능하고 커스텀 dto는 사용하면 자동으로 레트로핏이 직렬화가 안된다고 합니다

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

전반적으로 runcatching안쓰고 try catch를 썼던데 이유가 있을까용 ? 다른 코드들이랑 통일성있게 runcatching으로 수정하는게 어떨까욧 !?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

아 확인했습니다 지피티꺼 수정하는거를 까먹었네요

Comment on lines +176 to +182
item {
AnimatedVisibility(visible = showProgressBar) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = 20.dp, end = 20.dp, top = 32.dp),
) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

progress bar 공통컴포넌트로 분리할까여 ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

두번만 사용해서 분리 안해도 괜찮지 않을까요...?

@Nico1eKim Nico1eKim merged commit ef3ed38 into THIP-TextHip:develop Aug 15, 2025
1 check was pending
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[API] 피드 작성, 수정 구현 [(THIP2025-272)]

3 participants