From 267ba77fe582a8ffc676d2a132d10218d21703bb Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Fri, 15 Aug 2025 16:03:01 +0900 Subject: [PATCH 01/10] =?UTF-8?q?[feat]:=20=ED=94=BC=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=ED=99=94=EB=A9=B4=20=EB=84=A4=EB=B9=84=EA=B2=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EC=97=B0=EA=B2=B0=20(#87)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../texthip/thip/ui/feed/screen/FeedScreen.kt | 9 ++++++--- .../extensions/FeedNavigationExtensions.kt | 5 +++++ .../ui/navigator/navigations/FeedNavigation.kt | 16 ++++++++++++++++ .../thip/ui/navigator/routes/FeedRoutes.kt | 4 +--- app/src/main/res/values/strings.xml | 4 ++-- 5 files changed, 30 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt index 45220924..efbc81e0 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt @@ -49,6 +49,7 @@ import com.texthip.thip.ui.theme.ThipTheme.typography fun FeedScreen( navController: NavController? = null, onNavigateToMySubscription: () -> Unit = {}, + onNavigateToFeedWrite: () -> Unit = {}, nickname: String = "", userRole: String = "", feeds: List = emptyList(), @@ -261,7 +262,7 @@ fun FeedScreen( } FloatingButton( icon = painterResource(id = R.drawable.ic_write), - onClick = { } + onClick = onNavigateToFeedWrite ) } } @@ -302,7 +303,8 @@ private fun FeedScreenPreview() { selectedTabIndex = 1, feeds = mockFeeds, totalFeedCount = mockFeeds.size, - followerProfileImageUrls = mockFollowerImages + followerProfileImageUrls = mockFollowerImages, + onNavigateToFeedWrite = { } ) } } @@ -322,7 +324,8 @@ private fun FeedScreenWithoutDataPreview() { selectedTabIndex = 0, feeds = mockFeeds, totalFeedCount = mockFeeds.size, - followerProfileImageUrls = mockFollowerImages + followerProfileImageUrls = mockFollowerImages, + onNavigateToFeedWrite = { } ) } } diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/extensions/FeedNavigationExtensions.kt b/app/src/main/java/com/texthip/thip/ui/navigator/extensions/FeedNavigationExtensions.kt index ef71bb4e..02387a67 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/extensions/FeedNavigationExtensions.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/extensions/FeedNavigationExtensions.kt @@ -12,4 +12,9 @@ fun NavHostController.navigateToFeed() { // 내 띱 목록으로 fun NavHostController.navigateToMySubscription() { navigate(FeedRoutes.MySubscription) +} + +// 피드 작성으로 +fun NavHostController.navigateToFeedWrite() { + navigate(FeedRoutes.Write) } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt index 740a9711..f04ff8a1 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt @@ -4,7 +4,9 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.compose.composable import com.texthip.thip.ui.feed.screen.FeedScreen +import com.texthip.thip.ui.feed.screen.FeedWriteScreen import com.texthip.thip.ui.feed.screen.MySubscriptionScreen +import com.texthip.thip.ui.navigator.extensions.navigateToFeedWrite import com.texthip.thip.ui.navigator.extensions.navigateToMySubscription import com.texthip.thip.ui.navigator.routes.FeedRoutes import com.texthip.thip.ui.navigator.routes.MainTabRoutes @@ -23,10 +25,24 @@ fun NavGraphBuilder.feedNavigation(navController: NavHostController) { followerProfileImageUrls = emptyList(), onNavigateToMySubscription = { navController.navigateToMySubscription() + }, + onNavigateToFeedWrite = { + navController.navigateToFeedWrite() } ) } composable { MySubscriptionScreen(navController = navController) } + composable { + FeedWriteScreen( + onNavigateBack = { + navController.popBackStack() + }, + onFeedCreated = { feedId -> + // 피드 생성 성공 시 피드 목록으로 돌아가기 + navController.popBackStack() + } + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/routes/FeedRoutes.kt b/app/src/main/java/com/texthip/thip/ui/navigator/routes/FeedRoutes.kt index 4bb9935a..c8c9eeb6 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/routes/FeedRoutes.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/routes/FeedRoutes.kt @@ -4,9 +4,7 @@ import kotlinx.serialization.Serializable @Serializable sealed class FeedRoutes : Routes() { - // 향후 추가될 Feed 관련 화면들 - // @Serializable data object SubscriptionList : FeedRoutes - // @Serializable data object Detail : FeedRoutes @Serializable data object MySubscription : FeedRoutes() + @Serializable data object Write : FeedRoutes() } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b58bc2c7..423cfd56 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -330,9 +330,9 @@ 비공개로 설정 태그 사진 추가 - %1$d / %2$d + %1$d / %2$d개 선택된 태그 - %1$d / %2$d + %1$d / %2$d개 수정하기 삭제하기 이 피드를 삭제하시겠어요? From db8130428fac8aa49fbaabb090929f1eb14726b8 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Fri, 15 Aug 2025 16:05:10 +0900 Subject: [PATCH 02/10] =?UTF-8?q?[feat]:=20=ED=94=BC=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=ED=99=94=EB=A9=B4=EC=97=90=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=83=9C=EA=B7=B8=20response,=20service,=20reposit?= =?UTF-8?q?ory=20=EA=B5=AC=ED=98=84=20(#87)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/texthip/thip/data/di/ServiceModule.kt | 7 +++- .../feed/response/FeedWriteInfoResponse.kt | 14 ++++++++ .../thip/data/repository/FeedRepository.kt | 34 +++++++++++++++++++ .../texthip/thip/data/service/FeedService.kt | 12 +++++++ 4 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/texthip/thip/data/model/feed/response/FeedWriteInfoResponse.kt create mode 100644 app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt create mode 100644 app/src/main/java/com/texthip/thip/data/service/FeedService.kt diff --git a/app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt b/app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt index 946bfe8e..de7694bc 100644 --- a/app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt +++ b/app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt @@ -3,7 +3,7 @@ package com.texthip.thip.data.di import com.texthip.thip.data.service.BookService import com.texthip.thip.data.service.RecentSearchService import com.texthip.thip.data.service.CommentsService -import com.texthip.thip.data.service.GroupService +import com.texthip.thip.data.service.FeedService import com.texthip.thip.data.service.RoomsService import com.texthip.thip.data.service.UserService import dagger.Module @@ -43,4 +43,9 @@ object ServiceModule { @Singleton fun providesCommentsService(retrofit: Retrofit): CommentsService = retrofit.create(CommentsService::class.java) + + @Provides + @Singleton + fun provideFeedService(retrofit: Retrofit): FeedService = + retrofit.create(FeedService::class.java) } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/feed/response/FeedWriteInfoResponse.kt b/app/src/main/java/com/texthip/thip/data/model/feed/response/FeedWriteInfoResponse.kt new file mode 100644 index 00000000..23213657 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/feed/response/FeedWriteInfoResponse.kt @@ -0,0 +1,14 @@ +package com.texthip.thip.data.model.feed.response + +import kotlinx.serialization.Serializable + +@Serializable +data class FeedWriteInfoResponse( + val categoryList: List +) + +@Serializable +data class FeedCategory( + val category: String, + val tagList: List +) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt new file mode 100644 index 00000000..23488b90 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt @@ -0,0 +1,34 @@ +package com.texthip.thip.data.repository + +import com.texthip.thip.data.model.base.handleBaseResponse +import com.texthip.thip.data.model.feed.response.FeedWriteInfoResponse +import com.texthip.thip.data.service.FeedService +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class FeedRepository @Inject constructor( + private val feedService: FeedService +) { + + /** 피드 작성에 필요한 카테고리 및 태그 목록 조회 */ + suspend fun getFeedWriteInfo(): Result = runCatching { + val response = feedService.getFeedWriteInfo() + .handleBaseResponse() + .getOrThrow() + + // 카테고리 순서 조정 + val orderedCategories = response?.categoryList?.sortedBy { category -> + when (category.category) { + "문학" -> 0 + "과학·IT" -> 1 + "사회과학" -> 2 + "인문학" -> 3 + "예술" -> 4 + else -> 999 + } + } ?: emptyList() + + response?.copy(categoryList = orderedCategories) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/service/FeedService.kt b/app/src/main/java/com/texthip/thip/data/service/FeedService.kt new file mode 100644 index 00000000..3494101b --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/service/FeedService.kt @@ -0,0 +1,12 @@ +package com.texthip.thip.data.service + +import com.texthip.thip.data.model.base.BaseResponse +import com.texthip.thip.data.model.feed.response.FeedWriteInfoResponse +import retrofit2.http.GET + +interface FeedService { + + /** 피드 작성에 필요한 카테고리 및 태그 목록 조회 */ + @GET("feeds/write-info") + suspend fun getFeedWriteInfo(): BaseResponse +} \ No newline at end of file From 32f84f30b81f362626718e0dbb3022f95499316c Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Fri, 15 Aug 2025 16:05:42 +0900 Subject: [PATCH 03/10] =?UTF-8?q?[feat]:=20=ED=94=BC=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20ViewModel,=20UiState=20=EA=B5=AC=ED=98=84=20(#87)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/feed/viewmodel/FeedWriteUiState.kt | 65 ++++ .../ui/feed/viewmodel/FeedWriteViewModel.kt | 303 ++++++++++++++++++ 2 files changed, 368 insertions(+) create mode 100644 app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteUiState.kt create mode 100644 app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt diff --git a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteUiState.kt b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteUiState.kt new file mode 100644 index 00000000..e17d8e4e --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteUiState.kt @@ -0,0 +1,65 @@ +package com.texthip.thip.ui.feed.viewmodel + +import android.net.Uri +import com.texthip.thip.data.model.feed.response.FeedCategory +import com.texthip.thip.ui.group.makeroom.mock.BookData + +data class FeedWriteUiState( + val selectedBook: BookData? = null, + val showBookSearchSheet: Boolean = false, + val feedContent: String = "", + val imageUris: List = emptyList(), + val isPrivate: Boolean = false, + val selectedCategoryIndex: Int = -1, + val selectedTags: List = emptyList(), + val isLoading: Boolean = false, + val errorMessage: String? = null, + val savedBooks: List = emptyList(), + val groupBooks: List = emptyList(), + val isLoadingBooks: Boolean = false, + val searchResults: List = emptyList(), + val isSearching: Boolean = false, + val categories: List = emptyList(), + val isBookPreselected: Boolean = false, + val isLoadingCategories: Boolean = false +) { + // 유효성 검사 로직 + val isContentValid: Boolean + get() = feedContent.isNotBlank() && feedContent.length <= 2000 + + val isCategoryValid: Boolean + get() = selectedCategoryIndex >= 0 && selectedTags.isNotEmpty() + + val isImageCountValid: Boolean + get() = imageUris.size <= 3 + + val isFormValid: Boolean + get() = selectedBook != null && + isContentValid && + isCategoryValid && + isImageCountValid + + // 태그 개수 제한 (최대 5개) + val canAddMoreTags: Boolean + get() = selectedTags.size < 5 + + // 이미지 개수 제한 (최대 3개) + val canAddMoreImages: Boolean + get() = imageUris.size < 3 + + // 현재 선택된 카테고리의 태그 목록 + val availableTags: List + get() = if (selectedCategoryIndex >= 0 && selectedCategoryIndex < categories.size) { + categories[selectedCategoryIndex].tagList + } else { + emptyList() + } + + // 현재 선택된 카테고리 이름 + val selectedCategoryName: String? + get() = if (selectedCategoryIndex >= 0 && selectedCategoryIndex < categories.size) { + categories[selectedCategoryIndex].category + } else { + null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt new file mode 100644 index 00000000..d631e86f --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt @@ -0,0 +1,303 @@ +package com.texthip.thip.ui.feed.viewmodel + +import android.net.Uri +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.texthip.thip.R +import com.texthip.thip.data.model.book.response.BookSavedResponse +import com.texthip.thip.data.model.book.response.BookSearchItem +import com.texthip.thip.data.provider.StringResourceProvider +import com.texthip.thip.data.repository.BookRepository +import com.texthip.thip.data.repository.FeedRepository +import com.texthip.thip.ui.group.makeroom.mock.BookData +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class FeedWriteViewModel @Inject constructor( + private val feedRepository: FeedRepository, + private val bookRepository: BookRepository, + private val stringResourceProvider: StringResourceProvider +) : ViewModel() { + + private val _uiState = MutableStateFlow(FeedWriteUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private var searchJob: Job? = null + + private fun updateState(update: (FeedWriteUiState) -> FeedWriteUiState) { + _uiState.value = update(_uiState.value) + } + + init { + loadFeedWriteInfo() + } + + private fun loadFeedWriteInfo() { + viewModelScope.launch { + updateState { it.copy(isLoadingCategories = true) } + feedRepository.getFeedWriteInfo() + .onSuccess { response -> + updateState { + it.copy( + categories = response?.categoryList ?: emptyList(), + isLoadingCategories = false + ) + } + } + .onFailure { + updateState { + it.copy( + categories = emptyList(), + isLoadingCategories = false, + errorMessage = stringResourceProvider.getString(R.string.error_network_error) + ) + } + } + } + } + + fun selectBook(book: BookData) { + updateState { it.copy(selectedBook = book) } + } + + fun setPreselectedBook(isbn: String, title: String, imageUrl: String, author: String) { + val preselectedBook = BookData( + title = title, + imageUrl = imageUrl, + author = author, + isbn = isbn + ) + updateState { + it.copy( + selectedBook = preselectedBook, + isBookPreselected = true + ) + } + } + + fun toggleBookSearchSheet(show: Boolean) { + updateState { it.copy(showBookSearchSheet = show) } + if (show) { + loadBooks() + } + } + + private fun loadBooks() { + 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) } + } + } + } + + private fun BookSavedResponse.toBookData(): BookData { + return BookData( + title = this.bookTitle, + imageUrl = this.bookImageUrl, + author = this.authorName, + isbn = this.isbn + ) + } + + private fun BookSearchItem.toBookData(): BookData { + return BookData( + title = this.title, + imageUrl = this.imageUrl, + author = this.authorName, + isbn = this.isbn + ) + } + + fun searchBooks(query: String) { + searchJob?.cancel() + + if (query.isBlank()) { + updateState { it.copy(searchResults = emptyList(), isSearching = false) } + return + } + + searchJob = viewModelScope.launch { + delay(300) // 디바운싱 + updateState { it.copy(isSearching = true) } + + try { + val result = bookRepository.searchBooks(query, page = 1, isFinalized = false) + result.onSuccess { response -> + val searchResults = + response?.searchResult?.map { + it.toBookData() + } ?: emptyList() + updateState { + it.copy( + searchResults = searchResults, + isSearching = false + ) + } + }.onFailure { + updateState { + it.copy( + searchResults = emptyList(), + isSearching = false + ) + } + } + } catch (e: Exception) { + updateState { + it.copy( + searchResults = emptyList(), + isSearching = false + ) + } + } + } + } + + fun updateFeedContent(content: String) { + if (content.length <= 2000) { + updateState { it.copy(feedContent = content) } + } + } + + fun addImages(newImageUris: List) { + val currentState = _uiState.value + val availableSlots = 3 - currentState.imageUris.size + val imagesToAdd = newImageUris.take(availableSlots) + + updateState { + it.copy(imageUris = currentState.imageUris + imagesToAdd) + } + } + + fun removeImage(index: Int) { + val currentImages = _uiState.value.imageUris.toMutableList() + if (index in currentImages.indices) { + currentImages.removeAt(index) + updateState { it.copy(imageUris = currentImages) } + } + } + + fun togglePrivate(isPrivate: Boolean) { + updateState { it.copy(isPrivate = isPrivate) } + } + + fun selectCategory(index: Int) { + updateState { + it.copy( + selectedCategoryIndex = index, + selectedTags = emptyList() // 카테고리 변경 시 태그 초기화 + ) + } + } + + fun toggleTag(tag: String) { + val currentState = _uiState.value + val newSelectedTags = if (currentState.selectedTags.contains(tag)) { + currentState.selectedTags - tag + } else { + if (currentState.canAddMoreTags) { + currentState.selectedTags + tag + } else { + currentState.selectedTags + } + } + updateState { it.copy(selectedTags = newSelectedTags) } + } + + fun removeTag(tag: String) { + val currentTags = _uiState.value.selectedTags + updateState { + it.copy(selectedTags = currentTags - tag) + } + } + + fun createFeed(onSuccess: (Int) -> Unit, onError: (String) -> Unit) { + val currentState = _uiState.value + + if (!currentState.isFormValid) { + onError(stringResourceProvider.getString(R.string.error_form_validation)) + return + } + + val selectedBook = currentState.selectedBook + if (selectedBook?.isbn == null) { + onError(stringResourceProvider.getString(R.string.error_book_info_invalid)) + return + } + + viewModelScope.launch { + try { + updateState { it.copy(isLoading = true, errorMessage = null) } + + // TODO: 실제 피드 생성 API 구현 시 여기에 추가 + // val request = CreateFeedRequest( + // isbn = selectedBook.isbn, + // content = currentState.feedContent.trim(), + // imageUrls = currentState.imageUris.map { it.toString() }, + // isPrivate = currentState.isPrivate, + // category = currentState.selectedCategoryName ?: "", + // tags = currentState.selectedTags + // ) + // + // val result = feedRepository.createFeed(request) + // result.onSuccess { feedId -> + // onSuccess(feedId) + // }.onFailure { exception -> + // onError( + // stringResourceProvider.getString( + // R.string.error_feed_creation_failed, + // exception.message ?: "" + // ) + // ) + // } + + // 임시로 성공 처리 (API 구현 전) + delay(1000) // 로딩 시뮬레이션 + onSuccess(1) // 임시 피드 ID + + } catch (e: Exception) { + onError( + stringResourceProvider.getString( + R.string.error_network_error, + e.message ?: "" + ) + ) + } finally { + updateState { it.copy(isLoading = false) } + } + } + } + + fun clearError() { + updateState { it.copy(errorMessage = null) } + } +} \ No newline at end of file From e9b34efd67dc22ae5370ae2c10dd17ca5953bcd8 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Fri, 15 Aug 2025 16:07:53 +0900 Subject: [PATCH 04/10] =?UTF-8?q?[feat]:=20=EC=84=9C=EB=B8=8C=20=EC=9E=A5?= =?UTF-8?q?=EB=A5=B4=20=EA=B0=80=EC=9A=B4=EB=8D=B0=20=EC=A0=95=EB=A0=AC=20?= =?UTF-8?q?=EB=B0=8F=20=ED=94=BC=EB=93=9C=20=EC=9E=91=EC=84=B1=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=EA=B3=BC=20=EC=9E=A5=EB=A5=B4=20API=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=20(#87)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/ui/common/buttons/SubGenreChipRow.kt | 3 +- .../thip/ui/feed/screen/FeedWriteScreen.kt | 233 +++++++++++------- 2 files changed, 147 insertions(+), 89 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/common/buttons/SubGenreChipRow.kt b/app/src/main/java/com/texthip/thip/ui/common/buttons/SubGenreChipRow.kt index e5da0e3b..383e5f61 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/buttons/SubGenreChipRow.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/buttons/SubGenreChipRow.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -19,7 +20,7 @@ fun SubGenreChipGrid( ) { FlowRow( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), verticalArrangement = Arrangement.spacedBy(8.dp) ) { subGenres.forEach { genre -> diff --git a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedWriteScreen.kt b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedWriteScreen.kt index b0bac9dc..025e3efe 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedWriteScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedWriteScreen.kt @@ -26,10 +26,9 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.blur @@ -40,20 +39,22 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import coil.compose.AsyncImage import com.texthip.thip.R +import com.texthip.thip.data.model.feed.response.FeedCategory import com.texthip.thip.ui.common.buttons.GenreChipButton import com.texthip.thip.ui.common.buttons.GenreChipRow import com.texthip.thip.ui.common.buttons.SubGenreChipGrid import com.texthip.thip.ui.common.buttons.ToggleSwitchButton import com.texthip.thip.ui.common.topappbar.InputTopAppBar -import com.texthip.thip.ui.feed.mock.FeedData +import com.texthip.thip.ui.feed.viewmodel.FeedWriteUiState +import com.texthip.thip.ui.feed.viewmodel.FeedWriteViewModel import com.texthip.thip.ui.group.makeroom.component.GroupBookSearchBottomSheet import com.texthip.thip.ui.group.makeroom.component.GroupInputField import com.texthip.thip.ui.group.makeroom.component.GroupSelectBook import com.texthip.thip.ui.group.makeroom.component.SectionDivider -import com.texthip.thip.ui.group.makeroom.mock.dummyGroupBooks -import com.texthip.thip.ui.group.makeroom.mock.dummySavedBooks +import com.texthip.thip.ui.group.makeroom.mock.BookData import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography @@ -61,45 +62,84 @@ import com.texthip.thip.ui.theme.ThipTheme.typography @Composable fun FeedWriteScreen( onNavigateBack: () -> Unit, - modifier: Modifier = Modifier + onFeedCreated: (Int) -> Unit = {}, + modifier: Modifier = Modifier, + viewModel: FeedWriteViewModel = hiltViewModel() ) { - var feedData by remember { mutableStateOf(FeedData()) } - val scrollState = rememberScrollState() - val genres = listOf("문학", "과학·IT", "사회과학", "인문학", "예술") - val subGenreMap = mapOf( - 0 to listOf("소설", "에세이", "시", "고전", "추리", "판타지", "로맨스", "SF", "공포", "역사"), - 1 to listOf("AI", "프로그래밍", "로봇", "IT 일반", "수학", "물리", "화학"), - 2 to listOf("정치", "경제", "법", "사회", "교육"), - 3 to listOf("철학", "역사", "심리", "종교", "윤리"), - 4 to listOf("음악", "미술", "공예", "무용", "연극") + val uiState by viewModel.uiState.collectAsState() + + // 에러 메시지 표시 + LaunchedEffect(uiState.errorMessage) { + uiState.errorMessage?.let { message -> + viewModel.clearError() + } + } + + FeedWriteContent( + uiState = uiState, + onNavigateBack = onNavigateBack, + onCreateFeed = { + viewModel.createFeed( + onSuccess = { feedId -> + onFeedCreated(feedId) + }, + onError = { } + ) + }, + onSelectBook = viewModel::selectBook, + onToggleBookSearchSheet = viewModel::toggleBookSearchSheet, + onUpdateFeedContent = viewModel::updateFeedContent, + onAddImages = viewModel::addImages, + onRemoveImage = viewModel::removeImage, + onTogglePrivate = viewModel::togglePrivate, + onSelectCategory = viewModel::selectCategory, + onToggleTag = viewModel::toggleTag, + onRemoveTag = viewModel::removeTag, + onSearchBooks = viewModel::searchBooks, + modifier = modifier ) - val showBookSearchSheet = remember { mutableStateOf(false) } +} + +@Composable +fun FeedWriteContent( + modifier: Modifier = Modifier, + uiState: FeedWriteUiState, + onNavigateBack: () -> Unit = {}, + onCreateFeed: () -> Unit = {}, + onSelectBook: (BookData) -> Unit = {}, + onToggleBookSearchSheet: (Boolean) -> Unit = {}, + onUpdateFeedContent: (String) -> Unit = {}, + onAddImages: (List) -> Unit = {}, + onRemoveImage: (Int) -> Unit = {}, + onTogglePrivate: (Boolean) -> Unit = {}, + onSelectCategory: (Int) -> Unit = {}, + onToggleTag: (String) -> Unit = {}, + onRemoveTag: (String) -> Unit = {}, + onSearchBooks: (String) -> Unit = {} +) { + val scrollState = rememberScrollState() val imagePickerLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.GetMultipleContents() ) { uris: List -> if (uris.isNotEmpty()) { - val availableSlots = 3 - feedData.imageUris.size - val imagesToAdd = uris.take(availableSlots) // 3장까지만 유지 - feedData.imageUris.addAll(imagesToAdd) + onAddImages(uris) } } - val isImageLimitReached = feedData.imageUris.size >= 3 val focusManager = LocalFocusManager.current Box { Column( modifier = modifier .fillMaxSize() - .then(if (showBookSearchSheet.value) Modifier.blur(5.dp) else Modifier), + .then(if (uiState.showBookSearchSheet) Modifier.blur(5.dp) else Modifier), horizontalAlignment = Alignment.CenterHorizontally ) { - val isRightButtonEnabled = feedData.selectedBook != null && feedData.feedContent.isNotBlank() && feedData.selectedGenreIndex != -1 && feedData.selectedSubGenres.isNotEmpty() InputTopAppBar( title = stringResource(R.string.new_feed), rightButtonName = stringResource(R.string.registration), - isRightButtonEnabled = isRightButtonEnabled, + isRightButtonEnabled = uiState.isFormValid && !uiState.isLoading, onLeftClick = onNavigateBack, - onRightClick = {} + onRightClick = onCreateFeed ) Column( modifier = Modifier @@ -116,9 +156,10 @@ fun FeedWriteScreen( Spacer(modifier = Modifier.height(20.dp)) GroupSelectBook( - selectedBook = feedData.selectedBook, - onChangeBookClick = { showBookSearchSheet.value = true }, - onSelectBookClick = { showBookSearchSheet.value = true } + selectedBook = uiState.selectedBook, + onChangeBookClick = { onToggleBookSearchSheet(true) }, + onSelectBookClick = { onToggleBookSearchSheet(true) }, + isBookPreselected = uiState.isBookPreselected ) SectionDivider() @@ -126,11 +167,9 @@ fun FeedWriteScreen( GroupInputField( title = stringResource(R.string.write_feed), hint = stringResource(R.string.write_feed_hint), - value = feedData.feedContent, + value = uiState.feedContent, maxLength = 2000, - onValueChange = { newText -> - feedData = feedData.copy(feedContent = newText) - } + onValueChange = onUpdateFeedContent ) SectionDivider() @@ -151,10 +190,10 @@ fun FeedWriteScreen( modifier = Modifier .size(80.dp) .background(color = colors.DarkGrey02) - .border(width = 1.dp, color = if (isImageLimitReached) colors.DarkGrey else colors.Grey02, + .border(width = 1.dp, color = if (!uiState.canAddMoreImages) colors.DarkGrey else colors.Grey02, ) .let { - if (!isImageLimitReached) it.clickable { + if (uiState.canAddMoreImages) it.clickable { imagePickerLauncher.launch("image/*") } else it // 클릭 비활성화 }, @@ -164,20 +203,20 @@ fun FeedWriteScreen( Icon( painter = painterResource(id = R.drawable.ic_plus), contentDescription = null, - tint = if (isImageLimitReached) colors.DarkGrey else colors.White + tint = if (!uiState.canAddMoreImages) colors.DarkGrey else colors.White ) } } - items(feedData.imageUris.size) { index -> + items(uiState.imageUris.size) { index -> Box(modifier = Modifier.size(80.dp)) { AsyncImage( - model = feedData.imageUris[index], + model = uiState.imageUris[index], contentDescription = null, modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop ) IconButton( - onClick = { feedData.imageUris.removeAt(index) }, + onClick = { onRemoveImage(index) }, modifier = Modifier .align(Alignment.TopEnd) .size(24.dp) @@ -196,7 +235,7 @@ fun FeedWriteScreen( horizontalArrangement = Arrangement.End ) { Text( - text = stringResource(id = R.string.photo_count, feedData.imageUris.size, 3), + text = stringResource(id = R.string.photo_count, uiState.imageUris.size, 3), style = typography.info_r400_s12, color = colors.NeonGreen, ) @@ -219,10 +258,8 @@ fun FeedWriteScreen( color = colors.White ) ToggleSwitchButton( - isChecked = feedData.isPrivate, - onToggleChange = { isChecked -> - feedData = feedData.copy(isPrivate = isChecked) - } + isChecked = uiState.isPrivate, + onToggleChange = onTogglePrivate ) } SectionDivider() @@ -235,43 +272,29 @@ fun FeedWriteScreen( Spacer(modifier = Modifier.padding(top = 12.dp)) GenreChipRow( modifier = Modifier.width(18.dp), - genres = genres, - selectedIndex = feedData.selectedGenreIndex, - onSelect = { - feedData = feedData.copy(selectedGenreIndex = it, selectedSubGenres = emptyList()) - } + genres = uiState.categories.map { it.category }, + selectedIndex = uiState.selectedCategoryIndex, + onSelect = onSelectCategory ) Spacer(modifier = Modifier.height(12.dp)) - if (feedData.selectedGenreIndex != -1) { - val subGenres = subGenreMap[feedData.selectedGenreIndex].orEmpty() + if (uiState.selectedCategoryIndex != -1) { Spacer(modifier = Modifier.height(8.dp)) SubGenreChipGrid( - subGenres = subGenres, - selectedGenres = feedData.selectedSubGenres, - onGenreToggle = { genre -> - val newSelected = if (feedData.selectedSubGenres.contains(genre)) { - feedData.selectedSubGenres - genre - } else { - if (feedData.selectedSubGenres.size < 5) { - feedData.selectedSubGenres + genre - } else { - feedData.selectedSubGenres - } - } - - feedData = feedData.copy(selectedSubGenres = newSelected) - } + subGenres = uiState.availableTags, + selectedGenres = uiState.selectedTags, + onGenreToggle = onToggleTag ) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End ) { Text( - text = stringResource(id = R.string.tag_count, feedData.selectedSubGenres.size, 5), + modifier = Modifier.padding(top = 12.dp), + text = stringResource(id = R.string.tag_count, uiState.selectedTags.size, 5), style = typography.info_r400_s12, - color = colors.NeonGreen, - ) + color = colors.NeonGreen + ) } } Spacer(modifier = Modifier.height(12.dp)) @@ -281,25 +304,21 @@ fun FeedWriteScreen( color = colors.White ) Spacer(modifier = Modifier.height(12.dp)) - if (feedData.selectedSubGenres.isNotEmpty()) { + if (uiState.selectedTags.isNotEmpty()) { LazyRow( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - items(feedData.selectedSubGenres) { subGenre -> + items(uiState.selectedTags) { tag -> GenreChipButton( - text = subGenre, + text = tag, onClick = { - //해당 칩 눌렀을 때도 서브장르 삭제 - feedData = feedData.copy( - selectedSubGenres =feedData.selectedSubGenres - subGenre - ) + //해당 칩 눌렀을 때도 태그 삭제 + onRemoveTag(tag) }, onCloseClick = { - //x버튼 누르면 서브장르 삭제 - feedData = feedData.copy( - selectedSubGenres = feedData.selectedSubGenres - subGenre - ) + //x버튼 누르면 태그 삭제 + onRemoveTag(tag) } ) } @@ -309,18 +328,22 @@ fun FeedWriteScreen( } } - if (showBookSearchSheet.value) { + if (uiState.showBookSearchSheet) { GroupBookSearchBottomSheet( - onDismiss = { showBookSearchSheet.value = false }, + onDismiss = { onToggleBookSearchSheet(false) }, onBookSelect = { book -> - feedData = feedData.copy(selectedBook= book) - showBookSearchSheet.value = false + onSelectBook(book) + onToggleBookSearchSheet(false) }, onRequestBook = { - showBookSearchSheet.value = false + onToggleBookSearchSheet(false) }, - savedBooks = dummySavedBooks, - groupBooks = dummyGroupBooks + savedBooks = uiState.savedBooks, + groupBooks = uiState.groupBooks, + searchResults = uiState.searchResults, + isLoading = uiState.isLoadingBooks, + isSearching = uiState.isSearching, + onSearch = onSearchBooks ) } } @@ -331,8 +354,42 @@ fun FeedWriteScreen( @Composable private fun FeedWriteScreenPreview() { ThipTheme { - FeedWriteScreen( - onNavigateBack = { } + FeedWriteContent( + uiState = FeedWriteUiState( + selectedBook = BookData( + title = "미드나이트 라이브러리", + imageUrl = "https://picsum.photos/300/400?1", + author = "매트 헤이그", + isbn = "9788937477263" + ), + feedContent = "이 책을 읽고 정말 많은 생각이 들었습니다...", + selectedCategoryIndex = 0, + selectedTags = listOf("한국소설", "에세이"), + categories = listOf( + FeedCategory( + category = "문학", + tagList = listOf("한국소설", "외국소설", "에세이", "시", "고전") + ), + FeedCategory( + category = "과학·IT", + tagList = listOf("프로그래밍", "AI", "과학일반") + ), + FeedCategory( + category = "사회과학", + tagList = listOf("프로그래밍", "AI", "과학일반") + ), + FeedCategory( + category = "인문학", + tagList = listOf("프로그래밍", "AI", "과학일반") + ), + FeedCategory( + category = "예술", + tagList = listOf("프로그래밍", "AI", "과학일반") + ) + ), + imageUris = emptyList(), + isPrivate = false + ) ) } } From 6fdf0ae4a530f5ec35f4aa2713c33929bb5fc1a0 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Fri, 15 Aug 2025 17:44:30 +0900 Subject: [PATCH 05/10] =?UTF-8?q?[feat]:=20=ED=94=BC=EB=93=9C=20=EB=A7=8C?= =?UTF-8?q?=EB=93=A4=EA=B8=B0=20DTO,=20Service,=20Repository=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#87)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/feed/request/CreateFeedRequest.kt | 11 +++ .../model/feed/response/CreateFeedResponse.kt | 8 ++ .../thip/data/repository/FeedRepository.kt | 83 ++++++++++++++++++- .../texthip/thip/data/service/FeedService.kt | 14 ++++ 4 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/texthip/thip/data/model/feed/request/CreateFeedRequest.kt create mode 100644 app/src/main/java/com/texthip/thip/data/model/feed/response/CreateFeedResponse.kt diff --git a/app/src/main/java/com/texthip/thip/data/model/feed/request/CreateFeedRequest.kt b/app/src/main/java/com/texthip/thip/data/model/feed/request/CreateFeedRequest.kt new file mode 100644 index 00000000..24d596fe --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/feed/request/CreateFeedRequest.kt @@ -0,0 +1,11 @@ +package com.texthip.thip.data.model.feed.request + +import kotlinx.serialization.Serializable + +@Serializable +data class CreateFeedRequest( + val isbn: String, + val contentBody: String, + val isPublic: Boolean, + val tagList: List = emptyList() +) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/feed/response/CreateFeedResponse.kt b/app/src/main/java/com/texthip/thip/data/model/feed/response/CreateFeedResponse.kt new file mode 100644 index 00000000..1210489d --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/feed/response/CreateFeedResponse.kt @@ -0,0 +1,8 @@ +package com.texthip.thip.data.model.feed.response + +import kotlinx.serialization.Serializable + +@Serializable +data class CreateFeedResponse( + val feedId: Int +) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt index 23488b90..de165fdc 100644 --- a/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt @@ -1,14 +1,27 @@ package com.texthip.thip.data.repository +import android.content.Context +import android.net.Uri import com.texthip.thip.data.model.base.handleBaseResponse +import com.texthip.thip.data.model.feed.request.CreateFeedRequest +import com.texthip.thip.data.model.feed.response.CreateFeedResponse import com.texthip.thip.data.model.feed.response.FeedWriteInfoResponse import com.texthip.thip.data.service.FeedService +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.asRequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import java.io.File +import java.io.FileOutputStream import javax.inject.Inject import javax.inject.Singleton @Singleton class FeedRepository @Inject constructor( - private val feedService: FeedService + private val feedService: FeedService, + @param:ApplicationContext private val context: Context ) { /** 피드 작성에 필요한 카테고리 및 태그 목록 조회 */ @@ -31,4 +44,72 @@ class FeedRepository @Inject constructor( response?.copy(categoryList = orderedCategories) } + + /** 피드 생성 */ + suspend fun createFeed( + isbn: String, + contentBody: String, + isPublic: Boolean, + tagList: List, + imageUris: List + ): Result = runCatching { + val request = CreateFeedRequest( + isbn = isbn, + contentBody = contentBody, + isPublic = isPublic, + tagList = tagList + ) + + // JSON 요청 부분을 RequestBody로 변환 + val requestJson = Json.encodeToString(request) + val requestBody = requestJson.toRequestBody("application/json".toMediaType()) + + // 이미지 파일들을 MultipartBody.Part로 변환 + val imageParts = if (imageUris.isNotEmpty()) { + imageUris.mapNotNull { uri -> + try { + uriToMultipartBodyPart(uri, "images") + } catch (e: Exception) { + null + } + } + } else { + null + } + + feedService.createFeed(requestBody, imageParts) + .handleBaseResponse() + .getOrThrow() + } + + private fun uriToMultipartBodyPart(uri: Uri, paramName: String): MultipartBody.Part? { + return try { + val inputStream = context.contentResolver.openInputStream(uri) ?: return null + + // MIME 타입 확인 + 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 fileName = "feed_image_${System.currentTimeMillis()}.$extension" + val tempFile = File(context.cacheDir, fileName) + + // 임시 파일로 복사 + FileOutputStream(tempFile).use { output -> + inputStream.copyTo(output) + } + + // MultipartBody.Part 생성 + val requestFile = tempFile.asRequestBody(mimeType.toMediaType()) + MultipartBody.Part.createFormData(paramName, fileName, requestFile) + } catch (e: Exception) { + e.printStackTrace() + null + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/service/FeedService.kt b/app/src/main/java/com/texthip/thip/data/service/FeedService.kt index 3494101b..fc216f1e 100644 --- a/app/src/main/java/com/texthip/thip/data/service/FeedService.kt +++ b/app/src/main/java/com/texthip/thip/data/service/FeedService.kt @@ -1,12 +1,26 @@ package com.texthip.thip.data.service import com.texthip.thip.data.model.base.BaseResponse +import com.texthip.thip.data.model.feed.response.CreateFeedResponse import com.texthip.thip.data.model.feed.response.FeedWriteInfoResponse +import okhttp3.MultipartBody +import okhttp3.RequestBody import retrofit2.http.GET +import retrofit2.http.Multipart +import retrofit2.http.POST +import retrofit2.http.Part interface FeedService { /** 피드 작성에 필요한 카테고리 및 태그 목록 조회 */ @GET("feeds/write-info") suspend fun getFeedWriteInfo(): BaseResponse + + /** 피드 생성 */ + @Multipart + @POST("feeds") + suspend fun createFeed( + @Part("request") request: RequestBody, + @Part images: List? + ): BaseResponse } \ No newline at end of file From 4989bb589f9140348361820efcffea654c317664 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Fri, 15 Aug 2025 17:45:30 +0900 Subject: [PATCH 06/10] =?UTF-8?q?[feat]:=20=ED=94=BC=EB=93=9C=20=EB=A7=8C?= =?UTF-8?q?=EB=93=A4=EA=B8=B0=20ViewModel=EC=97=90=20=EC=97=B0=EA=B2=B0=20?= =?UTF-8?q?(#87)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/feed/viewmodel/FeedWriteViewModel.kt | 56 ++++++------------- 1 file changed, 16 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt index d631e86f..cfe747dc 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt @@ -67,21 +67,6 @@ class FeedWriteViewModel @Inject constructor( updateState { it.copy(selectedBook = book) } } - fun setPreselectedBook(isbn: String, title: String, imageUrl: String, author: String) { - val preselectedBook = BookData( - title = title, - imageUrl = imageUrl, - author = author, - isbn = isbn - ) - updateState { - it.copy( - selectedBook = preselectedBook, - isBookPreselected = true - ) - } - } - fun toggleBookSearchSheet(show: Boolean) { updateState { it.copy(showBookSearchSheet = show) } if (show) { @@ -258,31 +243,22 @@ class FeedWriteViewModel @Inject constructor( try { updateState { it.copy(isLoading = true, errorMessage = null) } - // TODO: 실제 피드 생성 API 구현 시 여기에 추가 - // val request = CreateFeedRequest( - // isbn = selectedBook.isbn, - // content = currentState.feedContent.trim(), - // imageUrls = currentState.imageUris.map { it.toString() }, - // isPrivate = currentState.isPrivate, - // category = currentState.selectedCategoryName ?: "", - // tags = currentState.selectedTags - // ) - // - // val result = feedRepository.createFeed(request) - // result.onSuccess { feedId -> - // onSuccess(feedId) - // }.onFailure { exception -> - // onError( - // stringResourceProvider.getString( - // R.string.error_feed_creation_failed, - // exception.message ?: "" - // ) - // ) - // } - - // 임시로 성공 처리 (API 구현 전) - delay(1000) // 로딩 시뮬레이션 - onSuccess(1) // 임시 피드 ID + val result = feedRepository.createFeed( + isbn = selectedBook.isbn, + contentBody = currentState.feedContent.trim(), + isPublic = !currentState.isPrivate, + tagList = currentState.selectedTags, + imageUris = currentState.imageUris + ) + + result.onSuccess { response -> + val feedId = response?.feedId ?: 1 + onSuccess(feedId) + }.onFailure { exception -> + onError( + exception.message ?: stringResourceProvider.getString(R.string.error_network_error) + ) + } } catch (e: Exception) { onError( From 60c5a558d85b4abe4a8caf921a74b0012d5c1257 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Fri, 15 Aug 2025 17:48:06 +0900 Subject: [PATCH 07/10] =?UTF-8?q?[feat]:=20=ED=94=BC=EB=93=9C=20=EB=A7=8C?= =?UTF-8?q?=EB=93=A4=EA=B8=B0=20=EA=B5=AC=ED=98=84=20(#87)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../texthip/thip/ui/feed/screen/FeedWriteScreen.kt | 13 +++---------- .../thip/ui/feed/viewmodel/FeedWriteUiState.kt | 7 ++----- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedWriteScreen.kt b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedWriteScreen.kt index 025e3efe..9620029c 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedWriteScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedWriteScreen.kt @@ -26,7 +26,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -61,19 +60,12 @@ import com.texthip.thip.ui.theme.ThipTheme.typography @Composable fun FeedWriteScreen( + modifier: Modifier = Modifier, onNavigateBack: () -> Unit, onFeedCreated: (Int) -> Unit = {}, - modifier: Modifier = Modifier, viewModel: FeedWriteViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsState() - - // 에러 메시지 표시 - LaunchedEffect(uiState.errorMessage) { - uiState.errorMessage?.let { message -> - viewModel.clearError() - } - } FeedWriteContent( uiState = uiState, @@ -83,7 +75,8 @@ fun FeedWriteScreen( onSuccess = { feedId -> onFeedCreated(feedId) }, - onError = { } + onError = { errorMessage -> + } ) }, onSelectBook = viewModel::selectBook, diff --git a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteUiState.kt b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteUiState.kt index e17d8e4e..a598ba83 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteUiState.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteUiState.kt @@ -27,17 +27,14 @@ data class FeedWriteUiState( val isContentValid: Boolean get() = feedContent.isNotBlank() && feedContent.length <= 2000 - val isCategoryValid: Boolean - get() = selectedCategoryIndex >= 0 && selectedTags.isNotEmpty() - val isImageCountValid: Boolean get() = imageUris.size <= 3 val isFormValid: Boolean get() = selectedBook != null && isContentValid && - isCategoryValid && - isImageCountValid + isImageCountValid && + selectedTags.size <= 5 // 태그는 최대 5개까지만 // 태그 개수 제한 (최대 5개) val canAddMoreTags: Boolean From 395eb3d86688d442cabe2849727cf64c0e8e1985 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Fri, 15 Aug 2025 17:59:44 +0900 Subject: [PATCH 08/10] =?UTF-8?q?[feat]:=20copilot=20pr=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81=20(#87)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/data/repository/FeedRepository.kt | 62 +++++++++++++------ 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt index de165fdc..5a99e4f3 100644 --- a/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt @@ -61,28 +61,36 @@ class FeedRepository @Inject constructor( ) // JSON 요청 부분을 RequestBody로 변환 - val requestJson = Json.encodeToString(request) + val requestJson = Json.encodeToString(CreateFeedRequest.serializer(), request) val requestBody = requestJson.toRequestBody("application/json".toMediaType()) - // 이미지 파일들을 MultipartBody.Part로 변환 - val imageParts = if (imageUris.isNotEmpty()) { - imageUris.mapNotNull { uri -> - try { - uriToMultipartBodyPart(uri, "images") - } catch (e: Exception) { - null + // 임시 파일 목록 추적 + val tempFiles = mutableListOf() + + try { + // 이미지 파일들을 MultipartBody.Part로 변환 + val imageParts = if (imageUris.isNotEmpty()) { + imageUris.mapNotNull { uri -> + try { + uriToMultipartBodyPart(uri, "images", tempFiles) + } catch (e: Exception) { + null + } } + } else { + null } - } else { - null - } - feedService.createFeed(requestBody, imageParts) - .handleBaseResponse() - .getOrThrow() + feedService.createFeed(requestBody, imageParts) + .handleBaseResponse() + .getOrThrow() + } finally { + // 임시 파일들 정리 + cleanupTempFiles(tempFiles) + } } - private fun uriToMultipartBodyPart(uri: Uri, paramName: String): MultipartBody.Part? { + private fun uriToMultipartBodyPart(uri: Uri, paramName: String, tempFiles: MutableList): MultipartBody.Part? { return try { val inputStream = context.contentResolver.openInputStream(uri) ?: return null @@ -99,9 +107,14 @@ class FeedRepository @Inject constructor( val fileName = "feed_image_${System.currentTimeMillis()}.$extension" val tempFile = File(context.cacheDir, fileName) - // 임시 파일로 복사 - FileOutputStream(tempFile).use { output -> - inputStream.copyTo(output) + // 임시 파일 목록에 추가 + tempFiles.add(tempFile) + + // 임시 파일로 복사 (use 블록으로 자동 리소스 해제) + inputStream.use { input -> + FileOutputStream(tempFile).use { output -> + input.copyTo(output) + } } // MultipartBody.Part 생성 @@ -112,4 +125,17 @@ class FeedRepository @Inject constructor( null } } + + /** 임시 파일들을 정리하는 함수 */ + private fun cleanupTempFiles(tempFiles: List) { + tempFiles.forEach { file -> + try { + if (file.exists()) { + file.delete() + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } } \ No newline at end of file From 3a24db511bd05f2d3c515c4ba86b57cc57b483c9 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Fri, 15 Aug 2025 18:17:05 +0900 Subject: [PATCH 09/10] =?UTF-8?q?[feat]:=20=ED=86=A0=EB=81=BC=20pr=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EB=B0=98=EC=98=81(SerialName,=20Dispatche?= =?UTF-8?q?r.IO,=20=EC=98=A4=EB=A5=98=EC=B2=98=EB=A6=AC)=20(#87)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/feed/request/CreateFeedRequest.kt | 5 +++ .../model/feed/response/CreateFeedResponse.kt | 2 ++ .../feed/response/FeedWriteInfoResponse.kt | 4 +++ .../thip/data/repository/FeedRepository.kt | 31 ++++++++++--------- .../ui/feed/viewmodel/FeedWriteViewModel.kt | 8 +++-- app/src/main/res/values/strings.xml | 3 ++ 6 files changed, 37 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/data/model/feed/request/CreateFeedRequest.kt b/app/src/main/java/com/texthip/thip/data/model/feed/request/CreateFeedRequest.kt index 24d596fe..b0b17e08 100644 --- a/app/src/main/java/com/texthip/thip/data/model/feed/request/CreateFeedRequest.kt +++ b/app/src/main/java/com/texthip/thip/data/model/feed/request/CreateFeedRequest.kt @@ -1,11 +1,16 @@ package com.texthip.thip.data.model.feed.request +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @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() ) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/feed/response/CreateFeedResponse.kt b/app/src/main/java/com/texthip/thip/data/model/feed/response/CreateFeedResponse.kt index 1210489d..467ee374 100644 --- a/app/src/main/java/com/texthip/thip/data/model/feed/response/CreateFeedResponse.kt +++ b/app/src/main/java/com/texthip/thip/data/model/feed/response/CreateFeedResponse.kt @@ -1,8 +1,10 @@ package com.texthip.thip.data.model.feed.response +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class CreateFeedResponse( + @SerialName("feedId") val feedId: Int ) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/feed/response/FeedWriteInfoResponse.kt b/app/src/main/java/com/texthip/thip/data/model/feed/response/FeedWriteInfoResponse.kt index 23213657..7b95c45f 100644 --- a/app/src/main/java/com/texthip/thip/data/model/feed/response/FeedWriteInfoResponse.kt +++ b/app/src/main/java/com/texthip/thip/data/model/feed/response/FeedWriteInfoResponse.kt @@ -1,14 +1,18 @@ package com.texthip.thip.data.model.feed.response +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class FeedWriteInfoResponse( + @SerialName("categoryList") val categoryList: List ) @Serializable data class FeedCategory( + @SerialName("category") val category: String, + @SerialName("tagList") val tagList: List ) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt index 5a99e4f3..0a8dc1ae 100644 --- a/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/FeedRepository.kt @@ -8,6 +8,8 @@ import com.texthip.thip.data.model.feed.response.CreateFeedResponse import com.texthip.thip.data.model.feed.response.FeedWriteInfoResponse import com.texthip.thip.data.service.FeedService import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import okhttp3.MediaType.Companion.toMediaType import okhttp3.MultipartBody @@ -21,7 +23,8 @@ import javax.inject.Singleton @Singleton class FeedRepository @Inject constructor( private val feedService: FeedService, - @param:ApplicationContext private val context: Context + @param:ApplicationContext private val context: Context, + private val json: Json ) { /** 피드 작성에 필요한 카테고리 및 태그 목록 조회 */ @@ -61,7 +64,7 @@ class FeedRepository @Inject constructor( ) // JSON 요청 부분을 RequestBody로 변환 - val requestJson = Json.encodeToString(CreateFeedRequest.serializer(), request) + val requestJson = json.encodeToString(CreateFeedRequest.serializer(), request) val requestBody = requestJson.toRequestBody("application/json".toMediaType()) // 임시 파일 목록 추적 @@ -70,11 +73,13 @@ class FeedRepository @Inject constructor( try { // 이미지 파일들을 MultipartBody.Part로 변환 val imageParts = if (imageUris.isNotEmpty()) { - imageUris.mapNotNull { uri -> - try { - uriToMultipartBodyPart(uri, "images", tempFiles) - } catch (e: Exception) { - null + withContext(Dispatchers.IO) { + imageUris.mapNotNull { uri -> + try { + uriToMultipartBodyPart(uri, "images", tempFiles) + } catch (e: Exception) { + null + } } } } else { @@ -92,8 +97,6 @@ class FeedRepository @Inject constructor( private fun uriToMultipartBodyPart(uri: Uri, paramName: String, tempFiles: MutableList): MultipartBody.Part? { return try { - val inputStream = context.contentResolver.openInputStream(uri) ?: return null - // MIME 타입 확인 val mimeType = context.contentResolver.getType(uri) ?: "image/jpeg" val extension = when (mimeType) { @@ -110,12 +113,12 @@ class FeedRepository @Inject constructor( // 임시 파일 목록에 추가 tempFiles.add(tempFile) - // 임시 파일로 복사 (use 블록으로 자동 리소스 해제) - inputStream.use { input -> - FileOutputStream(tempFile).use { output -> - input.copyTo(output) + // InputStream을 use 블록으로 안전하게 관리 + context.contentResolver.openInputStream(uri)?.use { inputStream -> + FileOutputStream(tempFile).use { outputStream -> + inputStream.copyTo(outputStream) } - } + } ?: return null // MultipartBody.Part 생성 val requestFile = tempFile.asRequestBody(mimeType.toMediaType()) diff --git a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt index cfe747dc..b6fc4b6b 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedWriteViewModel.kt @@ -252,8 +252,12 @@ class FeedWriteViewModel @Inject constructor( ) result.onSuccess { response -> - val feedId = response?.feedId ?: 1 - onSuccess(feedId) + val feedId = response?.feedId + if (feedId != null) { + onSuccess(feedId) + } else { + onError(stringResourceProvider.getString(R.string.error_feed_id_not_returned)) + } }.onFailure { exception -> onError( exception.message ?: stringResourceProvider.getString(R.string.error_network_error) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 423cfd56..6bf8cfce 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -404,6 +404,9 @@ 알 수 없는 오류가 발생했습니다. + + + 서버 feedId 반환 오류 과학/IT From 4eae562536af2b8a9c0ef26dfdbfbd916b9e29b2 Mon Sep 17 00:00:00 2001 From: rbqks529 Date: Fri, 15 Aug 2025 19:39:16 +0900 Subject: [PATCH 10/10] [feat]: (#87) --- .../texthip/thip/ui/feed/screen/FeedScreen.kt | 82 ++++++++++++++++++- .../navigator/navigations/FeedNavigation.kt | 15 +++- app/src/main/res/values/strings.xml | 2 + 3 files changed, 91 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt index efbc81e0..af4e3ed8 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt @@ -1,32 +1,43 @@ package com.texthip.thip.ui.feed.screen +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.navigation.NavController import com.texthip.thip.R import com.texthip.thip.ui.common.buttons.FloatingButton import com.texthip.thip.ui.common.header.AuthorHeader @@ -39,15 +50,15 @@ import com.texthip.thip.ui.feed.mock.MySubscriptionData import com.texthip.thip.ui.feed.viewmodel.MySubscriptionViewModel import com.texthip.thip.ui.mypage.component.SavedFeedCard import com.texthip.thip.ui.mypage.mock.FeedItem -import com.texthip.thip.ui.navigator.routes.FeedRoutes import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch @Composable fun FeedScreen( - navController: NavController? = null, onNavigateToMySubscription: () -> Unit = {}, onNavigateToFeedWrite: () -> Unit = {}, nickname: String = "", @@ -56,6 +67,8 @@ fun FeedScreen( totalFeedCount: Int = 0, selectedTabIndex: Int = 0, followerProfileImageUrls: List = emptyList(), + resultFeedId: Int? = null, + onResultConsumed: () -> Unit = {}, viewModel: MySubscriptionViewModel = hiltViewModel() ) { val selectedIndex = rememberSaveable { mutableIntStateOf(selectedTabIndex) } @@ -64,6 +77,29 @@ fun FeedScreen( addAll(feeds) } } + val scope = rememberCoroutineScope() + + 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 + } + } + } + } val mySubscriptions = listOf( MySubscriptionData( profileImageUrl = "https://example.com/image1.jpg", @@ -137,6 +173,44 @@ fun FeedScreen( modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(12.dp) ) { + item { + AnimatedVisibility(visible = showProgressBar) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(start = 20.dp, end = 20.dp, top = 32.dp), + ) { + Text( + modifier = Modifier.padding(bottom = 12.dp), + text = if (progress.value < 1.0f) { + stringResource(R.string.posting_in_progress_feed) + } else { + stringResource(R.string.posting_complete_feed) + }, + style = typography.view_m500_s14, + color = colors.NeonGreen + ) + + Box( + modifier = Modifier + .fillMaxWidth() + .height(8.dp) + .clip(RoundedCornerShape(12.dp)) + .background(color = colors.Grey02) // 트랙(배경) 색상 + ) { + Box( + modifier = Modifier + .fillMaxWidth(fraction = progress.value) + .fillMaxHeight() + .background( + color = colors.NeonGreen, + shape = RoundedCornerShape(12.dp) + ) + ) + } + } + } + } if (selectedIndex.value == 1) { // 내 피드 item { diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt index f04ff8a1..40df9757 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/FeedNavigation.kt @@ -13,16 +13,20 @@ import com.texthip.thip.ui.navigator.routes.MainTabRoutes // Feed fun NavGraphBuilder.feedNavigation(navController: NavHostController) { - composable { - //TODO 추후 view model 적용 예정 + composable { backStackEntry -> + val resultFeedId = backStackEntry.savedStateHandle.get("feedId") + FeedScreen( - navController = navController, nickname = "ThipUser01", userRole = "문학가", feeds = emptyList(), totalFeedCount = 0, selectedTabIndex = 0, followerProfileImageUrls = emptyList(), + resultFeedId = resultFeedId, + onResultConsumed = { + backStackEntry.savedStateHandle.remove("feedId") + }, onNavigateToMySubscription = { navController.navigateToMySubscription() }, @@ -40,7 +44,10 @@ fun NavGraphBuilder.feedNavigation(navController: NavHostController) { navController.popBackStack() }, onFeedCreated = { feedId -> - // 피드 생성 성공 시 피드 목록으로 돌아가기 + // 피드 생성 성공 시 결과를 저장하고 피드 목록으로 돌아가기 + navController.getBackStackEntry(MainTabRoutes.Feed) + .savedStateHandle + .set("feedId", feedId) navController.popBackStack() } ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6bf8cfce..af22d257 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -340,6 +340,8 @@ 찾는 사용자가 없어요 사용자 찾기 내가 찾는 사용자를 검색해보세요. + 글을 작성중이에요... + 새 글 작성을 완료했어요!