-
Notifications
You must be signed in to change notification settings - Fork 3
[FEAT] AI 독후감 작성 #152
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Nico1eKim
merged 12 commits into
THIP-TextHip:develop
from
Nico1eKim:feat/#148-ai_book_review
Oct 29, 2025
Merged
[FEAT] AI 독후감 작성 #152
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
ad03330
Merge branch 'develop' of https://github.com/THIP-TextHip/THIP-Android
Nico1eKim fc60496
[ui]: 기록장 화면에 ai FAB 버튼, dialog 추가 (#148)
Nico1eKim e9a317b
[ui]: ai 독후감 화면 완료 (#148)
Nico1eKim 59a21a1
[feat]: ai 독후감 화면 navigation 완료 (#148)
Nico1eKim f30b09c
[feat]: ai 독후감 화면 뒤로가기 누르면 dialog 뜨도록 구현 (#148)
Nico1eKim f271978
[feat]: ai 독후감 화면 data layer 생성 (#148)
Nico1eKim d2d61e4
[feat]: 사용자의 ai 이용 횟수 및 기록 작성 횟수 조회 api 연결 (#148)
Nico1eKim ad31bc3
[feat]: ai 기반 독후감 생성 data layer 생성 (#148)
Nico1eKim 8a74b53
[feat]: ai 기반 독후감 생성 viewmodel 생성 (#148)
Nico1eKim 9b50984
[feat]: ai 기반 독후감 생성 api 화면에 연결 (#148)
Nico1eKim 0d57d9a
Merge branch 'develop' of https://github.com/THIP-TextHip/THIP-Androi…
Nico1eKim ccad3dc
[refactor]: ai 독후감 숫자 수정 (#148)
Nico1eKim File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
9 changes: 9 additions & 0 deletions
9
app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsAiReviewResponse.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| ) |
9 changes: 9 additions & 0 deletions
9
app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsAiUsageResponse.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
268 changes: 268 additions & 0 deletions
268
app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteAiScreen.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 = {} | ||
| ) | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
디코딩 내구성 강화: 기본값과(필요 시) @SerialName 권장
백엔드 필드 변화/누락 시 디코딩 실패를 피하려면 기본값을 두는 편이 안전합니다. 또한 서버 응답 키가 스네이크 케이스라면
@SerialName매핑이 필요합니다.다음 변경을 고려해주세요:
프로젝트 내 직렬화 관례 확인:
🏁 Script executed:
Length of output: 73002
@SerialName 및 기본값 추가 필수
프로젝트 전역 관례상 모든 응답 DTO는
@SerialName어노테이션을 사용합니다. 현재RoomsAiUsageResponse는 이를 누락했으며, 백엔드 필드 누락 시 디코딩 실패를 방지하려면 기본값도 필요합니다.다음 변경을 적용하세요:
주의: 서버 응답의 실제 필드명(snake_case 또는 camelCase)을 확인하고
@SerialName값을 맞춰주세요.🤖 Prompt for AI Agents