Skip to content

[API] Rooms 나머지 API 연결#85

Closed
rbqks529 wants to merge 19 commits intoTHIP-TextHip:developfrom
rbqks529:feat/#83_API_Rooms
Closed

[API] Rooms 나머지 API 연결#85
rbqks529 wants to merge 19 commits intoTHIP-TextHip:developfrom
rbqks529:feat/#83_API_Rooms

Conversation

@rbqks529
Copy link
Collaborator

@rbqks529 rbqks529 commented Aug 14, 2025

➕ 이슈 링크


🔎 작업 내용

  • 모집중인 모임방 장르 색 수정
  • 모집중인 모임방에서 착 자세히 보기 화면으로 이동
  • 모임 만들기 로직 구현
  • 모임 참가 (비밀번호 입력 구현)
  • 모임 마감 구현
  • 모임 검색시 최근 검색어 로직 연결

📸 스크린샷


😢 해결하지 못한 과제

  • [] 아직 모임방 검색은 연결되어있지 않습니다


📢 리뷰어들에게

  • 모임방 검색은 연결이 안되어있고 이외에 모임방 로직은 구현완료 했습니다

Summary by CodeRabbit

  • 신기능

    • 모임방 비밀번호 입력 화면 및 비밀번호 기반 참여 흐름 추가
    • 최근 검색 서버 연동(조회/삭제) 및 UI 반영
    • 모집 마감 시 활동 화면으로 자동 이동
  • 개선

    • 책 선택 바텀시트에 실시간 검색·로딩 상태 및 빈결과 처리 추가
    • 모집 상세 배경 이미지 비동기 로딩 및 카테고리 색상 표시
    • 네비게이션/뒤로가기 흐름 개선 및 메시지 현지화 품질 향상
  • 변경

    • 도서 응답 필드명 및 일부 API 응답 구조 변경(아이디/이미지 URL 등)

@coderabbitai
Copy link

coderabbitai bot commented Aug 14, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Caution

Review failed

The pull request is closed.

Walkthrough

모임방 도메인으로 서비스/레포지토리/모델을 재구성하고, 비공개 방 잠금(Unlock) 플로우 및 모집 마감 흐름을 API와 연동했다. RecentSearch가 별도 서비스/레포지토리로 분리되었고, 다수의 UI·ViewModel이 RoomsRepository와 StringResourceProvider로 전환되었다.

Changes

Cohort / File(s) Summary
DI 및 서비스 정의
app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt, app/src/main/java/com/texthip/thip/data/service/RoomsService.kt, app/src/main/java/com/texthip/thip/data/service/RecentSearchService.kt, app/src/main/java/com/texthip/thip/data/service/BookService.kt, app/src/main/java/com/texthip/thip/data/service/GroupService.kt
GroupService 제거, RecentSearchService 추가, RoomsService에 모임방 엔드포인트 대거 추가, BookService에서 recent-search 관련 엔드포인트 제거 및 getBooks 경로 변경.
레포지토리 계층
app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt, .../RecentSearchRepository.kt, .../BookRepository.kt, .../GroupRepository.kt
RoomsRepository로 모임방 CRUD/조회/참여/비밀방/마감 API 구현, RecentSearchRepository 신설, BookRepository에서 recent-search API 제거 및 getBooks 반환 타입 변경, GroupRepository 삭제.
모델: rooms 패키지 정리
app/src/main/java/com/texthip/thip/data/model/rooms/...
기존 group.* 패키지 타입들을 rooms.*로 이동/정리, 비밀방 요청/응답 및 마감 응답 추가, RoomRecruitingResponsecategoryColor 필드 추가.
모델: 책 응답 변경
app/src/main/java/com/texthip/thip/data/model/book/response/BookListResponse.kt
BookSavedResponsebookId 추가, imageUrlbookImageUrl로 필드명/직렬화 키 변경 및 생성자 시그니처 재배치.
문자열/리소스 제공자
app/src/main/java/com/texthip/thip/data/provider/StringResourceProvider.kt, app/src/main/res/values/strings.xml
DI용 StringResourceProvider 추가 및 다수의 성공/에러 문자열 리소스 추가/수정.
UI / MakeRoom 관련
app/src/main/java/com/texthip/thip/ui/group/makeroom/*
방 생성 콜백이 roomId를 반환하도록 변경, 북 검색 디바운스·검색 상태·결과 추가, ViewModel이 RoomsRepositoryStringResourceProvider 사용.
Recruit / Unlock / Navigation
app/src/main/java/com/texthip/thip/ui/group/room/*, .../ui/navigator/*
Recruit 화면에 도서 상세/비밀번호/플레잉 네비게이션 콜백 추가, Unlock 화면·뷰모델 신설 및 비밀번호 API 연동, 모집 마감 API 연동 및 라우트(RoomUnlock) 추가.
검색/최근검색 관련
app/src/main/java/com/texthip/thip/ui/group/search/*, app/src/main/java/com/texthip/thip/ui/search/viewmodel/*
GroupSearch가 ViewModel 기반으로 전환되어 서버 RecentSearch와 연동, SearchBookViewModel이 RecentSearchRepository 사용으로 최근검색 로직 이전.
기타 임포트/패키지 경로 변경
.../ui/group/**, 여러 파일
다수 컴포저블/뷰모델의 모델 import 경로를 com.texthip.thip.data.model.rooms.response로 변경, 관련 생성자 타입 변경(예: ViewModel 의존성 변경).

Sequence Diagram(s)

sequenceDiagram
  participant UI as GroupRoomRecruitScreen
  participant VM as GroupRoomRecruitViewModel
  participant Repo as RoomsRepository
  participant SVC as RoomsService

  UI->>VM: 화면 진입(loadRoomDetail(roomId))
  VM->>Repo: getRoomRecruiting(roomId)
  Repo->>SVC: GET /rooms/{roomId}/recruiting
  SVC-->>Repo: BaseResponse<RoomRecruitingResponse>
  Repo-->>VM: Result<RoomRecruitingResponse?>
  VM-->>UI: roomDetail 업데이트

  UI->>VM: 참여 버튼 클릭
  VM->>Repo: joinOrCancelRoom(roomId, type="JOIN")
  Repo->>SVC: POST /rooms/{roomId}/participation
  SVC-->>Repo: BaseResponse<RoomJoinResponse>
  Repo-->>VM: Result<String>
  VM-->>UI: 토스트/버튼상태 갱신

  UI->>VM: 모집 마감 클릭
  VM->>Repo: closeRoom(roomId)
  Repo->>SVC: POST /rooms/{roomId}/close
  SVC-->>Repo: BaseResponse<RoomCloseResponse>
  Repo-->>VM: Result<RoomCloseResponse>
  VM-->>UI: shouldNavigateToRoomPlayingScreen=true, roomId 설정
Loading
sequenceDiagram
  participant UI as GroupRoomUnlockScreen
  participant VM as GroupRoomUnlockViewModel
  participant Repo as RoomsRepository
  participant SVC as RoomsService

  UI->>VM: checkPassword(roomId, password)
  VM->>Repo: postParticipateSecreteRoom(roomId, password)
  Repo->>SVC: POST /rooms/{roomId}/secret
  SVC-->>Repo: BaseResponse<RoomSecreteRoomResponse>
  Repo-->>VM: Result<RoomSecreteRoomResponse>
  alt matched == true
    VM-->>UI: passwordMatched=true
    UI-->>UI: onSuccessNavigation()
  else
    VM-->>UI: passwordMatched=false, 에러 표시
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Assessment against linked issues

Objective Addressed Explanation
모집 중인 모임방에서 나머지 네비게이션 연결 (#83)
모집중인 모임방 장르 색 변경 (#83)
방생성 api 연결 (#83)
책검색 api 연결 (#83)
비공개방 비밀번호 입력 api 연결 (#83)
모집 마감하기 연결 (#83)

Assessment — Out-of-scope changes

Code Change Explanation
StringResourceProvider 추가 (app/src/main/java/com/texthip/thip/data/provider/StringResourceProvider.kt) 리소스 주입/현지화 편의성 추가로 보이며, 링크된 이슈(#83)는 rooms API 연동·네비게이션 중심이어서 직접적 요구사항은 아님.
RecentSearchService/RecentSearchRepository 추가 (app/src/main/java/com/texthip/thip/data/service/RecentSearchService.kt, .../repository/RecentSearchRepository.kt) recent-search 분리 작업은 검색 UX 관련으로 유용하나 #83 체크리스트 항목은 rooms API 연결이 주 목적이므로 별도 개선 사항으로 분류됩니다.

Possibly related issues

Possibly related PRs

Suggested labels

✅ OK merge

Suggested reviewers

  • JJUYAAA
  • Nico1eKim

Poem

깡총깡총 방을 만들었네, 라우트는 새로 늘리고,
비밀 숫자 네 개로 문을 열면 친구들이 쏟아지네.
색은 장르를 닮아 반짝이고, 책은 검색해 골라 넣고,
마감 종이 울리면 모두 모여 읽자, 깡총! 🐰✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c43d7d4 and 8b573e9.

📒 Files selected for processing (3)
  • app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt (6 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt (13 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • 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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR implements the remaining Rooms API integrations including room creation, password authentication, recruitment closing, and search functionality.

  • Refactors repository structure to separate Rooms and RecentSearch functionality
  • Implements room creation workflow with proper navigation and error handling
  • Adds password authentication for private rooms with UI validation
  • Integrates recruitment closing and real-time search functionality

Reviewed Changes

Copilot reviewed 53 out of 53 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
RoomsRepository.kt Implements complete rooms API functionality including creation, joining, and password verification
GroupNavigation.kt Adds room unlock screen navigation and proper backstack management
GroupRoomRecruitScreen.kt Integrates password authentication flow and book detail navigation
SearchBookViewModel.kt Updates to use dedicated RecentSearchRepository
StringResourceProvider.kt New provider for centralized string resource management
data/model/rooms/ Moves all room-related models to proper package structure

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
You can also share your feedback on Copilot code review for a chance to win a $100 gift card. Take the survey.

@@ -1,19 +1,123 @@
package com.texthip.thip.data.repository

import android.R.attr.type
Copy link

Copilot AI Aug 14, 2025

Choose a reason for hiding this comment

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

This import appears to be unused and should be removed. It's importing from Android framework but the 'type' attribute is not used anywhere in the file.

Suggested change
import android.R.attr.type

Copilot uses AI. Check for mistakes.
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import android.util.Log
Copy link

Copilot AI Aug 14, 2025

Choose a reason for hiding this comment

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

This Log import appears to be unused and should be removed to keep the imports clean.

Suggested change
import android.util.Log

Copilot uses AI. Check for mistakes.
text = detail.category,
style = typography.info_m500_s12,
color = colors.SocialScience
color = Color(detail.categoryColor.removePrefix("#").toLong(16) or 0xFF000000)
Copy link

Copilot AI Aug 14, 2025

Choose a reason for hiding this comment

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

This color parsing could throw NumberFormatException if categoryColor contains invalid hex characters. Consider adding error handling or validation.

Suggested change
color = Color(detail.categoryColor.removePrefix("#").toLong(16) or 0xFF000000)
color = runCatching {
Color(detail.categoryColor.removePrefix("#").toLong(16) or 0xFF000000)
}.getOrElse { colors.White }

Copilot uses AI. Check for mistakes.
isLoadingMore = false,
hasMore = false, // null 응답 시 더 이상 로드할 수 없음을 명시
error = if (cursor == null) "모집중인 방 정보를 찾을 수 없습니다." else null
error = if (cursor == null) stringResourceProvider.getString(R.string.error_recruiting_rooms_not_found) else null
Copy link

Copilot AI Aug 14, 2025

Choose a reason for hiding this comment

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

The variable name 'cursor' is misleading here as it represents the entire response, not just a cursor value. Consider renaming for clarity.

Copilot uses AI. Check for mistakes.
@rbqks529 rbqks529 closed this Aug 14, 2025
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: 24

🔭 Outside diff range comments (16)
app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomMainResponse.kt (1)

14-14: deadlineDate 타입(문자열) 유지 적합성 — 검증 결과 및 권장 조치

요약: 코드베이스는 deadlineDate를 String으로 사용하고 있으며 DateUtils.extractDaysFromDeadline은 "일 뒤" 패턴만 파싱합니다. 반면 일부 더미/샘플에는 "D-2" 등 다른 포맷이 있어 포맷 일관성/파싱 취약점이 확인됩니다.

주의가 필요한 위치:

  • app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomMainResponse.kt — deadlineDate: String (라인 14)
  • app/src/main/java/com/texthip/thip/utils/rooms/DateUtils.kt — extractDaysFromDeadline(dateString: String) { dateString.contains("일 뒤") → ... } (현재 "일 뒤"만 처리)
  • app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt — DateUtils.extractDaysFromDeadline(room.deadlineDate) (라인 ~191)
  • app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt — extractDaysFromDeadline(detail.recruitEndDate / rec.recruitEndDate) (라인 ~333, ~393)
  • app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookGroupScreen.kt — extractDaysFromDeadline(item.deadlineEndDate) (라인 ~60)
  • app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt — 샘플 데이터에 "D-2", "D-3", ... 사용 (라인 ~225–270)
  • app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt — 샘플에 "3일 뒤", "2일 뒤", "7일 뒤" 사용 (라인 ~234–286)

권장 조치 (간단):

  • 권장(우선): API를 ISO-8601(또는 epoch) 형식으로 표준화하고, RoomMainResponse에서 문자열 대신 적절한 날짜 타입(kotlinx-datetime의 LocalDate/Instant 등)으로 직렬화/역직렬화한 뒤 도메인에서 날짜 처리를 하고 UI에서 상대 표기(예: "3일 뒤" 또는 "D-3")로 포맷하세요.
  • 대안(API가 상대 문자열을 반환하는 경우): 서버 포맷을 하나로 고정하고(예: 항상 "n일 뒤" 또는 항상 "D-n"), DateUtils를 그 포맷에 맞게 확장 및 단위 테스트 추가하세요.
  • 단기방책: DateUtils에 "D-\d+" 패턴 및 ISO-8601 파싱을 추가해 다양한 입력을 안전하게 처리하도록 하세요(입력 검증/예외 처리 포함). 샘플/목 데이터도 선택한 포맷으로 통일해야 합니다.

결론: 현재 구현은 상대 문자열("일 뒤")에 의존하고 포맷 불일치 위험이 있으므로 API/모델 또는 유틸 중 하나를 정리해야 합니다.

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

4-6: mock 패키지 의존 제거 필요 — 수정 필요

GroupMakeRoomUiState 및 관련 컴포넌트/뷰모델이 com.texthip.thip.ui.group.makeroom.mock의 BookData / GroupMakeRoomRequest에 의존하고 있습니다. 실제 API 연동 전환 전까지 mock 네임스페이스를 제거/대체해 주세요.

수정이 필요한 파일(검색 결과 기반):

  • app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomUiState.kt (BookData, GroupMakeRoomRequest)
  • app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt (BookData 사용/생성)
  • app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt (BookData 사용)
  • app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupSelectBook.kt (BookData)
  • app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookSearchBottomSheet.kt (BookData, dummyGroupBooks, dummySavedBooks)
  • app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookListWithScrollbar.kt (BookData)
  • app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupMakeRoomRequest.kt (mock 요청 모델)
  • app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupBookData.kt 및 기타 mock/*.kt (더미 데이터)
  • app/src/main/java/com/texthip/thip/ui/feed/mock/FeedData.kt (BookData import)
  • app/src/main/java/com/texthip/thip/ui/feed/screen/FeedWriteScreen.kt (dummyGroupBooks, dummySavedBooks)
  • app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookScreen.kt 및 ui/search/component/* (BookData 사용)

권장 조치:

  • BookData: UI 전용 모델 또는 공용 도메인/DTO로 이동(예: ui.model 또는 data.model.book 계층으로 분리).
  • GroupMakeRoomRequest: 실제 전송 모델(data.model.rooms.request.* 등)으로 교체.
  • mock/dummy 데이터는 previews/tests 전용 위치로 이동하거나 테스트용으로 분리.
  • 관련 파일들의 import 경로를 교체하고 컴파일/런타임에서 누락되는 부분이 없는지 확인.

필요하면 전체 의존 검색 명령(rg)을 돌려 남은 참조를 제거해 주세요.

app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMainCard.kt (1)

145-157: 진행도 막대 fraction 범위 보장 필요 (잠재적 크래시/레이아웃 오류)

fillMaxWidth(fraction = percentage / 100f)에서 서버 값이 0~100 범위를 벗어나면 Compose에서 예외 또는 레이아웃 이상이 발생할 수 있습니다. 입력을 [0f, 1f]로 클램핑하세요.

-                    val percentage = data.userPercentage.toFloat()
+                    val percentage = data.userPercentage.coerceIn(0, 100).toFloat()
                     Box(
...
                         Box(
                             modifier = Modifier
-                                .fillMaxWidth(fraction = percentage / 100f)
+                                .fillMaxWidth(fraction = percentage / 100f)
                                 .fillMaxHeight()
                                 .background(color = colors.Purple, shape = RoundedCornerShape(12.dp))
                         )
app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt (2)

34-38: reset 시 동시 로딩 가능성 — 이전 Job 취소로 경쟁 상태 제거 제안

현재 isLoadingData가 true일 때 reset=true로 호출하면 기존 로딩과 새로운 로딩이 겹칠 수 있습니다. state 업데이트 순서 뒤엉킴 방지를 위해 이전 로딩 Job을 취소하고 새 Job을 시작하는 방식이 안전합니다.

아래처럼 이전 Job을 추적/취소하도록 반영해 주세요.

 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.Job
 ...
     private var isLastPage = false
     private var isLoadingData = false
+    private var loadJob: Job? = null
 ...
-        viewModelScope.launch {
+        if (reset && isLoadingData) {
+            loadJob?.cancel()
+        }
+        loadJob = viewModelScope.launch {
             try {
                 isLoadingData = true
                 
                 if (reset) {
                     updateState { it.copy(isLoading = true, myRooms = emptyList(), hasMore = true) }
                     nextCursor = null
                     isLastPage = false
                 } else {
                     updateState { it.copy(isLoadingMore = true) }
                 }
 
                 repository.getMyRoomsByType(uiState.value.currentRoomType.value, nextCursor)
                     .onSuccess { myRoomListResponse ->
                         myRoomListResponse?.let { response ->
                             val currentList = if (reset) emptyList() else uiState.value.myRooms
                             updateState { 
                                 it.copy(
                                     myRooms = currentList + response.roomList,
                                     error = null,
                                     hasMore = !response.isLast
                                 ) 
                             }
                             nextCursor = response.nextCursor
                             isLastPage = response.isLast
                         } ?: run {
                             // null 응답 시 더 이상 로드할 수 없음을 명시
                             updateState { it.copy(hasMore = false) }
                             isLastPage = true
                         }
                     }
                     .onFailure { exception ->
-                        updateState { it.copy(error = exception.message) }
+                        updateState { it.copy(error = exception.message ?: exception.localizedMessage ?: "알 수 없는 오류가 발생했어요.") }
                     }
             } finally {
                 isLoadingData = false
                 updateState { it.copy(isLoading = false, isLoadingMore = false) }
             }
-        }
+        }

Also applies to: 38-77


38-51: 조치 필요 — RoomsRepository에 IO 디스패처 적용 필요 (withContext(Dispatchers.IO) 없음 확인됨)

검사 결과 app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt에서 withContext(Dispatchers.IO) 호출을 찾지 못했습니다. viewModelScope는 기본적으로 Main 디스패처이므로, Repository에서 네트워크/디스크 I/O를 명시적으로 IO로 옮기지 않으면 메인 스레드 차단 위험이 있습니다.

주의할 위치:

  • app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt — withContext(Dispatchers.IO) 사용 미발견
  • app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt (약 38–51행) — viewModelScope.launch에서 Repository 호출

권장 변경 예시(간단한 패턴):
suspend fun getMyRoomsByType(type: RoomType, cursor: String?): Result =
withContext(Dispatchers.IO) {
// 기존 네트워크/디스크 호출 로직
}

(필요한 import: kotlinx.coroutines.Dispatchers, kotlinx.coroutines.withContext)

대안: Retrofit의 suspend 함수나 사용하는 클라이언트가 이미 백그라운드 스레드에서 처리하는 경우에는 명시적 withContext가 불필요할 수 있으니, 구현 상세를 확인해 주세요.

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

81-100: 컴파일 오류: onFailure 블록에서 break 사용 불가

Result.onFailure { ... }는 람다이며, 그 안에서 break로 외부 for-loop를 빠져나올 수 없습니다. 현재 코드는 컴파일되지 않습니다. 루프 본문에서 결과를 분기 처리하도록 리팩터링하세요.

아래처럼 수정하면 컴파일 오류가 해소되고, 실패/종료 조건에서 올바르게 루프를 중단합니다.

-                repository.getMyJoinedRooms(page)
-                    .onSuccess { joinedRoomsResponse ->
-                        joinedRoomsResponse?.let { response ->
-                            updateState { 
-                                it.copy(
-                                    myJoinedRooms = it.myJoinedRooms + response.roomList,
-                                    hasMoreMyGroups = !response.last
-                                )
-                            }
-                            loadedPagesCount++
-                            currentMyGroupsPage = page + 1
-                        } ?: run {
-                            // null 응답 시 더 이상 로드할 수 없음을 명시
-                            updateState { it.copy(hasMoreMyGroups = false) }
-                        }
-                    }
-                    .onFailure {
-                        break
-                    }
+                val result = repository.getMyJoinedRooms(page)
+                if (result.isSuccess) {
+                    val joinedRoomsResponse = result.getOrNull()
+                    if (joinedRoomsResponse != null) {
+                        updateState {
+                            it.copy(
+                                myJoinedRooms = it.myJoinedRooms + joinedRoomsResponse.roomList,
+                                hasMoreMyGroups = !joinedRoomsResponse.last
+                            )
+                        }
+                        loadedPagesCount++
+                        currentMyGroupsPage = page + 1
+                        if (joinedRoomsResponse.last) break
+                    } else {
+                        // null 응답 시 더 이상 로드할 수 없음을 명시
+                        updateState { it.copy(hasMoreMyGroups = false) }
+                        break
+                    }
+                } else {
+                    // 실패 시 배치 로딩 종료
+                    break
+                }

161-179: refreshGroupData에서 awaitAll이 실질적으로 기다리지 않는 문제 — Job 반환 및 joinAll로 동기화

현재 loadUserName()loadRoomSections() 내부에서 다시 viewModelScope.launch를 호출하므로, async { loadUserName() }는 즉시 완료되어 awaitAll()이 실질적으로 대기하지 않습니다. isRefreshing 플래그가 너무 빨리 내려갈 수 있습니다. 두 함수를 Job을 반환하도록 바꾸고 joinAll로 기다리세요.

+import kotlinx.coroutines.Job
+import kotlinx.coroutines.joinAll
 ...
-    private fun loadUserName() {
-        viewModelScope.launch {
-            repository.getUserName()
-                .onSuccess { userName ->
-                    updateState { it.copy(userName = userName) }
-                }
-        }
-    }
+    private fun loadUserName(): Job = viewModelScope.launch {
+        repository.getUserName()
+            .onSuccess { userName ->
+                updateState { it.copy(userName = userName) }
+            }
+    }
 ...
-    private fun loadRoomSections() {
-        viewModelScope.launch {
-            updateState { it.copy(roomSectionsError = null) }
-
-            val genresResult = repository.getGenres()
-            val selectedIndex = uiState.value.selectedGenreIndex
-            val selectedGenre = if (genresResult.isSuccess) {
-                val genres = genresResult.getOrThrow()
-                if (selectedIndex >= 0 && selectedIndex < genres.size) {
-                    genres[selectedIndex]
-                } else {
-                    genres.firstOrNull() ?: Genre.getDefault()
-                }
-            } else {
-                Genre.getDefault()
-            }
-
-            repository.getRoomSections(selectedGenre)
-                .onSuccess { roomMainList ->
-                    updateState { it.copy(roomMainList = roomMainList) }
-                }
-                .onFailure { error ->
-                    updateState { it.copy(roomSectionsError = error.message) }
-                }
-        }
-    }
+    private fun loadRoomSections(): Job = viewModelScope.launch {
+        updateState { it.copy(roomSectionsError = null) }
+        val genresResult = repository.getGenres()
+        val selectedIndex = uiState.value.selectedGenreIndex
+        val selectedGenre = if (genresResult.isSuccess) {
+            val genres = genresResult.getOrThrow()
+            if (selectedIndex >= 0 && selectedIndex < genres.size) {
+                genres[selectedIndex]
+            } else {
+                genres.firstOrNull() ?: Genre.getDefault()
+            }
+        } else {
+            Genre.getDefault()
+        }
+        repository.getRoomSections(selectedGenre)
+            .onSuccess { roomMainList ->
+                updateState { it.copy(roomMainList = roomMainList) }
+            }
+            .onFailure { error ->
+                updateState { it.copy(roomSectionsError = error.message) }
+            }
+    }
 ...
-                val jobs = listOf(
-                    async { loadUserName() },
-                    async {
-                        resetMyGroupsData()
-                        loadPageBatchSuspend()
-                    },
-                    async { loadRoomSections() },
-                )
-
-                jobs.awaitAll()
+                val jobs = listOf(
+                    loadUserName(),
+                    viewModelScope.launch {
+                        resetMyGroupsData()
+                        loadPageBatchSuspend()
+                    },
+                    loadRoomSections(),
+                )
+                jobs.joinAll()

Also applies to: 47-55, 121-146, 10-16


148-157: 중복 호출 제거 및 Repository의 IO 처리 필요 — 검증 결과

검증 결과:

  • app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt에 있는 getGenres()는 non-suspend(≈line 34)이고, 해당 파일에서 withContext(Dispatchers.IO)를 사용하는 흔적이 없습니다. 따라서 ViewModel에서 직접 호출하면 메인 스레드 블로킹 위험이 있습니다.
  • selectGenre에서의 repository.getGenres() 호출은 loadRoomSections가 이미 범위 검증을 수행하므로 중복 호출입니다. 불필요하니 제거하는 것이 안전합니다.

조치 권장 (간단 요약)

  • GroupViewModel.kt — selectGenre (app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt: ~148-157)
    • repository.getGenres() 호출 제거 및 단순화:
-    fun selectGenre(genreIndex: Int) {
-        val genresResult = repository.getGenres()
-        if (genresResult.isSuccess) {
-            val genres = genresResult.getOrThrow()
-            if (genreIndex >= 0 && genreIndex < genres.size && genreIndex != uiState.value.selectedGenreIndex) {
-                updateState { it.copy(selectedGenreIndex = genreIndex) }
-                loadRoomSections()
-            }
-        }
-    }
+    fun selectGenre(genreIndex: Int) {
+        if (genreIndex != uiState.value.selectedGenreIndex) {
+            updateState { it.copy(selectedGenreIndex = genreIndex) }
+            loadRoomSections()
+        }
+    }
  • RoomsRepository.kt — app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt (line ~34)
    • getGenres()가 실제로 IO(네트워크/디스크)를 수행하면 메인 스레드 블로킹을 피하도록 변경하십시오. 예:
suspend fun getGenres(): Result<List<Genre>> = withContext(Dispatchers.IO) {
    runCatching { genreManager.getGenres() }
}
  • 또는 genreManager.getGenres()가 완전히 메모리 기반이고 빠르다면(블로킹 없음) 그 사실을 명확히 주석으로 남기고 호출부에서 중복 호출을 제거하세요.
  • Repository 내 다른 suspend 함수들도 호출하는 하위 API(예: Retrofit)이 자체적으로 IO 스레드를 사용하는지, 아니면 명시적 withContext(Dispatchers.IO)가 필요한지 검토하세요.

필요한 파일

  • app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt (selectGenre)
  • app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt (getGenres 및 기타 suspend 메서드)

요약: selectGenre의 중복 호출은 제거하고, RoomsRepository.getGenres()가 블로킹이면 suspend + withContext(Dispatchers.IO)로 안전하게 이동하세요.

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

59-61: 에러 처리 시 사용자 메시지 노출 누락

.onFailure에서 로딩 해제만 수행합니다. 공통 에러 메시지(네트워크 오류 등)를 노출해 주세요.

-                .onFailure { error ->
-                    updateState { it.copy(isLoading = false) }
-                }
+                .onFailure { error ->
+                    updateState { it.copy(isLoading = false) }
+                    showToastMessage(stringResourceProvider.getString(R.string.error_common_retry))
+                }
app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt (2)

49-55: 장르 로드 실패 분기 없음 — 사용자 피드백 추가 필요

getGenres 실패 시 상태가 그대로여서 사용자에게 원인을 전달하지 못합니다. 최소한 일반 오류 메시지를 상태에 반영하세요.

         viewModelScope.launch {
-            roomsRepository.getGenres()
-                .onSuccess { genresList ->
-                    updateState { it.copy(genres = genresList) }
-                }
+            roomsRepository.getGenres()
+                .onSuccess { genresList ->
+                    updateState { it.copy(genres = genresList) }
+                }
+                .onFailure {
+                    updateState { it.copy(errorMessage = stringResourceProvider.getString(R.string.error_data_load_failed)) }
+                }
         }

191-203: 폼/책 유효성 실패 시 UI 상태에도 에러 반영

현재 onError 콜백만 호출되어 화면의 LaunchedEffect(uiState.errorMessage)에 반영되지 않습니다. 일관된 에러 경로를 위해 상태에도 메시지를 설정하세요.

-        if (!currentState.isFormValid) {
-            onError(stringResourceProvider.getString(R.string.error_form_validation))
+        if (!currentState.isFormValid) {
+            val msg = stringResourceProvider.getString(R.string.error_form_validation)
+            updateState { it.copy(errorMessage = msg) }
+            onError(msg)
             return
         }
@@
-        if (selectedBook?.isbn == null) {
-            onError(stringResourceProvider.getString(R.string.error_book_info_invalid))
+        if (selectedBook?.isbn == null) {
+            val msg = stringResourceProvider.getString(R.string.error_book_info_invalid)
+            updateState { it.copy(errorMessage = msg) }
+            onError(msg)
             return
         }
app/src/main/java/com/texthip/thip/ui/group/search/screen/GroupSearchScreen.kt (1)

33-34: 잘못된 컴포넌트 import 경로 (빌드 실패 가능성)

GroupRecentSearch는 search/component 패키지에 존재합니다. 현재 myroom/component를 import하고 있어 컴파일 오류가 발생할 수 있습니다.

다음과 같이 수정해 주세요:

-import com.texthip.thip.ui.group.myroom.component.GroupRecentSearch
+import com.texthip.thip.ui.group.search.component.GroupRecentSearch
app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt (4)

285-289: RoomMates 화면에 하드코딩된 roomId 수정 필요

동적 roomId를 사용해야 합니다. 현재 1로 고정되어 있어 잘못된 데이터가 표시됩니다.

-        GroupRoomMatesScreen(
-//            roomId = roomId,
-            roomId = 1,
+        GroupRoomMatesScreen(
+            roomId = roomId,

310-312: Group Room 화면에서 Note 탭 진입 시 하드코딩된 roomId 수정 필요

동일하게 동적 roomId를 사용해야 합니다.

-        GroupRoomScreen(
-//            roomId = roomId,
-            roomId = 1,
+        GroupRoomScreen(
+            roomId = roomId,

345-347: NoteCreate 화면의 roomId 하드코딩 수정 필요

NoteCreate도 route의 roomId를 사용해야 합니다.

-        GroupNoteCreateScreen(
-            roomId = 1,
+        GroupNoteCreateScreen(
+            roomId = roomId,

367-369: VoteCreate 화면의 roomId 하드코딩 수정 필요

VoteCreate 역시 동적 roomId를 전달해야 합니다.

-        GroupVoteCreateScreen(
-//            roomId = roomId,
-            roomId = 1,
+        GroupVoteCreateScreen(
+            roomId = roomId,
♻️ Duplicate comments (2)
app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt (2)

84-106: 책 목록 로드 예외 삼킴 — 에러 메시지 상태 반영

catch 블록에서 단순 초기화만 수행해 원인 파악이 어렵습니다. 공통 네트워크 에러 포맷으로 errorMessage를 함께 설정하세요.

detekt가 지적한 SwallowedException을 해소합니다.

         } catch (e: Exception) {
-            updateState { it.copy(savedBooks = emptyList(), groupBooks = emptyList()) }
+            updateState {
+                it.copy(
+                    savedBooks = emptyList(),
+                    groupBooks = emptyList(),
+                    errorMessage = stringResourceProvider.getString(R.string.error_network_error, e.message ?: "")
+                )
+            }
         } finally {

127-151: 검색 디바운스 흐름 적절하나 예외 삼킴 — 에러 상태 노출 필요

검색 예외 시에도 원인을 전달해 재시도/네트워크 확인을 유도할 수 있도록 errorMessage를 설정하세요.

         } catch (e: Exception) {
-            updateState { it.copy(searchResults = emptyList(), isSearching = false) }
+            updateState {
+                it.copy(
+                    searchResults = emptyList(),
+                    isSearching = false,
+                    errorMessage = stringResourceProvider.getString(R.string.error_network_error, e.message ?: "")
+                )
+            }
         }
🧹 Nitpick comments (44)
app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupEmptyBookSheetContent.kt (2)

29-29: 상단 패딩 추가는 문제없지만, 인셋 기반 간격으로 전환 고려

고정 20.dp 대신 WindowInsets(예: statusBarsPadding(), windowInsetsPadding)나 디자인 토큰(테마 spacing)을 쓰면 디바이스별·시트 높이 변경에도 일관성이 좋아집니다.


45-53: 버튼 고정 폭(97.dp)은 다국어/접근성에 취약 — 가변 폭 및 클릭 핸들 단순화 권장

  • 고정 폭은 문자열 길이에 따라 잘림/줄바꿈 이슈가 생길 수 있습니다. 너비 제한을 제거하고 내용에 맞게 확장되도록 두는 편이 안전합니다.
  • 접근성 측면에서 최소 높이 48.dp 권장입니다.
  • onClick 람다는 직접 전달로 단순화 가능합니다.

아래처럼 수정 제안합니다.

         ActionMediumButton(
             text = stringResource(R.string.group_register_book),
             contentColor = colors.White,
             backgroundColor = colors.Purple,
             modifier = Modifier
-                .width(97.dp)
-                .height(44.dp),
-            onClick = { onRequestBook() },
+                .height(48.dp),
+            onClick = onRequestBook,
         )
app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomJoinResponse.kt (1)

8-10: String 기반의 type 필드 → 타입 세이프한 enum 도입 제안

서버가 허용하는 타입 값이 제한적이라면 문자열 대신 enum으로 모델링하면 오타/유효성 문제를 줄일 수 있습니다. 요청/응답에서 동일한 타입을 재사용할 수 있도록 공용 enum을 rooms 루트 패키지에 두는 것을 권장합니다.

  • 변경안(응답 모델):
 data class RoomJoinResponse(
     @SerialName("roomId") val roomId: Int,
-    @SerialName("type") val type: String
+    @SerialName("type") val type: RoomJoinType
 )
  • 공용 enum 추가(새 파일): app/src/main/java/com/texthip/thip/data/model/rooms/RoomJoinType.kt
package com.texthip.thip.data.model.rooms

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
enum class RoomJoinType {
    // 서버 실제 값에 맞춰 SerialName을 조정해 주세요.
    @SerialName("JOIN") JOIN,
    @SerialName("CANCEL") CANCEL,
}

요청 모델(RoomJoinRequest)도 동일 enum으로 맞추면 양방향 일관성을 확보할 수 있습니다.

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

4-9: UI 상태가 mock.RoomType에 의존 — 프로덕션 타입으로의 치환 여부 확인 필요

프로덕션 코드에서 mock 패키지 타입 사용은 유지보수/의미적 혼동을 야기할 수 있습니다. 도메인 혹은 UI 전용 실 타입(예: RoomsFilterType 등)으로 대체하는지 확인 요청드립니다.

app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomJoinRequest.kt (1)

7-9: 요청의 type도 enum으로 타입 세이프하게 모델링 제안

응답과 동일하게 요청도 enum을 사용하면 문자열 오타에 의한 서버 4xx를 줄일 수 있습니다.

  • 변경안(요청 모델):
 data class RoomJoinRequest(
-    @SerialName("type") val type: String
+    @SerialName("type") val type: RoomJoinType
 )
  • 공용 enum은 다음 위치를 재사용: com.texthip.thip.data.model.rooms.RoomJoinType
    서버 스펙상의 실제 문자열 값(JOIN/CANCEL 등)을 @SerialName에 반영했는지 확인해 주세요.
app/src/main/java/com/texthip/thip/data/service/RecentSearchService.kt (1)

14-16: 최근검색 type 문자열 파라미터의 타입 세이프티 강화 제안

type: String은 오타 입력 시 런타임 실패만 발생합니다. 허용 값이 제한적이라면 enum/sealed class로 캡슐화하여 컴파일 타임 검증을 확보하는 것을 권장합니다.

예시

enum class RecentSearchType(val value: String) { BOOK("BOOK"), ROOM("ROOM") }
// 사용: @Query("type") type: String = RecentSearchType.BOOK.value
app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupPager.kt (2)

31-42: 변수 이름 섀도잉 제거로 가독성 개선 제안

외부 scale과 내부 scale 변수가 섀도잉되고 있어 가독성이 떨어집니다. 의미가 분명한 이름으로 정리해주세요.

아래와 같이 리팩터를 제안합니다.

-    val scale = 0.86f
+    val scaleFactor = 0.86f
...
-        val pageSpacing = with(this) {
-            (-(cardWidth - (cardWidth * scale)) / 2f) + desiredGap
-        }
+        val pageSpacing = with(this) {
+            (-(cardWidth - (cardWidth * scaleFactor)) / 2f) + desiredGap
+        }
...
-                val scale = if (isCurrent) 1f else 0.86f
+                val itemScale = if (isCurrent) 1f else scaleFactor
...
-                        .graphicsLayer {
-                            scaleX = scale
-                            scaleY = scale
+                        .graphicsLayer {
+                            scaleX = itemScale
+                            scaleY = itemScale

Also applies to: 98-113


75-80: 불필요한 초기 스크롤 효과 제거 제안

rememberPagerState(initialPage = startPage)로 이미 시작 페이지가 설정되므로, pagerState.currentPage == 0 조건은 충족되지 않습니다. 해당 LaunchedEffect는 실행되지 않으며 제거 가능합니다.

-            // 초기 로딩 시에만 시작 페이지로 이동
-            LaunchedEffect(Unit) {
-                if (pagerState.currentPage == 0) {
-                    pagerState.scrollToPage(startPage)
-                }
-            }
app/src/main/java/com/texthip/thip/ui/group/done/screen/GroupDoneScreen.kt (1)

113-121: LazyColumn key 추가로 스크롤/성능 개선

리스트에 고유 key를 부여하면 컴포지션 재활용과 스크롤 위치 안정성에 유리합니다.

-                    items(uiState.expiredRooms) { room ->
+                    items(
+                        items = uiState.expiredRooms,
+                        key = { room -> room.roomId }
+                    ) { room ->
app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMainCard.kt (1)

78-84: 이미지 로딩 안정성 개선: placeholder/error 지정

URL이 비어있거나 실패할 때를 대비해 placeholder/error를 명시적으로 지정하는 쪽이 안전합니다. 현재는 Elvis로 리소스를 넘기지만, Coil의 placeholder/error 파라미터를 활용하면 의도가 더 분명합니다.

-                AsyncImage(
-                    model = data.bookImageUrl ?: R.drawable.img_book_cover_sample,
-                    contentDescription = "책 이미지",
-                    modifier = Modifier
-                        .size(width = 80.dp, height = 107.dp),
-                    contentScale = ContentScale.Crop
-                )
+                AsyncImage(
+                    model = data.bookImageUrl,
+                    placeholder = painterResource(id = R.drawable.img_book_cover_sample),
+                    error = painterResource(id = R.drawable.img_book_cover_sample),
+                    contentDescription = "책 이미지",
+                    modifier = Modifier
+                        .size(width = 80.dp, height = 107.dp),
+                    contentScale = ContentScale.Crop
+                )

가능하다면 contentDescription도 stringResource로 이동하면 접근성/현지화에 유리합니다.

app/src/main/java/com/texthip/thip/data/model/rooms/request/CreateRoomRequest.kt (1)

7-17: password 직렬화 동작 및 서버 기대치 확인 권고

password: String? = null은 kotlinx.serialization에서 기본값(null)과 encodeDefaults=false 조합이면 필드를 생략합니다. 서버가 “비공개가 아닐 때 password 필드 생략”을 기대하는지, 혹은 명시적 null/빈 문자열을 요구하는지 확인이 필요합니다. 또한 isPublic=true일 때는 password가 항상 null이어야 하는 정책 검증도 선행되어야 합니다.

정책이 “비공개 방만 password 포함”이라면, 호출부에서 생성 시점을 다음과 같이 일관화하는 것을 권장합니다:

  • isPublic=true → password=null
  • isPublic=false → password는 4~N 자리 등 유효성 만족
app/src/main/java/com/texthip/thip/data/model/rooms/response/MyRoomListResponse.kt (1)

13-22: room.type의 문자열 의존도 축소 제안 (타입 세이프티)

type: String은 오타/미정의 값에 취약합니다. API 계약이 안정적이라면 enum 또는 @serializable sealed class 기반으로 타입 안전성을 높이는 것을 권장합니다. UI 단의 RoomUtils.isRecruitingByType(room.type)도 enum 전환 시 간결해집니다.

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

59-62: 중복 내비게이션 방지: launchSingleTop 설정 제안

비밀번호 입력 화면은 연속 탭/중복 트리거가 발생하기 쉬워 백스택에 동일 화면이 누적될 수 있습니다. singleTop 옵션으로 중복 푸시를 방지하는 것이 안전합니다.

 // 비밀번호 입력 화면으로 이동
 fun NavHostController.navigateToGroupRoomUnlock(roomId: Int) {
-    navigate(GroupRoutes.RoomUnlock(roomId))
+    navigate(GroupRoutes.RoomUnlock(roomId)) {
+        launchSingleTop = true
+    }
 }
app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt (1)

49-58: 무한 스크롤 로딩 상태(UI) 반영 누락 가능성

reset=false 분기에서 내부 플래그(isLoadingMore)는 true로 설정되지만, UI 상태(isLoadingMore)는 시작 시점에 true로 업데이트되지 않습니다. 로딩 인디케이터/중복 호출 방지 로직이 uiState 기반이라면 반영이 되지 않을 수 있습니다.

다음과 같이 호출 시작 시 UI 상태도 즉시 업데이트하는 것을 권장합니다.

if (reset) {
    if (isInitialLoading) return
    isInitialLoading = true
    // updateState { it.copy(isLoading = true) } // 이미 존재
} else {
    if (isLoadingMore || isLastPage) return
    isLoadingMore = true
    updateState { it.copy(isLoadingMore = true) } // 시작 시점에 UI에도 반영
}
app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt (1)

156-167: LazyColumn 성능/안정성 개선: key 지정 권장

대용량 스크롤에서 항목 식별을 위해 key를 지정하면 재구성/스크롤 안정성이 좋아집니다.

-                        items(uiState.myRooms) { room ->
+                        items(
+                            items = uiState.myRooms,
+                            key = { it.roomId }
+                        ) { room ->
app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt (1)

69-71: 에러 메시지 null 가능성 처리

exception.message가 null일 수 있습니다. 사용자 메시지 가시성을 위해 기본 문구로 폴백하세요.

위의 Job 취소 제안 diff에 함께 반영했습니다:

  • exception.message ?: exception.localizedMessage ?: "알 수 없는 오류가 발생했어요."
app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt (1)

25-26: 제공자 함수 네이밍 일관성 정리 제안

다른 provider들이 provideXxxService 패턴을 따르는 반면, 이 함수만 providesRoomsService로 s가 붙어 있습니다. 일관성 유지를 위해 이름을 통일해 주세요.

-    fun providesRoomsService(retrofit: Retrofit): RoomsService =
+    fun provideRoomsService(retrofit: Retrofit): RoomsService =
         retrofit.create(RoomsService::class.java)
app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt (1)

7-8: RoomsRepository 의존성 전환 LGTM + 미사용 파라미터 정리 제안

  • RoomsRepository로의 전환은 전체 리팩터링 흐름과 일치합니다. Good.
  • 다만 @ApplicationContext context는 현재 사용되지 않습니다. 불필요한 주입은 제거하는 편이 낫습니다.
-import android.content.Context
 ...
-import dagger.hilt.android.qualifiers.ApplicationContext
 ...
 class GroupViewModel @Inject constructor(
-    private val repository: RoomsRepository,
-    @param:ApplicationContext private val context: Context
+    private val repository: RoomsRepository
 ) : ViewModel() {

Also applies to: 20-22

app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomRecruitingResponse.kt (1)

18-19: categoryColor 필드의 직렬화 안전성 검토

백엔드에서 categoryColor가 항상 제공되지 않는 경우, 현재 비-null String으로 선언하면 역직렬화가 실패합니다. 안전하게 처리하려면 nullable + 기본값을 권장합니다.

-    @SerialName("categoryColor") val categoryColor: String,
+    @SerialName("categoryColor") val categoryColor: String? = null,

백엔드 스펙이 “항상 존재”로 확정되어 있다면 현 상태 유지 가능하나, 앱 크래시 리스크를 최소화하려면 위와 같이 방어적으로 선언하는 것을 추천합니다.

app/src/main/java/com/texthip/thip/ui/search/viewmodel/SearchBookViewModel.kt (2)

204-205: "BOOK" 매직 스트링 제거: 타입/상수로 치환 권장

카테고리를 하드코딩 문자열로 두면 오타/변경에 취약합니다. 최소한 파일 레벨 상수로 추출하거나, 가능하면 Enum/Sealed class로 타입 안정성을 확보하세요.

빠른 적용(이 파일 내 상수):

-            recentSearchRepository.getRecentSearches("BOOK")
+            recentSearchRepository.getRecentSearches(RECENT_SEARCH_CATEGORY_BOOK)

이 변경을 지원하기 위한 추가 코드(이 파일 상단 또는 클래스 바깥):

private const val RECENT_SEARCH_CATEGORY_BOOK = "BOOK"

권장(전역 Enum):

// com.texthip.thip.data.model.recentsearch.RecentSearchCategory.kt
enum class RecentSearchCategory(val value: String) { BOOK("BOOK"), ROOM("ROOM") }

그 후 사용부:

recentSearchRepository.getRecentSearches(RecentSearchCategory.BOOK.value)

226-233: 삭제 후 즉시 UI를 낙관적 업데이트로 반영하면 UX 개선

API 성공 콜백에서 전체 목록 재요청 대신, 현재 상태에서 해당 항목만 제거하면 반응성이 좋아집니다(실패 시 롤백 가능). 예:

-            recentSearchRepository.deleteRecentSearch(recentSearchId)
-                .onSuccess {
-                    loadRecentSearches() // 삭제 성공 시 목록 새로고침
-                }
+            recentSearchRepository.deleteRecentSearch(recentSearchId)
+                .onSuccess {
+                    // 즉시 UI 갱신
+                    updateState { state ->
+                        state.copy(
+                            recentSearches = state.recentSearches.filterNot { it.recentSearchId == recentSearchId }
+                        )
+                    }
+                    // 내부 Map도 정리 (필요 시)
+                    recentSearchMap.entries.removeAll { it.value.recentSearchId == recentSearchId }
+                    // 필요하면 백그라운드에서 최신 동기화
+                    // loadRecentSearches()
+                }
                 .onFailure {
                     // 삭제 실패는 조용히 처리
                 }
app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitUiState.kt (1)

15-18: 단발성 네비게이션 플래그는 이벤트로 관리 권장

shouldNavigateToGroupScreen, shouldNavigateToRoomPlayingScreen, roomId 조합은 상태가 재구성 시 재소비되는 문제가 생기기 쉽습니다. UI 이벤트(Consumable Event/SingleShot)를 별도로 관리하세요.

예시(권장 설계):

data class GroupRoomRecruitUiState(
  // ...
  val navigation: Navigation? = null
)

sealed interface Navigation {
  data object ToGroupScreen : Navigation
  data class ToRoomPlayingScreen(val roomId: Int) : Navigation
}

소비 후 navigation = null로 리셋하면 재소비를 방지할 수 있습니다.

현재 ViewModel에서 위 플래그들을 네비게이션 직후 즉시 false/null로 되돌리는지 확인 부탁드립니다(재구성 시 재발동 방지).

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

21-21: 문자열 파라미터 대신 타입 안전한 enum/sealed class 사용을 고려해 주세요

type: String은 오타/유효하지 않은 값 전파 위험이 큽니다. 서버 스펙이 고정된 소수의 타입만 허용한다면 enum 또는 sealed class로 대체해 컴파일 타임 검증을 강화하는 것을 권장합니다.

app/src/main/java/com/texthip/thip/data/provider/StringResourceProvider.kt (1)

18-20: 복수형(plurals) 지원 메서드 추가 제안

다국어/현지화에서 수량에 따른 문구 변화가 필요한 경우가 많습니다. getQuantityString을 추가해 주세요.

다음 변경을 제안합니다:

 import android.content.Context
 import androidx.annotation.StringRes
+import androidx.annotation.PluralsRes
 import dagger.hilt.android.qualifiers.ApplicationContext
 import javax.inject.Inject
 import javax.inject.Singleton
@@
     fun getString(@StringRes resId: Int, vararg formatArgs: Any): String {
         return context.getString(resId, *formatArgs)
     }
+
+    fun getQuantityString(@PluralsRes resId: Int, quantity: Int, vararg formatArgs: Any): String {
+        return context.resources.getQuantityString(resId, quantity, *formatArgs)
+    }
app/src/main/java/com/texthip/thip/data/repository/RecentSearchRepository.kt (2)

15-19: 최근 검색어 조회 흐름 일관성 좋습니다

runCatching -> handleBaseResponse().getOrThrow() 패턴으로 예외 흐름을 통일한 점 좋습니다. 다만 type: String은 enum/sealed class로 대체 시 오타 방지에 유리합니다.


22-27: Null 본문을 Unit으로 매핑하는 처리 의도 확인

getOrThrow() 이후 ?: Unit으로 null 본문을 흡수하는 처리는 실무적으로 문제없지만, 서버가 항상 본문 없는 200/204를 보낸다면 네트워크 계층에서 Result<Unit>로 바로 매핑하는 편이 가독성이 높습니다. 현재 형태 유지도 가능하니 향후 API 계약에 맞춰 정리해 주세요.

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

51-56: 데이터 null 케이스에서 사용자 피드백 추가 권장

현재는 isLoading=false만 설정합니다. 사용자에게 로딩 실패를 알리는 토스트 등을 추가해 주세요.

-                        updateState { 
-                            it.copy(
-                                isLoading = false
-                            )
-                        }
+                        updateState { it.copy(isLoading = false) }
+                        showToastMessage(stringResourceProvider.getString(R.string.error_room_info_not_found))

104-106: 취소 실패 메시지 세분화 권장

참여/취소 실패 시 공통 메시지로만 처리 중입니다. close 로직처럼 에러 코드를 분기해 사용자 메시지를 세분화하면 UX가 개선됩니다. 에러 매핑은 ViewModel이 아닌 공통 에러 매퍼/도메인 계층으로 이동하는 것을 권장합니다.


120-122: 상태 접근 일관성 유지 (uiState vs _uiState)

동일 메서드에서 uiState_uiState를 혼용하면 혼동을 유발할 수 있습니다. 읽기는 uiState.value로 통일하세요.

-                val currentRoomId = _uiState.value.roomDetail?.roomId
+                val currentRoomId = uiState.value.roomDetail?.roomId
app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookSearchBottomSheet.kt (4)

53-58: 공백만 입력된 검색어 무시: isNotBlank 사용 권장

스페이스만 입력되어도 검색으로 간주됩니다. isNotBlank()trim()을 사용해 불필요한 검색을 줄이세요.

-    val displayBooks = if (searchText.isNotEmpty()) {
+    val displayBooks = if (searchText.isNotBlank()) {
         searchResults
     } else {
         currentBooks
     }

60-60: 검색 결과 없음 상태 판별 개선

검색 중에는 빈 결과를 에러로 보지 않도록 하고, 공백-only 검색어도 제외하세요.

-    val showNoSearchResultsError = searchText.isNotEmpty() && displayBooks.isEmpty() && !isSearching
+    val showNoSearchResultsError =
+        searchText.isNotBlank() && displayBooks.isEmpty() && !isSearching

83-96: 검색 결과 없음 UI 카피 적합성 재검토

EmptyBookSheetContent는 “도서 등록 요청” 맥락에 가깝습니다. “검색 결과가 없습니다” 전용 컴포저블/문구를 별도 제공하는 편이 UX에 더 적합합니다.

원하시면 ‘검색 결과 없음’ 전용 컴포저블 초안을 제공하겠습니다.


71-79: 검색어 트리밍 후 전달 권장

엔드포인트에 불필요한 선행/후행 공백이 전달되지 않도록 트리밍하세요.

-                    onSearch = { onSearch(searchText) },
+                    onSearch = { onSearch(searchText.trim()) },
app/src/main/java/com/texthip/thip/ui/group/search/viewmodel/GroupSearchViewModel.kt (1)

40-46: 매직 스트링("ROOM") 상수화로 오타 리스크 제거

검색 타입을 하드코딩하면 오타/변경에 취약합니다. 파일 내 상수로 추출해 반복 사용을 줄이세요.

 class GroupSearchViewModel @Inject constructor(
     private val recentSearchRepository: RecentSearchRepository
 ) : ViewModel() {
 
+    companion object {
+        private const val RECENT_SEARCH_TYPE_ROOM = "ROOM"
+    }
+
     private val _uiState = MutableStateFlow(GroupSearchUiState())
     val uiState: StateFlow<GroupSearchUiState> = _uiState.asStateFlow()
     
@@
-            recentSearchRepository.getRecentSearches("ROOM")
+            recentSearchRepository.getRecentSearches(RECENT_SEARCH_TYPE_ROOM)
@@
-            recentSearchRepository.deleteRecentSearch(recentSearchId)
+            recentSearchRepository.deleteRecentSearch(recentSearchId)

Also applies to: 82-88

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

386-396: 중복/유사 메시지 정리 제안

성공/실패 메시지가 기존 키(예: group_participant_close_alarm)와 의미가 중복됩니다. 메시지 키를 통합하거나 용도를 명확히 분리하면 유지보수성이 좋아집니다.

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

84-106: 네트워크 병렬화로 책 목록 로딩 지연 최소화(선택)

저장/참여중 책 목록을 순차 호출하고 있어 대기 시간이 늘어납니다. async/await로 병렬화하면 UX가 개선됩니다.

예시(파일 외 참고용):

viewModelScope.launch {
    updateState { it.copy(isLoadingBooks = true) }
    try {
        coroutineScope {
            val savedDeferred = async { bookRepository.getBooks("SAVED") }
            val groupDeferred = async { bookRepository.getBooks("JOINING") }

            savedDeferred.await().onSuccess { res ->
                updateState { it.copy(savedBooks = res?.bookList?.map { dto -> dto.toBookData() } ?: emptyList()) }
            }.onFailure {
                updateState { it.copy(savedBooks = emptyList()) }
            }

            groupDeferred.await().onSuccess { res ->
                updateState { it.copy(groupBooks = res?.bookList?.map { dto -> dto.toBookData() } ?: emptyList()) }
            }.onFailure {
                updateState { it.copy(groupBooks = emptyList()) }
            }
        }
    } finally {
        updateState { it.copy(isLoadingBooks = false) }
    }
}
app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomUnlockScreen.kt (2)

48-48: 수명 주기 안전한 수집으로 교체 권장: collectAsStateWithLifecycle 사용

ViewModel Flow 수집은 lifecycle-aware API를 통해 화면 회전/백그라운드 전환 시 안전합니다.

다음과 같이 수정해 주세요:

+import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@
-    val uiState by viewModel.uiState.collectAsState()
+    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

61-67: 성공 시 상태 리셋하여 중복 네비게이션 방지

성공 후 화면이 즉시 Pop되지 못하는 경우(애니메이션/지연 등) 재컴포지션에 의해 중복 트리거가 발생할 여지가 있습니다. 성공 직후 ViewModel 상태를 리셋해 주세요.

             true -> {
                 // 비밀번호 일치: 성공 콜백 호출하여 네비게이션 처리
                 onSuccessNavigation()
+                viewModel.resetPasswordState()
             }
app/src/main/java/com/texthip/thip/ui/group/search/screen/GroupSearchScreen.kt (1)

49-49: collectAsStateWithLifecycle 사용 권장

UI 수명 주기와 연동된 수집으로 전환하는 것이 안전합니다.

+import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@
-    val uiState by viewModel.uiState.collectAsState()
+    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt (1)

148-155: contentDescription는 stringResource 사용 권장

접근성/다국어 대응을 위해 하드코딩 문자열 대신 stringResource 사용을 권장합니다. 리소스가 없다면 추가를 제안합니다.

예)

  • strings.xml에 group_room_recruit_bg_image_cd 추가
  • contentDescription = stringResource(R.string.group_room_recruit_bg_image_cd)
app/src/main/java/com/texthip/thip/data/service/RoomsService.kt (1)

73-79: 오타: Secrete → Secret 네이밍 정정 권장 (API/모델 전반)

postParticipateSecreteRoom, RoomSecreteRoomRequest/Response의 Secrete(분비하다)는 오타로 보입니다. Secret이 정확합니다. 네이밍은 API 사용성과 유지보수성에 중요하므로 전반적인 리네이밍을 고려해 주세요.

범위가 넓다면:

  • 기존 메서드/타입은 deprecated로 유지
  • Secret 네이밍으로 신규 메서드/타입 추가 후 점진적 마이그레이션
app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt (3)

3-3: 불필요한 import 제거 필요

android.R.attr.type은 사용되지 않는 import입니다. 제거해 주세요.

-import android.R.attr.type

84-84: 에러 메시지 일관성 개선

NoSuchElementException 사용 시 에러 메시지가 일관되지 않습니다. 모두 "응답을 받을 수 없습니다"로 통일하거나, 더 구체적인 메시지로 개선하는 것이 좋겠습니다.

예를 들어:

  • "모임방 생성 응답이 null입니다."
  • "모임방 참여/취소 응답이 null입니다."
  • "비밀번호 확인 응답이 null입니다."
  • "모집 마감 응답이 null입니다."

Also applies to: 94-94, 104-104, 114-114


120-239: 기록장 API 메서드들의 일관성 개선

기록장 관련 API 메서드들이 새로 추가된 메서드들과 다른 스타일로 작성되어 있습니다. 가독성과 일관성을 위해 다음과 같이 개선하는 것을 고려해 보세요:

  1. 한 줄로 작성된 suspend fun을 여러 줄로 분리
  2. 주석 스타일 통일 (/** */ 사용)
  3. 파라미터가 많은 메서드의 경우 data class로 묶기 고려

예시:

-    suspend fun getRoomsPlaying(
-        roomId: Int
-    ) = runCatching {
+    /** 진행 중인 모임방 정보 조회 */
+    suspend fun getRoomsPlaying(roomId: Int): Result<RoomsPlayingResponse?> = runCatching {
         roomsService.getRoomsPlaying(
             roomId = roomId
-        ).handleBaseResponse().getOrThrow()
+        )
+            .handleBaseResponse()
+            .getOrThrow()
     }

Comment on lines +7 to +10
@Serializable
data class RoomSecreteRoomRequest(
@SerialName("password") val password: String
) No newline at end of file
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

클래스/파일명 오타: Secrete → Secret. 네이밍 일관성 및 DX 영향

도메인 용어는 “Secret room”이므로 RoomSecreteRoomRequest 표기가 잘못되었습니다. 동일 오타가 서비스/레포지토리/네이게이션 전반에 전파될 수 있어 추후 검색·리팩터링 난이도가 커집니다. 요청 본문에 대한 최소 유효성 검사도 함께 추가하는 것을 권장합니다.

아래처럼 클래스명을 바로잡고, 빈 비밀번호 방지를 위한 require를 추가해주세요.

-@Serializable
-data class RoomSecreteRoomRequest(
-    @SerialName("password") val password: String
-)
+@Serializable
+data class RoomSecretRoomRequest(
+    @SerialName("password") val password: String
+) {
+    init {
+        require(password.isNotBlank()) { "password must not be blank" }
+    }
+}

후속 조치

  • 파일명도 RoomSecretRoomRequest.kt로 변경 권장.
  • 서비스 메서드(postParticipateSecreteRoom) 및 모든 호출부의 오타를 함께 정정 필요.
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomSecreteRoomRequest.kt
around lines 7 to 10, the class and filename use the misspelling "Secrete"
instead of "Secret" and lack minimal validation; rename the class to
RoomSecretRoomRequest and move/rename the file to RoomSecretRoomRequest.kt, add
a require(password.isNotBlank()) check in the primary constructor to prevent
empty passwords, and update all impacted references (service, repository,
navigation and tests) including renaming any service method like
postParticipateSecreteRoom to postParticipateSecretRoom and fixing every call
site to match the corrected name.

Comment on lines +8 to +10
data class RoomSecreteRoomRequest(
@SerialName("password") val password: String
) No newline at end of file
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

레포지토리 전반 ‘Secrete’ 오타 사용 여부 점검 필요

동일 오타가 퍼져 있을 가능성이 큽니다. 일괄 정정 여부를 확인해주세요.

예상 결과

  • 모든 결과가 ‘Secret’으로 교체되어야 합니다. 남아있는 경우 해당 파일들에 대한 리네임/치환을 진행해주세요.

🏁 Script executed:

#!/bin/bash
# 레포지토리 전체에서 'Secrete' 오타 사용 위치 탐색
rg -n --hidden -S 'Secrete|RoomSecreteRoomRequest|postParticipateSecreteRoom' -g '!*build*' -g '!*gradle*' -A 2

Length of output: 4311


🏁 Script executed:

#!/bin/bash
# 대소문자 무시하고 'secrete' 오타 전체 탐색
rg -n --hidden -S -i 'secrete' -g '!*build*' -g '!*gradle*' -A 2 || true

Length of output: 4262


'Secrete' 오타 일괄 교정 필요 (→ 'Secret')

레포지토리 전반에서 'Secrete' 오타가 확인되었습니다. 아래 파일/심볼을 클래스명·파일명·메서드명·임포트를 리네임하고 참조를 모두 갱신하세요. API 엔드포인트나 직렬화 키(@SerialName 등)에 영향이 있는지도 같이 확인해주세요.

수정 대상(검색으로 확인된 항목)

  • app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomSecreteRoomRequest.kt
  • app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomSecreteRoomResponse.kt
  • app/src/main/java/com/texthip/thip/data/service/RoomsService.kt (import 및 suspend fun postParticipateSecreteRoom)
  • app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt (import 및 postParticipateSecreteRoom 호출 / RoomSecreteRoomRequest 생성)
  • app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomUnlockViewModel.kt (roomsRepository.postParticipateSecreteRoom 호출)

권장 작업

  • IDE의 안전한 리팩터(Refactor → Rename)로 클래스/파일/메서드명 일괄 변경
  • 파일명도 함께 변경하고 임포트 자동 갱신 확인
  • 변경 후 전체 검색(대소문자 무시)으로 누락된 참조 점검
  • 서버 쪽에 동일 오타가 노출돼 있다면 클라이언트·서버 동시 조치 필요
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomSecreteRoomRequest.kt
around lines 8-10, the symbol and filename use the misspelling "Secrete" and
must be renamed to "Secret"; update the class name to RoomSecretRoomRequest,
rename the file accordingly, and then apply the same rename throughout the repo
for all listed files and symbols (RoomSecreteRoomResponse,
postParticipateSecreteRoom → postParticipateSecretRoom, imports, repository
calls, and ViewModel usages). Use your IDE’s safe Refactor→Rename to change
class/method/file names and let it update imports, then search the codebase
(case-insensitive) for any remaining "Secrete" occurrences (including
@SerialName values, API endpoints, and server-exposed strings) and correct them
to "Secret"; after changes, run a build/tests to ensure no missing references
and verify serialization keys or API contract compatibility with the server,
coordinating server changes if the typo is also exposed there.

Comment on lines +1 to +11
package com.texthip.thip.data.model.rooms.response

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable


@Serializable
data class RoomSecreteRoomResponse(
@SerialName("matched") val matched: Boolean = false,
@SerialName("roomId") val roomId: Int = 0
) No newline at end of file
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

"Secrete" 표기 전역 사용처 스캔

레포지토리/서비스/뷰모델/리소스 전역에서 동일 오탈자가 반복될 가능성이 큽니다. 아래 스크립트로 전역 검색해 일괄 교정 범위를 파악해 주세요.


🏁 Script executed:

#!/bin/bash
# "Secrete" 오탈자 전역 검색
rg -n "Secrete" -A 2

Length of output: 4218


타이포 수정 필요: 'Secrete' → 'Secret'로 일괄 교정하세요

검색 결과 다음 파일들에서 'Secrete' 오탈자가 발견되었습니다. 클래스/파일명, 메서드명, import, 호출부를 모두 함께 바꿔주세요 (단, SerialName 등 API 스펙 필드는 변경하지 마세요).

  • app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomSecreteRoomRequest.kt — RoomSecreteRoomRequest → RoomSecretRoomRequest (파일명도 변경 권장)
  • app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomSecreteRoomResponse.kt — RoomSecreteRoomResponse → RoomSecretRoomResponse (파일명 변경 권장)
  • app/src/main/java/com/texthip/thip/data/service/RoomsService.kt — import / suspend fun postParticipateSecreteRoom(...) → postParticipateSecretRoom(...)
  • app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt — import / suspend fun postParticipateSecreteRoom(...) → postParticipateSecretRoom(...)
  • app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomUnlockViewModel.kt — 호출부 roomsRepository.postParticipateSecreteRoom(...) → postParticipateSecretRoom(...)

권장 변경 예시(간단한 diff):

  • data/model/.../response
    • data class RoomSecreteRoomResponse(
    • data class RoomSecretRoomResponse(
  • service / repository / viewmodel
    • suspend fun postParticipateSecreteRoom(
    • suspend fun postParticipateSecretRoom(
    • roomsRepository.postParticipateSecreteRoom(roomId, password)
    • roomsRepository.postParticipateSecretRoom(roomId, password)

주의: 참조가 여러 곳에 걸쳐 있으므로 IDE의 Rename 리팩토링 기능으로 일괄 교정하거나, 변경 후 빌드/참조 확인을 반드시 수행하세요.

Comment on lines +8 to +11
data class RoomSecreteRoomResponse(
@SerialName("matched") val matched: Boolean = false,
@SerialName("roomId") val roomId: Int = 0
) No newline at end of file
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

타입명 오탈자: Secrete → Secret으로 정정 권장

클래스/파일/메서드에 퍼져있는 "Secrete" 표기는 오탈자로 보입니다. 유지보수성과 검색 용이성을 위해 전반적인 리네이밍을 권장합니다.

아래 변경을 제안합니다(해당 파일 내 변경):

- data class RoomSecreteRoomResponse(
+ data class RoomSecretRoomResponse(
     @SerialName("matched") val matched: Boolean = false,
     @SerialName("roomId") val roomId: Int = 0
 )

추가로, 파일명과 사용처(Repository/Service/VM 메서드명 포함)도 일괄 수정해 주세요. 서버의 엔드포인트 경로 문자열은 변경 없이 클라이언트 타입/메서드명만 교정하면 됩니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
data class RoomSecreteRoomResponse(
@SerialName("matched") val matched: Boolean = false,
@SerialName("roomId") val roomId: Int = 0
)
data class RoomSecretRoomResponse(
@SerialName("matched") val matched: Boolean = false,
@SerialName("roomId") val roomId: Int = 0
)
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomSecreteRoomResponse.kt
around lines 8-11, the type name "Secrete" is a typo; rename the class to
RoomSecretRoomResponse and change the filename to RoomSecretRoomResponse.kt.
Update all references (imports/usages) across repository, service, and ViewModel
method names to the corrected type name, adjust any factory/mapper/tests
accordingly, and ensure package declarations/imports compile; do NOT change
server endpoint path strings—only client-side type and method names.

Comment on lines +33 to +36
/** 장르 목록 조회 */
fun getGenres(): Result<List<Genre>> {
return Result.success(genreManager.getGenres())
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

getGenres() 메서드의 예외 처리 개선 필요

genreManager.getGenres()가 예외를 던질 경우를 처리하지 않습니다. 일관된 에러 처리를 위해 runCatching을 사용해 주세요.

-    /** 장르 목록 조회 */
-    fun getGenres(): Result<List<Genre>> {
-        return Result.success(genreManager.getGenres())
-    }
+    /** 장르 목록 조회 */
+    fun getGenres(): Result<List<Genre>> = runCatching {
+        genreManager.getGenres()
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/** 장르 목록 조회 */
fun getGenres(): Result<List<Genre>> {
return Result.success(genreManager.getGenres())
}
/** 장르 목록 조회 */
fun getGenres(): Result<List<Genre>> = runCatching {
genreManager.getGenres()
}
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt around
lines 33 to 36, wrap the call to genreManager.getGenres() in runCatching so
exceptions are captured and converted into a Result; replace the current direct
Result.success(...) with runCatching { genreManager.getGenres() }.fold({
Result.success(it) }, { Result.failure(it) }) (or simply return runCatching {
genreManager.getGenres() }.toResult()), ensuring any thrown exception becomes a
Result.failure instead of letting it crash.

Comment on lines 37 to 41
.onFailure { exception ->
_uiState.value = _uiState.value.copy(
isLoading = false,
error = exception.message ?: "책 정보를 불러오는데 실패했습니다."
error = exception.message ?: stringResourceProvider.getString(R.string.error_book_detail_load_failed)
)
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

예외 메시지 직접 노출 지양 권장 (사용자 노출/보안 관점)

사용자에게 exception.message를 그대로 노출하면 내부 오류 상세가 UI에 드러날 수 있습니다. 항상 로컬라이즈된 문구만 보여주고, 실제 예외는 로깅으로 남기는 방식이 안전합니다.

아래처럼 교체하는 것을 권장합니다.

-                        error = exception.message ?: stringResourceProvider.getString(R.string.error_book_detail_load_failed)
+                        error = stringResourceProvider.getString(R.string.error_book_detail_load_failed)

추가로, 예외는 로깅에 남겨주세요(예: Timber).

// 파일 상단
import timber.log.Timber
// 실패 처리 내부
Timber.e(exception, "loadBookDetail failed. isbn=%s", isbn)
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/search/viewmodel/BookDetailViewModel.kt
around lines 37 to 41, do not expose exception.message to the UI: replace the
error assignment so the UI only receives a localized user-facing string (e.g.,
stringResourceProvider.getString(R.string.error_book_detail_load_failed)) and
remove usage of exception.message; also log the full exception for diagnostics
(add Timber import at the top and call Timber.e(exception, "loadBookDetail
failed. isbn=%s", isbn) inside the onFailure block) so internal details are
recorded but not shown to users.

Comment on lines 62 to 66
.onFailure { exception ->
_uiState.value = _uiState.value.copy(
isSaving = false,
error = exception.message ?: "책 저장에 실패했습니다."
error = exception.message ?: stringResourceProvider.getString(R.string.error_book_save_failed)
)
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

책 저장 실패 에러도 동일하게 로컬라이즈된 문구만 노출 권장

위와 같은 이유로 저장 실패 메시지도 예외 문자열 대신 리소스만 사용하세요.

-                        error = exception.message ?: stringResourceProvider.getString(R.string.error_book_save_failed)
+                        error = stringResourceProvider.getString(R.string.error_book_save_failed)

필요 시 예외는 로깅으로만 남기세요.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.onFailure { exception ->
_uiState.value = _uiState.value.copy(
isSaving = false,
error = exception.message ?: "책 저장에 실패했습니다."
error = exception.message ?: stringResourceProvider.getString(R.string.error_book_save_failed)
)
.onFailure { exception ->
_uiState.value = _uiState.value.copy(
isSaving = false,
error = stringResourceProvider.getString(R.string.error_book_save_failed)
)
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/search/viewmodel/BookDetailViewModel.kt
around lines 62 to 66, the failure branch sets the UI error to the
exception.message; change this to always use a localized resource string (e.g.
R.string.error_book_save_failed) for the user-facing error and set isSaving =
false, and move the exception details into a log statement (e.g. logger.e or
Timber.e) so the raw exception is not shown to users but is recorded for
debugging.

Comment on lines +20 to 22
private val bookRepository: BookRepository,
private val stringResourceProvider: StringResourceProvider
) : ViewModel() {
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

네트워크 디스패처 점검 필요

viewModelScope.launch는 기본적으로 Main 디스패처입니다. Repository 내부에서 IO 디스패처 사용이 보장되는지 확인해주세요.

아래 스크립트로 BookRepository가 IO 디스패처 전환을 사용하는지 확인할 수 있습니다.


🏁 Script executed:

#!/bin/bash
set -e

echo "[Check] IO dispatcher usage in BookRepository"

fd -a BookRepository.kt | while read -r f; do
  echo "== $f =="
  rg -n "withContext\\(Dispatchers\\.IO\\)" "$f" || echo "No withContext(Dispatchers.IO) found in $f"
done

Length of output: 750


조치 필요: BookRepository에 IO 디스패처 전환 추가/확인

viewModelScope.launch는 기본적으로 Main 디스패처입니다. 제출하신 스크립트 실행 결과 app/src/main/java/com/texthip/thip/data/repository/BookRepository.kt에서 withContext(Dispatchers.IO)를 찾을 수 없습니다. 아래 파일들을 확인하고 수정해주세요.

  • 확인/수정 대상 파일

    • app/src/main/java/com/texthip/thip/data/repository/BookRepository.kt — withContext(Dispatchers.IO) 없음
    • (참고) app/src/main/java/com/texthip/thip/ui/search/viewmodel/SearchBookGroupViewModel.kt — viewModelScope.launch 사용 지점 확인 필요
  • 권장 조치

    • Repository의 suspend 함수 내부에서 네트워크/디스크 작업을 withContext(Dispatchers.IO)로 감싸거나,
    • 호출하는 하위 레이어(DAO/네트워크 클라이언트)가 명확히 IO 디스패처로 전환하는지 검증하세요.
  • 간단 예시

    • suspend fun fetchBooks(...) = withContext(Dispatchers.IO) { /* 네트워크/디스크 작업 */ }

Comment on lines +96 to 97
error = exception.message ?: stringResourceProvider.getString(R.string.error_recruiting_rooms_load_failed)
)
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

예외 메시지 직접 노출 지양 권장

UI에는 항상 리소스 기반 문구만 노출하고, 예외는 로깅으로만 남겨주세요.

-                    error = exception.message ?: stringResourceProvider.getString(R.string.error_recruiting_rooms_load_failed)
+                    error = stringResourceProvider.getString(R.string.error_recruiting_rooms_load_failed)

필요 시 Timber.e(exception, "loadRooms failed. isbn=%s, cursor=%s", isbn, cursor) 형태로 로깅하세요.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
error = exception.message ?: stringResourceProvider.getString(R.string.error_recruiting_rooms_load_failed)
)
error = stringResourceProvider.getString(R.string.error_recruiting_rooms_load_failed)
)
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/ui/search/viewmodel/SearchBookGroupViewModel.kt
around lines 96-97, don't expose exception.message to the UI; replace the
dynamic exception text with a fixed resource string
(stringResourceProvider.getString(R.string.error_recruiting_rooms_load_failed))
for the error field, and add a Timber.e(exception, "loadRooms failed. isbn=%s,
cursor=%s", isbn, cursor) call to log the full exception and context (isbn,
cursor) instead of showing the exception to users.

Comment on lines +397 to +399
<!-- SearchBookGroupViewModel -->\n <string name="error_recruiting_rooms_not_found">모집중인 방 정보를 찾을 수 없습니다.</string>
<string name="error_recruiting_rooms_load_failed">모집중인 방을 불러오는데 실패했습니다.</string>

Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

XML에 리터럴 "\n" 문자열이 섞여 있어 리소스 컴파일 실패 위험

주석 뒤에 리터럴 "\n" 텍스트가 삽입되어 있습니다. 이는 XML 문서 내 불필요한 텍스트 노드로 간주되어 aapt 컴파일 오류를 유발할 수 있습니다. 즉시 제거가 필요합니다.

수정 제안:

-    <!-- SearchBookGroupViewModel -->\n    <string name="error_recruiting_rooms_not_found">모집중인 방 정보를 찾을 수 없습니다.</string>
+    <!-- SearchBookGroupViewModel -->
+    <string name="error_recruiting_rooms_not_found">모집중인 방 정보를 찾을 수 없습니다.</string>

빠른 확인용 스크립트입니다(문맥 확인):


🏁 Script executed:

#!/bin/bash
nl -ba app/src/main/res/values/strings.xml | sed -n '390,405p'
# 리터럴 '\n' 포함 라인 검색
rg -n "\\\\n" app/src/main/res/values/strings.xml -A 0 -B 0

Length of output: 730


긴급: app/src/main/res/values/strings.xml — 주석 뒤의 리터럴 "\n" 제거 필요

rg 검사 결과 397행에서 주석 뒤에 리터럴 "\n"이 삽입된 것이 확인되었습니다(이것은 XML 내 불필요한 텍스트 노드로 aapt 컴파일 오류를 유발할 수 있음). 같은 패턴은 126,176,180,315행에도 보이나 이들은 문자열 내부의 의도된 줄바꿈입니다.

수정 위치:

  • app/src/main/res/values/strings.xml — 397행: \n (제거 필요)

권장 수정(diff):

-    <!-- SearchBookGroupViewModel -->\n
+    <!-- SearchBookGroupViewModel -->
     <string name="error_recruiting_rooms_not_found">모집중인 방 정보를 찾을 수 없습니다.</string>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<!-- SearchBookGroupViewModel -->\n <string name="error_recruiting_rooms_not_found">모집중인 방 정보를 찾을 수 없습니다.</string>
<string name="error_recruiting_rooms_load_failed">모집중인 방을 불러오는데 실패했습니다.</string>
<!-- SearchBookGroupViewModel -->
<string name="error_recruiting_rooms_not_found">모집중인 방 정보를 찾을 수 없습니다.</string>
<string name="error_recruiting_rooms_load_failed">모집중인 방을 불러오는데 실패했습니다.</string>
🤖 Prompt for AI Agents
In app/src/main/res/values/strings.xml around lines 397 to 399, remove the stray
literal "\n" that appears immediately after the comment marker "<!--
SearchBookGroupViewModel -->\n" so the comment stands alone (i.e., delete the
backslash-n characters), ensure there are no other text nodes on that line, and
re-run resource build to confirm no aapt errors; similarly scan the file for any
other accidental literal "\n" occurrences outside string values and remove them.

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.

[API] 모임방 나머지 API 연결 및 버그 수정 [(THIP2025-266)]

2 participants