Skip to content

[FEAT] AI 독후감 작성#152

Merged
Nico1eKim merged 12 commits intoTHIP-TextHip:developfrom
Nico1eKim:feat/#148-ai_book_review
Oct 29, 2025
Merged

[FEAT] AI 독후감 작성#152
Nico1eKim merged 12 commits intoTHIP-TextHip:developfrom
Nico1eKim:feat/#148-ai_book_review

Conversation

@Nico1eKim
Copy link
Member

@Nico1eKim Nico1eKim commented Oct 29, 2025

➕ 이슈 링크


🔎 작업 내용

  • ai 독후감 화면 ui를 만들었습니다
  • ai 독후감 횟수 조회 api를 연결했습니다
  • ai 독후감 생성 api를 연결했습니다

📸 스크린샷

독후감 생성 기능

2025-10-27.2.24.08.mov

복사하기 기능

2025-10-27.2.24.28.mov


📢 리뷰어들에게

  • 영상에서 생성 횟수 잘못나오는건 수정했습니다 ~

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능
    • AI 독서감상문 생성 기능 추가 - 도서 노트에 대한 AI 자동 생성 리뷰 제공
    • AI 리뷰 텍스트 클립보드 복사 기능 추가
    • AI 리뷰 사용 현황 조회 기능 추가
    • AI 리뷰 생성 중 로딩 상태 및 오류 메시지 표시

@coderabbitai
Copy link

coderabbitai bot commented Oct 29, 2025

Walkthrough

AI 독후감 작성 기능을 구현합니다. 새로운 API 엔드포인트 두 개를 추가하고, 이에 대응하는 데이터 모델, 저장소 메서드, 뷰모델, UI 화면을 추가하며, 네비게이션 경로를 확장하고 필요한 리소스 문자열과 아이콘을 추가합니다.

Changes

코호트 / 파일 변경 요약
API 응답 모델
app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsAiReviewResponse.kt, RoomsAiUsageResponse.kt
직렬화 가능한 데이터 클래스 추가: RoomsAiReviewResponse (content, count), RoomsAiUsageResponse (recordReviewCount, recordCount)
서비스 계층
app/src/main/java/com/texthip/thip/data/service/RoomsService.kt
두 가지 새로운 엔드포인트 추가: GET /rooms/{roomId}/users/ai-usage, POST /rooms/{roomId}/record/ai-review
저장소 계층
app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt
두 가지 공개 suspend 함수 추가: getRoomsAiUsage(roomId), postRoomsAiReview(roomId)
AI 독후감 뷰모델 및 UI 상태
app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteAiViewModel.kt
새로운 GroupNoteAiViewModel 클래스와 GroupNoteAiUiState 데이터 클래스 추가; AI 독후감 생성 로직 및 에러 처리 구현
AI 독후감 화면
app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteAiScreen.kt
새로운 composable 함수 GroupNoteAiScreen, GroupNoteAiContent 및 미리보기 추가; 로딩, 완료, 에러 상태 UI 구현
기존 GroupNote 통합
app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt
onNavigateToAiReview 콜백 파라미터 추가; AI 독후감 다이얼로그 및 FAB 메뉴 항목 추가
GroupNote 뷰모델 확장
app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt
recordReviewCount, recordCount 필드 추가; CheckAiUsage 이벤트 및 loadAiUsageInfo() 메서드 구현
네비게이션
app/src/main/java/com/texthip/thip/ui/navigator/routes/GroupRoutes.kt, extensions/GroupNavigationExtensions.kt, navigations/GroupNavigation.kt
새로운 라우트 NoteAi(roomId) 추가; navigateToGroupNoteAi() 확장 함수 추가; AI 화면 composable 라우트 등록
UI 리소스
app/src/main/res/drawable/ic_ai_book_review.xml, values/strings.xml
벡터 드로어블 아이콘 및 9개의 UI 문자열 추가 (제목, 로딩, 완료, 클립보드, 다이얼로그 텍스트 등)

Sequence Diagram(s)

sequenceDiagram
    participant User as 사용자
    participant Screen as GroupNoteAiScreen
    participant ViewModel as GroupNoteAiViewModel
    participant Repo as RoomsRepository
    participant Service as RoomsService
    participant API as API

    User->>Screen: 화면 진입 (roomId 전달)
    Screen->>ViewModel: generateAiReview(roomId)
    ViewModel->>ViewModel: isLoading = true
    ViewModel->>Repo: postRoomsAiReview(roomId)
    Repo->>Service: postRoomsAiReview(roomId)
    Service->>API: POST /rooms/{roomId}/record/ai-review
    API-->>Service: BaseResponse<RoomsAiReviewResponse>
    Service-->>Repo: 응답 반환
    Repo-->>ViewModel: RoomsAiReviewResponse
    ViewModel->>ViewModel: aiReviewText = response.content<br/>isLoading = false
    Screen->>Screen: uiState 업데이트 감지
    Screen->>Screen: 완료 상태 UI 렌더링
    User->>Screen: 복사 버튼 클릭
    Screen->>Screen: 클립보드에 텍스트 복사<br/>토스트 메시지 표시
Loading
sequenceDiagram
    participant User as 사용자
    participant GroupNoteScreen as GroupNoteScreen
    participant GroupNoteViewModel as GroupNoteViewModel
    participant Repo as RoomsRepository
    participant Service as RoomsService
    participant API as API

    User->>GroupNoteScreen: 화면 진입 (roomId 전달)
    GroupNoteScreen->>GroupNoteViewModel: 화면 마운트
    GroupNoteViewModel->>GroupNoteViewModel: refreshAllData() 호출
    par 병렬 처리
        GroupNoteViewModel->>Repo: getRoomsAiUsage(roomId)
        Repo->>Service: getRoomsAiUsage(roomId)
        Service->>API: GET /rooms/{roomId}/users/ai-usage
        API-->>Service: BaseResponse<RoomsAiUsageResponse>
        Service-->>Repo: 응답 반환
        Repo-->>GroupNoteViewModel: RoomsAiUsageResponse
    and
        GroupNoteViewModel->>GroupNoteViewModel: 포스트 및 북 페이지 정보 로드
    end
    GroupNoteViewModel->>GroupNoteViewModel: recordReviewCount, recordCount 업데이트
    GroupNoteScreen->>GroupNoteScreen: uiState 업데이트 감지<br/>AI 독후감 버튼 활성화 상태 결정
    User->>GroupNoteScreen: AI 독후감 버튼 클릭
    GroupNoteScreen->>GroupNoteScreen: 확인 다이얼로그 표시
    User->>GroupNoteScreen: 확인
    GroupNoteScreen->>GroupNoteScreen: onNavigateToAiReview() 콜백
    GroupNoteScreen->>GroupNoteScreen: navigateToGroupNoteAi(roomId)로 이동
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

추가 검토 대상:

  • GroupNoteAiViewModel.kt의 에러 처리 로직: HttpException, IOException 등 다양한 예외 타입에 대한 처리 및 JSON 파싱 정확성 확인 필요
  • GroupNoteAiScreen.kt의 compose 상태 관리: 토스트 타이밍, 다이얼로그 상태, 클립보드 복사 등의 UI 상호작용 흐름 검증 필요
  • GroupNoteScreen.kt의 네비게이션 통합: 새로운 콜백 파라미터가 기존 preview 및 호출부에 올바르게 전파되었는지 확인 필요
  • GroupNavigation.kt의 라우트 등록: 새로운 GroupRoutes.NoteAi 라우트가 다른 네비게이션 그래프와의 상호작용에서 일관성 있게 처리되는지 확인 필요

Possibly related PRs

Suggested reviewers

  • rbqks529
  • JJUYAAA

Poem

🐰 독후감 기능을 녹여낸 코드,
AI의 손길로 그려진 리뷰,
로딩에서 완료까지 부드러운 흐름,
클립보드에 담긴 마음의 말들,
텍스힙의 새로운 이야기가 시작되네! ✨📚

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title Check ✅ Passed PR 제목 "[FEAT] AI 독후감 작성"은 이번 변경 사항의 핵심 내용을 명확하게 요약하고 있습니다. 제목이 간결하고 구체적이며, 실제로 추가된 AI 독후감 UI 화면, API 연동 등의 변경 내용과 정확히 일치합니다. 제목만으로도 팀 동료들이 이 PR의 주요 목표를 쉽게 파악할 수 있습니다.
Linked Issues Check ✅ Passed 연결된 이슈 #148의 모든 요구사항이 충족되었습니다. "AI 독후감 UI 만들기" 요구사항은 GroupNoteAiScreen에서 로딩, 성공, 에러 상태, 복사 기능 등을 포함한 완전한 UI 구현으로 충족되었으며, "AI 독후감 기능 연결" 요구사항은 RoomsService의 새로운 GET/POST 엔드포인트, RoomsRepository의 대응 메서드, GroupNoteAiViewModel의 상태 관리로 충족되었습니다. 추가로 GroupNoteViewModel에 AI 사용 횟수 조회 기능도 통합되었습니다.
Out of Scope Changes Check ✅ Passed PR의 모든 변경 사항이 이슈 #148의 범위 내에 있습니다. 데이터 모델(RoomsAiReviewResponse, RoomsAiUsageResponse), API 엔드포인트, Repository 메서드, 새로운 UI 화면(GroupNoteAiScreen), ViewModel, 네비게이션 설정, UI 문자열 및 아이콘 리소스에 이르기까지 모든 변경이 AI 독후감 작성 기능 구현에 직접적으로 관련되어 있으며, 범위를 벗어나는 수정 사항이 없습니다.
Description Check ✅ Passed PR 설명이 제공된 템플릿의 대부분 섹션을 충실히 따르고 있습니다. 이슈 링크(#148), 작업 내용(AI 독후감 UI 및 API 연동 내용), 스크린샷(2개의 기능 시연 영상), 리뷰어 메모(생성 횟수 버그 수정 언급) 등이 포함되어 있습니다. "해결하지 못한 과제" 섹션은 생략되어 있지만, 나머지 핵심 내용이 충분히 상세하고 명확하게 기술되어 있습니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt (1)

235-249: Back 버튼이 AI 다이얼로그를 닫지 못함(UX 막힘)

isOverlayVisible에 showAiReviewDialog를 포함하지만, BackHandler 분기에서 해당 케이스를 처리하지 않아 뒤로가기가 먹히지 않습니다. 아래처럼 분기를 추가하세요.

         } else if (isPinDialogVisible) {
             isPinDialogVisible = false
             postToPin = null
+        } else if (showAiReviewDialog) {
+            showAiReviewDialog = false
         }
🧹 Nitpick comments (9)
app/src/main/res/drawable/ic_ai_book_review.xml (1)

1-16: 하드코딩 색상 대신 테마 연동으로 접근성/다크모드 대응

#FEFEFE 고정 색상은 라이트/다크 전환 시 시인성 문제가 생길 수 있습니다. 벡터 루트에 tint를 적용해 테마 색으로 일괄 착색하는 방식을 권장합니다.

예시:

-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="24dp"
-    android:height="24dp"
-    android:viewportWidth="24"
-    android:viewportHeight="24">
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorOnSurface">
   <path
-      android:strokeColor="#FEFEFE"
+      android:strokeColor="#FFFFFFFF"
       android:strokeWidth="1.5"
       android:fillColor="#00000000"
       android:pathData="..."/>
   <path
-      android:strokeColor="#FEFEFE"
+      android:strokeColor="#FFFFFFFF"
       android:strokeWidth="1.5"
       android:fillColor="#00000000"
       android:pathData="..."/>
 </vector>
  • Compose Icon(painterResource(...), tint = ...)를 사용한다면, UI 쪽 tint로도 제어 가능합니다.
app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsAiReviewResponse.kt (1)

6-9: count 의미 명확화와 디코딩 내구성 보강

필드 이름이 모호합니다. 의미(예: 남은 생성 가능 횟수/누적 생성 횟수)를 코드에서 드러내면 추후 UI/로직 혼선을 줄일 수 있습니다. 기본값도 권장합니다.

선호안(의미를 ‘리뷰 누적 횟수’로 가정):

 @Serializable
-data class RoomsAiReviewResponse(
-    val content: String,
-    val count: Int
-)
+data class RoomsAiReviewResponse(
+    val content: String = "",
+    @SerialName("count")
+    val reviewCount: Int = 0
+)

변경이 부담된다면 최소한 기본값만 추가하고 KDoc으로 의미를 문서화하는 타협안도 가능합니다.

백엔드 스키마/문서에서 count의 의미와 케이스(스네이크/케멀)를 확인해주세요.

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

271-273: 잔여 횟수 표기 값 확인 및 하드코딩(5) 제거 필요

현재 GroupNoteScreen에서 이 문자열에 uiState.recordCount, 5를 전달합니다. recordCount가 ‘잔여’가 아닌 다른 의미(예: 누적 사용/작성 기록 수)라면 표기가 잘못됩니다. 또한 최대치 5는 하드코딩되어 변경에 취약합니다.

  • API 응답에서 한도(limit)와 잔여/사용 수치를 받아 정확한 값으로 대체하세요.
  • 한도가 API에 없다면 상수는 중앙 정의(예: BuildConfig/Remote Config)로 관리하세요.
app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt (2)

136-151: 중첩 코루틴/의미 없는 async-await 정리

loadAiUsageInfo(), loadBookPageInfo(), loadPosts() 모두 viewModelScope.launch를 내부에서 호출합니다. 그런데 refreshAllData()는 다시 async { … } 후 awaitAll()을 호출하고 있어, 실제로는 아무 것도 ‘기다리지’ 못합니다(레이스/가독성 저하).

  • 간단 해법: refreshAllData()에서 async/awaitAll과 외부 launch를 제거하고 함수들을 직접 호출하세요.

다음 패치를 권장합니다:

-    private fun refreshAllData() {
-        viewModelScope.launch {
-            val jobs = listOf(
-                async { loadPosts(isRefresh = true) },
-                async { loadBookPageInfo() },
-                async { loadAiUsageInfo() }
-            )
-            jobs.awaitAll()
-        }
-    }
+    private fun refreshAllData() {
+        loadPosts(isRefresh = true)
+        loadBookPageInfo()
+        loadAiUsageInfo()
+    }

장기적으로는 세 함수를 suspend로 전환하고 refreshAllData() 내부에서 coroutineScope { awaitAll(...) }로 동시 실행하는 구조를 추천합니다.

Also applies to: 153-161


207-209: AI 이용현황 조회 타이밍 노출

CheckAiUsage 이벤트는 비동기로 처리되어 즉시 최신 수치가 보장되지 않습니다. 다이얼로그를 띄우기 전 ‘로딩/오류’ 상태나 최소한의 업데이트 완료 신호가 필요합니다(아래 화면 로직 참조).

app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt (2)

596-610: 이용현황 조회 후 다이얼로그 표시로 전환

클릭 시 즉시 다이얼로그를 띄우고 CheckAiUsage를 발행하면, 아직 최신 수치가 반영되지 않은 상태로 %d/%d가 보일 수 있습니다.

  • 조회 완료 후 다이얼로그를 띄우는 플로우(예: pendingFlag + LaunchedEffect로 uiState 업데이트 감지)를 권장합니다.

742-764: 다이얼로그 카운트 값/문구 검증 및 매직 넘버(5) 삭제

  • 현재 첫 번째 인수로 uiState.recordCount를 사용합니다. ‘잔여 이용횟수’라면 recordReviewCount 또는 (limit - used)일 가능성이 큽니다. 정확한 필드 의미 확인이 필요합니다.
  • 최대치 5는 하드코딩을 지양하고 서버/설정에서 주입하세요.

예: limit을 uiState.aiUsageLimit으로 두고, used/remaining 중 의도에 맞는 값을 계산해 전달.

app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteAiViewModel.kt (1)

53-58: 예외 처리 정보를 개선하세요.

JSON 파싱 실패 시 예외(e)가 무시되어 디버깅이 어려울 수 있습니다. 파싱 실패 정보를 로깅하거나 더 구체적인 메시지를 제공하는 것을 고려해보세요.

다음과 같이 개선할 수 있습니다:

                                 try {
                                     val errorResponse = json.decodeFromString<BaseResponse<Unit>>(errorBody)
                                     errorResponse.message
                                 } catch (e: Exception) {
+                                    // 파싱 실패 시 원본 에러 바디 또는 HTTP 메시지 사용
+                                    // Log.w("GroupNoteAiViewModel", "Failed to parse error response", e)
                                     throwable.message ?: "HTTP 오류가 발생했습니다."
                                 }
app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteAiScreen.kt (1)

220-238: 애니메이션 지속 시간을 확인해주세요.

토스트 애니메이션의 durationMillis가 2000ms로 설정되어 있는데, 이는 토스트가 표시되는 총 3초(line 72) 중 상당 부분을 차지합니다. 애니메이션이 너무 느리게 느껴질 수 있습니다.

더 빠른 애니메이션을 원하는 경우 300-500ms 정도로 단축하는 것을 고려해보세요:

             enter = slideInVertically(
                 initialOffsetY = { -it },
-                animationSpec = tween(durationMillis = 2000)
+                animationSpec = tween(durationMillis = 300)
             ),
             exit = slideOutVertically(
                 targetOffsetY = { -it },
-                animationSpec = tween(durationMillis = 2000)
+                animationSpec = tween(durationMillis = 300)
             ),
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between de7165d and ccad3dc.

📒 Files selected for processing (13)
  • app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsAiReviewResponse.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsAiUsageResponse.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/service/RoomsService.kt (2 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteAiScreen.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt (7 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteAiViewModel.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt (7 hunks)
  • app/src/main/java/com/texthip/thip/ui/navigator/extensions/GroupNavigationExtensions.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt (4 hunks)
  • app/src/main/java/com/texthip/thip/ui/navigator/routes/GroupRoutes.kt (1 hunks)
  • app/src/main/res/drawable/ic_ai_book_review.xml (1 hunks)
  • app/src/main/res/values/strings.xml (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteAiScreen.kt (4)
app/src/main/java/com/texthip/thip/ui/common/topappbar/DefaultTopAppBar.kt (1)
  • DefaultTopAppBar (24-75)
app/src/main/java/com/texthip/thip/ui/common/modal/DialogPopup.kt (1)
  • DialogPopup (22-71)
app/src/main/java/com/texthip/thip/ui/common/modal/ToastWithDate.kt (1)
  • ToastWithDate (24-61)
app/src/main/java/com/texthip/thip/ui/theme/Theme.kt (1)
  • ThipTheme (41-69)
app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt (1)
app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteAiScreen.kt (1)
  • GroupNoteAiScreen (54-89)
app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt (2)
app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt (1)
  • onEvent (164-211)
app/src/main/java/com/texthip/thip/ui/common/modal/DialogPopup.kt (1)
  • DialogPopup (22-71)
🪛 detekt (1.23.8)
app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteAiViewModel.kt

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

(detekt.exceptions.SwallowedException)

🔇 Additional comments (14)
app/src/main/java/com/texthip/thip/ui/navigator/routes/GroupRoutes.kt (1)

74-75: 라우트 등록이 완벽하게 구성되어 있습니다.

검증 결과:

  • composable<GroupRoutes.NoteAi>이 GroupNavigation.kt 471번 라인에 등록됨
  • navigateToGroupNoteAi() 확장 함수가 GroupNavigationExtensions.kt 158번 라인에 정의됨
  • ✓ 라우트가 GroupNavigation.kt 413번 라인에서 올바르게 호출됨
  • ✓ 직렬화 및 역직렬화 체인이 정상 작동

추가 조치 불필요. 코드 변경사항이 완전히 통합되어 있습니다.

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

157-160: 그래프 등록 및 구현이 모두 올바르게 확인되었습니다.

검증 결과:

  • GroupRoutes.NoteAi 컴포저블이 GroupNavigation.kt:471에 올바르게 등록됨
  • GroupNoteAiScreen이 정상적으로 import되고 route 파라미터(roomId)와 콜백(onBackClick)이 정확히 전달됨
  • 구현이 기존 확장 함수 패턴과 일관성 있게 작성됨

launchSingleTop 전략은 필요에 따라 선택적으로 적용 가능한 최적화 사항입니다.

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

44-44: 문구 추가 적절

"AI 독서감상문 생성" 버튼 라벨 자연스럽습니다. 별도 이슈 없음.

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

412-414: AI 화면 네비게이션 연동 LGTM

  • onNavigateToAiReview → navigateToGroupNoteAi(roomId) 연결 적절.
  • NoteAi 경로 등록 및 뒤로가기 콜백도 일관적입니다.

Also applies to: 470-477, 18-18, 39-39

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

364-379: 레포 위임 로직 적절

  • handleBaseResponse().getOrThrow() 패턴 일관.
  • 예외는 Result로 포장되어 상위에서 처리 용이.
app/src/main/java/com/texthip/thip/data/service/RoomsService.kt (1)

221-229: 엔드포인트 정의 이상 없음

  • GET users/ai-usage, POST record/ai-review 시그니처 및 응답 타입 일관.
app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt (1)

88-89: 파라미터 전달/연동은 양호

onNavigateToAiReview 콜백 추가 및 전달 경로 일관.

Also applies to: 190-191, 208-209

app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteAiViewModel.kt (3)

18-22: UI 상태 구조가 명확합니다.

로딩, 성공, 에러 상태를 명확하게 표현하는 깔끔한 구조입니다.


24-29: LGTM!

Hilt 의존성 주입과 StateFlow 패턴이 올바르게 구현되었습니다.


31-31: 적절한 JSON 파서 설정입니다.

ignoreUnknownKeys를 사용하여 API 에러 응답 파싱 시 유연성을 확보했습니다.

app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteAiScreen.kt (4)

54-89: LGTM!

상태 관리와 side effect 처리가 적절하게 구현되어 있습니다. LaunchedEffectroomId를 키로 사용하여 올바르게 AI 리뷰를 트리거하고, 클립보드 복사와 토스트 자동 닫기 로직도 잘 구현되어 있습니다.


103-218: UI 구조가 명확하고 사용자 경험이 잘 고려되었습니다.

로딩, 성공, 에러 상태가 명확하게 분리되어 있고, 블러 효과와 다이얼로그를 통한 종료 확인 플로우가 좋습니다. 스크롤 가능한 콘텐츠와 하단 고정 버튼 배치도 적절합니다.


242-268: 프리뷰 구성이 좋습니다.

로딩과 완료 상태에 대한 프리뷰를 제공하여 디자인 타임 검증을 용이하게 합니다.


187-201: Modifier 체이닝 문법 오류를 수정하세요.

Line 189에서 fillMaxSize()padding() 사이에 점(.)이 누락되었습니다.

다음 diff를 적용하세요:

                 } else if (uiState.error != null) {
                     Column(
-                        modifier = Modifier.fillMaxSize().padding(16.dp),
+                        modifier = Modifier
+                            .fillMaxSize()
+                            .padding(16.dp),
                         verticalArrangement = Arrangement.Center,
                         horizontalAlignment = Alignment.CenterHorizontally
                     ) {

Likely an incorrect or invalid review comment.

Comment on lines +6 to +9
data class RoomsAiUsageResponse(
val recordReviewCount: Int,
val recordCount: Int
)
Copy link

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 매핑이 필요합니다.

다음 변경을 고려해주세요:

 @Serializable
 data class RoomsAiUsageResponse(
-    val recordReviewCount: Int,
-    val recordCount: Int
+    // 서버가 snake_case를 반환한다면 아래 주석을 해제하고 키를 맞춰주세요.
+    // @SerialName("record_review_count")
+    val recordReviewCount: Int = 0,
+    // @SerialName("record_count")
+    val recordCount: Int = 0
 )

프로젝트 내 직렬화 관례 확인:


🏁 Script executed:

#!/bin/bash
# @SerialName 사용 유무와 관례 확인
rg -nP --type=kotlin -C2 '@SerialName\('
rg -nP --type=kotlin -C2 '\bRoomsAiUsageResponse\b|recordReviewCount|record_count'

Length of output: 73002


@SerialName 및 기본값 추가 필수

프로젝트 전역 관례상 모든 응답 DTO는 @SerialName 어노테이션을 사용합니다. 현재 RoomsAiUsageResponse는 이를 누락했으며, 백엔드 필드 누락 시 디코딩 실패를 방지하려면 기본값도 필요합니다.

다음 변경을 적용하세요:

 @Serializable
 data class RoomsAiUsageResponse(
-    val recordReviewCount: Int,
-    val recordCount: Int
+    @SerialName("record_review_count")
+    val recordReviewCount: Int = 0,
+    @SerialName("record_count")
+    val recordCount: Int = 0
 )

주의: 서버 응답의 실제 필드명(snake_case 또는 camelCase)을 확인하고 @SerialName 값을 맞춰주세요.

🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsAiUsageResponse.kt
around lines 6 to 9, the data class is missing @SerialName annotations and
default values required by project conventions; add
kotlinx.serialization.SerialName to each property matching the server field
names (verify snake_case vs camelCase) and give each property a safe default
(e.g., 0) so decoding won’t fail if fields are absent, and ensure the file
imports kotlinx.serialization.SerialName.

Comment on lines +33 to +35
fun generateAiReview(roomId: Int) {
if (!_uiState.value.isLoading && _uiState.value.aiReviewText != null) return

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

재시도 동작을 확인해주세요.

가드 절이 이미 리뷰가 생성된 경우 재생성을 방지하지만, 이전에 에러가 발생한 경우에도 재시도를 막습니다. 에러 후 재시도가 필요한 경우 이 로직을 조정해야 할 수 있습니다.

다음 스크립트로 에러 상태에서의 재시도 처리를 확인할 수 있습니다:


🏁 Script executed:

#!/bin/bash
# Description: Check if there's retry mechanism in the UI layer for error state

# Search for retry button or mechanism in the screen
ast-grep --pattern 'Button($$$) {
  $$$
  generateAiReview($$$)
  $$$
}'

# Search for error state handling with actions
rg -n "uiState.error" --type=kt -A 5 -B 2

Length of output: 26


🏁 Script executed:

#!/bin/bash
# Description: Verify retry mechanism and error state handling in GroupNoteAiViewModel

# Check guard clause logic and error handling in the file
rg -n "generateAiReview\|uiState\|isLoading\|error" app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteAiViewModel.kt -A 3 -B 1

# Search for error state handling in UI/Composables
fd -e kt -path "*/ui/group/note/*" | head -20

# Look for Button or error callback patterns in the note UI
fd -e kt -path "*/ui/group/note/*" -exec grep -l "error\|retry\|Error" {} \;

Length of output: 337


🏁 Script executed:

#!/bin/bash
# Description: Check the complete generateAiReview function and error handling

# Display lines around the generateAiReview function
sed -n '33,75p' app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteAiViewModel.kt

Length of output: 1756


🏁 Script executed:

#!/bin/bash
# Description: Search for error state UI rendering and retry mechanisms

# Search for how error state is displayed in composables
rg -n "aiReviewText\|error.*State\|Error" app/src/main/java/com/texthip/thip/ui/group/note/ -A 2 -B 2

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Description: Find all Kotlin files in the group/note UI directory

find app/src/main/java/com/texthip/thip/ui/group/note -name "*.kt" | head -30

Length of output: 1513


🏁 Script executed:

#!/bin/bash
# Description: Search for screen composables that use generateAiReview

find app/src/main/java/com/texthip/thip/ui/group/note -name "*.kt" -exec grep -l "generateAiReview\|GroupNoteAiViewModel" {} \;

Length of output: 159


🏁 Script executed:

#!/bin/bash
# Description: Check for error UI handling patterns

find app/src/main/java/com/texthip/thip/ui/group/note -name "*.kt" -exec grep -l "error\|Error" {} \;

Length of output: 648


🏁 Script executed:

#!/bin/bash
# Description: Display GroupNoteAiScreen.kt to check error handling and retry mechanism

cat app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteAiScreen.kt

Length of output: 10467


🏁 Script executed:

#!/bin/bash
# Description: Search for all calls to generateAiReview in the screen

grep -n "generateAiReview" app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteAiScreen.kt -A 2 -B 2

Length of output: 101


throwable.message() 호출 구문 오류 수정 필요

검증 결과, 원본 리뷰 코멘트의 가드 절 분석은 부정확합니다. 가드 절(!_uiState.value.isLoading && _uiState.value.aiReviewText != null)은 에러 발생 후에도 재시도를 막지 않습니다. 왜냐하면 에러 상태에서 aiReviewTextnull로 유지되기 때문입니다.

그러나 실제 결정적 문제는 다음과 같습니다:

  • 라인 57, 60: throwable.message()throwable.message로 수정 필요. message는 메서드가 아닌 프로퍼티입니다. 현재 코드는 컴파일 오류를 발생시킵니다.
  • 라인 65: throwable.message는 올바른 구문입니다.

추가로, 에러 상태에서 재시도 버튼이 UI에 제공되지 않아 사용자는 화면을 벗어났다가 다시 돌아와야 재시도할 수 있습니다.

🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteAiViewModel.kt
around lines 33–65, change the guard so retries are blocked only when a
successful review exists (e.g., if (!_uiState.value.isLoading &&
_uiState.value.aiReviewText != null && _uiState.value.aiReviewError == null)
return), and replace the two incorrect throwable.message() calls on lines ~57
and ~60 with the property access throwable.message; ensure the ViewModel sets an
aiReviewError field on exceptions so the UI can show a retry button.

Comment on lines +49 to +65
val errorMessage = when (throwable) {
is HttpException -> {
val errorBody = throwable.response()?.errorBody()?.string()
if (errorBody != null) {
try {
val errorResponse = json.decodeFromString<BaseResponse<Unit>>(errorBody)
errorResponse.message
} catch (e: Exception) {
throwable.message()
}
} else {
throwable.message()
}
}
is IOException -> "네트워크 연결을 확인해주세요."
else -> throwable.message ?: "알 수 없는 오류가 발생했습니다."
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

message 프로퍼티 접근 오류를 수정하세요.

throwable.message()는 잘못된 문법입니다. message는 함수가 아닌 프로퍼티입니다.

다음 diff를 적용하여 수정하세요:

                         is HttpException -> {
                             val errorBody = throwable.response()?.errorBody()?.string()
                             if (errorBody != null) {
                                 try {
                                     val errorResponse = json.decodeFromString<BaseResponse<Unit>>(errorBody)
                                     errorResponse.message
                                 } catch (e: Exception) {
-                                    throwable.message()
+                                    throwable.message ?: "HTTP 오류가 발생했습니다."
                                 }
                             } else {
-                                throwable.message()
+                                throwable.message ?: "HTTP 오류가 발생했습니다."
                             }
                         }
🧰 Tools
🪛 detekt (1.23.8)

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

(detekt.exceptions.SwallowedException)

🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteAiViewModel.kt
around lines 49 to 65, the code incorrectly calls throwable.message() as a
function; change all usages of throwable.message() to the message property
(throwable.message) and ensure you handle nullability (e.g., use ?: to provide a
fallback string) so the catch branches compile and return a valid message.

@Nico1eKim Nico1eKim merged commit f491719 into THIP-TextHip:develop Oct 29, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] AI 독후감 작성 [(THIP2025-374)]

1 participant