From fc6049636843251fd46c90cd0763bbfbe1a50c12 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Sun, 26 Oct 2025 15:09:14 +0900 Subject: [PATCH 01/10] =?UTF-8?q?[ui]:=20=EA=B8=B0=EB=A1=9D=EC=9E=A5=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EC=97=90=20ai=20FAB=20=EB=B2=84=ED=8A=BC,=20?= =?UTF-8?q?dialog=20=EC=B6=94=EA=B0=80=20(#148)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/group/note/screen/GroupNoteScreen.kt | 30 +++++++++++++++++-- .../main/res/drawable/ic_ai_book_review.xml | 16 ++++++++++ app/src/main/res/values/strings.xml | 4 +++ 3 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/drawable/ic_ai_book_review.xml 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 add51661..1254e7f0 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 @@ -43,7 +43,6 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.texthip.thip.R -import com.texthip.thip.ui.feed.viewmodel.FeedViewModel import com.texthip.thip.data.model.rooms.response.PostList import com.texthip.thip.data.model.rooms.response.RoomsRecordsPinResponse import com.texthip.thip.ui.common.bottomsheet.MenuBottomSheet @@ -54,6 +53,7 @@ import com.texthip.thip.ui.common.header.HeaderMenuBarTab 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.feed.viewmodel.FeedViewModel import com.texthip.thip.ui.group.note.component.CommentBottomSheet import com.texthip.thip.ui.group.note.component.FilterHeaderSection import com.texthip.thip.ui.group.note.component.TextCommentCard @@ -213,8 +213,9 @@ 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 isOverlayVisible = - isCommentBottomSheetVisible || selectedPostForMenu != null || isPinDialogVisible || showDeleteDialog + isCommentBottomSheetVisible || selectedPostForMenu != null || isPinDialogVisible || showDeleteDialog || showAiReviewDialog var postToDelete by remember { mutableStateOf(null) } var toastMessage by remember { mutableStateOf("") } @@ -575,6 +576,11 @@ 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 = { showAiReviewDialog = true } ) ) ) @@ -707,6 +713,26 @@ 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, 4, 5), // TODO: 숫자 서버에서 받아오기 + onConfirm = { +// onNavigateToAiReview() + showAiReviewDialog = false + }, + onCancel = { + showAiReviewDialog = false + } + ) + } + } + AnimatedVisibility( modifier = Modifier .padding(horizontal = 20.dp, vertical = 16.dp), 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 27ba25e0..2548b9b5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -41,8 +41,12 @@ 인물관계도 보기 기록 작성 투표 생성 + AI 독서감상문 생성 기록 수정 투표 수정 + AI 독서감상문 생성 (Beta) + 기록장에서 작성한 기록을 기반으로\n독서감상문을 생성하시겠어요?\n(서비스 내 잔여 이용횟수 : %d/%d) + 시간 전 From e9a317b070acb040c520835d02518648d2bf118a Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Sun, 26 Oct 2025 17:16:15 +0900 Subject: [PATCH 02/10] =?UTF-8?q?[ui]:=20ai=20=EB=8F=85=ED=9B=84=EA=B0=90?= =?UTF-8?q?=20=ED=99=94=EB=A9=B4=20=EC=99=84=EB=A3=8C=20(#148)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/group/note/screen/GroupNoteAiScreen.kt | 234 ++++++++++++++++++ app/src/main/res/values/strings.xml | 6 + 2 files changed, 240 insertions(+) create mode 100644 app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteAiScreen.kt 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..88b3494a --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteAiScreen.kt @@ -0,0 +1,234 @@ +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.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 com.texthip.thip.R +import com.texthip.thip.ui.common.modal.ToastWithDate +import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar +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 + +private const val DUMMY_AI_REVIEW = + "레이 커즈와일의 마침내 특이점이 시작된다는 읽는 내내 머릿속이 폭발하는 느낌이었다. 인공지능, 나노기술, 생명공학이 동시에 발전해서 결국 인간의 지능과 기계를 융합하는 시대가 온다는 주장인데, 솔직히 처음엔 SF소설 같은 이야기로 느껴졌다. \n" + + "\n" + + "하지만 커즈와일이 데이터와 과학적 근거를 차근차근 쌓아가며 기술 발전이 기하급수적이라는 걸 보여줄 때는 설득력이 꽤 컸다. 특히 인간 수명 연장과 의식 업로드에 대한 부분은 조금 무섭기도 하고 설레기도 했다. \n" + + "\n" + + "이 책이 단순히 기술 낙관주의가 아니라, 우리가 맞이할 변화에 대해 어떤 윤리적 기준과 사회적 합의가 필요한지 고민하게 만든 점이 좋았다. 읽고 나니 ‘미래는 멀리 있지 않다’는 말이 실감났다. 당장 내가 AI를 어떻게 활용하고, 기술과 함께 어떻게 성장할지 스스로 계획을 세우고 싶어졌다. 띱에서 다른 사람들은 이 책을 읽고 어떤 생각을 했을지 궁금하다. \n" + + "\n" + + "레이 커즈와일의 마침내 특이점이 시작된다는 읽는 내내 머릿속이 폭발하는 느낌이었다. 인공지능, 나노기술, 생명공학이 동시에 발전해서 결국 인간의 지능과 기계를 융합하는 시대가 온다는 주장인데, 솔직히 처음엔 SF소설 같은 이야기로 느껴졌다. \n" + + "\n" + + "하지만 커즈와일이 데이터와 과학적 근거를 차근차근 쌓아가며 기술 발전이 기하급수적이라는 걸 보여줄 때는 설득력이 꽤 컸다. 특히 인간 수명 연장과 의식 업로드에 대한 부분은 조금 무섭기도 하고 설레기도 했다. 이 책이 단순히 기술 낙관주의가 아니라, 우리가 맞이할 변화에 대해 어떤 윤리적 기준과 사회적 합의가 필요한지 고민하게 만든 점이 좋았다. 읽고 나니 ‘미래는 멀리 있지 않다’는 말이 실감났다. 당장 내가 AI를 어떻게 활용하고, 기술과 함께 어떻게 성장할지 스스로 계획을 세우고 싶어졌다. 띱에서 다른 사람들은 이 책 읽고 어떤 생각을 했을지 궁금하다. 레이 커즈와일의 마침내 특이점이 시작된다는 읽는 내내 머릿속이 폭발하는 느낌이었다. 인공지능, 나노기술, 생명공학이 동시에 발전해서 결국 인간의 지능과 기계를 융합하는 시대가 온다는 주장인데, 솔직히 처음엔 SF소설 같은 이야기로 느껴졌다. 하지만 커즈와일이 데이터와 과학적 근거를 차근차근 쌓아가며 기술 발전이 기하급수적이라는 걸 보여줄 때는 설득력이 꽤 컸다. 특히 인간 수명 연장과 의식 업로드에 대한 부분은 조금 무섭기도 하고 설레기도 했다. 이 책이 단순히 기술 낙관주의가 아니라, 우리가 맞이할 변화에 대해 어떤 윤리적 기준과 사회적 합의가 필요한지 고민하게 만든 점이 좋았다. 읽고 나니 ‘미래는 멀리 있지 않다’는 말이 실감났다. 당장 내가 AI를 어떻게 활용하고, 기술과 함께 어떻게 성장할지 스스로 계획을 세우고 싶어졌다. 띱에서 다른 사람들은 이 책 읽고 어떤 생각을 했을지 궁금하다." + +@Composable +fun GroupNoteAiScreen( + roomId: Int, // TODO: 이 roomId로 ViewModel에서 AI 독후감 요청 + onBackClick: () -> Unit, +) { + var isLoading by remember { mutableStateOf(true) } + var aiReviewText by remember { mutableStateOf(null) } + val clipboardManager = LocalClipboardManager.current + + var showToast by remember { mutableStateOf(false) } + + // TODO: HiltViewModel을 사용해 실제 데이터 로직 구현 + LaunchedEffect(key1 = roomId) { + delay(3000) // 3초 딜레이 (네트워크 요청 시뮬레이션) + aiReviewText = DUMMY_AI_REVIEW + isLoading = false + } + + LaunchedEffect(showToast) { + if (showToast) { + delay(3000L) + showToast = false // onHideToast() 역할 + } + } + + GroupNoteAiContent( + isLoading = isLoading, + aiReviewText = aiReviewText, + showToast = showToast, + onBackClick = onBackClick, + onCopyClick = { text -> + clipboardManager.setText(AnnotatedString(text)) + showToast = true + } + ) +} + +@Composable +fun GroupNoteAiContent( + isLoading: Boolean, + aiReviewText: String?, + showToast: Boolean = false, + onBackClick: () -> Unit, + onCopyClick: (String) -> Unit +) { + Box(modifier = Modifier.fillMaxSize()) { + Column(modifier = Modifier.fillMaxSize()) { + DefaultTopAppBar( + title = stringResource(R.string.ai_book_review_title), + onLeftClick = onBackClick + ) + + Box(modifier = Modifier.fillMaxSize()) { + if (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 (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 = 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(aiReviewText) }, + contentAlignment = Alignment.Center + ) { + Text( + text = stringResource(R.string.copy_to_clipboard), + style = typography.smalltitle_sb600_s18_h24, + color = colors.White + ) + } + } + } + } + + 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( + isLoading = true, + aiReviewText = null, + onBackClick = {}, + onCopyClick = {}, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun GroupNoteAiScreenDonePreview() { + ThipTheme { + GroupNoteAiContent( + isLoading = false, + aiReviewText = DUMMY_AI_REVIEW, + onBackClick = {}, + onCopyClick = {} + ) + } +} \ 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 2548b9b5..4b1013b9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -265,6 +265,12 @@ 전체 모임방을 한 눈에 둘러보세요! 전체 모임방 둘러보기 전체 + AI 독서감상문 + 독서 감상문을 생성중이에요! + 조금만 기다려주세요 + 내 기록과 총평을 바탕으로 생성된 감상문입니다. + 클립보드에 복사 + 클립보드에 복사가 완료되었어요 피드 From 59a21a1ffd90cc2ad23db6f0e472cad8e9363723 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Sun, 26 Oct 2025 17:16:25 +0900 Subject: [PATCH 03/10] =?UTF-8?q?[feat]:=20ai=20=EB=8F=85=ED=9B=84?= =?UTF-8?q?=EA=B0=90=20=ED=99=94=EB=A9=B4=20navigation=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20(#148)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/ui/group/note/screen/GroupNoteScreen.kt | 14 +++++++++++--- .../extensions/GroupNavigationExtensions.kt | 5 +++++ .../ui/navigator/navigations/GroupNavigation.kt | 14 ++++++++++++++ .../thip/ui/navigator/routes/GroupRoutes.kt | 3 +++ 4 files changed, 33 insertions(+), 3 deletions(-) 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 1254e7f0..74dca813 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 @@ -84,6 +84,7 @@ fun GroupNoteScreen( onEditVoteClick: (post: PostList) -> Unit = {}, onNavigateToUserProfile: (userId: Long) -> Unit = {}, onNavigateToMyProfile: () -> Unit = {}, + onNavigateToAiReview: () -> Unit = {}, resultTabIndex: Int? = null, onResultConsumed: () -> Unit = {}, initialPage: Int? = null, @@ -185,6 +186,7 @@ fun GroupNoteScreen( onNavigateToUserProfile(userId) } }, + onNavigateToAiReview = onNavigateToAiReview, showProgressBar = showProgressBar, progress = progress.value, openComments = openComments @@ -202,6 +204,7 @@ fun GroupNoteContent( onEditNoteClick: (post: PostList) -> Unit, onEditVoteClick: (post: PostList) -> Unit, onNavigateToUserProfile: (userId: Long) -> Unit, + onNavigateToAiReview: () -> Unit, showProgressBar: Boolean, progress: Float, openComments: Boolean = false @@ -721,9 +724,13 @@ fun GroupNoteContent( ) { DialogPopup( title = stringResource(R.string.ai_review_dialog_title), - description = stringResource(R.string.ai_review_dialog_description, 4, 5), // TODO: 숫자 서버에서 받아오기 + description = stringResource( + R.string.ai_review_dialog_description, + 4, + 5 + ), // TODO: 숫자 서버에서 받아오기 onConfirm = { -// onNavigateToAiReview() + onNavigateToAiReview() showAiReviewDialog = false }, onCancel = { @@ -795,7 +802,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/navigator/extensions/GroupNavigationExtensions.kt b/app/src/main/java/com/texthip/thip/ui/navigator/extensions/GroupNavigationExtensions.kt index 85a5ccec..4536d5f2 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 @@ -157,4 +157,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 2f73ad51..bb0e9516 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 @@ -16,6 +16,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 @@ -37,6 +38,7 @@ import com.texthip.thip.ui.navigator.extensions.navigateToGroupDone 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 @@ -422,6 +424,9 @@ fun NavGraphBuilder.groupNavigation( onNavigateToMyProfile = { navController.navigate(FeedRoutes.My) }, + onNavigateToAiReview = { + navController.navigateToGroupNoteAi(roomId) + }, viewModel = viewModel ) } @@ -476,4 +481,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 ddc9eb09..65b48ea9 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 @@ -73,4 +73,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 From f30b09c01b41c5bc4ec73ff652ce8aee08311429 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Sun, 26 Oct 2025 17:36:06 +0900 Subject: [PATCH 04/10] =?UTF-8?q?[feat]:=20ai=20=EB=8F=85=ED=9B=84?= =?UTF-8?q?=EA=B0=90=20=ED=99=94=EB=A9=B4=20=EB=92=A4=EB=A1=9C=EA=B0=80?= =?UTF-8?q?=EA=B8=B0=20=EB=88=84=EB=A5=B4=EB=A9=B4=20dialog=20=EB=9C=A8?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EA=B5=AC=ED=98=84=20(#148)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/group/note/screen/GroupNoteAiScreen.kt | 48 ++++++++++++++++--- app/src/main/res/values/strings.xml | 6 +-- 2 files changed, 44 insertions(+), 10 deletions(-) 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 index 88b3494a..e5d4df87 100644 --- 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 @@ -28,6 +28,7 @@ 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 @@ -38,6 +39,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex 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.theme.ThipTheme @@ -66,10 +68,11 @@ fun GroupNoteAiScreen( val clipboardManager = LocalClipboardManager.current var showToast by remember { mutableStateOf(false) } + var showExitDialog by remember { mutableStateOf(false) } // TODO: HiltViewModel을 사용해 실제 데이터 로직 구현 LaunchedEffect(key1 = roomId) { - delay(3000) // 3초 딜레이 (네트워크 요청 시뮬레이션) + delay(3000) aiReviewText = DUMMY_AI_REVIEW isLoading = false } @@ -77,7 +80,7 @@ fun GroupNoteAiScreen( LaunchedEffect(showToast) { if (showToast) { delay(3000L) - showToast = false // onHideToast() 역할 + showToast = false } } @@ -85,11 +88,14 @@ fun GroupNoteAiScreen( isLoading = isLoading, aiReviewText = aiReviewText, showToast = showToast, - onBackClick = onBackClick, + showExitDialog = showExitDialog, + onBackClick = { showExitDialog = true }, onCopyClick = { text -> clipboardManager.setText(AnnotatedString(text)) showToast = true - } + }, + onConfirmExit = onBackClick, + onDismissExitDialog = { showExitDialog = false } ) } @@ -98,11 +104,20 @@ fun GroupNoteAiContent( isLoading: Boolean, aiReviewText: String?, showToast: Boolean = false, + showExitDialog: Boolean = false, onBackClick: () -> Unit, - onCopyClick: (String) -> Unit + onCopyClick: (String) -> Unit, + onConfirmExit: () -> Unit, + onDismissExitDialog: () -> Unit ) { + val isOverlayVisible = showExitDialog + Box(modifier = Modifier.fillMaxSize()) { - Column(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 @@ -185,6 +200,21 @@ fun GroupNoteAiContent( } } + 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( @@ -216,6 +246,8 @@ private fun GroupNoteAiScreenLoadingPreview() { aiReviewText = null, onBackClick = {}, onCopyClick = {}, + onConfirmExit = {}, + onDismissExitDialog = {} ) } } @@ -228,7 +260,9 @@ private fun GroupNoteAiScreenDonePreview() { isLoading = false, aiReviewText = DUMMY_AI_REVIEW, onBackClick = {}, - onCopyClick = {} + onCopyClick = {}, + onConfirmExit = {}, + onDismissExitDialog = {} ) } } \ 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 4b1013b9..4c159f90 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -44,9 +44,6 @@ AI 독서감상문 생성 기록 수정 투표 수정 - AI 독서감상문 생성 (Beta) - 기록장에서 작성한 기록을 기반으로\n독서감상문을 생성하시겠어요?\n(서비스 내 잔여 이용횟수 : %d/%d) - 시간 전 @@ -271,6 +268,9 @@ 내 기록과 총평을 바탕으로 생성된 감상문입니다. 클립보드에 복사 클립보드에 복사가 완료되었어요 + AI 독서감상문 생성 (Beta) + 기록장에서 작성한 기록을 기반으로\n독서감상문을 생성하시겠어요?\n(서비스 내 잔여 이용횟수 : %d/%d) + 생성된 감상문은 다시 볼 수 없으며, 잔여 이용횟수는 차감돼요. 계속하시겠어요? 피드 From f271978f86f6f71d25f2bb0efdda5bf02e2f2abc Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Sun, 26 Oct 2025 17:59:10 +0900 Subject: [PATCH 05/10] =?UTF-8?q?[feat]:=20ai=20=EB=8F=85=ED=9B=84?= =?UTF-8?q?=EA=B0=90=20=ED=99=94=EB=A9=B4=20data=20layer=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20(#148)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/model/rooms/response/RoomsAiUsageResponse.kt | 9 +++++++++ .../com/texthip/thip/data/repository/RoomsRepository.kt | 8 ++++++++ .../java/com/texthip/thip/data/service/RoomsService.kt | 6 ++++++ 3 files changed, 23 insertions(+) create mode 100644 app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsAiUsageResponse.kt 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..87d3aa13 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,12 @@ class RoomsRepository @Inject constructor( ) ).handleBaseResponse().getOrThrow() } + + suspend fun getRoomsAiUsage( + roomId: Int, + ) = runCatching { + roomsService.getRoomsAiUsage( + 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..c3ab405e 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,7 @@ 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.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 +216,9 @@ interface RoomsService { @Path("voteId") voteId: Int, @Body request: RoomsPatchVoteRequest ): BaseResponse + + @GET("rooms/{roomId}/users/ai-usage") + suspend fun getRoomsAiUsage( + @Path("roomId") roomId: Int + ): BaseResponse } \ No newline at end of file From d2d61e4e7c219aa5c65bb3d742679f6e18ec4dfe Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Sun, 26 Oct 2025 18:30:09 +0900 Subject: [PATCH 06/10] =?UTF-8?q?[feat]:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=EC=9D=98=20ai=20=EC=9D=B4=EC=9A=A9=20=ED=9A=9F=EC=88=98=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B8=B0=EB=A1=9D=20=EC=9E=91=EC=84=B1=20=ED=9A=9F?= =?UTF-8?q?=EC=88=98=20=EC=A1=B0=ED=9A=8C=20api=20=EC=97=B0=EA=B2=B0=20(#1?= =?UTF-8?q?48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/group/note/screen/GroupNoteScreen.kt | 14 +++++-- .../note/viewmodel/GroupNoteViewModel.kt | 42 +++++++++++++++---- 2 files changed, 43 insertions(+), 13 deletions(-) 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 74dca813..1b37442b 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 @@ -217,6 +217,7 @@ fun GroupNoteContent( 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 || showAiReviewDialog var postToDelete by remember { mutableStateOf(null) } @@ -583,7 +584,12 @@ fun GroupNoteContent( FabMenuItem( icon = painterResource(R.drawable.ic_ai_book_review), text = stringResource(R.string.create_ai_book_review), - onClick = { showAiReviewDialog = true } + onClick = { + scope.launch { + onEvent(GroupNoteEvent.CheckAiUsage) + showAiReviewDialog = true + } + } ) ) ) @@ -726,9 +732,9 @@ fun GroupNoteContent( title = stringResource(R.string.ai_review_dialog_title), description = stringResource( R.string.ai_review_dialog_description, - 4, - 5 - ), // TODO: 숫자 서버에서 받아오기 + uiState.recordReviewCount, + uiState.recordCount + ), onConfirm = { onNavigateToAiReview() showAiReviewDialog = false 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 } From ad31bc322be1d02b006cb7d54b10f217308bceb6 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Mon, 27 Oct 2025 14:08:24 +0900 Subject: [PATCH 07/10] =?UTF-8?q?[feat]:=20ai=20=EA=B8=B0=EB=B0=98=20?= =?UTF-8?q?=EB=8F=85=ED=9B=84=EA=B0=90=20=EC=83=9D=EC=84=B1=20data=20layer?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=20(#148)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/model/rooms/response/RoomsAiReviewResponse.kt | 9 +++++++++ .../com/texthip/thip/data/repository/RoomsRepository.kt | 8 ++++++++ .../java/com/texthip/thip/data/service/RoomsService.kt | 6 ++++++ 3 files changed, 23 insertions(+) create mode 100644 app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsAiReviewResponse.kt 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/repository/RoomsRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt index 87d3aa13..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 @@ -368,4 +368,12 @@ class RoomsRepository @Inject constructor( 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 c3ab405e..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,7 @@ 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 @@ -221,4 +222,9 @@ interface RoomsService { 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 From 8a74b53772d5591264d2fc2be98ca1d02dba7ef4 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Mon, 27 Oct 2025 14:08:34 +0900 Subject: [PATCH 08/10] =?UTF-8?q?[feat]:=20ai=20=EA=B8=B0=EB=B0=98=20?= =?UTF-8?q?=EB=8F=85=ED=9B=84=EA=B0=90=20=EC=83=9D=EC=84=B1=20viewmodel=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20(#148)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../note/viewmodel/GroupNoteAiViewModel.kt | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteAiViewModel.kt 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 From 9b5098473ca1bba20a675896ce86025c4190bd6b Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Mon, 27 Oct 2025 14:08:44 +0900 Subject: [PATCH 09/10] =?UTF-8?q?[feat]:=20ai=20=EA=B8=B0=EB=B0=98=20?= =?UTF-8?q?=EB=8F=85=ED=9B=84=EA=B0=90=20=EC=83=9D=EC=84=B1=20api=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EC=97=90=20=EC=97=B0=EA=B2=B0=20(#148)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/group/note/screen/GroupNoteAiScreen.kt | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) 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 index e5d4df87..db6a067f 100644 --- 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 @@ -38,43 +38,33 @@ 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 -private const val DUMMY_AI_REVIEW = - "레이 커즈와일의 마침내 특이점이 시작된다는 읽는 내내 머릿속이 폭발하는 느낌이었다. 인공지능, 나노기술, 생명공학이 동시에 발전해서 결국 인간의 지능과 기계를 융합하는 시대가 온다는 주장인데, 솔직히 처음엔 SF소설 같은 이야기로 느껴졌다. \n" + - "\n" + - "하지만 커즈와일이 데이터와 과학적 근거를 차근차근 쌓아가며 기술 발전이 기하급수적이라는 걸 보여줄 때는 설득력이 꽤 컸다. 특히 인간 수명 연장과 의식 업로드에 대한 부분은 조금 무섭기도 하고 설레기도 했다. \n" + - "\n" + - "이 책이 단순히 기술 낙관주의가 아니라, 우리가 맞이할 변화에 대해 어떤 윤리적 기준과 사회적 합의가 필요한지 고민하게 만든 점이 좋았다. 읽고 나니 ‘미래는 멀리 있지 않다’는 말이 실감났다. 당장 내가 AI를 어떻게 활용하고, 기술과 함께 어떻게 성장할지 스스로 계획을 세우고 싶어졌다. 띱에서 다른 사람들은 이 책을 읽고 어떤 생각을 했을지 궁금하다. \n" + - "\n" + - "레이 커즈와일의 마침내 특이점이 시작된다는 읽는 내내 머릿속이 폭발하는 느낌이었다. 인공지능, 나노기술, 생명공학이 동시에 발전해서 결국 인간의 지능과 기계를 융합하는 시대가 온다는 주장인데, 솔직히 처음엔 SF소설 같은 이야기로 느껴졌다. \n" + - "\n" + - "하지만 커즈와일이 데이터와 과학적 근거를 차근차근 쌓아가며 기술 발전이 기하급수적이라는 걸 보여줄 때는 설득력이 꽤 컸다. 특히 인간 수명 연장과 의식 업로드에 대한 부분은 조금 무섭기도 하고 설레기도 했다. 이 책이 단순히 기술 낙관주의가 아니라, 우리가 맞이할 변화에 대해 어떤 윤리적 기준과 사회적 합의가 필요한지 고민하게 만든 점이 좋았다. 읽고 나니 ‘미래는 멀리 있지 않다’는 말이 실감났다. 당장 내가 AI를 어떻게 활용하고, 기술과 함께 어떻게 성장할지 스스로 계획을 세우고 싶어졌다. 띱에서 다른 사람들은 이 책 읽고 어떤 생각을 했을지 궁금하다. 레이 커즈와일의 마침내 특이점이 시작된다는 읽는 내내 머릿속이 폭발하는 느낌이었다. 인공지능, 나노기술, 생명공학이 동시에 발전해서 결국 인간의 지능과 기계를 융합하는 시대가 온다는 주장인데, 솔직히 처음엔 SF소설 같은 이야기로 느껴졌다. 하지만 커즈와일이 데이터와 과학적 근거를 차근차근 쌓아가며 기술 발전이 기하급수적이라는 걸 보여줄 때는 설득력이 꽤 컸다. 특히 인간 수명 연장과 의식 업로드에 대한 부분은 조금 무섭기도 하고 설레기도 했다. 이 책이 단순히 기술 낙관주의가 아니라, 우리가 맞이할 변화에 대해 어떤 윤리적 기준과 사회적 합의가 필요한지 고민하게 만든 점이 좋았다. 읽고 나니 ‘미래는 멀리 있지 않다’는 말이 실감났다. 당장 내가 AI를 어떻게 활용하고, 기술과 함께 어떻게 성장할지 스스로 계획을 세우고 싶어졌다. 띱에서 다른 사람들은 이 책 읽고 어떤 생각을 했을지 궁금하다." - @Composable fun GroupNoteAiScreen( - roomId: Int, // TODO: 이 roomId로 ViewModel에서 AI 독후감 요청 + roomId: Int, onBackClick: () -> Unit, + viewModel: GroupNoteAiViewModel = hiltViewModel() ) { - var isLoading by remember { mutableStateOf(true) } - var aiReviewText by remember { mutableStateOf(null) } + val uiState by viewModel.uiState.collectAsStateWithLifecycle() val clipboardManager = LocalClipboardManager.current var showToast by remember { mutableStateOf(false) } var showExitDialog by remember { mutableStateOf(false) } - // TODO: HiltViewModel을 사용해 실제 데이터 로직 구현 LaunchedEffect(key1 = roomId) { - delay(3000) - aiReviewText = DUMMY_AI_REVIEW - isLoading = false + viewModel.generateAiReview(roomId) } LaunchedEffect(showToast) { @@ -85,8 +75,7 @@ fun GroupNoteAiScreen( } GroupNoteAiContent( - isLoading = isLoading, - aiReviewText = aiReviewText, + uiState = uiState, showToast = showToast, showExitDialog = showExitDialog, onBackClick = { showExitDialog = true }, @@ -101,8 +90,7 @@ fun GroupNoteAiScreen( @Composable fun GroupNoteAiContent( - isLoading: Boolean, - aiReviewText: String?, + uiState: GroupNoteAiUiState, showToast: Boolean = false, showExitDialog: Boolean = false, onBackClick: () -> Unit, @@ -124,7 +112,7 @@ fun GroupNoteAiContent( ) Box(modifier = Modifier.fillMaxSize()) { - if (isLoading) { + if (uiState.isLoading) { // 로딩 중 Column( modifier = Modifier.fillMaxSize(), @@ -147,7 +135,7 @@ fun GroupNoteAiContent( textAlign = TextAlign.Center ) } - } else if (aiReviewText != null) { + } else if (uiState.aiReviewText != null) { // 로딩 완료 Column( modifier = Modifier @@ -174,7 +162,7 @@ fun GroupNoteAiContent( } Spacer(modifier = Modifier.height(10.dp)) Text( - text = aiReviewText, + text = uiState.aiReviewText, style = typography.feedcopy_r400_s14_h20, color = colors.White ) @@ -187,7 +175,7 @@ fun GroupNoteAiContent( .fillMaxWidth() .height(50.dp) .background(colors.Purple) - .clickable { onCopyClick(aiReviewText) }, + .clickable { onCopyClick(uiState.aiReviewText) }, contentAlignment = Alignment.Center ) { Text( @@ -196,6 +184,20 @@ fun GroupNoteAiContent( 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 + ) + } } } } @@ -242,8 +244,7 @@ fun GroupNoteAiContent( private fun GroupNoteAiScreenLoadingPreview() { ThipTheme { GroupNoteAiContent( - isLoading = true, - aiReviewText = null, + uiState = GroupNoteAiUiState(isLoading = true), onBackClick = {}, onCopyClick = {}, onConfirmExit = {}, @@ -257,8 +258,7 @@ private fun GroupNoteAiScreenLoadingPreview() { private fun GroupNoteAiScreenDonePreview() { ThipTheme { GroupNoteAiContent( - isLoading = false, - aiReviewText = DUMMY_AI_REVIEW, + uiState = GroupNoteAiUiState(isLoading = false, aiReviewText = "레이 커즈와일의 마침내 특이점이 시작된다는 읽는 내내 머릿속이 폭발하는 느낌이었다. 인공지능, 나노기술, 생명공학이 동시에 발전해서 결국 인간의 지능과 기계를 융합하는 시대가 온다는 주장인데, 솔직히 처음엔 SF소설 같은 이야기로 느껴졌다."), onBackClick = {}, onCopyClick = {}, onConfirmExit = {}, From ccad3dcce60dee55149512f2f63c6973632fa8d0 Mon Sep 17 00:00:00 2001 From: Naeun Kim <102296721+Nico1eKim@users.noreply.github.com> Date: Wed, 29 Oct 2025 11:55:42 +0900 Subject: [PATCH 10/10] =?UTF-8?q?[refactor]:=20ai=20=EB=8F=85=ED=9B=84?= =?UTF-8?q?=EA=B0=90=20=EC=88=AB=EC=9E=90=20=EC=88=98=EC=A0=95=20(#148)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 0339cf1e..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 @@ -749,8 +749,8 @@ fun GroupNoteContent( title = stringResource(R.string.ai_review_dialog_title), description = stringResource( R.string.ai_review_dialog_description, - uiState.recordReviewCount, - uiState.recordCount + uiState.recordCount, + 5 ), onConfirm = { onNavigateToAiReview()