Skip to content

[API] 방 검색 API 연결 및 방 시간 UI 로직 수정#106

Merged
rbqks529 merged 8 commits intoTHIP-TextHip:developfrom
rbqks529:feat/#103_API_Rooms_Search
Aug 18, 2025
Merged

[API] 방 검색 API 연결 및 방 시간 UI 로직 수정#106
rbqks529 merged 8 commits intoTHIP-TextHip:developfrom
rbqks529:feat/#103_API_Rooms_Search

Conversation

@rbqks529
Copy link
Collaborator

@rbqks529 rbqks529 commented Aug 18, 2025

➕ 이슈 링크


🔎 작업 내용

  • 모임방 검색 구현
  • 서버에서 보내주는 시간 response에 맞게 코드 수정

📸 스크린샷


😢 해결하지 못한 과제

  • [] TASK


📢 리뷰어들에게

  • 참고해야 할 사항들을 적어주세요

Summary by CodeRabbit

  • 신기능

    • 서버 연동 방 검색 결과 제공 및 무한 스크롤(자동 로드, 하단 로딩 인디케이터) 지원
    • 라이브 검색/완료 검색 구분, 장르·정렬 필터, 페이징과 최근검색 추가/삭제 기능
    • 검색 결과 선택 시 방 ID로 바로 모집 화면으로 이동
  • UI 변경

    • 마감일 표기를 숫자 기반에서 서버 제공 문자열로 일원화(문구·색상·레이블 수정)
    • 검색 필드 재배치로 레이아웃 개선
  • 기타

    • 남은 일수 계산 유틸 제거 (표기 단순화)

@coderabbitai
Copy link

coderabbitai bot commented Aug 18, 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

모임방 검색 API 모델·서비스·레포지토리를 추가하고, 검색 화면을 ViewModel 기반 uiState로 재구성하며 검색 결과 컴포저블을 SearchRoomItem으로 전환하고 무한 스크롤·페이징을 도입했습니다. 마감일 처리를 서버 문자열로 전환하고 관련 유틸을 제거했습니다.

Changes

Cohort / File(s) Summary
Rooms 검색 API 모델/서비스/레포지토리
app/src/main/java/.../data/model/rooms/response/RoomsSearchResponse.kt, app/src/main/java/.../data/service/RoomsService.kt, app/src/main/java/.../data/repository/RoomsRepository.kt
RoomsSearchResponse 및 SearchRoomItem 데이터 클래스 추가; RoomsService.searchRooms GET 엔드포인트 추가; RoomsRepository.searchRooms 래핑 메서드 추가.
검색 화면 상태/흐름 개편 (ViewModel 전환)
app/src/main/java/.../ui/group/search/viewmodel/GroupSearchViewModel.kt, app/src/main/java/.../ui/group/search/screen/GroupSearchScreen.kt, app/src/main/java/.../ui/navigator/navigations/GroupNavigation.kt
GroupSearchViewModel에 uiState·검색/필터/정렬·페이징·최근검색 로직 추가; 화면은 roomId 콜백으로 변경; 로컬 상태 제거 및 네비게이션 단순화.
검색 결과 리스트(무한 스크롤 도입)
app/src/main/java/.../ui/group/search/component/GroupLiveSearchResult.kt, app/src/main/java/.../ui/group/search/component/GroupFilteredSearchResult.kt
입력 타입을 SearchRoomItem으로 교체; LazyListState 기반 끝단 감지 후 onLoadMore 호출; canLoadMore/isLoadingMore/onLoadMore 파라미터 및 하단 로딩 인디케이터 추가.
카드 컴포넌트 마감일 표기 변경
app/src/main/java/.../ui/common/cards/CardItemRoom.kt, app/src/main/java/.../ui/common/cards/CardItemRoomSmall.kt
endDate 타입을 Int?String?으로 변경하고 문자열 직접 결합 방식으로 마감일 표시 로직 수정 및 프리뷰 업데이트.
그룹 관련 화면 마감일 소스 변경 및 시그니처 조정
app/src/main/java/.../ui/group/myroom/*, app/src/main/java/.../ui/group/room/screen/GroupRoomRecruitScreen.kt, app/src/main/java/.../ui/search/screen/SearchBookGroupScreen.kt
DateUtils/RoomUtils 기반 일수 계산 제거, 서버 제공 문자열(deadlineDate 등) 사용으로 변경; Recruit 화면 시그니처에서 onNavigateToGroupScreen 제거 등 일부 시그니처/네비게이션 변경.
유틸/리소스 정리
app/src/main/java/.../utils/rooms/DateUtils.kt(삭제), app/src/main/java/.../utils/rooms/RoomUtils.kt (메서드 제거), app/src/main/res/values/strings.xml
DateUtils 파일 삭제 및 RoomUtils의 getEndDateInDays 제거; 문자열 리소스 재구성(card_item_end, time_remaining_after, time_remaining_left 등).
기타 레이아웃/목업 정리
app/src/main/java/.../ui/group/screen/GroupScreen.kt, app/src/main/java/.../ui/group/myroom/mock/GroupCardItemRoomData.kt
검색 필드 상단 패딩 조정(16dp→72dp); mock 데이터의 endDate 타입을 Int?String?으로 변경.

Sequence Diagram(s)

sequenceDiagram
  participant UI as GroupSearchScreen
  participant VM as GroupSearchViewModel
  participant Repo as RoomsRepository
  participant Svc as RoomsService
  participant API as /rooms/search

  UI->>VM: updateSearchQuery(query)
  VM-->>VM: debounce 300ms
  VM->>Repo: searchRooms(keyword, category, sort, isFinalized, cursor=null)
  Repo->>Svc: GET /rooms/search (query params)
  Svc->>API: 요청
  API-->>Svc: BaseResponse<RoomsSearchResponse>
  Svc-->>Repo: RoomsSearchResponse
  Repo-->>VM: Result<RoomsSearchResponse?>
  VM-->>UI: uiState(searchResults, nextCursor, hasMore)
Loading
sequenceDiagram
  participant UI as Live/FilteredResult (LazyList)
  participant VM as GroupSearchViewModel
  participant Repo as RoomsRepository
  participant Svc as RoomsService
  participant API as /rooms/search

  UI-->>UI: near end? (derivedStateOf)
  UI->>VM: onLoadMore()
  VM->>Repo: searchRooms(..., cursor=nextCursor)
  Repo->>Svc: GET /rooms/search (cursor)
  API-->>Svc: Response(nextCursor,isLast,roomList)
  Repo-->>VM: Result appended
  VM-->>UI: append results, update nextCursor/hasMore
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Assessment against linked issues

Objective Addressed Explanation
[#103] 모임방 검색 api

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
검색 화면 UI 및 상태 전면 개편 (app/src/main/java/.../ui/group/search/*) 링크된 이슈는 API 구현(모임방 검색 API) 중심이나 PR에 대규모 UI/state/pagination 리팩터가 포함되어 있어 범위 확장됨.
네비게이션 단순화: Recruit로 단일 이동 (app/src/main/java/.../ui/navigator/navigations/GroupNavigation.kt) API 구현 목적과 직접 관련 없는 네비게이션/화면 흐름 정책 변경임.
GroupRoomRecruitScreen 시그니처 변경 (onNavigateToGroupScreen 제거) (app/src/main/java/.../ui/group/room/screen/GroupRoomRecruitScreen.kt) API 작업 범위를 벗어난 UI 시그니처 변경임.

Possibly related issues

Possibly related PRs

Suggested reviewers

  • Nico1eKim
  • JJUYAAA

Poem

나는 토끼, 키워드를 톡! 🐇
목록 끝자락이 보이면 더불러와 콩닥콩닥
마감은 이제 "뒤" 또는 "남음"으로 속삭여요
서버가 준 문자열로 그대로 보여줄게요
클릭 한 번에 ID 따라 폴짝! ✨

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear 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 7a3c03b and 1098add.

📒 Files selected for processing (2)
  • app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt (1 hunks)
  • app/src/main/res/values/strings.xml (1 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 room search API integration and updates the UI logic to handle time-related data as returned by the server. The main changes involve refactoring time display from integer-based days to string-based time representations and integrating actual API calls for room search functionality.

  • Replaces integer-based day calculations with string-based time display throughout the UI
  • Integrates room search API with proper pagination and filtering
  • Removes legacy date utility classes and simplifies time handling logic

Reviewed Changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
app/src/main/res/values/strings.xml Updates string resources for time display formatting
app/src/main/java/com/texthip/thip/utils/rooms/RoomUtils.kt Removes deprecated date calculation method
app/src/main/java/com/texthip/thip/utils/rooms/DateUtils.kt Deletes entire utility class for date calculations
app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookGroupScreen.kt Updates to use API response objects directly instead of mock data
app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt Simplifies navigation logic and removes unused view model
app/src/main/java/com/texthip/thip/ui/group/search/viewmodel/GroupSearchViewModel.kt Complete rewrite to implement actual search functionality with API integration
app/src/main/java/com/texthip/thip/ui/group/search/screen/GroupSearchScreen.kt Major refactor to use view model state and remove local state management
app/src/main/java/com/texthip/thip/ui/group/search/component/GroupLiveSearchResult.kt Updates component to use API response objects and adds infinite scroll
app/src/main/java/com/texthip/thip/ui/group/search/component/GroupFilteredSearchResult.kt Updates component to use API response objects and adds infinite scroll
app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt Updates time display logic to use string manipulation instead of calculations
app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoom.kt Changes endDate parameter type from Int to String
app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoomSmall.kt Changes endDate parameter type from Int to String
app/src/main/java/com/texthip/thip/data/service/RoomsService.kt Adds new API endpoint for room search
app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt Implements room search repository method
app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsSearchResponse.kt Defines new data models for search API response

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.

if (lastVisibleIndex != null &&
if (lastVisibleIndex != null &&
recruitingList.isNotEmpty() &&
!isLoadingMore &&
Copy link

Copilot AI Aug 18, 2025

Choose a reason for hiding this comment

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

[nitpick] This complex boolean condition for load more logic spans multiple lines and could be extracted into a computed property or helper function for better readability.

Copilot uses AI. Check for mistakes.
loadMoreJob?.cancel()

// 공백도 검색 가능하도록 수정 (빈 문자열만 제외)
if (query.isNotEmpty()) {
Copy link

Copilot AI Aug 18, 2025

Choose a reason for hiding this comment

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

The comment mentions allowing blank searches but the condition only checks for empty strings. Consider using query.isNotBlank() if spaces should trigger search, or update the comment to match the actual behavior.

Copilot uses AI. Check for mistakes.

fun onSearchButtonClick() {
val query = uiState.value.searchQuery
if (query.isNotEmpty()) { // 공백도 검색 가능 (빈 문자열만 제외)
Copy link

Copilot AI Aug 18, 2025

Choose a reason for hiding this comment

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

Inconsistent behavior: the comment states that spaces are searchable, but isNotEmpty() allows strings with only spaces. If spaces should be searchable, use isNotBlank() instead.

Copilot uses AI. Check for mistakes.
text = detail.recruitEndDate.replace(
"뒤",
"남음"
),
Copy link

Copilot AI Aug 18, 2025

Choose a reason for hiding this comment

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

Using string replacement for time formatting is fragile. Consider using a dedicated utility function to handle time string transformations consistently across the app.

Suggested change
),
text = formatRecruitEndDate(detail.recruitEndDate),

Copilot uses AI. Check for mistakes.
endDate?.let {
Text(
text = stringResource(R.string.card_item_end_date_recruit, endDate),
text = endDate + stringResource(R.string.card_item_end),
Copy link

Copilot AI Aug 18, 2025

Choose a reason for hiding this comment

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

String concatenation for time display formatting should be handled consistently. Consider using string formatting or a utility function to ensure proper formatting across all time displays.

Suggested change
text = endDate + stringResource(R.string.card_item_end),
text = stringResource(R.string.card_item_end, endDate),

Copilot uses AI. Check for mistakes.
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: 7

🔭 Outside diff range comments (1)
app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoomSmall.kt (1)

146-152: 문자열 덧붙이기 대신 포맷 리소스로 구성하고, 불필요한 따옴표/공백 제거 필요

현재 endDate + stringResource(R.string.card_item_end)는:

  • 리소스 값의 선행 공백/따옴표 유무에 따라 화면 문자열이 어색해질 수 있고,
  • 현지화(RTL/LTR) 조합에 취약합니다.
    또한 let 블록 내에서 endDate 캡처 변수만 사용하고 it을 쓰지 않는 점도 사소한 가독성 저하입니다.

권장: 포맷 리소스를 도입해 일관되게 조합하세요. 우선 strings.xml에서 따옴표와 선행 공백을 제거한 뒤, 포맷 문자열을 추가하고 아래처럼 적용하는 것을 추천합니다.

적용 예(이 파일 내 diff):

-                    endDate?.let {
-                        Text(
-                            text = endDate + stringResource(R.string.card_item_end),
-                            color = colors.Red,
-                            style = typography.menu_sb600_s12_h20
-                        )
-                    }
+                    endDate?.let {
+                        Text(
+                            text = stringResource(
+                                R.string.card_item_deadline_format,
+                                it,
+                                stringResource(R.string.card_item_end)
+                            ),
+                            color = colors.Red,
+                            style = typography.menu_sb600_s12_h20
+                        )
+                    }

추가로, Preview의 샘플 데이터도 일관되게 완성형 문자열을 사용해 주세요:

-                endDate = "3일 뒤"
+                endDate = "3일 뒤"
...
-                endDate = "5"
+                endDate = "5일 뒤"

strings.xml 변경(선택 영역 밖, 지원용 코드):

<string name="card_item_end">모집 마감</string>
<string name="card_item_deadline_format">%1$s %2$s</string>
🧹 Nitpick comments (28)
app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt (1)

111-113: 상단 여백 72.dp 하드코딩은 디바이스/인셋 변화에 취약합니다. 시스템 인셋 + 앱바 높이를 반영하도록 개선 제안

현재 TopAppBar가 오버레이되는 구조에서 고정 72.dp는 상태바/노치/앱바 높이 변경 시 겹침/과도한 공백이 생길 수 있습니다. WindowInsets와 앱바 높이를 더한 계산값을 사용하면 안정적입니다.

예시(참고용, 파일 상단에 필요한 import 추가):

import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.ui.unit.dp

// GroupContent 내부 어딘가
val topPadding = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 56.dp + 16.dp
// 56.dp: 앱바 높이(디자인 시스템 상수로 대체 권장), 16.dp: 기존 마진

GroupSearchTextField(
    modifier = Modifier.padding(top = topPadding, bottom = 32.dp),
    onClick = onNavigateToGroupSearch
)
app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt (3)

150-168: LazyColumn 아이템에 안정적인 key를 부여해주세요 (스크롤 위치 보존/성능 개선)

현재 items에 key가 없어 데이터 변경 시 스크롤 포지션 점프/재조합 비용 증가 가능성이 있습니다. roomId를 key로 지정하는 것을 권장합니다.

-                    LazyColumn(
+                    LazyColumn(
                         state = listState,
                         verticalArrangement = Arrangement.spacedBy(20.dp),
                         contentPadding = PaddingValues(bottom = 20.dp),
                         modifier = Modifier.fillMaxSize()
                     ) {
-                        items(uiState.myRooms) { room ->
+                        items(
+                            items = uiState.myRooms,
+                            key = { it.roomId }
+                        ) { room ->
                             CardItemRoom(
                                 title = room.roomName,
                                 participants = room.memberCount,
                                 maxParticipants = room.recruitCount,
                                 isRecruiting = RoomUtils.isRecruitingByType(room.type),
                                 endDate = room.endDate,
                                 imageUrl = room.bookImageUrl,
                                 onClick = { onCardClick(room) }
                             )
                         }
                     }

82-86: 무한 스크롤 트리거 안정성 개선 제안 (snapshotFlow 활용)

현 구현도 동작은 하나, shouldLoadMore가 true로 유지되는 동안 UI 프레임마다 재평가될 수 있습니다. listState 기반으로 snapshotFlow를 사용하면 스크롤 경계 지점에서만 로드 트리거되어 안정적입니다.

예시:

LaunchedEffect(listState, uiState.canLoadMore, uiState.isLoadingMore) {
    if (!uiState.canLoadMore || uiState.isLoadingMore) return@LaunchedEffect
    snapshotFlow {
        val lastVisible = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1
        val total = listState.layoutInfo.totalItemsCount
        lastVisible >= total - 3 && total > 0
    }.collect { shouldLoad ->
        if (shouldLoad) onLoadMore()
    }
}

88-95: 주석 문장 보완 제안

“// Filter 상태를”로 끝나는 미완 주석입니다. 간단히 문장을 완결해 가독성을 높여주세요.

-    // Filter 상태를 
+    // Filter 상태를 UI에 반영하기 위한 선택 상태(boolean 배열)
app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt (3)

68-70: 장르 문자열 계산은 remember로 메모이즈하면 미세 성능 개선

재구성 빈도가 높을 수 있는 섹션이므로 toDisplayStrings() 결과를 remember로 고정하면 불필요 계산을 줄일 수 있습니다.

-            val genreStrings = Genre.entries.toDisplayStrings()
+            val genreStrings = remember { Genre.entries.toDisplayStrings() }

73-81: roomSections를 Pair 대신 명시적 데이터 구조로 표현하면 가독성↑

Pair<String, List>는 first/second 해석 비용이 있어 필드명 있는 data class(예: Section(title, rooms))를 권장합니다. 유지보수성 측면에서 의미가 분명해집니다.

예시:

data class RoomSection(val title: String, val rooms: List<RoomMainResponse>)
val roomSections = listOf(
    RoomSection(stringResource(R.string.room_section_deadline), roomMainList?.deadlineRoomList ?: emptyList()),
    RoomSection(stringResource(R.string.room_section_popular), roomMainList?.popularRoomList ?: emptyList()),
)

197-205: endDate에 deadlineDate 문자열 직접 매핑(LGTM)

카드 컴포넌트의 endDate:String? 이관과 일치합니다. 문자열 리소스/표시 포맷과의 호환성도 좋아 보입니다. isRecruiting을 true로 고정한 가정은 API 계약(양 섹션 모두 모집중) 변경 시 취약할 수 있으니, 추후 필드가 내려오면 값 전달로 대체하는 것을 권장합니다.

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

125-136: searchRooms 파라미터에 타입 안정성(정의된 Enum/래퍼) 도입 제안

category/sort를 생 문자열로 노출하면 오타/백엔드 파라미터 변경에 취약합니다. Enum + 매핑 또는 옵션 래퍼를 도입해 안정성을 높이세요. 또한 UI에서 null 처리 부담을 줄이려면 빈 응답을 표준화해 non-null로 반환하는 것도 고려해볼만 합니다.

예시:

enum class RoomSort(val api: String) { DEADLINE("deadline"), POPULAR("popular"), LATEST("latest") }
enum class RoomCategory(val api: String) { ALL("all"), LITERATURE("literature") /* ... */ }

suspend fun searchRooms(
    keyword: String,
    category: RoomCategory,
    sort: RoomSort = RoomSort.DEADLINE,
    isFinalized: Boolean = false,
    cursor: String? = null
): Result<RoomsSearchResponse> = runCatching {
    roomsService.searchRooms(keyword, category.api, sort.api, isFinalized, cursor)
        .handleBaseResponse()
        .getOrThrow()
        ?: RoomsSearchResponse(emptyList(), null) // 모델에 맞는 빈 응답 기본값
}

원하시면 UI 요구사항에 맞춘 도메인 모델 변환(Repository 레벨에서 DTO→Domain)까지 함께 정리해드리겠습니다.

app/src/main/java/com/texthip/thip/data/service/RoomsService.kt (1)

92-101: 검색 API 엔드포인트 추가 LGTM, 정렬 파라미터의 타입 안정성 개선 제안

Retrofit 시그니처와 기본값 구성이 명확합니다. 단, sort가 매직 스트링(예: "deadline", "like", "latest")으로 흐르는 것을 방지하려면 타입 안정성을 주는 래퍼를 권장합니다.

가벼운 개선안:

  • 정렬 키를 sealed class 또는 enum으로 정의하고, .queryValue로 매핑해 전달
  • 혹은 상수 집합을 @stringdef로 제한

예시(선택 영역 밖, 지원용 코드):

enum class RoomSearchSort(val query: String) {
    DEADLINE("deadline"),
    LIKE("like"),
    LATEST("latest"),
}

Service 사용부 예:

suspend fun searchRooms(
    keyword: String,
    category: String,
    sort: RoomSearchSort = RoomSearchSort.DEADLINE,
    isFinalized: Boolean = false,
    cursor: String? = null
) = roomsService.searchRooms(
    keyword = keyword,
    category = category,
    sort = sort.query,
    isFinalized = isFinalized,
    cursor = cursor
)
app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsSearchResponse.kt (1)

13-22: 필드 네이밍/타입 합의 정리 제안(특히 isPublic, recruitCount, deadlineDate)

  • isPublic은 UI에서 주로 isSecret로 쓰이므로 매핑 계층에서 val isSecret = !isPublic 형태의 변환을 중앙화하면 혼선을 줄일 수 있습니다.
  • recruitCount가 "정원(최대 인원)" 의미라면, 도메인 계층에서는 capacity/maxMemberCount 같은 명시적 이름으로 노출되게 매핑하는 것을 권장합니다.
  • deadlineDate가 서버 포맷 문자열(예: "3일 뒤")이라면 괜찮지만, 비어있음이 의미 있는 상태라면 String?로 모델링하는 편이 더 명확합니다. 반대로 항상 내려온다면 기본값 "" 유지도 OK입니다.

선택 사항(선택 영역 밖, 지원용 코드): UI 매핑 예시

data class UiSearchRoomItem(
    val roomId: Int,
    val imageUrl: String?,
    val title: String,
    val participants: Int,
    val maxParticipants: Int,
    val deadlineText: String?, // null: 미노출
    val isSecret: Boolean,
)

fun SearchRoomItem.toUi(): UiSearchRoomItem = UiSearchRoomItem(
    roomId = roomId,
    imageUrl = bookImageUrl,
    title = roomName,
    participants = memberCount,
    maxParticipants = recruitCount,
    deadlineText = deadlineDate.ifBlank { null },
    isSecret = !isPublic,
)
app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt (2)

108-119: 람다 인자 포맷 변경은 비기능적 변경

호출부 포맷 변경(멀티라인)은 가독성을 약간 개선하지만 기능 변화는 없습니다. 그대로 진행해도 무방합니다.


342-345: 서버 응답 문자열에 대한 단순 치환은 취약합니다 (i18n·표기 일관성 문제)

"뒤" -> "남음" 단순 치환은 서버에서 "D-5", "모집 중", 다른 언어 응답을 내려줄 때 모두 대응하지 못합니다. 또한 다른 컴포넌트(CardItemRoom, CardItemRoomSmall)와의 접미사 처리 방식이 제각각이라 표기 불일치가 생길 수 있습니다. 접미사 문자열 리소스를 사용하고, 케이스별로 안전하게 처리하거나, 더 바람직하게는 ViewModel/Mapper에서 일관된 최종 표기를 만들어 UI는 그대로 표시만 하도록 분리하는 것을 권장합니다.

아래는 최소 침습 수정안(접미사 리소스 사용 + 안전 처리)입니다.

-                                Text(
-                                    text = detail.recruitEndDate.replace(
-                                        "뒤",
-                                        "남음"
-                                    ),
-                                    style = typography.info_m500_s12,
-                                    color = colors.NeonGreen
-                                )
+                                Text(
+                                    text = when {
+                                        detail.recruitEndDate.endsWith("뒤") ->
+                                            detail.recruitEndDate.replace("뒤", stringResource(R.string.card_item_end))
+                                        detail.recruitEndDate.contains("D-") ->
+                                            detail.recruitEndDate + stringResource(R.string.card_item_end)
+                                        else -> detail.recruitEndDate
+                                    },
+                                    style = typography.info_m500_s12,
+                                    color = colors.NeonGreen
+                                )

UI 전역에서 마감일 표기가 일관되게 나오는지(예: "3일 남음", "D-5 남음", "모집 중" 등) 한 번 눈으로 확인 부탁드립니다. 필요 시 ViewModel에서 통일된 포맷을 제공하도록 변경해드릴 수 있습니다.

app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoom.kt (3)

152-158: '3일 뒤' + '남음' 중복 표기 가능성 — 안전한 접미사 처리 또는 ViewModel로 위임 권장

현재는 endDate + 접미사 방식이라 서버가 "3일 뒤"를 주는 경우 "3일 뒤남음"처럼 부자연스러운 표기가 됩니다. 아래처럼 안전 처리하거나, 더 나은 접근으로는 ViewModel/Mapper에서 최종 문자열을 생성해 Card는 그대로 출력만 하도록 하세요.

국소 수정안:

-                        Text(
-                            text = endDate
-                                    + if (isRecruiting) stringResource(
-                                R.string.card_item_end
-                            ) else stringResource(R.string.card_item_finish),
+                        val suffix = if (isRecruiting) stringResource(R.string.card_item_end)
+                                     else stringResource(R.string.card_item_finish)
+                        Text(
+                            text = when {
+                                endDate.endsWith("뒤") -> endDate.replace("뒤", suffix)
+                                endDate.contains("D-") -> endDate + suffix
+                                else -> endDate
+                            },
                             color = if (isRecruiting) colors.Red else colors.Grey01,
                             style = typography.menu_sb600_s12_h20,
                             maxLines = 1
                         )

또는, endDate는 완성된 표시 문자열(예: "D-5 남음", "모집 종료")만 전달받고 여기서는 그대로 출력하도록 변경하는 것도 추천합니다.


149-156: 미세: let 블록 안에서 endDate 대신 it 사용

널 가드로 let을 사용하는 만큼 블록 내부에서는 캡처된 값을 사용하는 것이 가독성과 안전성에 유리합니다.

-                    endDate?.let {
+                    endDate?.let { end ->
                         Spacer(modifier = Modifier.height(5.dp))
 
                         Text(
-                            text = endDate
+                            text = end
                                     + if (isRecruiting) stringResource(
                                 R.string.card_item_end
                             ) else stringResource(R.string.card_item_finish),

180-206: Preview의 endDate 예시 값 통일 제안

프리뷰에서 "3일 뒤", "3" 등 혼재되어 있어 렌더 결과 확인이 어렵습니다. 서버 형식과 맞는 대표 포맷 1~2가지를 일관되게 사용하는 것을 제안드립니다(예: "D-3", "3일 뒤").

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

81-96: 무한 스크롤 트리거에 isLoadingMore 가드 추가 권장

현재 shouldLoadMoreisLoadingMore 상태를 고려하지 않아 레이아웃 정보 갱신 타이밍에 따라 중복 호출 가능성이 있습니다. 가벼운 가드를 추가해 중복 로딩을 방지하세요.

-            val shouldLoadMore by remember {
+            val shouldLoadMore by remember {
                 derivedStateOf {
                     val lastVisibleItem = listState.layoutInfo.visibleItemsInfo.lastOrNull()
-                    lastVisibleItem != null && lastVisibleItem.index >= roomList.size - 3 && canLoadMore
+                    lastVisibleItem != null &&
+                    lastVisibleItem.index >= roomList.size - 3 &&
+                    canLoadMore &&
+                    !isLoadingMore
                 }
-            }
+            }

스크롤 하단에서 빠르게 상하로 튕길 때 onLoadMore가 1회만 호출되는지 로그로 한 번 확인 부탁드립니다.

app/src/main/java/com/texthip/thip/ui/group/search/component/GroupLiveSearchResult.kt (2)

35-49: 무한 스크롤 트리거에 isLoadingMore 가드 추가 권장 (중복 로딩 방지)

shouldLoadMore 계산에 !isLoadingMore를 포함시켜 중복 호출을 방지하세요. GroupFilteredSearchResult와 로직을 맞추면 일관성도 좋아집니다.

-    val shouldLoadMore by remember {
+    val shouldLoadMore by remember {
         derivedStateOf {
             val lastVisibleItem = listState.layoutInfo.visibleItemsInfo.lastOrNull()
-            lastVisibleItem != null && lastVisibleItem.index >= roomList.size - 3 && canLoadMore
+            lastVisibleItem != null &&
+            lastVisibleItem.index >= roomList.size - 3 &&
+            canLoadMore &&
+            !isLoadingMore
         }
-    }
+    }

51-87: 중복 코드 축소 제안: 무한 스크롤 감지 유틸/컴포저블로 공통화

GroupFilteredSearchResult와 본 컴포저블의 무한 스크롤 감지·로딩 인디케이터 로직이 거의 동일합니다. 공통 훅/유틸(예: rememberEndlessListLoader(state, canLoadMore, isLoadingMore, threshold = 3))로 추출하면 유지보수성이 좋아집니다.

원하시면 공통 헬퍼 컴포저블/함수를 제안드리겠습니다.

app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookGroupScreen.kt (1)

185-201: 무한 스크롤 snapshotFlow에 중복 방지 연산자 추가 제안

레이아웃 변경이 빈번할 때 같은 인덱스가 연속 방출될 수 있습니다. distinctUntilChanged()를 추가해 여지를 줄이세요.

-                            LaunchedEffect(listState, canLoadMore, isLoadingMore) {
-                                snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index }
-                                    .collect { lastVisibleIndex ->
+                            LaunchedEffect(listState, canLoadMore, isLoadingMore) {
+                                snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index }
+                                    .distinctUntilChanged()
+                                    .collect { lastVisibleIndex ->
                                         if (lastVisibleIndex != null &&
                                             recruitingList.isNotEmpty() &&
                                             !isLoadingMore &&
                                             lastVisibleIndex >= recruitingList.size - 3 &&
                                             canLoadMore
                                         ) {
                                             onLoadMore()
                                         }
                                     }
                             }

주의: kotlinx.coroutines.flow.distinctUntilChanged 임포트가 필요합니다.

app/src/main/java/com/texthip/thip/ui/group/search/screen/GroupSearchScreen.kt (5)

125-139: 라이브 검색 중 로딩 상태 표시가 없어 빈 화면으로 보일 수 있습니다.

uiState.isSearching 동안에는 showEmptyState가 false이고 hasResults도 false일 수 있어 아무것도 렌더링되지 않습니다. 간단한 로딩 인디케이터를 추가하는 것이 좋습니다.

아래처럼 분기 하나를 추가해 주세요.

                     uiState.isLiveSearching -> {
-                        if (uiState.showEmptyState) {
+                        if (uiState.showEmptyState) {
                             GroupEmptyResult(
                                 mainText = stringResource(R.string.group_no_search_result1),
                                 subText = stringResource(R.string.group_no_search_result2)
                             )
-                        } else if (uiState.hasResults) {
+                        } else if (uiState.isSearching) {
+                            Box(
+                                modifier = Modifier.fillMaxSize(),
+                                contentAlignment = Alignment.Center
+                            ) {
+                                androidx.compose.material3.CircularProgressIndicator(color = colors.White)
+                            }
+                        } else if (uiState.hasResults) {
                             GroupLiveSearchResult(
                                 roomList = uiState.searchResults,
                                 onRoomClick = { room -> onRoomClick(room.roomId) },
                                 canLoadMore = uiState.canLoadMore,
                                 isLoadingMore = uiState.isLoadingMore,
                                 onLoadMore = { viewModel.loadMoreRooms() }
                             )
                         }
                     }

58-62: 정렬 타입의 하드코딩 문자열 제거 제안.

"deadline"/"memberCount" 문자열 비교는 오타 리스크가 커서 상수/Enum으로 통일하는 것이 안전합니다. ViewModel에 상수를 두고 참조하도록 변경을 권장합니다.

ViewModel에 상수 추가 후 이 분기를 다음과 같이 바꿔주세요.

-    val selectedSortOptionIndex = when (uiState.selectedSort) {
-        "deadline" -> 0
-        "memberCount" -> 1
-        else -> 0
-    }
+    val selectedSortOptionIndex = when (uiState.selectedSort) {
+        GroupSearchViewModel.SORT_DEADLINE -> 0
+        GroupSearchViewModel.SORT_MEMBER_COUNT -> 1
+        else -> 0
+    }

ViewModel 쪽 변경은 아래 코멘트에 함께 제안합니다.


182-188: 정렬 타입 매핑의 중복 문자열을 상수로 통일.

UI와 ViewModel 모두에서 같은 문자열을 사용합니다. 상수 사용으로 단일 출처 유지가 좋습니다.

-                    val sortType = when (sortOptions.indexOf(selected)) {
-                        0 -> "deadline"
-                        1 -> "memberCount"
-                        else -> "deadline"
-                    }
+                    val sortType = when (sortOptions.indexOf(selected)) {
+                        0 -> GroupSearchViewModel.SORT_DEADLINE
+                        1 -> GroupSearchViewModel.SORT_MEMBER_COUNT
+                        else -> GroupSearchViewModel.SORT_DEADLINE
+                    }
                     viewModel.updateSortType(sortType)

103-170: 방어적 렌더링 분기 보강 제안.

ViewModel 오류/널 응답 시 모든 UI 플래그가 false가 되면(버그 시나리오) 빈 화면이 됩니다. ViewModel 수정이 근본 해결이나, 여기에도 마지막 else 분기를 두고 에러/빈 상태를 보여주면 안전합니다.

원한다면 마지막에 else -> GroupEmptyResult(...) 또는 에러 메시지 표시 분기를 추가해 드릴 수 있습니다.


195-201: Preview에서 hiltViewModel 사용 회피 권장.

Preview에서 Hilt ViewModel을 직접 생성하면 미리보기 런타임에 실패할 수 있습니다. 미리보기 전용 Fake ViewModel을 주입하거나 파라미터를 노출하는 오버로드를 두는 방식을 권장합니다.

원하시면 Preview용 스텁 ViewModel 샘플 코드를 제공하겠습니다.

app/src/main/java/com/texthip/thip/ui/group/search/viewmodel/GroupSearchViewModel.kt (4)

35-36: 정렬 타입 문자열 상수화로 단일 출처 유지.

문자열 "deadline"/"memberCount"를 상수로 노출해 UI와 ViewModel이 공유하도록 하세요.

-    val selectedSort: String = "deadline", // "deadline" 또는 "memberCount"
+    val selectedSort: String = SORT_DEADLINE, // "deadline" 또는 "memberCount"

ViewModel 클래스 내부에 아래 상수를 추가하세요.

 class GroupSearchViewModel @Inject constructor(
     private val roomsRepository: RoomsRepository,
     private val recentSearchRepository: RecentSearchRepository
 ) : ViewModel() {
+    companion object {
+        const val SORT_DEADLINE = "deadline"
+        const val SORT_MEMBER_COUNT = "memberCount"
+        const val RECENT_SCOPE_ROOM = "ROOM"
+    }

UI 파일의 문자열 비교/매핑은 이 상수를 사용하도록 변경해주세요(별도 코멘트 참고).


151-157: 정렬 타입 업데이트 문자열 하드코딩 제거.

상수를 사용해 오타 리스크를 줄이세요.

-    fun updateSortType(sort: String) {
+    fun updateSortType(sort: String) {
         updateState { it.copy(selectedSort = sort) }
         // 정렬 변경 시 새로운 검색 수행 (공백도 허용)
         if (uiState.value.searchQuery.isNotEmpty() && !uiState.value.isInitial) {
             performSearchWithCurrentQuery()
         }
     }

사용처는 SORT_DEADLINE/SORT_MEMBER_COUNT로 제한하는 것을 권장합니다(예: UI 매핑에서만 생성).


219-219: 에러 메시지의 문자열 리소스/이벤트화 권장.

ViewModel에서 하드코딩된 문자열을 직접 노출하면 현지화와 테스트가 어렵습니다. 에러 코드를 상태로 들고, UI에서 stringResource로 변환하거나, UI Event(Snackbar/Toast)로 전달하는 패턴을 권장합니다.

원하시면 ErrorState/UiEvent로 리팩터를 제안해 드리겠습니다.


281-295: "ROOM" 스코프 문자열 상수화 제안.

매직 스트링 대신 상수를 사용하면 추후 도메인 확장/변경에 안전합니다.

-            recentSearchRepository.getRecentSearches("ROOM")
+            recentSearchRepository.getRecentSearches(RECENT_SCOPE_ROOM)

상수는 companion object에 추가하는 안을 위에서 제안했습니다.

📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear 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 359f86a and 377e531.

📒 Files selected for processing (19)
  • app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsSearchResponse.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt (2 hunks)
  • app/src/main/java/com/texthip/thip/data/service/RoomsService.kt (2 hunks)
  • app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoom.kt (5 hunks)
  • app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoomSmall.kt (3 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt (3 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardItemRoomData.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt (6 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt (7 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/search/component/GroupFilteredSearchResult.kt (6 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/search/component/GroupLiveSearchResult.kt (3 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/search/screen/GroupSearchScreen.kt (3 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/search/viewmodel/GroupSearchViewModel.kt (4 hunks)
  • app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookGroupScreen.kt (9 hunks)
  • app/src/main/java/com/texthip/thip/utils/rooms/DateUtils.kt (0 hunks)
  • app/src/main/java/com/texthip/thip/utils/rooms/RoomUtils.kt (0 hunks)
  • app/src/main/res/values/strings.xml (1 hunks)
💤 Files with no reviewable changes (2)
  • app/src/main/java/com/texthip/thip/utils/rooms/RoomUtils.kt
  • app/src/main/java/com/texthip/thip/utils/rooms/DateUtils.kt
🧰 Additional context used
🧬 Code Graph Analysis (3)
app/src/main/java/com/texthip/thip/ui/group/search/component/GroupFilteredSearchResult.kt (1)
app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoomSmall.kt (1)
  • CardItemRoomSmall (36-157)
app/src/main/java/com/texthip/thip/ui/group/search/component/GroupLiveSearchResult.kt (1)
app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoomSmall.kt (1)
  • CardItemRoomSmall (36-157)
app/src/main/java/com/texthip/thip/ui/group/search/screen/GroupSearchScreen.kt (5)
app/src/main/java/com/texthip/thip/ui/group/search/component/GroupRecentSearch.kt (1)
  • GroupRecentSearch (30-65)
app/src/main/java/com/texthip/thip/ui/group/search/component/GroupEmptyResult.kt (1)
  • GroupEmptyResult (18-40)
app/src/main/java/com/texthip/thip/ui/group/search/component/GroupLiveSearchResult.kt (1)
  • GroupLiveSearchResult (27-88)
app/src/main/java/com/texthip/thip/ui/group/search/component/GroupFilteredSearchResult.kt (1)
  • GroupFilteredSearchResult (37-136)
app/src/main/java/com/texthip/thip/ui/common/buttons/FilterButton.kt (1)
  • FilterButton (37-128)
🔇 Additional comments (22)
app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt (1)

133-136: 검증 완료: searchRooms 파라미터 순서 일치
RoomsService.searchRooms 인터페이스 정의
— keyword, category, sort, isFinalized, cursor

호출부(RoomsRepository.kt 라인 133–136)에서도 동일한 순서와 타입으로 전달하고 있어 문제가 없습니다.

app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoomSmall.kt (1)

42-42: endDate 타입 변경 확인 완료

모든 호출부(GroupFilteredSearchResult, GroupLiveSearchResult, GroupRoomRecruitScreen 및 Preview)에서 String 형태로 endDate가 전달되고 있음을 확인했습니다. 문제 없이 머지 가능합니다.

app/src/main/java/com/texthip/thip/data/service/RoomsService.kt (1)

31-31: 모델 임포트 추가 적절

신규 응답 모델 임포트가 정확합니다. 빌드 안정성 측면에서 문제 없습니다.

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

173-181: 일관성 확인 완료: Int 타입으로 일치
navigateToGroupRecruit(roomId: Int) 확장함수의 파라미터와
GroupSearchScreen(onRoomClick: (Int) -> Unit)의 인자는 모두 Int 타입으로 정의되어 있어
타입 불일치 우려가 없습니다. 해당 코멘트는 더 이상 적용되지 않습니다.

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

6-11: 응답 래퍼 모델 정의 적절

페이지네이션 필드(nextCursor, isLast)와 리스트 기본값이 명확합니다. 직렬화 어노테이션 사용도 올바릅니다.

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

58-67: AI 요약과 불일치: onNavigateToGroupScreen 파라미터는 제거되지 않았습니다

AI 요약에는 공개 API에서 onNavigateToGroupScreen 파라미터가 제거되었다고 되어 있으나, 본 코드에는 동일 파라미터가 그대로 존재하고 사용되고 있습니다. 요약 업데이트가 필요해 보입니다.


99-107: 비밀방 참여 분기 처리 로직 적절

비밀방 여부에 따라 비밀번호 입력 화면으로 분기하는 로직이 명확하며, 공개방은 즉시 참여 트리거로 분기되어 UX 의도에 부합합니다.


400-407: 추천 카드 endDate 바인딩 변경 적절

CardItemRoomSmallendDate가 문자열로 전환됨에 따라 rec.recruitEndDate를 그대로 전달하는 변경이 일관성 있게 반영되었습니다.

app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoom.kt (2)

41-41: endDate 타입을 Int? → String?로 전환한 공개 API 변경 적절

서버의 문자열 기반 마감일 응답에 맞춰 컴포넌트 API를 문자열로 변경한 방향이 타당합니다. 호출부와 미리보기도 일관되게 반영되었습니다.

호출하는 모든 곳이 새 시그니처로 컴파일되는지 CI에서 확인 부탁드립니다.


85-87: 레이아웃 포맷 조정은 OK

fillMaxWidth()와 고정 높이 지정의 포맷 조정은 기능 영향이 없고 현재 UI 구조에 적합합니다.

app/src/main/java/com/texthip/thip/ui/group/search/component/GroupFilteredSearchResult.kt (2)

97-133: 로딩 인디케이터 아이템 처리 깔끔 — LGTM

isLoadingMore일 때 하단에 인디케이터 아이템을 추가하는 방식이 깔끔하고 레이아웃 교란을 최소화합니다.


100-107: 데이터 모델 매핑 적절 및 비밀방 여부 반영 OK

SearchRoomItemCardItemRoomSmall 필드 매핑이 정확하고, isSecret = !room.isPublic 처리도 명확합니다.

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

54-61: 데이터 모델 매핑 적절 및 비밀방 여부 반영 OK

라이브 검색 결과에서도 SearchRoomItem 매핑과 !room.isPublic 처리가 정확합니다.

app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookGroupScreen.kt (5)

59-76: VM 데이터 그대로 연동하는 방향 좋습니다

UI에서 별도 매핑 없이 uiState.recruitingRooms를 직접 전달하고, onLoadMore에서 VM 메서드를 호출하는 구조가 단순하고 명확합니다.


80-96: 모집 중 리스트 타입 교체(RecruitingRoomItem) 적절

CardItemRoom에 필요한 필드가 모두 포함되어 있어, 별도 변환 없이 바인딩할 수 있는 점이 좋습니다.


208-217: CardItemRoom 바인딩 변경 적절

문자열 기반 마감일(deadlineEndDate)과 이미지 URL 바인딩이 새 모델에 맞게 정확히 반영되었습니다. isRecruiting = true 하드코딩도 화면 성격상 타당합니다.


240-258: 하단 CTA 버튼과 리스트 패딩 처리 깔끔 — LGTM

contentPadding = PaddingValues(bottom = 80.dp)로 겹침 방지, 버튼 스타일 일관성 유지 모두 좋습니다.


266-289: Preview 데이터 최신 모델 사용 OK

미리보기가 RecruitingRoomItem으로 갱신되어 디자인 검증에 유용합니다.

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

65-69: 검색 완료 시 포커스 해제 처리 LGTM.

검색 완료 신호에 맞춘 포커스 해제는 UX 상 자연스럽습니다.


93-99: 텍스트 바인딩 및 검색 트리거 LGTM.

uiState 바인딩과 onSearch에서 ViewModel 액션 호출이 일관적입니다.

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

345-349: onCleared에서 Job 취소 처리 LGTM.

코루틴 리소스 정리가 명확합니다.


242-249: 라이브 검색 페이지네이션 isFinalized 플래그 일관성 검증 필요

  • 첫 페이지 호출: isFinalized = !isLiveSearch (라이브 모드 → false)
  • 추가 페이지 로딩: isFinalized = true (항상 true)

이대로 두면 백엔드가 페이지별 isFinalized를 다르게 해석했을 때 결과 리스트가 일관되지 않을 수 있습니다.
백엔드 API 명세 또는 실제 동작을 확인해 주세요.

필요 시:

  • 라이브 모드 추가 페이지 로딩에도 isFinalized = false 적용
  • 또는 라이브 검색에서 무한 스크롤 비활성화

레포 내 호출 위치 빠르게 확인용 스크립트:

rg -nP --glob '!**/build/**' -C3 '\bsearchRooms\s*\('
rg -nP --glob '!**/build/**' -C2 '\bisFinalized\s*='

Comment on lines +9 to +12
val endDate: String? = null, // 마감 시간 텍스트 (예: "8시간 뒤")
val imageUrl: String? = null, // API에서 받은 이미지 URL
val isSecret: Boolean = false
)


) 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

endDate의 타입 변경(Int? → String?) 일관성 좋습니다

UI 전반이 문자열 기반 마감 표기로 이행되면서 본 데이터 모델 수정이 방향성에 부합합니다.

레거시(Int) 타입이나 변환 유틸 의존이 남아있는지 확인 부탁드립니다:


🏁 Script executed:

#!/bin/bash
# endDate가 Int로 선언/사용되는 곳 탐지
rg -nP -C2 '\bendDate\s*:\s*Int\??' --type=kt

# 남은 일수 계산 유틸 호출 탐지
rg -nP -C2 'getEndDateInDays|DateUtils' --type=kt

Length of output: 153


endDate의 Int 타입 선언/사용 및 변환 유틸 호출 여부를 Kotlin 파일 전역에서 재검증합니다. 다음 스크립트를 실행해주세요:


🏁 Script executed:

#!/bin/bash
# endDate가 Int로 선언/사용되는 곳 탐지 (Kotlin 파일)
rg -nP -C2 '\bendDate\s*:\s*Int\??' -g '*.kt'

# 남은 일수 계산 유틸 호출 탐지 (Kotlin 파일)
rg -nP -C2 'getEndDateInDays|DateUtils' -g '*.kt'

Length of output: 577


endDate 변환 유틸리티 정비 필요

String?으로 변경된 endDate 타입에 맞춰, 기존의 정수형 변환 유틸(getEndDateInDays)이 아직 남아 있습니다. 해당 메서드가 더 이상 사용되지 않거나, 새로운 문자열 기반 로직으로 대체되어야 합니다.

수정 대상:

  • app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt
    • fun MyRoomCardData.getEndDateInDays(): Int { … }

조치:

  • 해당 메서드를 제거하거나
  • 문자열 파싱 로직으로 업데이트하여 코드베이스 전체에서 일관된 String 타입 처리 보장
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardItemRoomData.kt
around lines 9 to 12, the endDate field was changed to String? but the old
integer-based utility getEndDateInDays() in
app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt still
assumes an Int; remove that obsolete method or replace its logic with a
String-based parser that accepts the nullable endDate string, safely handles
null/empty values, parses formats used by the API (e.g., “8시간 뒤”, ISO
timestamps, or relative strings), and returns a consistent Int result (or change
callers to expect String), updating all usages across the codebase to use the
new string-handling utility or removed method accordingly.

Comment on lines +163 to 166
endDate = room.endDate,
imageUrl = room.bookImageUrl,
onClick = { onCardClick(room) }
)
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

endDate를 문자열로 직접 전달하도록 변경된 점 확인(LGTM) + 전파 영향 검증 요청

CardItemRoom의 endDate가 String?로 이관되었다면 본 변경은 타당합니다. 남은 참조 중 Int 기반(endDate: Int? 또는 변환 유틸) 사용이 없는지 전체 코드베이스 검증을 권장합니다.

다음 스크립트로 시그니처/잔존 참조를 확인해주세요:


🏁 Script executed:

#!/bin/bash
# CardItemRoom의 endDate 파라미터 타입이 String?인지 확인
rg -nP -C3 'fun\s+CardItemRoom\s*\([^)]*endDate\s*:\s*String\?' --type=kt

# endDate를 Int로 사용하는 잔존 호출/정의 탐지
rg -nP -C2 '\bendDate\s*:\s*Int\??' --type=kt

# 삭제/미사용이어야 할 과거 유틸 호출 탐지
rg -nP -C2 'getEndDateInDays|DateUtils' --type=kt

Length of output: 256


파일 타입 인식 에러로 --type=kt 대신 glob 패턴을 사용해 .kt 파일에서 재검증합니다.


🏁 Script executed:

#!/bin/bash
# CardItemRoom의 endDate 파라미터 타입이 String?인지 확인
rg -nP -C3 'fun\s+CardItemRoom\s*\([^)]*endDate\s*:\s*String\?' -g '*.kt'

# endDate를 Int로 사용하는 잔존 호출/정의 탐지
rg -nP -C2 '\bendDate\s*:\s*Int\??' -g '*.kt'

# 삭제/미사용이어야 할 과거 유틸 호출 탐지
rg -nP -C2 'getEndDateInDays|DateUtils' -g '*.kt'

Length of output: 653


endDate를 String?으로 이관 확인 완료, 구 변환 유틸 제거 필요

CardItemRoom의 endDate 파라미터가 정상적으로 String?로 변경되었으나, mock 데이터 클래스에 여전히 Int 변환용 유틸(getEndDateInDays())이 남아 있습니다. 코드 정합성을 위해 해당 메서드 및 호출부를 제거하거나 String? 기반 로직으로 대체해주세요.

  • app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt:32-34 (fun getEndDateInDays())
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt
around lines 163-166 and
app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt around
lines 32-34: the CardItemRoom now accepts endDate as String?, but the mock still
exposes an Int conversion util getEndDateInDays() and/or calls it; remove the
getEndDateInDays() function from MyRoomCardData and eliminate any calls to it,
and update the mock data to supply endDate as a nullable String (formatted the
same way production code expects) so the screen passes room.endDate directly to
CardItemRoom without converting to Int.

@@ -31,73 +26,44 @@ import com.texthip.thip.ui.common.buttons.FilterButton
import com.texthip.thip.ui.common.forms.SearchBookTextField
import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar
import com.texthip.thip.ui.group.myroom.component.GroupRecentSearch
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

잘못된 import 경로로 인한 컴파일 오류 가능성 (GroupRecentSearch).

GroupRecentSearch의 실제 패키지는 ui/group/search/component로 보입니다. 현재 ui/group/myroom/component로 import되어 있어 컴파일 실패가 예상됩니다.

아래 변경으로 경로를 수정하세요.

-import com.texthip.thip.ui.group.myroom.component.GroupRecentSearch
+import com.texthip.thip.ui.group.search.component.GroupRecentSearch

필요 시 아래 스크립트로 파일 위치를 확인해 주세요.


🏁 Script executed:

#!/bin/bash
# GroupRecentSearch 실제 파일 위치 및 패키지 확인
fd -t f 'GroupRecentSearch.kt' | while read -r f; do
  echo "File: $f"
  sed -n '1,5p' "$f"
  echo
done

Length of output: 545


GroupRecentSearch 패키지 선언 및 import 경로 정정 필요

GroupRecentSearch.kt 파일의 패키지 선언이 실제 경로(ui/group/search/component)와 불일치하여, 아래와 같이 수정이 필요합니다.

• app/src/main/java/com/texthip/thip/ui/group/search/component/GroupRecentSearch.kt

- package com.texthip.thip.ui.group.myroom.component
+ package com.texthip.thip.ui.group.search.component

• app/src/main/java/com/texthip/thip/ui/group/search/screen/GroupSearchScreen.kt

- import com.texthip.thip.ui.group.myroom.component.GroupRecentSearch
+ import com.texthip.thip.ui.group.search.component.GroupRecentSearch
📝 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
import com.texthip.thip.ui.group.myroom.component.GroupRecentSearch
import com.texthip.thip.ui.group.search.component.GroupRecentSearch
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/ui/group/search/screen/GroupSearchScreen.kt
around line 28, the import points to
com.texthip.thip.ui.group.myroom.component.GroupRecentSearch which doesn't match
the actual file location; change the import to
com.texthip.thip.ui.group.search.component.GroupRecentSearch. Also open
app/src/main/java/com/texthip/thip/ui/group/search/component/GroupRecentSearch.kt
and correct its package declaration to package
com.texthip.thip.ui.group.search.component so it matches the file path and the
updated import.

Comment on lines +49 to +51
<string name="card_item_end">" 모집 마감"</string>
<string name="time_remaining_after">뒤</string>
<string name="time_remaining_left">남음</string>
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에서는 포맷 리소스로 안전하게 결합하는 편이 낫습니다.

아래처럼 조정해 주세요.

적용 diff:

-    <string name="card_item_end">" 모집 마감"</string>
+    <string name="card_item_end">모집 마감</string>

추가(선택 영역 밖, 지원용 코드): 카드에서 사용할 포맷 문자열

<string name="card_item_deadline_format">%1$s %2$s</string>

참고: 기존 "card_item_end_date"의 따옴표/후행 공백("%1$s일 뒤 ")도 동일한 이슈가 있으니, 사용하지 않는다면 제거하거나 따옴표/공백을 정리하는 것을 권장합니다.

🤖 Prompt for AI Agents
In app/src/main/res/values/strings.xml around lines 49-51, remove the stray
quotation marks and leading space from the value for card_item_end (change from
"\" 모집 마감\"" to "모집 마감"), ensure time_remaining_after and time_remaining_left
remain plain tokens, and add a format resource string
(card_item_deadline_format) to combine tokens safely in UI (use %1$s %2$s when
formatting) so UI concatenation uses the format resource rather than relying on
embedded spaces/quotes; also review and clean up similar quotes/trailing spaces
in card_item_end_date if unused.

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: 0

♻️ Duplicate comments (1)
app/src/main/java/com/texthip/thip/ui/group/search/screen/GroupSearchScreen.kt (1)

28-28: GroupRecentSearch import 경로 수정 확인

이전 리뷰에서 지적된 잘못된 import 경로가 올바르게 수정되었습니다.

🧹 Nitpick comments (7)
app/src/main/java/com/texthip/thip/ui/group/search/screen/GroupSearchScreen.kt (7)

12-13: collectAsState 대신 collectAsStateWithLifecycle 사용 권장

Compose 화면 Lifecycle에 맞춘 안전한 수집을 위해 collectAsStateWithLifecycle() 사용을 권장합니다.

다음 변경을 제안합니다:

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

Also applies to: 43-43


47-48: toDisplayStrings 계산 최적화(불필요한 재계산 방지)

recomposition마다 매번 계산하지 않도록 remember로 메모이징하는 편이 좋습니다.

-    val genreDisplayNames = uiState.genres.toDisplayStrings()
+    val genreDisplayNames = remember(uiState.genres) { uiState.genres.toDisplayStrings() }

97-99: 검색 즉시 키보드 숨김으로 UX 개선

검색 완료 시점이 아니라 버튼을 누르는 즉시 키보드를 숨기면 사용자 체감이 더 자연스럽습니다. 완료 시점에 한 번 더 clearFocus()가 호출되어도 부작용 없습니다.

-                    onSearch = {
-                        viewModel.onSearchButtonClick()
-                    }
+                    onSearch = {
+                        viewModel.onSearchButtonClick()
+                        focusManager.clearFocus()
+                    }

104-122: 최근 검색 분기 중복 제거

빈 목록/비빈 목록에 따라 동일 컴포넌트를 두 번 호출하고 있어 불필요한 분기가 존재합니다. GroupRecentSearch 자체가 빈 목록 처리 UI를 포함하므로 단일 호출로 단순화할 수 있습니다.

-                    uiState.isInitial -> {
-                        if (uiState.recentSearches.isEmpty()) {
-                            GroupRecentSearch(
-                                recentSearches = emptyList(),
-                                onSearchClick = {},
-                                onRemove = {}
-                            )
-                        } else {
-                            GroupRecentSearch(
-                                recentSearches = uiState.recentSearches.map { it.searchTerm },
-                                onSearchClick = { keyword ->
-                                    viewModel.updateSearchQuery(keyword)
-                                    viewModel.onSearchButtonClick()
-                                },
-                                onRemove = { keyword ->
-                                    viewModel.deleteRecentSearchByKeyword(keyword)
-                                }
-                            )
-                        }
-                    }
+                    uiState.isInitial -> {
+                        GroupRecentSearch(
+                            recentSearches = uiState.recentSearches.map { it.searchTerm },
+                            onSearchClick = { keyword ->
+                                viewModel.updateSearchQuery(keyword)
+                                viewModel.onSearchButtonClick()
+                            },
+                            onRemove = { keyword ->
+                                viewModel.deleteRecentSearchByKeyword(keyword)
+                            }
+                        )
+                    }

146-161: selectedGenreIndex 중복 계산 제거 및 안전 인덱싱

이미 계산한 selectedGenreIndex를 재활용하면 가독성과 안전성이 좋아집니다. 또한 getOrNull로 인덱스 범위를 안전하게 처리할 수 있습니다.

-                            onGenreSelect = { index ->
-                                val currentSelectedIndex = if (uiState.selectedGenre != null) {
-                                    uiState.genres.indexOf(uiState.selectedGenre)
-                                } else -1
-
-                                val selectedGenre = if (index == currentSelectedIndex) {
-                                    // 같은 장르를 다시 터치하면 선택 해제
-                                    null
-                                } else if (index >= 0 && index < uiState.genres.size) {
-                                    // 새로운 장르 선택
-                                    uiState.genres[index]
-                                } else {
-                                    null
-                                }
-                                viewModel.updateSelectedGenre(selectedGenre)
-                            },
+                            onGenreSelect = { index ->
+                                val selectedGenre =
+                                    if (index == selectedGenreIndex) null
+                                    else uiState.genres.getOrNull(index)
+                                viewModel.updateSelectedGenre(selectedGenre)
+                            },

125-140: 라이브 검색 중 로딩 상태 피드백 추가 제안

isLiveSearching 상태에서 showEmptyState == false이고 hasResults == false인 구간에 아무 UI가 노출되지 않습니다. 로딩 인디케이터(스피너 등)를 추가하면 사용자 피드백이 개선됩니다.

원한다면 uiState에 로딩 플래그가 있는지 확인하여 조건부로 CircularProgressIndicator를 렌더링하는 패치를 제안드릴 수 있습니다.


176-179: FilterButton 위치 하드코딩(196.dp) 의존성

상단 앱바/검색창 높이가 변경되면 정렬 버튼 위치가 어긋날 수 있습니다. 상단 영역의 실제 높이에 앵커링하거나, 상단 Column 내 해당 섹션 바로 아래에 배치하는 방식으로 레이아웃 의존성을 줄이는 것을 권장합니다.

원하시면 Insets/측정(Modifier.onGloballyPositioned) 기반으로 안전하게 위치시키는 예시 코드를 드리겠습니다.

📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear 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 377e531 and 7a3c03b.

📒 Files selected for processing (3)
  • app/src/main/java/com/texthip/thip/ui/group/search/component/GroupRecentSearch.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/search/screen/GroupSearchScreen.kt (3 hunks)
  • app/src/main/java/com/texthip/thip/ui/group/search/viewmodel/GroupSearchViewModel.kt (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/main/java/com/texthip/thip/ui/group/search/viewmodel/GroupSearchViewModel.kt
🧰 Additional context used
🧬 Code Graph Analysis (1)
app/src/main/java/com/texthip/thip/ui/group/search/screen/GroupSearchScreen.kt (5)
app/src/main/java/com/texthip/thip/ui/group/search/component/GroupRecentSearch.kt (1)
  • GroupRecentSearch (27-62)
app/src/main/java/com/texthip/thip/ui/group/search/component/GroupEmptyResult.kt (1)
  • GroupEmptyResult (18-40)
app/src/main/java/com/texthip/thip/ui/group/search/component/GroupLiveSearchResult.kt (1)
  • GroupLiveSearchResult (27-88)
app/src/main/java/com/texthip/thip/ui/group/search/component/GroupFilteredSearchResult.kt (1)
  • GroupFilteredSearchResult (37-136)
app/src/main/java/com/texthip/thip/ui/common/buttons/FilterButton.kt (1)
  • FilterButton (37-128)
🔇 Additional comments (1)
app/src/main/java/com/texthip/thip/ui/group/search/component/GroupRecentSearch.kt (1)

1-1: LGTM – 잔여 import 경로 없음 확인됨

  • com.texthip.thip.ui.group.myroom.component.GroupRecentSearch로 남아있는 import가 전체 검색 결과 존재하지 않습니다.
  • Compose 버전이 1.5 미만이라면 FlowRow 사용 시 @OptIn(ExperimentalLayoutApi::class) 어노테이션을 빌드 에러 발생 시점에 추가해 주세요.

@rbqks529 rbqks529 merged commit 1c13431 into THIP-TextHip:develop Aug 18, 2025
1 check was pending
@coderabbitai coderabbitai bot mentioned this pull request Aug 19, 2025
21 tasks
@coderabbitai coderabbitai bot mentioned this pull request Sep 3, 2025
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] 모임방 검색 [(THIP2025-303)]

2 participants