diff --git a/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsAiReviewResponse.kt b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsAiReviewResponse.kt new file mode 100644 index 00000000..db35700e --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsAiReviewResponse.kt @@ -0,0 +1,9 @@ +package com.texthip.thip.data.model.rooms.response + +import kotlinx.serialization.Serializable + +@Serializable +data class RoomsAiReviewResponse( + val content: String, + val count: Int +) diff --git a/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsAiUsageResponse.kt b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsAiUsageResponse.kt new file mode 100644 index 00000000..0c34c553 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsAiUsageResponse.kt @@ -0,0 +1,9 @@ +package com.texthip.thip.data.model.rooms.response + +import kotlinx.serialization.Serializable + +@Serializable +data class RoomsAiUsageResponse( + val recordReviewCount: Int, + val recordCount: Int +) diff --git a/app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt index 6db0d4a0..2e338d39 100644 --- a/app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt @@ -360,4 +360,20 @@ class RoomsRepository @Inject constructor( ) ).handleBaseResponse().getOrThrow() } + + suspend fun getRoomsAiUsage( + roomId: Int, + ) = runCatching { + roomsService.getRoomsAiUsage( + roomId = roomId, + ).handleBaseResponse().getOrThrow() + } + + suspend fun postRoomsAiReview( + roomId: Int, + ) = runCatching { + roomsService.postRoomsAiReview( + roomId = roomId, + ).handleBaseResponse().getOrThrow() + } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/service/RoomsService.kt b/app/src/main/java/com/texthip/thip/data/service/RoomsService.kt index 28ad0581..0f6e2485 100644 --- a/app/src/main/java/com/texthip/thip/data/service/RoomsService.kt +++ b/app/src/main/java/com/texthip/thip/data/service/RoomsService.kt @@ -19,6 +19,8 @@ import com.texthip.thip.data.model.rooms.response.RoomJoinResponse import com.texthip.thip.data.model.rooms.response.RoomMainList import com.texthip.thip.data.model.rooms.response.RoomRecruitingResponse import com.texthip.thip.data.model.rooms.response.RoomSecretRoomResponse +import com.texthip.thip.data.model.rooms.response.RoomsAiReviewResponse +import com.texthip.thip.data.model.rooms.response.RoomsAiUsageResponse import com.texthip.thip.data.model.rooms.response.RoomsBookPageResponse import com.texthip.thip.data.model.rooms.response.RoomsCreateDailyGreetingResponse import com.texthip.thip.data.model.rooms.response.RoomsCreateVoteResponse @@ -215,4 +217,14 @@ interface RoomsService { @Path("voteId") voteId: Int, @Body request: RoomsPatchVoteRequest ): BaseResponse + + @GET("rooms/{roomId}/users/ai-usage") + suspend fun getRoomsAiUsage( + @Path("roomId") roomId: Int + ): BaseResponse + + @POST("rooms/{roomId}/record/ai-review") + suspend fun postRoomsAiReview( + @Path("roomId") roomId: Int + ): BaseResponse } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteAiScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteAiScreen.kt new file mode 100644 index 00000000..db6a067f --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteAiScreen.kt @@ -0,0 +1,268 @@ +package com.texthip.thip.ui.group.note.screen + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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 +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.texthip.thip.R +import com.texthip.thip.ui.common.modal.DialogPopup +import com.texthip.thip.ui.common.modal.ToastWithDate +import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar +import com.texthip.thip.ui.group.note.viewmodel.GroupNoteAiUiState +import com.texthip.thip.ui.group.note.viewmodel.GroupNoteAiViewModel +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 + +@Composable +fun GroupNoteAiScreen( + roomId: Int, + onBackClick: () -> Unit, + viewModel: GroupNoteAiViewModel = hiltViewModel() +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val clipboardManager = LocalClipboardManager.current + + var showToast by remember { mutableStateOf(false) } + var showExitDialog by remember { mutableStateOf(false) } + + LaunchedEffect(key1 = roomId) { + viewModel.generateAiReview(roomId) + } + + LaunchedEffect(showToast) { + if (showToast) { + delay(3000L) + showToast = false + } + } + + GroupNoteAiContent( + uiState = uiState, + showToast = showToast, + showExitDialog = showExitDialog, + onBackClick = { showExitDialog = true }, + onCopyClick = { text -> + clipboardManager.setText(AnnotatedString(text)) + showToast = true + }, + onConfirmExit = onBackClick, + onDismissExitDialog = { showExitDialog = false } + ) +} + +@Composable +fun GroupNoteAiContent( + uiState: GroupNoteAiUiState, + showToast: Boolean = false, + showExitDialog: Boolean = false, + onBackClick: () -> Unit, + onCopyClick: (String) -> Unit, + onConfirmExit: () -> Unit, + onDismissExitDialog: () -> Unit +) { + val isOverlayVisible = showExitDialog + + Box(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .fillMaxSize() + .then(if (isOverlayVisible) Modifier.blur(5.dp) else Modifier) + ) { + DefaultTopAppBar( + title = stringResource(R.string.ai_book_review_title), + onLeftClick = onBackClick + ) + + Box(modifier = Modifier.fillMaxSize()) { + if (uiState.isLoading) { + // 로딩 중 + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + CircularProgressIndicator() + Spacer(modifier = Modifier.height(20.dp)) + Text( + text = stringResource(R.string.ai_review_loading), + style = typography.smalltitle_sb600_s18_h24, + color = colors.White, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(R.string.ai_review_loading_subtext), + style = typography.copy_r400_s14, + color = colors.Grey, + textAlign = TextAlign.Center + ) + } + } else if (uiState.aiReviewText != null) { + // 로딩 완료 + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(start = 26.dp, end = 26.dp, top = 10.dp, bottom = 50.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Icon( + painter = painterResource(R.drawable.ic_information), + contentDescription = "Done Icon", + tint = Color.Unspecified, + modifier = Modifier.align(Alignment.CenterVertically) + ) + + Text( + text = stringResource(R.string.ai_review_done_info), + style = typography.info_r400_s12, + color = colors.Grey01 + ) + } + Spacer(modifier = Modifier.height(10.dp)) + Text( + text = uiState.aiReviewText, + style = typography.feedcopy_r400_s14_h20, + color = colors.White + ) + Spacer(modifier = Modifier.height(24.dp)) + } + + Box( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .height(50.dp) + .background(colors.Purple) + .clickable { onCopyClick(uiState.aiReviewText) }, + contentAlignment = Alignment.Center + ) { + Text( + text = stringResource(R.string.copy_to_clipboard), + style = typography.smalltitle_sb600_s18_h24, + color = colors.White + ) + } + } else if (uiState.error != null) { + Column( + modifier = Modifier.fillMaxSize().padding(16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = uiState.error, + style = typography.copy_r400_s14, + color = colors.Grey, + textAlign = TextAlign.Center + ) + } + } + } + } + + if (showExitDialog) { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + DialogPopup( + title = stringResource(R.string.ai_review_dialog_title), + description = stringResource(R.string.ai_review_exit_dialog_description), + onConfirm = onConfirmExit, + onCancel = onDismissExitDialog + ) + } + } + + AnimatedVisibility( + visible = showToast, + enter = slideInVertically( + initialOffsetY = { -it }, + animationSpec = tween(durationMillis = 2000) + ), + exit = slideOutVertically( + targetOffsetY = { -it }, + animationSpec = tween(durationMillis = 2000) + ), + modifier = Modifier + .align(Alignment.TopCenter) + .padding(horizontal = 20.dp, vertical = 16.dp) + .zIndex(2f) + ) { + ToastWithDate( + message = stringResource(R.string.copy_to_clipboard_done) + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun GroupNoteAiScreenLoadingPreview() { + ThipTheme { + GroupNoteAiContent( + uiState = GroupNoteAiUiState(isLoading = true), + onBackClick = {}, + onCopyClick = {}, + onConfirmExit = {}, + onDismissExitDialog = {} + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun GroupNoteAiScreenDonePreview() { + ThipTheme { + GroupNoteAiContent( + uiState = GroupNoteAiUiState(isLoading = false, aiReviewText = "레이 커즈와일의 마침내 특이점이 시작된다는 읽는 내내 머릿속이 폭발하는 느낌이었다. 인공지능, 나노기술, 생명공학이 동시에 발전해서 결국 인간의 지능과 기계를 융합하는 시대가 온다는 주장인데, 솔직히 처음엔 SF소설 같은 이야기로 느껴졌다."), + onBackClick = {}, + onCopyClick = {}, + onConfirmExit = {}, + onDismissExitDialog = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt index 57272c38..534aa29f 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt @@ -85,6 +85,7 @@ fun GroupNoteScreen( onEditVoteClick: (post: PostList) -> Unit = {}, onNavigateToUserProfile: (userId: Long) -> Unit = {}, onNavigateToMyProfile: () -> Unit = {}, + onNavigateToAiReview: () -> Unit = {}, resultTabIndex: Int? = null, onResultConsumed: () -> Unit = {}, initialPage: Int? = null, @@ -186,6 +187,7 @@ fun GroupNoteScreen( onNavigateToUserProfile(userId) } }, + onNavigateToAiReview = onNavigateToAiReview, showProgressBar = showProgressBar, progress = progress.value, openComments = openComments @@ -203,6 +205,7 @@ fun GroupNoteContent( onEditNoteClick: (post: PostList) -> Unit, onEditVoteClick: (post: PostList) -> Unit, onNavigateToUserProfile: (userId: Long) -> Unit, + onNavigateToAiReview: () -> Unit, showProgressBar: Boolean, progress: Float, openComments: Boolean = false @@ -214,8 +217,10 @@ fun GroupNoteContent( var isPinDialogVisible by remember { mutableStateOf(false) } var postToPin by remember { mutableStateOf(null) } var showToast by remember { mutableStateOf(false) } + var showAiReviewDialog by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() val isOverlayVisible = - isCommentBottomSheetVisible || selectedPostForMenu != null || isPinDialogVisible || showDeleteDialog + isCommentBottomSheetVisible || selectedPostForMenu != null || isPinDialogVisible || showDeleteDialog || showAiReviewDialog var postToDelete by remember { mutableStateOf(null) } var toastMessage by remember { mutableStateOf("") } @@ -592,6 +597,16 @@ fun GroupNoteContent( icon = painterResource(R.drawable.ic_vote), text = stringResource(R.string.create_vote), onClick = onCreateVoteClick + ), + FabMenuItem( + icon = painterResource(R.drawable.ic_ai_book_review), + text = stringResource(R.string.create_ai_book_review), + onClick = { + scope.launch { + onEvent(GroupNoteEvent.CheckAiUsage) + showAiReviewDialog = true + } + } ) ) ) @@ -724,6 +739,30 @@ fun GroupNoteContent( } } + if (showAiReviewDialog) { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + DialogPopup( + title = stringResource(R.string.ai_review_dialog_title), + description = stringResource( + R.string.ai_review_dialog_description, + uiState.recordCount, + 5 + ), + onConfirm = { + onNavigateToAiReview() + showAiReviewDialog = false + }, + onCancel = { + showAiReviewDialog = false + } + ) + } + } + AnimatedVisibility( modifier = Modifier .padding(horizontal = 20.dp, vertical = 16.dp), @@ -786,7 +825,8 @@ private fun GroupNoteScreenPreview() { progress = 0.5f, onNavigateToUserProfile = {}, onEditNoteClick = {}, - onEditVoteClick = {} + onEditVoteClick = {}, + onNavigateToAiReview = {} ) } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteAiViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteAiViewModel.kt new file mode 100644 index 00000000..623eae20 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteAiViewModel.kt @@ -0,0 +1,76 @@ +package com.texthip.thip.ui.group.note.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.texthip.thip.data.model.base.BaseResponse +import com.texthip.thip.data.repository.RoomsRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.serialization.json.Json +import retrofit2.HttpException +import java.io.IOException +import javax.inject.Inject + +data class GroupNoteAiUiState( + val isLoading: Boolean = true, + val aiReviewText: String? = null, + val error: String? = null +) + +@HiltViewModel +class GroupNoteAiViewModel @Inject constructor( + private val repository: RoomsRepository +) : ViewModel() { + private val _uiState = MutableStateFlow(GroupNoteAiUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private val json = Json { ignoreUnknownKeys = true } + + fun generateAiReview(roomId: Int) { + if (!_uiState.value.isLoading && _uiState.value.aiReviewText != null) return + + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true, error = null) } + + repository.postRoomsAiReview(roomId) + .onSuccess { response -> + _uiState.update { + it.copy( + isLoading = false, + aiReviewText = response?.content + ) + } + } + .onFailure { throwable -> + val errorMessage = when (throwable) { + is HttpException -> { + val errorBody = throwable.response()?.errorBody()?.string() + if (errorBody != null) { + try { + val errorResponse = json.decodeFromString>(errorBody) + errorResponse.message + } catch (e: Exception) { + throwable.message() + } + } else { + throwable.message() + } + } + is IOException -> "네트워크 연결을 확인해주세요." + else -> throwable.message ?: "알 수 없는 오류가 발생했습니다." + } + + _uiState.update { + it.copy( + isLoading = false, + error = errorMessage + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt index 018dcaaa..13a8ab4a 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt @@ -1,6 +1,5 @@ package com.texthip.thip.ui.group.note.viewmodel -import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.texthip.thip.data.model.rooms.request.RoomsPostsRequestParams @@ -31,6 +30,8 @@ data class GroupNoteUiState( val recentBookPage: Int = 0, val totalBookPage: Int = 0, val isOverviewPossible: Boolean = false, + val recordReviewCount: Int = 0, + val recordCount: Int = 0, // 필터 및 탭 상태 val selectedTabIndex: Int = 0, @@ -40,7 +41,7 @@ data class GroupNoteUiState( val isOverview: Boolean = false, val isPageFilter: Boolean = false, val totalEnabled: Boolean = false, - + // 스크롤 관련 상태 val scrollToPostId: Int? = null ) @@ -66,6 +67,7 @@ sealed interface GroupNoteEvent { data class OnPinRecord(val recordId: Int, val content: String) : GroupNoteEvent data object RefreshPosts : GroupNoteEvent data object ClearScrollTarget : GroupNoteEvent + data object CheckAiUsage : GroupNoteEvent } @@ -131,11 +133,31 @@ class GroupNoteViewModel @Inject constructor( } } + private fun loadAiUsageInfo() { + viewModelScope.launch { + roomsRepository.getRoomsAiUsage(roomId) + .onSuccess { usageResponse -> + _uiState.update { + it.copy( + recordReviewCount = usageResponse?.recordReviewCount ?: 0, + recordCount = usageResponse?.recordCount ?: 0 + ) + } + } + .onFailure { throwable -> + _uiState.update { it.copy(error = throwable.message) } + } + } + } + private fun refreshAllData() { viewModelScope.launch { - val postsJob = async { loadPosts(isRefresh = true) } - val bookPageJob = async { loadBookPageInfo() } - awaitAll(postsJob, bookPageJob) + val jobs = listOf( + async { loadPosts(isRefresh = true) }, + async { loadBookPageInfo() }, + async { loadAiUsageInfo() } + ) + jobs.awaitAll() } } @@ -182,8 +204,8 @@ class GroupNoteViewModel @Inject constructor( GroupNoteEvent.ClearScrollTarget -> { _uiState.update { it.copy(scrollToPostId = null) } } - else -> { - Log.w("GroupNoteViewModel", "Unhandled event received: $event") + GroupNoteEvent.CheckAiUsage -> { + loadAiUsageInfo() } } } @@ -231,7 +253,8 @@ class GroupNoteViewModel @Inject constructor( roomPostType = postType ) .onFailure { - val rollbackPosts = currentPosts.toMutableList().apply { this[postIndex] = oldPost } + val rollbackPosts = + currentPosts.toMutableList().apply { this[postIndex] = oldPost } _uiState.update { it.copy(posts = rollbackPosts) } } } @@ -289,7 +312,8 @@ class GroupNoteViewModel @Inject constructor( // 기존 순서는 유지하고 내용만 업데이트 val updatedVoteItems = postToUpdate.voteItems.map { originalItem -> - val newItem = serverVoteItems.find { it.voteItemId == originalItem.voteItemId } + val newItem = + serverVoteItems.find { it.voteItemId == originalItem.voteItemId } newItem ?: originalItem } diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/extensions/GroupNavigationExtensions.kt b/app/src/main/java/com/texthip/thip/ui/navigator/extensions/GroupNavigationExtensions.kt index ac301186..8febe92e 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/extensions/GroupNavigationExtensions.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/extensions/GroupNavigationExtensions.kt @@ -152,4 +152,9 @@ fun NavHostController.navigateToGroupVoteCreate( options = options ) ) +} + +// AI 독후감 생성 화면으로 이동 +fun NavHostController.navigateToGroupNoteAi(roomId: Int) { + navigate(GroupRoutes.NoteAi(roomId = roomId)) } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt index 03a8ca83..5fd21db9 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt @@ -15,6 +15,7 @@ import com.texthip.thip.ui.group.makeroom.viewmodel.GroupMakeRoomViewModel import com.texthip.thip.ui.group.myroom.mock.RoomType import com.texthip.thip.ui.group.myroom.screen.GroupMyScreen import com.texthip.thip.ui.group.myroom.viewmodel.GroupMyViewModel +import com.texthip.thip.ui.group.note.screen.GroupNoteAiScreen import com.texthip.thip.ui.group.note.screen.GroupNoteCreateScreen import com.texthip.thip.ui.group.note.screen.GroupNoteScreen import com.texthip.thip.ui.group.note.screen.GroupVoteCreateScreen @@ -35,6 +36,7 @@ import com.texthip.thip.ui.navigator.extensions.navigateToFeedWrite import com.texthip.thip.ui.navigator.extensions.navigateToGroupMakeRoom import com.texthip.thip.ui.navigator.extensions.navigateToGroupMy import com.texthip.thip.ui.navigator.extensions.navigateToGroupNote +import com.texthip.thip.ui.navigator.extensions.navigateToGroupNoteAi import com.texthip.thip.ui.navigator.extensions.navigateToGroupNoteCreate import com.texthip.thip.ui.navigator.extensions.navigateToGroupRecruit import com.texthip.thip.ui.navigator.extensions.navigateToGroupRoom @@ -407,6 +409,9 @@ fun NavGraphBuilder.groupNavigation( onNavigateToMyProfile = { navController.navigate(FeedRoutes.My) }, + onNavigateToAiReview = { + navController.navigateToGroupNoteAi(roomId) + }, viewModel = viewModel ) } @@ -461,4 +466,13 @@ fun NavGraphBuilder.groupNavigation( } ) } + + // AI 독후감 스크린 + composable { backStackEntry -> + val route = backStackEntry.toRoute() + GroupNoteAiScreen( + roomId = route.roomId, + onBackClick = { navigateBack() } + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/routes/GroupRoutes.kt b/app/src/main/java/com/texthip/thip/ui/navigator/routes/GroupRoutes.kt index d67312eb..6e9b6c78 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/routes/GroupRoutes.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/routes/GroupRoutes.kt @@ -70,4 +70,7 @@ sealed class GroupRoutes : Routes() { val title: String? = null, val options: List? = null ) + + @Serializable + data class NoteAi(val roomId: Int) : GroupRoutes() } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_ai_book_review.xml b/app/src/main/res/drawable/ic_ai_book_review.xml new file mode 100644 index 00000000..b16a0c38 --- /dev/null +++ b/app/src/main/res/drawable/ic_ai_book_review.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e23ee58a..fe89d6fe 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -41,6 +41,7 @@ 인물관계도 보기 기록 작성 투표 생성 + AI 독서감상문 생성 기록 수정 투표 수정 @@ -261,6 +262,15 @@ 전체 모임방을 한 눈에 둘러보세요! 전체 모임방 둘러보기 전체 + AI 독서감상문 + 독서 감상문을 생성중이에요! + 조금만 기다려주세요 + 내 기록과 총평을 바탕으로 생성된 감상문입니다. + 클립보드에 복사 + 클립보드에 복사가 완료되었어요 + AI 독서감상문 생성 (Beta) + 기록장에서 작성한 기록을 기반으로\n독서감상문을 생성하시겠어요?\n(서비스 내 잔여 이용횟수 : %d/%d) + 생성된 감상문은 다시 볼 수 없으며, 잔여 이용횟수는 차감돼요. 계속하시겠어요? 피드