diff --git a/app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt b/app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt index ea3fc9e0..bf7eafad 100644 --- a/app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt +++ b/app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt @@ -1,5 +1,7 @@ package com.texthip.thip.data.di +import com.texthip.thip.data.service.BookService +import com.texthip.thip.data.service.GroupService import com.texthip.thip.data.service.RoomsService import dagger.Module import dagger.Provides @@ -11,6 +13,18 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) object ServiceModule { + + @Provides + @Singleton + fun provideGroupService(retrofit: Retrofit): GroupService { + return retrofit.create(GroupService::class.java) + } + + @Provides + @Singleton + fun provideBookService(retrofit: Retrofit): BookService { + return retrofit.create(BookService::class.java) + } @Provides @Singleton fun providesRoomsService(retrofit: Retrofit): RoomsService = diff --git a/app/src/main/java/com/texthip/thip/data/group/repository/GroupRepository.kt b/app/src/main/java/com/texthip/thip/data/group/repository/GroupRepository.kt deleted file mode 100644 index 4fbd66fa..00000000 --- a/app/src/main/java/com/texthip/thip/data/group/repository/GroupRepository.kt +++ /dev/null @@ -1,324 +0,0 @@ -package com.texthip.thip.data.group.repository - -import com.texthip.thip.R -import com.texthip.thip.ui.group.myroom.mock.GroupBookData -import com.texthip.thip.ui.group.myroom.mock.GroupCardData -import com.texthip.thip.ui.group.myroom.mock.GroupCardItemRoomData -import com.texthip.thip.ui.group.myroom.mock.GroupRoomData -import com.texthip.thip.ui.group.myroom.mock.GroupRoomSectionData -import kotlinx.coroutines.delay - -// 그룹 데이터를 제공하는 Repository -// 실제로는 서버의 API와 통신할 거라서 다 삭제하고 함수 구조만 유지한 채 수정하면 될 듯 합니다. - -class GroupRepository { - - private val genres = listOf("문학", "과학·IT", "사회과학", "인문학", "예술") - private val roomDetailsCache = mutableMapOf() - - suspend fun getUserName(): Result { - return try { - Result.success("규빈") // 임시 이름 - } catch (e: Exception) { - Result.failure(e) - } - } - - suspend fun getMyGroups(): Result> { - return try { - delay(200) - val myGroups = listOf( - GroupCardData(23, "호르몬 체인지 완독하는 방", 22, R.drawable.bookcover_sample, 40, "uibowl1"), - GroupCardData(24, "명작 읽기방", 10, R.drawable.bookcover_sample, 70, "joyce"), - GroupCardData(25, "또 다른 방", 13, R.drawable.bookcover_sample, 10, "other") - ) - Result.success(myGroups) - } catch (e: Exception) { - Result.failure(e) - } - } - - suspend fun getRoomSections(): Result> { - return try { - - // 마감 임박한 독서 모임방 - val deadlineRooms = listOf( - GroupCardItemRoomData(1, "시집만 읽는 사람들 3월", 22, 30, true, 3, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(2, "일본 소설 좋아하는 사람들", 15, 20, true, 2, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(3, "명작 같이 읽기방", 22, 30, true, 3, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(4, "물리책 읽는 방", 13, 20, true, 1, R.drawable.bookcover_sample, 1), - GroupCardItemRoomData(5, "코딩 과학 동아리", 12, 15, true, 5, R.drawable.bookcover_sample, 1), - GroupCardItemRoomData(6, "사회과학 인문 탐구", 8, 12, true, 4, R.drawable.bookcover_sample, 2) - ) - - // 인기 있는 독서 모임방 - val popularRooms = listOf( - GroupCardItemRoomData(7, "베스트셀러 토론방", 28, 30, true, 7, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(8, "인기 소설 완독방", 25, 25, false, 5, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(9, "트렌드 과학서 읽기", 20, 25, true, 10, R.drawable.bookcover_sample, 1), - GroupCardItemRoomData(10, "화제의 경영서", 18, 20, true, 8, R.drawable.bookcover_sample, 2), - GroupCardItemRoomData(11, "인기 철학서 모임", 15, 18, true, 12, R.drawable.bookcover_sample, 3), - GroupCardItemRoomData(12, "예술서 베스트", 12, 15, true, 6, R.drawable.bookcover_sample, 4) - ) - - // 인플루언서, 작가 독서 모임방 - val influencerRooms = listOf( - GroupCardItemRoomData(13, "작가와 함께하는 독서방", 30, 30, false, 14, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(14, "유명 북튜버와 읽기", 18, 20, true, 8, R.drawable.bookcover_sample, 2), - GroupCardItemRoomData(15, "작가 초청 인문학방", 15, 20, true, 12, R.drawable.bookcover_sample, 3), - GroupCardItemRoomData(16, "인플루언서 과학책", 22, 25, true, 9, R.drawable.bookcover_sample, 1), - GroupCardItemRoomData(17, "유명작가 예술론", 16, 18, true, 11, R.drawable.bookcover_sample, 4) - ) - - val sections = listOf( - GroupRoomSectionData( - title = "마감 임박한 독서 모임방", - rooms = deadlineRooms, - genres = genres - ), - GroupRoomSectionData( - title = "인기 있는 독서 모임방", - rooms = popularRooms, - genres = genres - ), - GroupRoomSectionData( - title = "인플루언서·작가 독서 모임방", - rooms = influencerRooms, - genres = genres - ) - ) - - // 상세 데이터 캐시에 저장 - (deadlineRooms + popularRooms + influencerRooms).forEach { room -> - initializeRoomDetail(room) - } - - Result.success(sections) - } catch (e: Exception) { - Result.failure(e) - } - } - - suspend fun getDoneGroups(): Result> { - return try { - val doneGroups = listOf( - GroupCardItemRoomData(18, "완료된 독서 모임방 1", 15, 20, false, null, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(19, "완료된 독서 모임방 2", 25, 30, false, null, R.drawable.bookcover_sample, 1), - GroupCardItemRoomData(20, "완료된 독서 모임방 3", 12, 15, false, null, R.drawable.bookcover_sample, 2), - GroupCardItemRoomData(21, "호르몬 체인지 완독한 방", 22, 22, false, null, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(22, "명작 읽기방 완료", 10, 10, false, null, R.drawable.bookcover_sample, 0) - ) - - // 상세 데이터 캐시에 저장 - doneGroups.forEach { room -> - initializeRoomDetail(room) - } - - Result.success(doneGroups) - } catch (e: Exception) { - Result.failure(e) - } - } - - suspend fun getMyRoomGroups(): Result> { - return try { - val myRoomGroups = listOf( - GroupCardItemRoomData(23, "호르몬 체인지 완독하는 방", 22, 30, true, 5, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(24, "명작 읽기방", 10, 20, true, 3, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(25, "또 다른 방", 13, 25, false, 10, R.drawable.bookcover_sample, 1), - GroupCardItemRoomData(26, "내가 참여한 과학책방", 18, 25, true, 7, R.drawable.bookcover_sample, 1), - GroupCardItemRoomData(27, "인문학 토론방", 12, 20, true, 2, R.drawable.bookcover_sample, 3) - ) - - // 상세 데이터 캐시에 저장 - myRoomGroups.forEach { room -> - initializeRoomDetail(room) - } - - Result.success(myRoomGroups) - } catch (e: Exception) { - Result.failure(e) - } - } - - suspend fun getSearchGroups(): Result> { - return try { - // 기존에 로드된 섹션 데이터들을 합쳐서 반환 - val sectionsResult = getRoomSections() - if (sectionsResult.isSuccess) { - val allRooms = sectionsResult.getOrThrow().flatMap { it.rooms } - Result.success(allRooms) - } else { - Result.failure(sectionsResult.exceptionOrNull() ?: Exception("Failed to load search groups")) - } - } catch (e: Exception) { - Result.failure(e) - } - } - - suspend fun getRoomDetail(roomId: Int): Result { - return try { - delay(150) - val roomDetail = roomDetailsCache[roomId] - if (roomDetail != null) { - Result.success(roomDetail) - } else { - Result.failure(Exception("Room not found: $roomId")) - } - } catch (e: Exception) { - Result.failure(e) - } - } - - suspend fun searchRooms(query: String): Result> { - return try { - val searchResult = getSearchGroups() - if (searchResult.isSuccess) { - val filteredRooms = searchResult.getOrThrow().filter { room -> - room.title.contains(query, ignoreCase = true) - } - Result.success(filteredRooms) - } else { - searchResult - } - } catch (e: Exception) { - Result.failure(e) - } - } - - suspend fun getGenres(): Result> { - return try { - delay(50) - Result.success(genres) - } catch (e: Exception) { - Result.failure(e) - } - } - - - private fun initializeRoomDetail(room: GroupCardItemRoomData) { - val bookData = GroupBookData( - title = "심장보다 단단한 토마토 한 알", - author = "고선지", - publisher = "푸른출판사", - description = "${room.title}에서 읽는 책입니다. 감동적인 이야기로 가득한 작품입니다.", - imageRes = room.imageRes ?: R.drawable.bookcover_sample - ) - - val recommendations = getRecommendations(room.id) - - val roomDetail = GroupRoomData( - id = room.id, - title = room.title, - isSecret = room.isSecret, - description = "${room.title} 모임입니다. 함께 책을 읽고 토론해요.", - startDate = "2025.01.12", - endDate = "2025.02.12", - members = room.participants, - maxMembers = room.maxParticipants, - daysLeft = room.endDate ?: 0, - genre = genres[room.genreIndex], - bookData = bookData, - recommendations = recommendations - ) - - roomDetailsCache[room.id] = roomDetail - - // 추천 모임방들의 상세 정보도 캐시에 추가 - recommendations.forEach { recommendedRoom -> - if (!roomDetailsCache.containsKey(recommendedRoom.id)) { - initializeRecommendedRoomDetail(recommendedRoom) - } - } - } - - // 추천 모임방 예시 by gpt - private fun initializeRecommendedRoomDetail(room: GroupCardItemRoomData) { - val bookTitles = listOf( - "데미안", "1984", "노인과 바다", "위대한 개츠비", "햄릿", - "코스모스", "이기적 유전자", "블랙홀과 시간여행", "총균쇠", - "국부론", "자본론", "사피엔스", "총균쇠", "정의란 무엇인가", - "예술의 역사", "음악의 역사", "미학 오디세이" - ) - - val authors = listOf( - "헤르만 헤세", "조지 오웰", "어니스트 헤밍웨이", "스콧 피츠제럴드", - "칼 세이건", "리처드 도킨스", "킵 손", "재레드 다이아몬드", - "아담 스미스", "칼 마르크스", "유발 하라리", "마이클 샌델" - ) - - val publishers = listOf("푸른출판사", "문학동네", "민음사", "창비", "열린책들", "김영사") - - val bookData = GroupBookData( - title = bookTitles.random(), - author = authors.random(), - publisher = publishers.random(), - description = "${room.title}에서 읽는 흥미로운 책입니다. 함께 읽으며 깊이 있는 토론을 나눠보세요.", - imageRes = room.imageRes ?: R.drawable.bookcover_sample - ) - - val roomDetail = GroupRoomData( - id = room.id, - title = room.title, - isSecret = room.isSecret, - description = "${room.title} 모임입니다. 다양한 관점으로 책을 읽고 의견을 나눠보세요.", - startDate = "2025.01.15", - endDate = "2025.02.15", - members = room.participants, - maxMembers = room.maxParticipants, - daysLeft = room.endDate ?: 0, - genre = genres.getOrElse(room.genreIndex) { genres[0] }, - bookData = bookData, - recommendations = getRecommendations(room.id) // 추천 모임방에도 추천 제공 - ) - - roomDetailsCache[room.id] = roomDetail - } - - private fun getRecommendations(roomId: Int): List { - // 추천 모임방 더미데이터 풀 - val recommendationPool = listOf( - // 문학 관련 추천 - GroupCardItemRoomData(1001, "한국 근현대 소설 읽기", 18, 25, true, 3, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(1002, "일본 문학 애호가들", 22, 30, true, 1, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(1003, "시 읽기 모임", 16, 25, true, 2, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(1004, "해외문학 번역서 읽기", 15, 22, true, 3, R.drawable.bookcover_sample, 0, true), - GroupCardItemRoomData(1005, "고전 문학 탐구", 20, 25, true, 5, R.drawable.bookcover_sample, 0), - - // 과학·IT 관련 추천 - GroupCardItemRoomData(1006, "SF 소설 탐험대", 12, 20, true, 7, R.drawable.bookcover_sample, 1), - GroupCardItemRoomData(1007, "과학도서 함께 읽기", 7, 15, true, 9, R.drawable.bookcover_sample, 1), - GroupCardItemRoomData(1008, "컴퓨터 과학 스터디", 14, 18, true, 4, R.drawable.bookcover_sample, 1), - GroupCardItemRoomData(1009, "물리학 입문서 모임", 10, 16, true, 6, R.drawable.bookcover_sample, 1), - - // 사회과학 관련 추천 - GroupCardItemRoomData(1010, "경제경영서 스터디", 9, 12, true, 6, R.drawable.bookcover_sample, 2), - GroupCardItemRoomData(1011, "사회학 도서 토론", 13, 18, true, 4, R.drawable.bookcover_sample, 2), - GroupCardItemRoomData(1012, "정치학 입문 모임", 11, 15, true, 8, R.drawable.bookcover_sample, 2), - - // 인문학 관련 추천 - GroupCardItemRoomData(1013, "철학 에세이 읽기 모임", 8, 15, true, 5, R.drawable.bookcover_sample, 3), - GroupCardItemRoomData(1014, "인문학 고전 읽기", 20, 25, true, 5, R.drawable.bookcover_sample, 3, true), - GroupCardItemRoomData(1015, "심리학 도서 스터디", 10, 16, true, 7, R.drawable.bookcover_sample, 3), - GroupCardItemRoomData(1016, "역사서 탐구 모임", 11, 16, true, 8, R.drawable.bookcover_sample, 3), - - // 예술 관련 추천 - GroupCardItemRoomData(1017, "미술사 도서 읽기", 14, 20, true, 3, R.drawable.bookcover_sample, 4), - GroupCardItemRoomData(1018, "음악 관련 서적 모임", 12, 18, true, 5, R.drawable.bookcover_sample, 4), - - // 기타 장르 - GroupCardItemRoomData(1019, "로맨스 소설 감상회", 14, 20, true, 4, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(1020, "미스터리 소설 동호회", 15, 18, true, 2, R.drawable.bookcover_sample, 0, true), - GroupCardItemRoomData(1021, "자기계발서 함께 읽기", 25, 30, true, 3, R.drawable.bookcover_sample, 2, true), - GroupCardItemRoomData(1022, "판타지 소설 동호회", 24, 30, true, 1, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(1023, "여행 에세이 모임", 13, 18, true, 4, R.drawable.bookcover_sample, 3), - GroupCardItemRoomData(1024, "추리소설 마니아", 19, 24, true, 6, R.drawable.bookcover_sample, 0) - ) - - // 현재 방과 관련 없는 추천을 제공하기 위해 현재 roomId와 다른 것들만 필터링 - val filteredRecommendations = recommendationPool.filter { it.id != roomId } - - // 랜덤하게 3-5개의 추천 반환 - return filteredRecommendations.shuffled().take(5) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/manager/Genre.kt b/app/src/main/java/com/texthip/thip/data/manager/Genre.kt new file mode 100644 index 00000000..3bc7a8dc --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/manager/Genre.kt @@ -0,0 +1,24 @@ +package com.texthip.thip.data.manager + +/** + * 도서 장르를 나타내는 enum class + */ +enum class Genre( + val displayKey: String, + val apiCategory: String, + val networkApiCategory: String = apiCategory +) { + LITERATURE("literature", "문학"), + SCIENCE_IT("science_it", "과학·IT", "과학/IT"), + SOCIAL_SCIENCE("social_science", "사회과학"), + HUMANITIES("humanities", "인문학"), + ART("art", "예술"); + + companion object { + fun getDefault() = LITERATURE + + fun fromDisplayKey(displayKey: String): Genre? { + return entries.find { it.displayKey == displayKey } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/manager/GenreManager.kt b/app/src/main/java/com/texthip/thip/data/manager/GenreManager.kt new file mode 100644 index 00000000..c4cefe27 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/manager/GenreManager.kt @@ -0,0 +1,24 @@ +package com.texthip.thip.data.manager + +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class GenreManager @Inject constructor() { + + fun getGenres(): List { + return Genre.entries + } + + fun mapGenreToApiCategory(genre: Genre): String { + return genre.networkApiCategory + } + + fun getDefaultGenre(): Genre { + return Genre.getDefault() + } + + fun getGenreByDisplayKey(displayKey: String): Genre? { + return Genre.fromDisplayKey(displayKey) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/manager/UserDataManager.kt b/app/src/main/java/com/texthip/thip/data/manager/UserDataManager.kt new file mode 100644 index 00000000..11cfd151 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/manager/UserDataManager.kt @@ -0,0 +1,19 @@ +package com.texthip.thip.data.manager + +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class UserDataManager @Inject constructor() { + + private var cachedUserName: String? = null + + fun getUserName(): String { + return cachedUserName ?: "사용자" + } + + fun cacheUserName(name: String) { + cachedUserName = name + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/book/response/BookListResponse.kt b/app/src/main/java/com/texthip/thip/data/model/book/response/BookListResponse.kt new file mode 100644 index 00000000..54a61093 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/book/response/BookListResponse.kt @@ -0,0 +1,19 @@ +package com.texthip.thip.data.model.book.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + + +@Serializable +data class BookListResponse( + @SerialName("bookList") val bookList: List +) + +@Serializable +data class BookSavedResponse( + @SerialName("isbn") val isbn: String, + @SerialName("bookTitle") val bookTitle: String, + @SerialName("authorName") val authorName: String, + @SerialName("publisher") val publisher: String, + @SerialName("imageUrl") val imageUrl: String? +) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/group/request/CreateRoomRequest.kt b/app/src/main/java/com/texthip/thip/data/model/group/request/CreateRoomRequest.kt new file mode 100644 index 00000000..b8b88fd8 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/group/request/CreateRoomRequest.kt @@ -0,0 +1,17 @@ +package com.texthip.thip.data.model.group.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class CreateRoomRequest( + @SerialName("isbn") val isbn: String, + @SerialName("category") val category: String, + @SerialName("roomName") val roomName: String, + @SerialName("description") val description: String, + @SerialName("progressStartDate") val progressStartDate: String, + @SerialName("progressEndDate") val progressEndDate: String, + @SerialName("recruitCount") val recruitCount: Int, + @SerialName("password") val password: String? = null, + @SerialName("isPublic") val isPublic: Boolean +) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/group/request/RoomJoinRequest.kt b/app/src/main/java/com/texthip/thip/data/model/group/request/RoomJoinRequest.kt new file mode 100644 index 00000000..3e545262 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/group/request/RoomJoinRequest.kt @@ -0,0 +1,9 @@ +package com.texthip.thip.data.model.group.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RoomJoinRequest( + @SerialName("type") val type: String +) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/group/response/CreateRoomResponse.kt b/app/src/main/java/com/texthip/thip/data/model/group/response/CreateRoomResponse.kt new file mode 100644 index 00000000..a1c57cf1 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/group/response/CreateRoomResponse.kt @@ -0,0 +1,9 @@ +package com.texthip.thip.data.model.group.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class CreateRoomResponse( + @SerialName("roomId") val roomId: Int +) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/group/response/JoinedRoomListResponse.kt b/app/src/main/java/com/texthip/thip/data/model/group/response/JoinedRoomListResponse.kt new file mode 100644 index 00000000..c5fb0eb6 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/group/response/JoinedRoomListResponse.kt @@ -0,0 +1,25 @@ +package com.texthip.thip.data.model.group.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + + +@Serializable +data class JoinedRoomListResponse( + @SerialName("roomList") val roomList: List, + @SerialName("nickname") val nickname: String, + @SerialName("page") val page: Int, + @SerialName("size") val size: Int, + @SerialName("last") val last: Boolean, + @SerialName("first") val first: Boolean +) + +@Serializable +data class JoinedRoomResponse( + @SerialName("roomId") val roomId: Int, + @SerialName("bookImageUrl") val bookImageUrl: String?, + @SerialName("bookTitle") val bookTitle: String, + @SerialName("memberCount") val memberCount: Int, + @SerialName("userPercentage") val userPercentage: Int +) + diff --git a/app/src/main/java/com/texthip/thip/data/model/group/response/MyRoomListResponse.kt b/app/src/main/java/com/texthip/thip/data/model/group/response/MyRoomListResponse.kt new file mode 100644 index 00000000..aa9c6a4d --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/group/response/MyRoomListResponse.kt @@ -0,0 +1,22 @@ +package com.texthip.thip.data.model.group.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class MyRoomListResponse( + @SerialName("roomList") val roomList: List, + @SerialName("nextCursor") val nextCursor: String?, + @SerialName("isLast") val isLast: Boolean +) + +@Serializable +data class MyRoomResponse( + @SerialName("roomId") val roomId: Int, + @SerialName("bookImageUrl") val bookImageUrl: String, + @SerialName("roomName") val roomName: String, + @SerialName("recruitCount") val recruitCount: Int, + @SerialName("memberCount") val memberCount: Int, + @SerialName("endDate") val endDate: String, + @SerialName("type") val type: String +) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/group/response/RoomJoinResponse.kt b/app/src/main/java/com/texthip/thip/data/model/group/response/RoomJoinResponse.kt new file mode 100644 index 00000000..2af96a65 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/group/response/RoomJoinResponse.kt @@ -0,0 +1,10 @@ +package com.texthip.thip.data.model.group.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RoomJoinResponse( + @SerialName("roomId") val roomId: Int, + @SerialName("type") val type: String +) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/group/response/RoomMainResponse.kt b/app/src/main/java/com/texthip/thip/data/model/group/response/RoomMainResponse.kt new file mode 100644 index 00000000..a0b9bb51 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/group/response/RoomMainResponse.kt @@ -0,0 +1,21 @@ +package com.texthip.thip.data.model.group.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + + +@Serializable +data class RoomMainResponse( + @SerialName("roomId") val roomId: Int, + @SerialName("bookImageUrl") val bookImageUrl: String?, + @SerialName("roomName") val roomName: String, + @SerialName("recruitCount") val recruitCount: Int, + @SerialName("memberCount") val memberCount: Int, + @SerialName("deadlineDate") val deadlineDate: String +) + +@Serializable +data class RoomMainList( + @SerialName("deadlineRoomList") val deadlineRoomList: List = emptyList(), + @SerialName("popularRoomList") val popularRoomList: List = emptyList() +) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/group/response/RoomRecruitingResponse.kt b/app/src/main/java/com/texthip/thip/data/model/group/response/RoomRecruitingResponse.kt new file mode 100644 index 00000000..0d35628a --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/group/response/RoomRecruitingResponse.kt @@ -0,0 +1,38 @@ +package com.texthip.thip.data.model.group.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RoomRecruitingResponse( + @SerialName("isHost") val isHost: Boolean, + @SerialName("isJoining") val isJoining: Boolean, + @SerialName("roomId") val roomId: Int, + @SerialName("roomName") val roomName: String, + @SerialName("roomImageUrl") val roomImageUrl: String?, + @SerialName("isPublic") val isPublic: Boolean, + @SerialName("progressStartDate") val progressStartDate: String, + @SerialName("progressEndDate") val progressEndDate: String, + @SerialName("recruitEndDate") val recruitEndDate: String, + @SerialName("category") val category: String, + @SerialName("roomDescription") val roomDescription: String, + @SerialName("memberCount") val memberCount: Int, + @SerialName("recruitCount") val recruitCount: Int, + @SerialName("isbn") val isbn: String, + @SerialName("bookImageUrl") val bookImageUrl: String, + @SerialName("bookTitle") val bookTitle: String, + @SerialName("authorName") val authorName: String, + @SerialName("bookDescription") val bookDescription: String, + @SerialName("publisher") val publisher: String, + @SerialName("recommendRooms") val recommendRooms: List +) + +@Serializable +data class RecommendRoomResponse( + @SerialName("roomId") val roomId: Int, + @SerialName("roomImageUrl") val roomImageUrl: String?, + @SerialName("roomName") val roomName: String, + @SerialName("memberCount") val memberCount: Int, + @SerialName("recruitCount") val recruitCount: Int, + @SerialName("recruitEndDate") val recruitEndDate: String +) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/repository/BookRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/BookRepository.kt new file mode 100644 index 00000000..5641421f --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/repository/BookRepository.kt @@ -0,0 +1,20 @@ +package com.texthip.thip.data.repository + +import com.texthip.thip.data.model.base.handleBaseResponse +import com.texthip.thip.data.service.BookService +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class BookRepository @Inject constructor( + private val bookService: BookService +) { + + /** 저장된 책 또는 모임 책 목록 조회 */ + suspend fun getBooks(type: String) = runCatching { + bookService.getBooks(type) + .handleBaseResponse() + .getOrThrow() + ?.bookList ?: emptyList() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt new file mode 100644 index 00000000..a5c04326 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt @@ -0,0 +1,87 @@ +package com.texthip.thip.data.repository + +import com.texthip.thip.data.manager.GenreManager +import com.texthip.thip.data.manager.UserDataManager +import com.texthip.thip.data.manager.Genre +import com.texthip.thip.data.model.base.handleBaseResponse +import com.texthip.thip.data.model.group.request.CreateRoomRequest +import com.texthip.thip.data.model.group.request.RoomJoinRequest +import com.texthip.thip.data.model.group.response.JoinedRoomListResponse +import com.texthip.thip.data.model.group.response.MyRoomListResponse +import com.texthip.thip.data.model.group.response.RoomMainList +import com.texthip.thip.data.model.group.response.RoomRecruitingResponse +import com.texthip.thip.data.service.GroupService +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class GroupRepository @Inject constructor( + private val groupService: GroupService, + private val genreManager: GenreManager, + private val userDataManager: UserDataManager +) { + + /** 장르 목록 조회 */ + fun getGenres(): Result> { + return Result.success(genreManager.getGenres()) + } + + /** 사용자 이름 조회(캐싱 데이터 사용)*/ + fun getUserName(): Result { + return Result.success(userDataManager.getUserName()) + } + + /** 내가 참여 중인 모임방 목록 조회 */ + suspend fun getMyJoinedRooms(page: Int): Result = runCatching { + val response = groupService.getJoinedRooms(page) + .handleBaseResponse() + .getOrThrow() + + response?.let { joinedRoomsDto -> + userDataManager.cacheUserName(joinedRoomsDto.nickname) + } + + response + } + + /** 카테고리별 모임방 섹션 조회 (마감임박/인기) */ + suspend fun getRoomSections(genre: Genre? = null): Result = runCatching { + val selectedGenre = genre ?: genreManager.getDefaultGenre() + val apiCategory = genreManager.mapGenreToApiCategory(selectedGenre) + + groupService.getRooms(apiCategory) + .handleBaseResponse() + .getOrThrow() + } + + /** 타입별 내 모임방 목록 조회 */ + suspend fun getMyRoomsByType(type: String?, cursor: String? = null): Result = runCatching { + groupService.getMyRooms(type, cursor) + .handleBaseResponse() + .getOrThrow() + } + + /** 모집중인 모임방 상세 정보 조회 */ + suspend fun getRoomRecruiting(roomId: Int): Result = runCatching { + groupService.getRoomRecruiting(roomId) + .handleBaseResponse() + .getOrThrow()!! + } + + /** 새 모임방 생성 */ + suspend fun createRoom(request: CreateRoomRequest): Result = runCatching { + groupService.createRoom(request) + .handleBaseResponse() + .getOrThrow()!! + .roomId + } + + /** 모임방 참여 또는 취소 */ + suspend fun joinOrCancelRoom(roomId: Int, type: String): Result = runCatching { + val request = RoomJoinRequest(type = type) + groupService.joinOrCancelRoom(roomId, request) + .handleBaseResponse() + .getOrThrow()!! + .type + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/service/BookService.kt b/app/src/main/java/com/texthip/thip/data/service/BookService.kt new file mode 100644 index 00000000..e167a521 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/service/BookService.kt @@ -0,0 +1,15 @@ +package com.texthip.thip.data.service + +import com.texthip.thip.data.model.base.BaseResponse +import com.texthip.thip.data.model.book.response.BookListResponse +import retrofit2.http.GET +import retrofit2.http.Query + +interface BookService { + + /** 저장된 책 또는 모임 책 목록 조회 */ + @GET("books") + suspend fun getBooks( + @Query("type") type: String + ): BaseResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/service/GroupService.kt b/app/src/main/java/com/texthip/thip/data/service/GroupService.kt new file mode 100644 index 00000000..b02895c2 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/service/GroupService.kt @@ -0,0 +1,57 @@ +package com.texthip.thip.data.service + +import com.texthip.thip.data.model.base.BaseResponse +import com.texthip.thip.data.model.book.response.BookListResponse +import com.texthip.thip.data.model.group.request.CreateRoomRequest +import com.texthip.thip.data.model.group.request.RoomJoinRequest +import com.texthip.thip.data.model.group.response.CreateRoomResponse +import com.texthip.thip.data.model.group.response.JoinedRoomListResponse +import com.texthip.thip.data.model.group.response.MyRoomListResponse +import com.texthip.thip.data.model.group.response.RoomJoinResponse +import com.texthip.thip.data.model.group.response.RoomRecruitingResponse +import com.texthip.thip.data.model.group.response.RoomMainList +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path +import retrofit2.http.Query + +interface GroupService { + + /** 참여 중인 모임방 목록 조회 */ + @GET("rooms/home/joined") + suspend fun getJoinedRooms( + @Query("page") page: Int = 1 + ): BaseResponse + + /** 카테고리별 모임방 목록 조회 (마감임박/인기) */ + @GET("rooms") + suspend fun getRooms( + @Query("category") category: String = "문학" + ): BaseResponse + + /** 내가 만든/참여한 모임방 목록 조회 */ + @GET("rooms/my") + suspend fun getMyRooms( + @Query("type") type: String? = null, + @Query("cursor") cursor: String? = null + ): BaseResponse + + /** 모집중인 모임방 상세 정보 조회 */ + @GET("rooms/{roomId}/recruiting") + suspend fun getRoomRecruiting(@Path("roomId") roomId: Int): BaseResponse + + /** 새 모임방 생성 */ + @POST("rooms") + suspend fun createRoom( + @Body request: CreateRoomRequest + ): BaseResponse + + /** 모임방 참여/취소 */ + @POST("rooms/{roomId}/join") + suspend fun joinOrCancelRoom( + @Path("roomId") roomId: Int, + @Body request: RoomJoinRequest + ): BaseResponse + +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/common/buttons/GenreChipRow.kt b/app/src/main/java/com/texthip/thip/ui/common/buttons/GenreChipRow.kt index 904abaaf..364cac9f 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/buttons/GenreChipRow.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/buttons/GenreChipRow.kt @@ -1,8 +1,14 @@ package com.texthip.thip.ui.common.buttons -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.texthip.thip.ui.theme.ThipTheme @@ -20,6 +26,8 @@ fun GenreChipRow( ) { genres.forEachIndexed { idx, genre -> OptionChipButton( + modifier = Modifier + .clip(RoundedCornerShape(20.dp)), // 버튼 모양에 맞게 클리핑 text = genre, isFilled = true, isSelected = selectedIndex == idx, diff --git a/app/src/main/java/com/texthip/thip/ui/common/cards/CardBookList.kt b/app/src/main/java/com/texthip/thip/ui/common/cards/CardBookList.kt index 864ca8ef..717dd18f 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/cards/CardBookList.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/cards/CardBookList.kt @@ -1,20 +1,15 @@ package com.texthip.thip.ui.common.cards -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -23,13 +18,11 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage import com.texthip.thip.R import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography @@ -40,7 +33,7 @@ fun CardBookList( title: String, author: String, publisher: String, - imageRes: Int? = R.drawable.bookcover_sample, // 기본 이미지 리소스 + imageUrl: String? = null, // API에서 받은 이미지 URL isBookmarked: Boolean = false, onBookmarkClick: () -> Unit = {} ) { @@ -50,20 +43,12 @@ fun CardBookList( .background(Color.Transparent), ) { // 책 이미지 - Box( - modifier = Modifier - .size(width = 80.dp, height = 108.dp) - ) { - - imageRes?.let { - Image( - painter = painterResource(id = it), - contentDescription = null, - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop - ) - } - } + AsyncImage( + model = imageUrl ?: R.drawable.img_book_cover_sample, + contentDescription = "책 이미지", + modifier = Modifier.size(width = 80.dp, height = 108.dp), + contentScale = ContentScale.Crop + ) Spacer(modifier = Modifier.width(12.dp)) diff --git a/app/src/main/java/com/texthip/thip/ui/common/cards/CardBookSearch.kt b/app/src/main/java/com/texthip/thip/ui/common/cards/CardBookSearch.kt index 3f66b388..15cb00a6 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/cards/CardBookSearch.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/cards/CardBookSearch.kt @@ -1,13 +1,10 @@ package com.texthip.thip.ui.common.cards -import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -17,9 +14,9 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage import com.texthip.thip.R import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography @@ -30,7 +27,7 @@ fun CardBookSearch( modifier: Modifier = Modifier, number: Int? = null, title: String, - imageRes: Int? = R.drawable.bookcover_sample, // 기본 이미지 리소스 + imageUrl: String? = null, // API에서 받은 이미지 URL onClick: () -> Unit = {} ) { Row( @@ -49,19 +46,12 @@ fun CardBookSearch( } // 이미지 - Box( - modifier = Modifier - .size(width = 45.dp, height = 60.dp) - ) { - imageRes?.let { - Image( - painter = painterResource(id = it), - contentDescription = null, - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop - ) - } - } + AsyncImage( + model = imageUrl ?: R.drawable.img_book_cover_sample, + contentDescription = "책 이미지", + modifier = Modifier.size(width = 45.dp, height = 60.dp), + contentScale = ContentScale.Crop + ) Spacer(modifier = Modifier.width(8.dp)) diff --git a/app/src/main/java/com/texthip/thip/ui/common/cards/CardInputBook.kt b/app/src/main/java/com/texthip/thip/ui/common/cards/CardInputBook.kt index 5a59a36f..bfffacff 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/cards/CardInputBook.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/cards/CardInputBook.kt @@ -34,7 +34,7 @@ fun CardInputBook( modifier: Modifier = Modifier, title: String, author: String, - imageRes: Int? = R.drawable.bookcover_sample, // 기본 이미지 리소스 + imageRes: Int? = R.drawable.img_book_cover_sample, // 기본 이미지 리소스 onChangeClick: () -> Unit = {} ) { Row( diff --git a/app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoom.kt b/app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoom.kt index 33a02afb..179ea627 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoom.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoom.kt @@ -1,14 +1,11 @@ package com.texthip.thip.ui.common.cards -import androidx.compose.foundation.Image import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -29,6 +26,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage import com.texthip.thip.R import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors @@ -42,7 +40,7 @@ fun CardItemRoom( maxParticipants: Int, isRecruiting: Boolean, endDate: Int? = null, - imageRes: Int? = R.drawable.bookcover_sample, + imageUrl: String? = null, hasBorder: Boolean = false, onClick: () -> Unit = {} ) { @@ -74,19 +72,12 @@ fun CardItemRoom( modifier = Modifier.fillMaxWidth() ) { // 이미지 - Box( - modifier = Modifier - .size(width = 80.dp, height = 107.dp) - ) { - imageRes?.let { - Image( - painter = painterResource(id = it), - contentDescription = null, - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop - ) - } - } + AsyncImage( + model = imageUrl ?: R.drawable.img_book_cover_sample, + contentDescription = "책 이미지", + modifier = Modifier.size(width = 80.dp, height = 107.dp), + contentScale = ContentScale.Crop + ) Spacer(modifier = Modifier.width(12.dp)) @@ -190,7 +181,7 @@ fun CardItemRoomPreview() { maxParticipants = 30, isRecruiting = true, endDate = 3, - imageRes = R.drawable.bookcover_sample + imageUrl = null ) CardItemRoom( title = "모임방 이름입니다. 모임방 이름입니다.", @@ -198,7 +189,7 @@ fun CardItemRoomPreview() { maxParticipants = 30, isRecruiting = false, endDate = 3, - imageRes = R.drawable.bookcover_sample + imageUrl = null ) CardItemRoom( title = "모임방 이름입니다. 모임방 이름입니다.", @@ -206,7 +197,7 @@ fun CardItemRoomPreview() { maxParticipants = 30, isRecruiting = true, endDate = 3, - imageRes = R.drawable.bookcover_sample, + imageUrl = null, hasBorder = true ) CardItemRoom( @@ -215,7 +206,7 @@ fun CardItemRoomPreview() { maxParticipants = 30, isRecruiting = false, endDate = 3, - imageRes = R.drawable.bookcover_sample, + imageUrl = null, hasBorder = true ) CardItemRoom( @@ -223,7 +214,7 @@ fun CardItemRoomPreview() { participants = 22, maxParticipants = 30, isRecruiting = false, - imageRes = R.drawable.bookcover_sample, + imageUrl = null, hasBorder = true ) } diff --git a/app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoomSmall.kt b/app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoomSmall.kt index 9859ce13..f933f1b4 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoomSmall.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/cards/CardItemRoomSmall.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage import com.texthip.thip.R import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors @@ -40,7 +41,7 @@ fun CardItemRoomSmall( participants: Int, maxParticipants: Int, endDate: Int?, - imageRes: Int? = R.drawable.bookcover_sample_small, + imageUrl: String? = null, // API에서 받은 이미지 URL isWide: Boolean = false, isSecret: Boolean = false, onClick: () -> Unit = {} @@ -77,14 +78,12 @@ fun CardItemRoomSmall( modifier = Modifier .size(width = 60.dp, height = 80.dp) ) { - imageRes?.let { - Image( - painter = painterResource(id = it), - contentDescription = null, - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop - ) - } + AsyncImage( + model = imageUrl ?: R.drawable.bookcover_sample_small, + contentDescription = "책 이미지", + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop + ) if (isSecret) { Image( painter = painterResource(id = R.drawable.ic_secret_cover), @@ -168,8 +167,7 @@ fun CardItemRoomSmallPreview() { title = "방 제목입니다 방 제목입니다", participants = 22, maxParticipants = 30, - endDate = 3, - imageRes = R.drawable.bookcover_sample + endDate = 3 ) CardItemRoomSmall( @@ -177,7 +175,6 @@ fun CardItemRoomSmallPreview() { participants = 18, maxParticipants = 25, endDate = 5, - imageRes = R.drawable.bookcover_sample, isWide = true ) } diff --git a/app/src/main/java/com/texthip/thip/ui/common/cards/CardRoomBook.kt b/app/src/main/java/com/texthip/thip/ui/common/cards/CardRoomBook.kt index eab849c6..ae264ad8 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/cards/CardRoomBook.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/cards/CardRoomBook.kt @@ -1,13 +1,10 @@ package com.texthip.thip.ui.common.cards -import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -24,12 +21,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage import com.texthip.thip.R import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography @@ -41,7 +38,7 @@ fun CardRoomBook( author: String, publisher: String, description: String, - imageRes: Int? = R.drawable.bookcover_sample, + imageUrl: String? = null, // API에서 받은 이미지 URL onClick: () -> Unit = {} ) { Card( @@ -88,19 +85,12 @@ fun CardRoomBook( modifier = Modifier.fillMaxWidth() ) { // 책 이미지 - Box( - modifier = Modifier - .size(width = 80.dp, height = 107.dp) - ) { - imageRes?.let { - Image( - painter = painterResource(id = it), - contentDescription = null, - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop - ) - } - } + AsyncImage( + model = imageUrl ?: R.drawable.img_book_cover_sample, + contentDescription = "책 이미지", + modifier = Modifier.size(width = 80.dp, height = 107.dp), + contentScale = ContentScale.Crop + ) Spacer(modifier = Modifier.width(16.dp)) diff --git a/app/src/main/java/com/texthip/thip/ui/common/forms/SearchBookTextField.kt b/app/src/main/java/com/texthip/thip/ui/common/forms/SearchBookTextField.kt index 066ada9d..11233ceb 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/forms/SearchBookTextField.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/forms/SearchBookTextField.kt @@ -53,7 +53,7 @@ fun SearchBookTextField( .fillMaxWidth() .height(40.dp) .clip(shape) - .background(colors.DarkGrey), + .background(colors.DarkGrey02), contentAlignment = Alignment.CenterStart ) { Row( diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt b/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt new file mode 100644 index 00000000..c42f61be --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/group/done/mock/MyRoomCardData.kt @@ -0,0 +1,39 @@ +package com.texthip.thip.ui.group.done.mock + +import com.texthip.thip.ui.group.myroom.mock.RoomType + +data class MyRoomCardData( + val roomId: Int, + val bookImageUrl: String?, + val roomName: String, + val recruitCount: Int, + val memberCount: Int, + val endDate: String, + val type: String +) + +data class MyRoomsPaginationResult( + val data: List, + val nextCursor: String?, + val isLast: Boolean +) + +// 타입 기반 모집 상태 확인 함수 +fun MyRoomCardData.isRecruitingByType(): Boolean { + return when (type) { + RoomType.RECRUITING.value -> true + RoomType.PLAYING_AND_RECRUITING.value -> false + RoomType.PLAYING.value -> false + RoomType.EXPIRED.value -> false + else -> false + } +} + +fun MyRoomCardData.getEndDateInDays(): Int { + return when { + endDate.contains("일 뒤") -> { + endDate.replace("일 뒤", "").trim().toIntOrNull() ?: 0 + } + else -> 0 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/screen/GroupDoneScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/done/screen/GroupDoneScreen.kt new file mode 100644 index 00000000..32f8d9ca --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/group/done/screen/GroupDoneScreen.kt @@ -0,0 +1,190 @@ +package com.texthip.thip.ui.group.done.screen + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.texthip.thip.R +import com.texthip.thip.data.model.group.response.MyRoomResponse +import com.texthip.thip.ui.common.cards.CardItemRoom +import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar +import com.texthip.thip.ui.group.done.viewmodel.GroupDoneUiState +import com.texthip.thip.ui.group.done.viewmodel.GroupDoneViewModel +import com.texthip.thip.ui.theme.ThipTheme +import com.texthip.thip.ui.theme.ThipTheme.colors +import com.texthip.thip.ui.theme.ThipTheme.typography +import com.texthip.thip.utils.rooms.RoomUtils + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun GroupDoneScreen( + onNavigateBack: () -> Unit = {}, + viewModel: GroupDoneViewModel = hiltViewModel() +) { + val uiState by viewModel.uiState.collectAsState() + + GroupDoneContent( + uiState = uiState, + onNavigateBack = onNavigateBack, + onRefresh = { viewModel.refreshData() }, + onLoadMore = { viewModel.loadMoreExpiredRooms() } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun GroupDoneContent( + uiState: GroupDoneUiState, + onNavigateBack: () -> Unit = {}, + onRefresh: () -> Unit = {}, + onLoadMore: () -> Unit = {} +) { + val listState = rememberLazyListState() + + // 무한 스크롤을 위한 로직 + val shouldLoadMore by remember { + derivedStateOf { + val lastVisibleIndex = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 + val totalItems = listState.layoutInfo.totalItemsCount + lastVisibleIndex >= totalItems - 3 // 마지막 3개 아이템에 도달했을 때 + } + } + + LaunchedEffect(shouldLoadMore) { + if (shouldLoadMore && uiState.canLoadMore) { + onLoadMore() + } + } + + Column( + Modifier.fillMaxSize() + ) { + DefaultTopAppBar( + title = stringResource(R.string.group_done_title), + onLeftClick = onNavigateBack, + ) + + PullToRefreshBox( + isRefreshing = uiState.isLoading, + onRefresh = onRefresh, + modifier = Modifier.fillMaxSize() + ) { + Column( + Modifier + .background(colors.Black) + .fillMaxSize() + .padding(horizontal = 20.dp) + ) { + LazyColumn( + state = listState, + verticalArrangement = Arrangement.spacedBy(20.dp), + contentPadding = PaddingValues(bottom = 20.dp), + modifier = Modifier + .fillMaxSize() + .padding(top = 16.dp) + ) { + item { + Text( + text = stringResource(R.string.group_done_user_comment, uiState.userName), + color = colors.White, + style = typography.menu_r400_s14_h24 + ) + } + + items(uiState.expiredRooms) { room -> + CardItemRoom( + title = room.roomName, + imageUrl = room.bookImageUrl, + participants = room.memberCount, + maxParticipants = room.recruitCount, // 모집 인원 수 사용 + isRecruiting = RoomUtils.isRecruitingByType(room.type), + onClick = { /* 완료된 모임방은 클릭 불가 */ } + ) + } + } + } + } + } +} + + +@Preview +@Composable +fun GroupDoneScreenPreview() { + ThipTheme { + GroupDoneContent( + uiState = GroupDoneUiState( + userName = "김독서", + expiredRooms = listOf( + MyRoomResponse( + roomId = 1, + roomName = "🌙 미드나이트 라이브러리 함께읽기", + bookImageUrl = "https://picsum.photos/300/400?1", + memberCount = 18, + recruitCount = 20, + endDate = "2025-01-31", + type = "EXPIRED" + ), + MyRoomResponse( + roomId = 2, + roomName = "📚 현대문학 깊이읽기 모임", + bookImageUrl = "https://picsum.photos/300/400?2", + memberCount = 12, + recruitCount = 15, + endDate = "2024-12-28", + type = "EXPIRED" + ), + MyRoomResponse( + roomId = 3, + roomName = "🔬 과학책으로 세상보기", + bookImageUrl = "https://picsum.photos/300/400?3", + memberCount = 25, + recruitCount = 30, + endDate = "2024-12-15", + type = "EXPIRED" + ), + MyRoomResponse( + roomId = 4, + roomName = "✨ 철학 고전 탐구하기", + bookImageUrl = "https://picsum.photos/300/400?4", + memberCount = 10, + recruitCount = 12, + endDate = "2024-11-20", + type = "EXPIRED" + ), + MyRoomResponse( + roomId = 5, + roomName = "🎨 예술과 문학의 만남", + bookImageUrl = "https://picsum.photos/300/400?5", + memberCount = 16, + recruitCount = 20, + endDate = "2024-10-31", + type = "EXPIRED" + ) + ), + isLoading = false, + hasMore = true + ) + ) + } +} + diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneUiState.kt b/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneUiState.kt new file mode 100644 index 00000000..8af6d4bc --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneUiState.kt @@ -0,0 +1,15 @@ +package com.texthip.thip.ui.group.done.viewmodel + +import com.texthip.thip.data.model.group.response.MyRoomResponse + +data class GroupDoneUiState( + val expiredRooms: List = emptyList(), + val isLoading: Boolean = false, + val isLoadingMore: Boolean = false, + val hasMore: Boolean = true, + val userName: String = "", + val error: String? = null +) { + val hasContent: Boolean get() = expiredRooms.isNotEmpty() + val canLoadMore: Boolean get() = !isLoading && !isLoadingMore && hasMore +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt new file mode 100644 index 00000000..f76b7678 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/group/done/viewmodel/GroupDoneViewModel.kt @@ -0,0 +1,105 @@ +package com.texthip.thip.ui.group.done.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.texthip.thip.data.repository.GroupRepository +import com.texthip.thip.ui.group.myroom.mock.RoomType +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class GroupDoneViewModel @Inject constructor( + private val repository: GroupRepository +) : ViewModel() { + + private val _uiState = MutableStateFlow(GroupDoneUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private var nextCursor: String? = null + private var isLastPage = false + private var isLoadingMore = false + private var isInitialLoading = false + + private fun updateState(update: (GroupDoneUiState) -> GroupDoneUiState) { + _uiState.value = update(_uiState.value) + } + + init { + loadInitialData() + } + + private fun loadInitialData() { + loadUserName() + loadExpiredRooms(reset = true) + } + + private fun loadUserName() { + viewModelScope.launch { + repository.getUserName() + .onSuccess { name -> + updateState { it.copy(userName = name) } + } + } + } + + fun loadExpiredRooms(reset: Boolean = false) { + // 중복 호출 방지 + if (reset) { + if (isInitialLoading) return + isInitialLoading = true + } else { + if (isLoadingMore || isLastPage) return + isLoadingMore = true + } + + viewModelScope.launch { + try { + if (reset) { + updateState { it.copy(isLoading = true, expiredRooms = emptyList(), hasMore = true) } + nextCursor = null + isLastPage = false + } + + repository.getMyRoomsByType(RoomType.EXPIRED.value, nextCursor) + .onSuccess { myRoomListResponse -> + myRoomListResponse?.let { response -> + val currentList = if (reset) emptyList() else uiState.value.expiredRooms + updateState { + it.copy( + expiredRooms = currentList + response.roomList, + error = null, + isLoadingMore = false, + hasMore = !response.isLast + ) + } + nextCursor = response.nextCursor + isLastPage = response.isLast + } + } + .onFailure { exception -> + updateState { it.copy(error = exception.message) } + } + } finally { + if (reset) { + updateState { it.copy(isLoading = false) } + isInitialLoading = false + } else { + updateState { it.copy(isLoadingMore = false) } + isLoadingMore = false + } + } + } + } + + fun loadMoreExpiredRooms() { + loadExpiredRooms(reset = false) + } + + fun refreshData() { + loadExpiredRooms(reset = true) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookListWithScrollbar.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookListWithScrollbar.kt index db9fc7fb..e49ce2c8 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookListWithScrollbar.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookListWithScrollbar.kt @@ -42,7 +42,7 @@ fun GroupBookListWithScrollbar( books.forEachIndexed { index, book -> CardBookSearch( title = book.title, - imageRes = book.imageRes, + imageUrl = book.imageUrl, onClick = { onBookClick(book) } ) @@ -67,7 +67,7 @@ fun PreviewBookListWithScrollbar() { ThipTheme { Column { GroupBookListWithScrollbar( - books = List(20) { BookData("Book $it", R.drawable.bookcover_sample) }, + books = List(20) { BookData("Book $it", null) }, onBookClick = {} ) } diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookSearchBottomSheet.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookSearchBottomSheet.kt index 829e1701..67d8afe9 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookSearchBottomSheet.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupBookSearchBottomSheet.kt @@ -18,21 +18,22 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.texthip.thip.R +import com.texthip.thip.ui.common.bottomsheet.CustomBottomSheet import com.texthip.thip.ui.common.forms.SearchBookTextField +import com.texthip.thip.ui.common.header.HeaderMenuBarTab import com.texthip.thip.ui.group.makeroom.mock.BookData import com.texthip.thip.ui.group.makeroom.mock.dummyGroupBooks import com.texthip.thip.ui.group.makeroom.mock.dummySavedBooks import com.texthip.thip.ui.theme.ThipTheme -import com.texthip.thip.ui.common.bottomsheet.CustomBottomSheet -import com.texthip.thip.ui.common.header.HeaderMenuBarTab @Composable fun GroupBookSearchBottomSheet( onDismiss: () -> Unit, onBookSelect: (BookData) -> Unit, onRequestBook: () -> Unit, - savedBooks: List = emptyList(), - groupBooks: List = emptyList() + savedBooks: List, + groupBooks: List, + isLoading: Boolean = false ) { val hasBooks = savedBooks.isNotEmpty() || groupBooks.isNotEmpty() var selectedTab by rememberSaveable { mutableIntStateOf(0) } @@ -57,6 +58,9 @@ fun GroupBookSearchBottomSheet( } } + // 검색 결과가 있는지 확인 + val hasSearchResults = searchText.isEmpty() || filteredBooks.isNotEmpty() + CustomBottomSheet( onDismiss = onDismiss ) { @@ -75,56 +79,59 @@ fun GroupBookSearchBottomSheet( Spacer(Modifier.height(20.dp)) } - if (hasBooks) { + // 책이 있고 검색 결과가 있을 때만 탭 표시 + if (hasBooks && hasSearchResults) { HeaderMenuBarTab( titles = tabs, selectedTabIndex = selectedTab, onTabSelected = { selectedTab = it - // searchText = "" }, indicatorColor = ThipTheme.colors.White, modifier = Modifier.fillMaxWidth() ) + } - Column( - Modifier - .fillMaxWidth() - .padding(start = 20.dp, end = 20.dp, bottom = 20.dp) - ) { - Spacer(Modifier.height(20.dp)) + Column( + Modifier + .fillMaxWidth() + .padding(start = 20.dp, end = 20.dp, bottom = 90.dp) + ) { + Spacer(Modifier.height(20.dp)) - when { - currentBooks.isEmpty() -> { - EmptyBookSheetContent(onRequestBook = onRequestBook) - } - filteredBooks.isEmpty() && searchText.isNotEmpty() -> { - SearchEmptyContent( - searchText = searchText, - onRequestBook = onRequestBook - ) - } - else -> { - GroupBookListWithScrollbar( - books = filteredBooks, - onBookClick = onBookSelect - ) - } + when { + // 로딩 중 + isLoading -> { + EmptyBookSheetContent(onRequestBook = onRequestBook) + } + // 검색 결과가 없을 때 (검색어가 있지만 결과가 없음) + searchText.isNotEmpty() && filteredBooks.isEmpty() -> { + SearchEmptyContent( + searchText = searchText, + onRequestBook = onRequestBook + ) + } + // 전체 책이 없을 때 + !hasBooks -> { + EmptyBookSheetContent(onRequestBook = onRequestBook) + } + // 현재 탭의 책이 없을 때 + currentBooks.isEmpty() -> { + EmptyBookSheetContent(onRequestBook = onRequestBook) + } + // 정상적으로 책 목록이 있을 때 + else -> { + GroupBookListWithScrollbar( + books = filteredBooks, + onBookClick = onBookSelect + ) } - } - } else { - Column( - Modifier - .fillMaxWidth() - .padding(start = 20.dp, end = 20.dp, bottom = 20.dp) - ) { - Spacer(Modifier.height(20.dp)) - EmptyBookSheetContent(onRequestBook = onRequestBook) } } } } + // 검색 결과가 없을 때 표시할 컴포넌트 (필요시 구현) @Composable private fun SearchEmptyContent( @@ -147,7 +154,8 @@ fun PreviewBookSearchBottomSheet_HasBooks() { onBookSelect = {}, onRequestBook = {}, savedBooks = dummySavedBooks, // 데이터 있음 - groupBooks = dummyGroupBooks + groupBooks = dummyGroupBooks, + isLoading = false ) } } @@ -164,7 +172,8 @@ fun PreviewBookSearchBottomSheet_Empty() { onBookSelect = {}, onRequestBook = {}, savedBooks = emptyList(), // 데이터 없음 - groupBooks = emptyList() + groupBooks = emptyList(), + isLoading = false ) } } diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupSelectBook.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupSelectBook.kt index dc9864fd..98115678 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupSelectBook.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupSelectBook.kt @@ -1,7 +1,6 @@ package com.texthip.thip.ui.group.makeroom.component import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.Image import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -19,10 +18,12 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage import com.texthip.thip.R import com.texthip.thip.ui.common.buttons.OptionChipButton import com.texthip.thip.ui.group.makeroom.mock.BookData @@ -83,12 +84,13 @@ fun GroupSelectBook( .height(80.dp), verticalAlignment = Alignment.Bottom ) { - Image( - painter = painterResource(selectedBook.imageRes), + AsyncImage( + model = selectedBook.imageUrl ?: R.drawable.img_book_cover_sample, contentDescription = selectedBook.title, modifier = Modifier .height(80.dp) - .width(60.dp) + .width(60.dp), + contentScale = ContentScale.Crop ) Spacer(modifier = Modifier.width(12.dp)) Column( @@ -128,7 +130,7 @@ fun GroupSelectBook( private val dummyBook = BookData( title = "호르몬 체인지", - imageRes = R.drawable.bookcover_sample, + imageUrl = null, author = "최정화" ) diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupBookData.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupBookData.kt index e80bd652..4367ed9a 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupBookData.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupBookData.kt @@ -1,30 +1,29 @@ package com.texthip.thip.ui.group.makeroom.mock -import com.texthip.thip.R - data class BookData( val title: String, - val imageRes: Int, // drawable 리소스 or 이미지 URL - val author: String? = null + val imageUrl: String? = null, // 이미지 URL + val author: String? = null, + val isbn: String? = null // 방 생성 시 필요한 ISBN ) val dummySavedBooks = listOf( - BookData("토마토 컬러면", R.drawable.bookcover_sample, "최정화"), - BookData("사슴", R.drawable.bookcover_sample, "최정화"), - BookData("토마토 컬러면", R.drawable.bookcover_sample, "최정화"), - BookData("사슴", R.drawable.bookcover_sample, "최정화"), - BookData("토마토 컬러면", R.drawable.bookcover_sample, "최정화"), - BookData("사슴", R.drawable.bookcover_sample, "최정화"), - BookData("토마토 컬러면", R.drawable.bookcover_sample, "최정화"), - BookData("사슴", R.drawable.bookcover_sample, "최정화") + BookData("토마토 컬러면", null, "최정화"), + BookData("사슴", null, "최정화"), + BookData("토마토 컬러면", null, "최정화"), + BookData("사슴", null, "최정화"), + BookData("토마토 컬러면", null, "최정화"), + BookData("사슴", null, "최정화"), + BookData("토마토 컬러면", null, "최정화"), + BookData("사슴", null, "최정화") ) val dummyGroupBooks = listOf( - BookData("명작 읽기방", R.drawable.bookcover_sample), - BookData("또 다른 방", R.drawable.bookcover_sample), - BookData("명작 읽기방", R.drawable.bookcover_sample), - BookData("또 다른 방", R.drawable.bookcover_sample), - BookData("명작 읽기방", R.drawable.bookcover_sample), - BookData("또 다른 방", R.drawable.bookcover_sample), - BookData("명작 읽기방", R.drawable.bookcover_sample), - BookData("또 다른 방", R.drawable.bookcover_sample) + BookData("명작 읽기방", null), + BookData("또 다른 방", null), + BookData("명작 읽기방", null), + BookData("또 다른 방", null), + BookData("명작 읽기방", null), + BookData("또 다른 방", null), + BookData("명작 읽기방", null), + BookData("또 다른 방", null) ) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt index 3920a5f4..a4fc96ff 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/screen/GroupMakeRoomScreen.kt @@ -24,8 +24,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.hilt.navigation.compose.hiltViewModel import com.texthip.thip.R +import com.texthip.thip.data.manager.Genre import com.texthip.thip.ui.common.buttons.GenreChipRow import com.texthip.thip.ui.common.buttons.ToggleSwitchButton import com.texthip.thip.ui.common.forms.WarningTextField @@ -37,24 +38,22 @@ import com.texthip.thip.ui.group.makeroom.component.GroupSelectBook import com.texthip.thip.ui.group.makeroom.component.GroupMemberLimitPicker import com.texthip.thip.ui.group.makeroom.component.SectionDivider import com.texthip.thip.ui.group.makeroom.mock.BookData -import com.texthip.thip.ui.group.makeroom.mock.dummyGroupBooks -import com.texthip.thip.ui.group.makeroom.mock.dummySavedBooks +import com.texthip.thip.ui.group.makeroom.viewmodel.GroupMakeRoomUiState import com.texthip.thip.ui.group.makeroom.viewmodel.GroupMakeRoomViewModel import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography +import com.texthip.thip.utils.rooms.toDisplayStrings @Composable fun GroupMakeRoomScreen( - viewModel: GroupMakeRoomViewModel, onNavigateBack: () -> Unit, onGroupCreated: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + viewModel: GroupMakeRoomViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsState() - val scrollState = rememberScrollState() - val genres = viewModel.genres // 에러 메시지 표시 LaunchedEffect(uiState.errorMessage) { @@ -63,6 +62,54 @@ fun GroupMakeRoomScreen( } } + GroupMakeRoomContent( + uiState = uiState, + onNavigateBack = onNavigateBack, + onGroupCreated = onGroupCreated, + onCreateGroup = { + viewModel.createGroup( + onSuccess = { roomId -> + // TODO: 생성된 roomId를 사용하여 해당 방으로 이동할 수 있음 + onGroupCreated() + }, + onError = { errorMessage -> + // TODO: 에러 메시지 표시 (토스트 메시지 등) + // 현재는 uiState.errorMessage를 통해 처리 + } + ) + }, + onSelectBook = viewModel::selectBook, + onToggleBookSearchSheet = viewModel::toggleBookSearchSheet, + onSelectGenre = viewModel::selectGenre, + onUpdateRoomTitle = viewModel::updateRoomTitle, + onUpdateRoomDescription = viewModel::updateRoomDescription, + onSetDateRange = viewModel::setDateRange, + onSetMemberLimit = viewModel::setMemberLimit, + onTogglePrivate = viewModel::togglePrivate, + onUpdatePassword = viewModel::updatePassword, + modifier = modifier + ) +} + +@Composable +fun GroupMakeRoomContent( + modifier: Modifier = Modifier, + uiState: GroupMakeRoomUiState, + onNavigateBack: () -> Unit = {}, + onGroupCreated: () -> Unit = {}, // 그룹이 만들어졌을때 로직 + onCreateGroup: () -> Unit = {}, + onSelectBook: (BookData) -> Unit = {}, + onToggleBookSearchSheet: (Boolean) -> Unit = {}, + onSelectGenre: (Int) -> Unit = {}, + onUpdateRoomTitle: (String) -> Unit = {}, + onUpdateRoomDescription: (String) -> Unit = {}, + onSetDateRange: (java.time.LocalDate, java.time.LocalDate) -> Unit = { _, _ -> }, + onSetMemberLimit: (Int) -> Unit = {}, + onTogglePrivate: (Boolean) -> Unit = {}, + onUpdatePassword: (String) -> Unit = {} +) { + val scrollState = rememberScrollState() + Box { Column( modifier = modifier @@ -75,12 +122,7 @@ fun GroupMakeRoomScreen( title = stringResource(R.string.group_making_group), isRightButtonEnabled = uiState.isFormValid && !uiState.isLoading, onLeftClick = onNavigateBack, - onRightClick = { - viewModel.createGroup( - onSuccess = onGroupCreated, - onError = { /* 에러는 uiState.errorMessage로 처리 */ } - ) - } + onRightClick = onCreateGroup ) Column( @@ -94,8 +136,8 @@ fun GroupMakeRoomScreen( GroupSelectBook( selectedBook = uiState.selectedBook, - onChangeBookClick = { viewModel.toggleBookSearchSheet(true) }, - onSelectBookClick = { viewModel.toggleBookSearchSheet(true) } + onChangeBookClick = { onToggleBookSearchSheet(true) }, + onSelectBookClick = { onToggleBookSearchSheet(true) } ) SectionDivider() @@ -108,9 +150,9 @@ fun GroupMakeRoomScreen( Spacer(modifier = Modifier.padding(top = 12.dp)) GenreChipRow( modifier = Modifier.width(18.dp), - genres = genres, + genres = uiState.genres.toDisplayStrings(), selectedIndex = uiState.selectedGenreIndex, - onSelect = viewModel::selectGenre + onSelect = onSelectGenre ) Spacer(modifier = Modifier.height(12.dp)) @@ -132,7 +174,7 @@ fun GroupMakeRoomScreen( hint = stringResource(R.string.group_room_title_hint), value = uiState.roomTitle, maxLength = 15, - onValueChange = viewModel::updateRoomTitle + onValueChange = onUpdateRoomTitle ) SectionDivider() @@ -141,20 +183,20 @@ fun GroupMakeRoomScreen( title = stringResource(R.string.group_room_explain), hint = stringResource(R.string.group_room_explain_hint), value = uiState.roomDescription, - onValueChange = viewModel::updateRoomDescription + onValueChange = onUpdateRoomDescription ) SectionDivider() GroupRoomDurationPicker( - onDateRangeSelected = viewModel::setDateRange + onDateRangeSelected = onSetDateRange ) SectionDivider() GroupMemberLimitPicker( selectedCount = uiState.memberLimit, - onCountSelected = viewModel::setMemberLimit + onCountSelected = onSetMemberLimit ) SectionDivider() @@ -177,7 +219,7 @@ fun GroupMakeRoomScreen( ) ToggleSwitchButton( isChecked = uiState.isPrivate, - onToggleChange = viewModel::togglePrivate + onToggleChange = onTogglePrivate ) } @@ -185,7 +227,7 @@ fun GroupMakeRoomScreen( Spacer(modifier = Modifier.height(12.dp)) WarningTextField( value = uiState.password, - onValueChange = viewModel::updatePassword, + onValueChange = onUpdatePassword, hint = stringResource(R.string.group_password_hint), showWarning = uiState.password.isNotEmpty() && uiState.password.length < 4, warningMessage = stringResource(R.string.group_private_warning_message), @@ -204,16 +246,17 @@ fun GroupMakeRoomScreen( if (uiState.showBookSearchSheet) { GroupBookSearchBottomSheet( - onDismiss = { viewModel.toggleBookSearchSheet(false) }, + onDismiss = { onToggleBookSearchSheet(false) }, onBookSelect = { book: BookData -> - viewModel.selectBook(book) - viewModel.toggleBookSearchSheet(false) + onSelectBook(book) + onToggleBookSearchSheet(false) }, onRequestBook = { - viewModel.toggleBookSearchSheet(false) + onToggleBookSearchSheet(false) }, - savedBooks = dummySavedBooks, - groupBooks = dummyGroupBooks + savedBooks = uiState.savedBooks, + groupBooks = uiState.groupBooks, + isLoading = uiState.isLoadingBooks ) } @@ -235,13 +278,51 @@ fun GroupMakeRoomScreen( @Preview @Composable private fun GroupMakeRoomScreenPreview() { - val mockViewModel: GroupMakeRoomViewModel = viewModel() - ThipTheme { - GroupMakeRoomScreen( - viewModel = mockViewModel, - onNavigateBack = { }, - onGroupCreated = { } + GroupMakeRoomContent( + uiState = GroupMakeRoomUiState( + selectedBook = BookData( + title = "미드나이트 라이브러리", + imageUrl = "https://picsum.photos/300/400?1", + author = "매트 헤이그", + isbn = "9788937477263" + ), + selectedGenreIndex = 2, + roomTitle = "인생에 대해 고민하는 독서모임", + roomDescription = "매트 헤이그의 미드나이트 라이브러리를 함께 읽으며 인생의 가능성과 선택에 대해 이야기해요. 따뜻한 마음으로 서로의 이야기를 들어주실 분들과 함께하고 싶어요.", + memberLimit = 12, + isPrivate = true, + password = "1234", + genres = Genre.entries.toList(), + savedBooks = listOf( + BookData( + title = "코스모스", + imageUrl = "https://picsum.photos/300/400?2", + author = "칼 세이건", + isbn = "9788983711892" + ), + BookData( + title = "사피엔스", + imageUrl = "https://picsum.photos/300/400?3", + author = "유발 하라리", + isbn = "9788934972464" + ) + ), + groupBooks = listOf( + BookData( + title = "1984", + imageUrl = "https://picsum.photos/300/400?4", + author = "조지 오웰", + isbn = "9788937460777" + ), + BookData( + title = "어린왕자", + imageUrl = "https://picsum.photos/300/400?5", + author = "생텍쥐페리", + isbn = "9788932917245" + ) + ) + ) ) } } diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupMakeRoomUiState.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomUiState.kt similarity index 80% rename from app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupMakeRoomUiState.kt rename to app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomUiState.kt index 7143a1d2..9e95010a 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/mock/GroupMakeRoomUiState.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomUiState.kt @@ -1,5 +1,8 @@ -package com.texthip.thip.ui.group.makeroom.mock +package com.texthip.thip.ui.group.makeroom.viewmodel +import com.texthip.thip.data.manager.Genre +import com.texthip.thip.ui.group.makeroom.mock.BookData +import com.texthip.thip.ui.group.makeroom.mock.GroupMakeRoomRequest import java.time.LocalDate import java.time.temporal.ChronoUnit @@ -15,7 +18,11 @@ data class GroupMakeRoomUiState( val isPrivate: Boolean = false, val password: String = "", val isLoading: Boolean = false, - val errorMessage: String? = null + val errorMessage: String? = null, + val savedBooks: List = emptyList(), + val groupBooks: List = emptyList(), + val isLoadingBooks: Boolean = false, + val genres: List = emptyList() ) { // 유효성 검사 로직 val isDurationValid: Boolean diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt index a857e861..4652e06b 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomViewModel.kt @@ -1,139 +1,193 @@ package com.texthip.thip.ui.group.makeroom.viewmodel +import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.texthip.thip.R +import com.texthip.thip.data.model.book.response.BookSavedResponse +import com.texthip.thip.data.model.group.request.CreateRoomRequest +import com.texthip.thip.data.manager.Genre +import com.texthip.thip.data.repository.BookRepository +import com.texthip.thip.data.repository.GroupRepository import com.texthip.thip.ui.group.makeroom.mock.BookData -import com.texthip.thip.ui.group.makeroom.mock.GroupMakeRoomRequest -import com.texthip.thip.ui.group.makeroom.mock.GroupMakeRoomUiState +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import java.time.LocalDate - -// 나중에 서버와 연동할 때 사용할 뷰모델 예시 -class GroupMakeRoomViewModel( - private val groupRepository: GroupRepository = MockGroupRepository() // 기본값으로 Mock Repository 사용 +import java.time.format.DateTimeFormatter +import javax.inject.Inject + +@HiltViewModel +class GroupMakeRoomViewModel @Inject constructor( + private val groupRepository: GroupRepository, + private val bookRepository: BookRepository, + @param:ApplicationContext private val context: Context ) : ViewModel() { private val _uiState = MutableStateFlow(GroupMakeRoomUiState()) val uiState: StateFlow = _uiState.asStateFlow() + + companion object { + private val DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy.MM.dd") + } + + private fun updateState(update: (GroupMakeRoomUiState) -> GroupMakeRoomUiState) { + _uiState.value = update(_uiState.value) + } + + init { + loadGenres() + } + + private fun loadGenres() { + viewModelScope.launch { + groupRepository.getGenres() + .onSuccess { genresList -> + updateState { it.copy(genres = genresList) } + } + } + } - val genres = listOf("문학", "과학·IT", "사회과학", "인문학", "예술") - - // 책 선택 fun selectBook(book: BookData) { - _uiState.value = _uiState.value.copy(selectedBook = book) + updateState { it.copy(selectedBook = book) } } - // 책 검색 시트 표시 상태 변경 fun toggleBookSearchSheet(show: Boolean) { - _uiState.value = _uiState.value.copy(showBookSearchSheet = show) + updateState { it.copy(showBookSearchSheet = show) } + if (show) { + loadBooks() + } + } + + private fun loadBooks() { + viewModelScope.launch { + updateState { it.copy(isLoadingBooks = true) } + try { + val savedBooksResult = bookRepository.getBooks("saved") + savedBooksResult.onSuccess { bookDtos -> + updateState { it.copy(savedBooks = bookDtos.map { dto -> dto.toBookData() }) } + }.onFailure { + updateState { it.copy(savedBooks = emptyList()) } + } + + val groupBooksResult = bookRepository.getBooks("joining") + groupBooksResult.onSuccess { bookDtos -> + updateState { it.copy(groupBooks = bookDtos.map { dto -> dto.toBookData() }) } + }.onFailure { + updateState { it.copy(groupBooks = emptyList()) } + } + } catch (e: Exception) { + updateState { it.copy(savedBooks = emptyList(), groupBooks = emptyList()) } + } finally { + updateState { it.copy(isLoadingBooks = false) } + } + } + } + + private fun BookSavedResponse.toBookData(): BookData { + return BookData( + title = this.bookTitle, + imageUrl = this.imageUrl, + author = this.authorName, + isbn = this.isbn + ) } - // 장르 선택 fun selectGenre(index: Int) { - _uiState.value = _uiState.value.copy(selectedGenreIndex = index) + updateState { it.copy(selectedGenreIndex = index) } } - // 방 제목 변경 fun updateRoomTitle(title: String) { - _uiState.value = _uiState.value.copy(roomTitle = title) + updateState { it.copy(roomTitle = title) } } - // 방 설명 변경 fun updateRoomDescription(description: String) { - _uiState.value = _uiState.value.copy(roomDescription = description) + updateState { it.copy(roomDescription = description) } } - // 모임 날짜 범위 설정 fun setDateRange(startDate: LocalDate, endDate: LocalDate) { - _uiState.value = _uiState.value.copy( - meetingStartDate = startDate, - meetingEndDate = endDate - ) + updateState { + it.copy( + meetingStartDate = startDate, + meetingEndDate = endDate + ) + } } - // 인원 수 설정 fun setMemberLimit(count: Int) { - _uiState.value = _uiState.value.copy(memberLimit = count) + updateState { it.copy(memberLimit = count) } } - // 비밀방 설정 fun togglePrivate(isPrivate: Boolean) { - _uiState.value = _uiState.value.copy( - isPrivate = isPrivate, - password = if (!isPrivate) "" else _uiState.value.password - ) + updateState { + it.copy( + isPrivate = isPrivate, + password = if (!isPrivate) "" else it.password + ) + } } - // 비밀번호 설정 fun updatePassword(password: String) { - _uiState.value = _uiState.value.copy(password = password) + updateState { it.copy(password = password) } } - // 그룹 생성 요청 - fun createGroup(onSuccess: () -> Unit, onError: (String) -> Unit) { + fun createGroup(onSuccess: (Int) -> Unit, onError: (String) -> Unit) { val currentState = _uiState.value if (!currentState.isFormValid) { - //onError("입력 정보를 확인해주세요") + onError(context.getString(R.string.error_form_validation)) + return + } + + val selectedBook = currentState.selectedBook + if (selectedBook?.isbn == null) { + onError(context.getString(R.string.error_book_info_invalid)) return } viewModelScope.launch { try { - _uiState.value = currentState.copy(isLoading = true, errorMessage = null) - - val request = currentState.toRequest() - val result = groupRepository.createGroup(request) - - if (result.isSuccess) { - onSuccess() - } else { - //onError(result.message ?: "그룹 생성에 실패했습니다") + updateState { it.copy(isLoading = true, errorMessage = null) } + + val request = CreateRoomRequest( + isbn = selectedBook.isbn, + category = getApiCategoryName(currentState.selectedGenreIndex), + roomName = currentState.roomTitle.trim(), + description = currentState.roomDescription.trim(), + progressStartDate = currentState.meetingStartDate.format(DATE_FORMATTER), + progressEndDate = currentState.meetingEndDate.format(DATE_FORMATTER), + recruitCount = currentState.memberLimit, + password = if (currentState.isPrivate) currentState.password else null, + isPublic = !currentState.isPrivate + ) + + val result = groupRepository.createRoom(request) + result.onSuccess { roomId -> + onSuccess(roomId) + }.onFailure { exception -> + onError(context.getString(R.string.error_room_creation_failed, exception.message ?: "")) } } catch (e: Exception) { - //onError("네트워크 오류가 발생했습니다: ${e.message}") + onError(context.getString(R.string.error_network_error, e.message ?: "")) } finally { - _uiState.value = _uiState.value.copy(isLoading = false) + updateState { it.copy(isLoading = false) } } } } + + private fun getApiCategoryName(genreIndex: Int): String { + val currentGenres = uiState.value.genres + if (genreIndex >= 0 && genreIndex < currentGenres.size) { + val genre = currentGenres[genreIndex] + return genre.networkApiCategory + } + return Genre.getDefault().networkApiCategory + } - // 에러 메시지 클리어 fun clearError() { - _uiState.value = _uiState.value.copy(errorMessage = null) - } -} - -// Repository 예시 -interface GroupRepository { - suspend fun createGroup(request: GroupMakeRoomRequest): ApiResult -} - -// API 응답 클래스 예시 -data class ApiResult( - val isSuccess: Boolean, - val data: T? = null, - val message: String? = null -) - -data class GroupCreateResponse( - val groupId: String, - val groupName: String -) - -// Mock Repository 구현 -class MockGroupRepository : GroupRepository { - override suspend fun createGroup(request: GroupMakeRoomRequest): ApiResult { - // 임시로 성공 응답 반환 - return ApiResult( - isSuccess = true, - data = GroupCreateResponse( - groupId = "mock_group_${System.currentTimeMillis()}", - groupName = request.roomTitle - ) - ) + updateState { it.copy(errorMessage = null) } } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt index fa6984c4..20b791f2 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupDeadlineRoomSection.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -19,10 +18,6 @@ import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush @@ -34,25 +29,26 @@ import androidx.compose.ui.unit.dp import com.texthip.thip.R import com.texthip.thip.ui.common.buttons.GenreChipRow import com.texthip.thip.ui.common.cards.CardItemRoom -import com.texthip.thip.ui.group.myroom.mock.GroupCardItemRoomData -import com.texthip.thip.ui.group.myroom.mock.GroupRoomSectionData +import com.texthip.thip.data.model.group.response.RoomMainList +import com.texthip.thip.data.manager.Genre +import com.texthip.thip.data.model.group.response.RoomMainResponse import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography +import com.texthip.thip.utils.rooms.DateUtils +import com.texthip.thip.utils.rooms.toDisplayStrings @SuppressLint("UnusedBoxWithConstraintsScope") @Composable fun GroupRoomDeadlineSection( - roomSections: List, - onRoomClick: (GroupCardItemRoomData) -> Unit + roomMainList: RoomMainList?, + selectedGenreIndex: Int, + errorMessage: String? = null, + onGenreSelect: (Int) -> Unit, + onRoomClick: (RoomMainResponse) -> Unit ) { val sideMargin = 30.dp - val pagerState = rememberPagerState( - initialPage = 0, - pageCount = { roomSections.size } - ) - Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier @@ -66,20 +62,33 @@ fun GroupRoomDeadlineSection( val horizontalPadding = sideMargin val cardWidth = maxWidth - (horizontalPadding * 2) val scale = 0.94f - val desiredGap = 12.dp // TODO: 이 부분을 10dp로 하면 양 옆의 카드에 살짝 다음 내용이 보여서 12정도가 어떤지 + val desiredGap = 12.dp val pageSpacing = (-(cardWidth - (cardWidth * scale)) / 2) + desiredGap + // Genre enum을 현지화된 문자열로 변환 + val genreStrings = Genre.entries.toDisplayStrings() + + // 마감 임박 방 목록과 인기 방 목록을 섹션으로 구성 + val roomSections = listOf( + Pair(stringResource(R.string.room_section_deadline), roomMainList?.deadlineRoomList ?: emptyList()), + Pair(stringResource(R.string.room_section_popular), roomMainList?.popularRoomList ?: emptyList()) + ) + + val effectivePagerState = rememberPagerState( + initialPage = 0, + pageCount = { roomSections.size } + ) + HorizontalPager( - state = pagerState, + state = effectivePagerState, contentPadding = PaddingValues(horizontal = 30.dp), pageSpacing = pageSpacing, modifier = Modifier.fillMaxWidth() ) { page -> - val section = roomSections[page] - var selectedGenre by remember { mutableIntStateOf(0) } + val (sectionTitle, rooms) = roomSections[page] - val isCurrent = pagerState.currentPage == page + val isCurrent = effectivePagerState.currentPage == page val scale = if (isCurrent) 1f else 0.94f Box( @@ -105,72 +114,102 @@ fun GroupRoomDeadlineSection( horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = section.title, + text = sectionTitle, style = typography.title_b700_s20_h24, color = colors.White ) Spacer(Modifier.height(40.dp)) GenreChipRow( - genres = section.genres, - selectedIndex = selectedGenre, - onSelect = { idx -> selectedGenre = idx } + genres = genreStrings, + selectedIndex = selectedGenreIndex, + onSelect = onGenreSelect ) Spacer(Modifier.height(20.dp)) - val cards = section.rooms.filter { it.genreIndex == selectedGenre } Column( verticalArrangement = Arrangement.spacedBy(20.dp), modifier = Modifier .fillMaxWidth() .height(584.dp) ) { - if (cards.isEmpty()) { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Spacer(Modifier.height(40.dp)) - Text( - text = stringResource(R.string.group_no_room_exist), - style = typography.smalltitle_sb600_s16_h20, - color = colors.White, - textAlign = TextAlign.Center - ) - Spacer(Modifier.height(8.dp)) - Text( - text = stringResource(R.string.group_no_room_error_comment), - style = typography.copy_r400_s14, - color = colors.Grey, - textAlign = TextAlign.Center - ) + when { + // 에러 상태 + errorMessage != null -> { + Column( + modifier = Modifier + .padding(top = 30.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(R.string.error_data_load_failed), + style = typography.smalltitle_sb600_s16_h20, + color = colors.White, + textAlign = TextAlign.Center + ) + Spacer(Modifier.height(8.dp)) + Text( + text = errorMessage, + style = typography.copy_r400_s14, + color = colors.Grey, + textAlign = TextAlign.Center + ) + } } - } else { - Column( - verticalArrangement = Arrangement.spacedBy(20.dp), - modifier = Modifier.fillMaxWidth() - ) { - cards.forEach { room -> - CardItemRoom( - title = room.title, - participants = room.participants, - maxParticipants = room.maxParticipants, - isRecruiting = room.isRecruiting, - endDate = room.endDate, - imageRes = room.imageRes, - onClick = { onRoomClick(room) }, - hasBorder = true, + // 데이터 없음 상태 + rooms.isEmpty() -> { + Column( + modifier = Modifier + .padding(top = 30.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(R.string.group_no_room_exist), + style = typography.smalltitle_sb600_s16_h20, + color = colors.White, + textAlign = TextAlign.Center + ) + Spacer(Modifier.height(8.dp)) + Text( + text = stringResource(R.string.group_no_room_error_comment), + style = typography.copy_r400_s14, + color = colors.Grey, + textAlign = TextAlign.Center + ) + } + } + // 정상 데이터 표시 + else -> { + Column( + verticalArrangement = Arrangement.spacedBy(20.dp), + modifier = Modifier.fillMaxWidth() + ) { + rooms.forEach { room -> + // RoomMainResponse를 CardItemRoom에 맞게 변환 + val daysLeft = DateUtils.extractDaysFromDeadline(room.deadlineDate) + CardItemRoom( + title = room.roomName, + participants = room.memberCount, + maxParticipants = room.recruitCount, + isRecruiting = true, // RoomMainResponse에는 모집중인 방만 있음 + endDate = daysLeft, + imageUrl = room.bookImageUrl, + onClick = { onRoomClick(room) }, + hasBorder = true, + ) + } + } + + if (rooms.size < 4) { + Spacer( + modifier = Modifier + .weight(1f, fill = true) + .fillMaxWidth() ) } } - } - - if (cards.size < 4) { - Spacer( - modifier = Modifier - .weight(1f, fill = true) - .fillMaxWidth() - ) } } } @@ -180,143 +219,52 @@ fun GroupRoomDeadlineSection( } } -@Preview() + +@Preview @Composable fun PreviewGroupRoomPagerSection() { ThipTheme { - val genres = listOf("문학", "과학·IT", "사회과학", "인문학", "예술") - - // 마감 임박한 독서 모임방 + // RoomMainResponse 형태의 더미 데이터 val deadlineRooms = listOf( - GroupCardItemRoomData( - id = 1, - title = "시집만 읽는 사람들 3월", - participants = 22, - maxParticipants = 30, - isRecruiting = true, - endDate = 3, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 2, - title = "일본 소설 좋아하는 사람들", - participants = 15, - maxParticipants = 20, - isRecruiting = true, - endDate = 2, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 3, - title = "명작 같이 읽기방", - participants = 22, - maxParticipants = 30, - isRecruiting = true, - endDate = 3, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 4, - title = "명작 같이 읽기방", - participants = 22, - maxParticipants = 30, - isRecruiting = true, - endDate = 3, - genreIndex = 0 + RoomMainResponse( + roomId = 1, + roomName = "시집만 읽는 사람들 3월", + memberCount = 22, + recruitCount = 30, + deadlineDate = "3일 뒤", + bookImageUrl = "https://picsum.photos/300/200?1" ), - GroupCardItemRoomData( - id = 5, - title = "물리책 읽는 방", - participants = 13, - maxParticipants = 20, - isRecruiting = true, - endDate = 1, - genreIndex = 1 + RoomMainResponse( + roomId = 2, + roomName = "일본 소설 좋아하는 사람들", + memberCount = 15, + recruitCount = 20, + deadlineDate = "2일 뒤", + bookImageUrl = "https://picsum.photos/300/200?2" ) ) - // 인기 있는 독서 모임방 val popularRooms = listOf( - GroupCardItemRoomData( - id = 6, - title = "베스트셀러 토론방", - participants = 28, - maxParticipants = 30, - isRecruiting = true, - endDate = 7, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 7, - title = "인기 소설 완독방", - participants = 25, - maxParticipants = 25, - isRecruiting = false, - endDate = 5, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 8, - title = "트렌드 과학서 읽기", - participants = 20, - maxParticipants = 25, - isRecruiting = true, - endDate = 10, - genreIndex = 1 - ) - ) - - // 인플루언서, 작가 독서 모임방 - val influencerRooms = listOf( - GroupCardItemRoomData( - id = 9, - title = "작가와 함께하는 독서방", - participants = 30, - maxParticipants = 30, - isRecruiting = false, - endDate = 14, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 10, - title = "유명 북튜버와 읽기", - participants = 18, - maxParticipants = 20, - isRecruiting = true, - endDate = 8, - genreIndex = 2 - ), - GroupCardItemRoomData( - id = 11, - title = "작가 초청 인문학방", - participants = 15, - maxParticipants = 20, - isRecruiting = true, - endDate = 12, - genreIndex = 3 + RoomMainResponse( + roomId = 6, + roomName = "베스트셀러 토론방", + memberCount = 28, + recruitCount = 30, + deadlineDate = "7일 뒤", + bookImageUrl = "https://picsum.photos/300/200?6" ) ) - val roomSections = listOf( - GroupRoomSectionData( - title = stringResource(R.string.deadline_string), - rooms = deadlineRooms, - genres = genres - ), - GroupRoomSectionData( - title = "인기 있는 독서 모임방", - rooms = popularRooms, - genres = genres - ), - GroupRoomSectionData( - title = "인플루언서·작가 독서 모임방", - rooms = influencerRooms, - genres = genres - ) + val roomMainList = RoomMainList( + deadlineRoomList = deadlineRooms, + popularRoomList = emptyList() ) GroupRoomDeadlineSection( - roomSections = roomSections, + roomMainList = roomMainList, + selectedGenreIndex = 0, + errorMessage = null, + onGenreSelect = {}, onRoomClick = {} ) } @@ -326,31 +274,27 @@ fun PreviewGroupRoomPagerSection() { @Composable fun PreviewGroupRoomPagerSectionEmptyGenre() { ThipTheme { - val genres = listOf("문학", "과학·IT", "사회과학", "인문학", "예술") - - // 특정 장르에만 데이터가 있는 경우 (문학 장르만 데이터 존재) val deadlineRooms = listOf( - GroupCardItemRoomData( - id = 12, - title = "시집만 읽는 사람들 3월", - participants = 22, - maxParticipants = 30, - isRecruiting = true, - endDate = 3, - genreIndex = 0 // 문학 장르만 + RoomMainResponse( + roomId = 12, + roomName = "시집만 읽는 사람들 3월", + memberCount = 22, + recruitCount = 30, + deadlineDate = "3일 뒤", + bookImageUrl = "https://picsum.photos/300/200?12" ) ) - val roomSections = listOf( - GroupRoomSectionData( - title = "마감 임박한 독서 모임방", - rooms = deadlineRooms, - genres = genres - ) + val roomMainList = RoomMainList( + deadlineRoomList = deadlineRooms, + popularRoomList = emptyList() ) GroupRoomDeadlineSection( - roomSections = roomSections, + roomMainList = roomMainList, + selectedGenreIndex = 0, + errorMessage = null, + onGenreSelect = {}, onRoomClick = {} ) } diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMainCard.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMainCard.kt index 3fc92203..0a531efc 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMainCard.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMainCard.kt @@ -25,22 +25,23 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import coil.compose.AsyncImage import com.texthip.thip.R -import com.texthip.thip.ui.group.myroom.mock.GroupCardData +import com.texthip.thip.data.model.group.response.JoinedRoomResponse import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography @Composable fun GroupMainCard( - data: GroupCardData, - backgroundColor: Color = colors.White, + data: JoinedRoomResponse, + userName: String = "", + backgroundColor: Color = Color.White, onClick: () -> Unit = {} ) { // 그라데이션 @@ -74,11 +75,12 @@ fun GroupMainCard( verticalAlignment = Alignment.CenterVertically ) { // 책 이미지 - Image( - painter = painterResource(id = data.imageRes), + AsyncImage( + model = data.bookImageUrl ?: R.drawable.img_book_cover_sample, contentDescription = "책 이미지", modifier = Modifier - .size(width = 80.dp, height = 107.dp) + .size(width = 80.dp, height = 107.dp), + contentScale = ContentScale.Crop ) Spacer(Modifier.width(12.dp)) @@ -88,7 +90,7 @@ fun GroupMainCard( Spacer(Modifier.height(2.dp)) // 제목 Text( - text = data.title, + text = data.bookTitle, style = typography.smalltitle_sb600_s18_h24, color = colors.Black, maxLines = 1 @@ -107,7 +109,7 @@ fun GroupMainCard( verticalAlignment = Alignment.CenterVertically ) { Text( - text = stringResource(R.string.group_participant, data.members), + text = stringResource(R.string.group_participant, data.memberCount), color = colors.Grey02, style = typography.menu_sb600_s12, ) @@ -122,13 +124,13 @@ fun GroupMainCard( // 닉네임 + 진행도 Row(verticalAlignment = Alignment.Bottom) { Text( - text = stringResource(R.string.group_progress, data.nickname), + text = stringResource(R.string.group_progress, userName), color = colors.Grey02, style = typography.view_m500_s14 ) Spacer(Modifier.width(4.dp)) Text( - text = "${data.progress}", + text = "${data.userPercentage}", color = colors.Purple, style = typography.smalltitle_sb600_s16_h20 ) @@ -140,7 +142,7 @@ fun GroupMainCard( } Spacer(Modifier.height(10.dp)) - val percentage = data.progress.toFloat() + val percentage = data.userPercentage.toFloat() Box( modifier = Modifier .fillMaxWidth() @@ -167,13 +169,14 @@ fun GroupMainCard( fun PreviewMyGroupMainCard() { ThipTheme { GroupMainCard( - data = GroupCardData( - title = "호르몬 체인지 완독하는 방", - members = 22, - imageRes = R.drawable.bookcover_sample, - progress = 42, - nickname = "uibowl" + data = JoinedRoomResponse( + roomId = 1, + bookTitle = "호르몬 체인지 완독하는 방", + memberCount = 22, + bookImageUrl = "https://picsum.photos/300/200?1", + userPercentage = 40 ), + userName = "uibowl1님", onClick = {} ) } diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMyRoomFilterRow.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMyRoomFilterRow.kt index ab7067fe..33166c67 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMyRoomFilterRow.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupMyRoomFilterRow.kt @@ -25,11 +25,13 @@ fun GroupMyRoomFilterRow( OptionChipButton( text = stringResource(R.string.on_going), isFilled = true, + isSelected = selectedStates[0], onClick = { onToggle(0) } ) OptionChipButton( text = stringResource(R.string.recruiting), isFilled = true, + isSelected = selectedStates[1], onClick = { onToggle(1) } ) } diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupPager.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupPager.kt index 4ee9a057..8e3c2d42 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupPager.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/component/GroupPager.kt @@ -16,16 +16,17 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.texthip.thip.R -import com.texthip.thip.ui.group.myroom.mock.GroupCardData +import com.texthip.thip.data.model.group.response.JoinedRoomResponse import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors @SuppressLint("UnusedBoxWithConstraintsScope") @Composable fun GroupPager( - groupCards: List, - onCardClick: (GroupCardData) -> Unit + groupCards: List, + userName: String = "", + onCardClick: (JoinedRoomResponse) -> Unit, + onCardVisible: ((Int) -> Unit)? = null ) { val scale = 0.86f val desiredGap = 10.dp @@ -56,6 +57,7 @@ fun GroupPager( ) { GroupMainCard( data = groupCards[0], + userName = userName, onClick = { onCardClick(groupCards[0]) }, backgroundColor = colors.White ) @@ -70,9 +72,17 @@ fun GroupPager( pageCount = { infinitePageCount } ) - // 시작 페이지로 이동 - LaunchedEffect(groupCards.size) { - pagerState.scrollToPage(startPage) + // 초기 로딩 시에만 시작 페이지로 이동 + LaunchedEffect(Unit) { + if (pagerState.currentPage == 0) { + pagerState.scrollToPage(startPage) + } + } + + // 현재 보이는 카드 인덱스를 ViewModel에 알림 + LaunchedEffect(pagerState.currentPage) { + val currentPageIndex = ((pagerState.currentPage - startPage) % groupCards.size + groupCards.size) % groupCards.size + onCardVisible?.invoke(currentPageIndex) } HorizontalPager( @@ -104,6 +114,7 @@ fun GroupPager( ) { GroupMainCard( data = groupCards[actualIndex], + userName = userName, onClick = { onCardClick(groupCards[actualIndex]) }, backgroundColor = bgColor ) @@ -125,58 +136,58 @@ fun GroupPager( } -@Preview() +@Preview @Composable fun PreviewMyGroupPager() { ThipTheme { val list = listOf( - GroupCardData( - title = "호르몬 체인지 완독하는 방", - members = 22, - imageRes = R.drawable.bookcover_sample, - progress = 40, - nickname = "uibowl1님" + JoinedRoomResponse( + roomId = 1, + bookTitle = "호르몬 체인지 완독하는 방", + memberCount = 22, + bookImageUrl = "https://picsum.photos/300/200?1", + userPercentage = 40 ), - GroupCardData( - title = "명작 읽기방", - members = 10, - imageRes = R.drawable.bookcover_sample, - progress = 70, - nickname = "joyce님" + JoinedRoomResponse( + roomId = 2, + bookTitle = "명작 읽기방", + memberCount = 10, + bookImageUrl = "https://picsum.photos/300/200?2", + userPercentage = 70 ), - GroupCardData( - title = "또 다른 방", - members = 13, - imageRes = R.drawable.bookcover_sample, - progress = 10, - nickname = "other님" + JoinedRoomResponse( + roomId = 3, + bookTitle = "또 다른 방", + memberCount = 13, + bookImageUrl = "https://picsum.photos/300/200?3", + userPercentage = 10 ) ) GroupPager(groupCards = list, onCardClick = {}) } } -@Preview() +@Preview @Composable fun PreviewSingleGroupPager() { ThipTheme { - val singleList = listOf( - GroupCardData( - title = "단일 그룹", - members = 15, - imageRes = R.drawable.bookcover_sample, - progress = 60, - nickname = "single님" + val single = listOf( + JoinedRoomResponse( + roomId = 4, + bookTitle = "단일 그룹", + memberCount = 15, + bookImageUrl = "https://picsum.photos/300/200?4", + userPercentage = 60 ) ) - GroupPager(groupCards = singleList, onCardClick = {}) + GroupPager(groupCards = single, onCardClick = {}, userName = "규빈") } } -@Preview() +@Preview @Composable fun PreviewEmptyGroupPager() { ThipTheme { GroupPager(groupCards = emptyList(), onCardClick = {}) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupBookData.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupBookData.kt index ee24be4c..17136442 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupBookData.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupBookData.kt @@ -7,5 +7,5 @@ data class GroupBookData( val author: String, val publisher: String, val description: String, - val imageRes: Int = R.drawable.bookcover_sample + val imageUrl: String? = null // API에서 받은 이미지 URL ) diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardData.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardData.kt index 79b6b34b..cbb6f718 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardData.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardData.kt @@ -3,10 +3,10 @@ package com.texthip.thip.ui.group.myroom.mock import com.texthip.thip.R data class GroupCardData( - val id: Int = 0, // 모임방 ID 추가 + val id: Int, val title: String, val members: Int, - val imageRes: Int = R.drawable.bookcover_sample, - val progress: Int, // 진행률 (0~100) + val imageUrl: String?, // API에서 받은 이미지 URL + val progress: Int, // 0~100 val nickname: String -) \ No newline at end of file +) diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardItemRoomData.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardItemRoomData.kt index 00a64c30..7c149e9f 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardItemRoomData.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupCardItemRoomData.kt @@ -1,7 +1,5 @@ package com.texthip.thip.ui.group.myroom.mock -import com.texthip.thip.R - data class GroupCardItemRoomData( val id: Int, val title: String, @@ -9,8 +7,7 @@ data class GroupCardItemRoomData( val maxParticipants: Int, val isRecruiting: Boolean, val endDate: Int? = null, // 남은 일 수 - val imageRes: Int? = R.drawable.bookcover_sample, - val genreIndex: Int, // 장르 인덱스 + val imageUrl: String? = null, // API에서 받은 이미지 URL val isSecret: Boolean = false ) diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupRoomData.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupRoomData.kt index 4a227a4c..42bec61f 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupRoomData.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/GroupRoomData.kt @@ -12,5 +12,8 @@ data class GroupRoomData( val daysLeft: Int, val genre: String, val bookData: GroupBookData, - val recommendations: List + val recommendations: List, + val buttonType: GroupBottomButtonType? = null, // API에서 결정된 버튼 타입 + val roomImageUrl: String? = null, // 방 대표 이미지 URL + val bookImageUrl: String? = null // 책 이미지 URL ) diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/RoomType.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/RoomType.kt new file mode 100644 index 00000000..fe71e4f1 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/mock/RoomType.kt @@ -0,0 +1,8 @@ +package com.texthip.thip.ui.group.myroom.mock + +enum class RoomType(val value: String) { + PLAYING_AND_RECRUITING("playingAndRecruiting"), + RECRUITING("recruiting"), + PLAYING("playing"), + EXPIRED("expired") +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt index 2e436a72..011b68f9 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/screen/GroupMyScreen.kt @@ -10,109 +10,181 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text +import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import com.texthip.thip.R +import com.texthip.thip.data.model.group.response.MyRoomResponse import com.texthip.thip.ui.common.cards.CardItemRoom import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar import com.texthip.thip.ui.group.myroom.component.GroupMyRoomFilterRow -import com.texthip.thip.ui.group.myroom.mock.GroupCardItemRoomData +import com.texthip.thip.ui.group.myroom.mock.RoomType +import com.texthip.thip.ui.group.myroom.viewmodel.GroupMyUiState +import com.texthip.thip.ui.group.myroom.viewmodel.GroupMyViewModel import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography +import com.texthip.thip.utils.rooms.RoomUtils +@OptIn(ExperimentalMaterial3Api::class) @Composable fun GroupMyScreen( - allDataList: List, - onCardClick: (GroupCardItemRoomData) -> Unit = {}, - onNavigateBack: () -> Unit = {} + onCardClick: (MyRoomResponse) -> Unit = {}, + onNavigateBack: () -> Unit = {}, + viewModel: GroupMyViewModel = hiltViewModel() ) { - var selectedStates by remember { mutableStateOf(booleanArrayOf(false, false)) } + val uiState by viewModel.uiState.collectAsState() + + GroupMyContent( + uiState = uiState, + onCardClick = onCardClick, + onNavigateBack = onNavigateBack, + onRefresh = { viewModel.refreshData() }, + onLoadMore = { viewModel.loadMoreMyRooms() }, + onChangeRoomType = { viewModel.changeRoomType(it) } + ) +} - val filteredList = remember(selectedStates, allDataList) { - if (selectedStates.all { !it } || selectedStates.all { it }) { - allDataList - } else if (selectedStates[0]) { - allDataList.filter { !it.isRecruiting } - } else if (selectedStates[1]) { - allDataList.filter { it.isRecruiting } - } else { - allDataList +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun GroupMyContent( + uiState: GroupMyUiState, + onCardClick: (MyRoomResponse) -> Unit = {}, + onNavigateBack: () -> Unit = {}, + onRefresh: () -> Unit = {}, + onLoadMore: () -> Unit = {}, + onChangeRoomType: (RoomType) -> Unit = {} +) { + val listState = rememberLazyListState() + + // 무한 스크롤 로직 + val shouldLoadMore by remember { + derivedStateOf { + val lastVisibleIndex = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 + val totalItems = listState.layoutInfo.totalItemsCount + lastVisibleIndex >= totalItems - 3 + } + } + + LaunchedEffect(shouldLoadMore) { + if (shouldLoadMore && uiState.canLoadMore) { + onLoadMore() + } + } + + // Filter 상태를 + val selectedStates = remember(uiState.currentRoomType) { + when (uiState.currentRoomType) { + RoomType.PLAYING -> booleanArrayOf(true, false) + RoomType.RECRUITING -> booleanArrayOf(false, true) + else -> booleanArrayOf(false, false) // playingAndRecruiting } } Column( Modifier - .background(colors.Black) .fillMaxSize() ) { DefaultTopAppBar( title = stringResource(R.string.my_group_room), onLeftClick = onNavigateBack, ) - Column( - Modifier - .background(colors.Black) - .fillMaxSize() - .padding(horizontal = 20.dp) + + PullToRefreshBox( + isRefreshing = uiState.isLoading, + onRefresh = onRefresh, + modifier = Modifier.fillMaxSize() ) { - Spacer(modifier = Modifier.height(20.dp)) + Column( + Modifier + .background(colors.Black) + .fillMaxSize() + .padding(horizontal = 20.dp) + ) { + Spacer(modifier = Modifier.height(20.dp)) - GroupMyRoomFilterRow( - selectedStates = selectedStates, - onToggle = { idx -> - selectedStates = selectedStates.copyOf().also { it[idx] = !it[idx] } - } - ) + GroupMyRoomFilterRow( + selectedStates = selectedStates, + onToggle = { idx -> + val newRoomType = when { + // 진행중 버튼을 눌렀을 때 + idx == 0 -> { + if (selectedStates[0]) { + // 이미 선택된 상태면 전체로 변경 + RoomType.PLAYING_AND_RECRUITING + } else { + // 선택되지 않은 상태면 진행중만 + RoomType.PLAYING + } + } + // 모집중 버튼을 눌렀을 때 + idx == 1 -> { + if (selectedStates[1]) { + RoomType.PLAYING_AND_RECRUITING + } else { + RoomType.RECRUITING + } + } + else -> RoomType.PLAYING_AND_RECRUITING + } + onChangeRoomType(newRoomType) + } + ) - Spacer(modifier = Modifier.height(20.dp)) + Spacer(modifier = Modifier.height(20.dp)) - if (filteredList.isNotEmpty()) { - LazyColumn( - verticalArrangement = Arrangement.spacedBy(20.dp), - contentPadding = PaddingValues(bottom = 20.dp), - modifier = Modifier.fillMaxSize() - ) { - items(filteredList) { item -> - CardItemRoom( - title = item.title, - participants = item.participants, - maxParticipants = item.maxParticipants, - isRecruiting = item.isRecruiting, - endDate = item.endDate, - imageRes = item.imageRes, - onClick = { onCardClick(item) } - ) + if (uiState.myRooms.isNotEmpty()) { + LazyColumn( + state = listState, + verticalArrangement = Arrangement.spacedBy(20.dp), + contentPadding = PaddingValues(bottom = 20.dp), + modifier = Modifier.fillMaxSize() + ) { + items(uiState.myRooms) { room -> + CardItemRoom( + title = room.roomName, + participants = room.memberCount, + maxParticipants = room.recruitCount, + isRecruiting = RoomUtils.isRecruitingByType(room.type), + endDate = RoomUtils.getEndDateInDays(room.endDate), + imageUrl = room.bookImageUrl, + onClick = { onCardClick(room) } + ) + } } - } - } else { - Column( - modifier = Modifier - .fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = stringResource(R.string.group_myroom_error_comment1), - color = colors.White, - style = typography.smalltitle_sb600_s18_h24 - ) - Spacer(modifier = Modifier.height(8.dp)) + } else if (!uiState.isLoading) { + Column( + modifier = Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(R.string.group_myroom_error_comment1), + color = colors.White, + style = typography.smalltitle_sb600_s18_h24 + ) + Spacer(modifier = Modifier.height(8.dp)) - Text( - text = stringResource(R.string.group_myroom_error_comment2), - color = colors.Grey, - style = typography.copy_r400_s14 - ) + Text( + text = stringResource(R.string.group_myroom_error_comment2), + color = colors.Grey, + style = typography.copy_r400_s14 + ) + } } } } @@ -121,126 +193,70 @@ fun GroupMyScreen( @Preview @Composable -fun MyGroupListFilterScreenPreview() { +fun GroupMyScreenPreview() { ThipTheme { - val dataList = listOf( - GroupCardItemRoomData( - id = 1, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = true, - endDate = 3, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 2, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = false, - endDate = 30, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 3, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = true, - endDate = 1, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 4, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = false, - endDate = 3, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 5, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = true, - endDate = 3, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 6, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = false, - endDate = 30, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 7, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = true, - endDate = 1, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 8, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = false, - endDate = 3, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 9, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = true, - endDate = 3, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 10, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = false, - endDate = 30, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 11, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = true, - endDate = 1, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 12, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = false, - endDate = 3, - genreIndex = 0 + GroupMyContent( + uiState = GroupMyUiState( + myRooms = listOf( + MyRoomResponse( + roomId = 1, + roomName = "🌙 미드나이트 라이브러리 함께읽기", + bookImageUrl = "https://picsum.photos/300/400?1", + memberCount = 18, + recruitCount = 20, + type = "RECRUITING", + endDate = "2025-02-15" + ), + MyRoomResponse( + roomId = 2, + roomName = "📚 현대문학 깊이 탐구하기", + bookImageUrl = "https://picsum.photos/300/400?2", + memberCount = 12, + recruitCount = 15, + type = "PLAYING", + endDate = "2025-01-28" + ), + MyRoomResponse( + roomId = 3, + roomName = "🔬 과학책으로 세상 이해하기", + bookImageUrl = "https://picsum.photos/300/400?3", + memberCount = 25, + recruitCount = 30, + type = "RECRUITING", + endDate = "2025-03-01" + ), + MyRoomResponse( + roomId = 4, + roomName = "✨ 철학 고전 함께 읽기", + bookImageUrl = "https://picsum.photos/300/400?4", + memberCount = 8, + recruitCount = 12, + type = "PLAYING", + endDate = "2025-02-10" + ), + MyRoomResponse( + roomId = 5, + roomName = "🎨 예술과 문학의 아름다운 만남", + bookImageUrl = "https://picsum.photos/300/400?5", + memberCount = 6, + recruitCount = 10, + type = "RECRUITING", + endDate = "2025-02-20" + ), + MyRoomResponse( + roomId = 6, + roomName = "💭 심리학 도서 탐험대", + bookImageUrl = "https://picsum.photos/300/400?6", + memberCount = 14, + recruitCount = 18, + type = "PLAYING", + endDate = "2025-01-30" + ) + ), + currentRoomType = RoomType.PLAYING_AND_RECRUITING, + isLoading = false, + hasMore = true ) ) - GroupMyScreen(allDataList = dataList) - } -} - -@Preview() -@Composable -fun MyGroupListEmptyScreenPreview() { - ThipTheme { - GroupMyScreen(allDataList = emptyList()) } } diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyUiState.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyUiState.kt new file mode 100644 index 00000000..65f5a5b2 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyUiState.kt @@ -0,0 +1,16 @@ +package com.texthip.thip.ui.group.myroom.viewmodel + +import com.texthip.thip.data.model.group.response.MyRoomResponse +import com.texthip.thip.ui.group.myroom.mock.RoomType + +data class GroupMyUiState( + val myRooms: List = emptyList(), + val currentRoomType: RoomType = RoomType.PLAYING_AND_RECRUITING, + val isLoading: Boolean = false, + val isLoadingMore: Boolean = false, + val hasMore: Boolean = true, + val error: String? = null +) { + val hasContent: Boolean get() = myRooms.isNotEmpty() + val canLoadMore: Boolean get() = !isLoading && !isLoadingMore && hasMore +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt new file mode 100644 index 00000000..6b8d79df --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/group/myroom/viewmodel/GroupMyViewModel.kt @@ -0,0 +1,89 @@ +package com.texthip.thip.ui.group.myroom.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.texthip.thip.data.repository.GroupRepository +import com.texthip.thip.ui.group.myroom.mock.RoomType +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class GroupMyViewModel @Inject constructor( + private val repository: GroupRepository +) : ViewModel() { + + private val _uiState = MutableStateFlow(GroupMyUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private var nextCursor: String? = null + private var isLastPage = false + private var isLoadingData = false + + private fun updateState(update: (GroupMyUiState) -> GroupMyUiState) { + _uiState.value = update(_uiState.value) + } + + init { + loadMyRooms(reset = true) + } + + fun loadMyRooms(reset: Boolean = false) { + if (isLoadingData && !reset) return + if (isLastPage && !reset) return + + 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 + } + } + .onFailure { exception -> + updateState { it.copy(error = exception.message) } + } + } finally { + isLoadingData = false + updateState { it.copy(isLoading = false, isLoadingMore = false) } + } + } + } + + fun loadMoreMyRooms() { + loadMyRooms(reset = false) + } + + fun refreshData() { + loadMyRooms(reset = true) + } + + fun changeRoomType(roomType: RoomType) { + if (roomType != uiState.value.currentRoomType) { + updateState { it.copy(currentRoomType = roomType) } + loadMyRooms(reset = true) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/mock/RoomAction.kt b/app/src/main/java/com/texthip/thip/ui/group/room/mock/RoomAction.kt new file mode 100644 index 00000000..a7a8861b --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/group/room/mock/RoomAction.kt @@ -0,0 +1,6 @@ +package com.texthip.thip.ui.group.room.mock + +enum class RoomAction(val value: String) { + JOIN("join"), + CANCEL("cancel") +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt index 6d385dda..ba5fe966 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomRecruitScreen.kt @@ -17,14 +17,13 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush @@ -35,41 +34,88 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex +import androidx.hilt.navigation.compose.hiltViewModel import com.texthip.thip.R +import com.texthip.thip.data.model.group.response.RecommendRoomResponse import com.texthip.thip.ui.common.cards.CardItemRoomSmall import com.texthip.thip.ui.common.cards.CardRoomBook import com.texthip.thip.ui.common.modal.DialogPopup import com.texthip.thip.ui.common.modal.ToastWithDate import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar -import com.texthip.thip.ui.group.myroom.mock.GroupBookData import com.texthip.thip.ui.group.myroom.mock.GroupBottomButtonType -import com.texthip.thip.ui.group.myroom.mock.GroupCardItemRoomData -import com.texthip.thip.ui.group.myroom.mock.GroupRoomData +import com.texthip.thip.ui.group.room.viewmodel.GroupRoomRecruitUiState +import com.texthip.thip.ui.group.room.viewmodel.GroupRoomRecruitViewModel import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography +import com.texthip.thip.utils.rooms.DateUtils import kotlinx.coroutines.delay @Composable fun GroupRoomRecruitScreen( - detail: GroupRoomData, - buttonType: GroupBottomButtonType, - onRecommendationClick: (GroupCardItemRoomData) -> Unit = {}, - onParticipation: () -> Unit = {}, // 참여 - onCancelParticipation: () -> Unit = {}, // 참여 취소 - onCloseRecruitment: () -> Unit = {}, // 모집 마감 - onBackClick: () -> Unit = {} // 뒤로가기 추가 + roomId: Int, + onRecommendationClick: (RecommendRoomResponse) -> Unit = {}, + onNavigateToGroupScreen: (String) -> Unit = {}, // GroupScreen으로 네비게이션 + 토스트 메시지 + onBackClick: () -> Unit = {}, // 뒤로가기 + viewModel: GroupRoomRecruitViewModel = hiltViewModel() +) { + val uiState by viewModel.uiState.collectAsState() + + // 데이터 로딩 + LaunchedEffect(roomId) { + viewModel.loadRoomDetail(roomId) + } + + // GroupScreen으로 네비게이션 + LaunchedEffect(uiState.shouldNavigateToGroupScreen, uiState.toastMessage) { + if (uiState.shouldNavigateToGroupScreen) { + onNavigateToGroupScreen(uiState.toastMessage) + viewModel.onNavigatedToGroupScreen() + } + } + + GroupRoomRecruitContent( + uiState = uiState, + onRecommendationClick = onRecommendationClick, + onBackClick = onBackClick, + onParticipationClick = { viewModel.onParticipationClick() }, + onCancelParticipationClick = { title, description -> viewModel.onCancelParticipationClick(title, description) }, + onCloseRecruitmentClick = { title, description -> viewModel.onCloseRecruitmentClick(title, description) }, + onDialogConfirm = { viewModel.onDialogConfirm() }, + onDialogCancel = { viewModel.onDialogCancel() }, + onHideToast = { viewModel.hideToast() } + ) +} + +@Composable +fun GroupRoomRecruitContent( + uiState: GroupRoomRecruitUiState, + onRecommendationClick: (RecommendRoomResponse) -> Unit = {}, + onBackClick: () -> Unit = {}, + onParticipationClick: () -> Unit = {}, + onCancelParticipationClick: (String, String) -> Unit = { _, _ -> }, + onCloseRecruitmentClick: (String, String) -> Unit = { _, _ -> }, + onDialogConfirm: () -> Unit = {}, + onDialogCancel: () -> Unit = {}, + onHideToast: () -> Unit = {} ) { val context = LocalContext.current - var currentButtonType by remember { mutableStateOf(buttonType) } - var showToast by remember { mutableStateOf(false) } - var toastMessage by remember { mutableStateOf("") } - var showDialog by remember { mutableStateOf(false) } - var dialogTitle by remember { mutableStateOf("") } - var dialogDescription by remember { mutableStateOf("") } - var pendingAction by remember { mutableStateOf<(() -> Unit)?>(null) } Box(Modifier.fillMaxSize()) { + // 로딩 상태 + if (uiState.isLoading) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(color = colors.White) + } + return@Box + } + + // 데이터가 없는 경우 + val detail = uiState.roomDetail ?: return@Box + Image( painter = painterResource(id = R.drawable.group_room_recruiting), contentDescription = "배경 이미지", @@ -107,7 +153,7 @@ fun GroupRoomRecruitScreen( DefaultTopAppBar( isRightIconVisible = false, isTitleVisible = false, - onLeftClick = onBackClick, // 뒤로가기 콜백 연결 + onLeftClick = onBackClick, ) Column( @@ -119,11 +165,11 @@ fun GroupRoomRecruitScreen( verticalAlignment = Alignment.CenterVertically ) { Text( - text = detail.title, + text = detail.roomName, style = typography.bigtitle_b700_s22_h24, color = colors.White ) - if (detail.isSecret) { + if (!detail.isPublic) { Spacer(Modifier.width(2.dp)) Icon( painter = painterResource(id = R.drawable.ic_lock), @@ -148,7 +194,7 @@ fun GroupRoomRecruitScreen( ) Text( - text = detail.description, + text = detail.roomDescription, style = typography.copy_r400_s12_h20, color = colors.Grey, modifier = Modifier @@ -182,8 +228,8 @@ fun GroupRoomRecruitScreen( modifier = Modifier.padding(top = 12.dp), text = stringResource( R.string.group_room_period, - detail.startDate, - detail.endDate + detail.progressStartDate, + detail.progressEndDate ), style = typography.timedate_r400_s11, color = colors.Grey @@ -217,7 +263,7 @@ fun GroupRoomRecruitScreen( Text( text = stringResource( R.string.group_room_screen_participant_count, - detail.members + detail.memberCount ), style = typography.menu_sb600_s12, color = colors.White @@ -226,7 +272,7 @@ fun GroupRoomRecruitScreen( Text( text = stringResource( R.string.group_room_screen_participant_count_max, - detail.maxMembers + detail.recruitCount ), style = typography.info_m500_s12, color = colors.Grey @@ -253,10 +299,12 @@ fun GroupRoomRecruitScreen( color = colors.White ) Spacer(Modifier.width(4.dp)) + // recruitEndDate에서 남은 일수 추출 + val daysLeft = DateUtils.extractDaysFromDeadline(detail.recruitEndDate) Text( text = stringResource( R.string.group_room_screen_end_date, - detail.daysLeft + daysLeft ), style = typography.info_m500_s12, color = colors.NeonGreen @@ -277,7 +325,7 @@ fun GroupRoomRecruitScreen( ) Spacer(Modifier.width(4.dp)) Text( - text = detail.genre, + text = detail.category, style = typography.info_m500_s12, color = colors.SocialScience ) @@ -287,15 +335,15 @@ fun GroupRoomRecruitScreen( //읽을 책 정보 CardRoomBook( - title = detail.bookData.title, - author = detail.bookData.author, - publisher = detail.bookData.publisher, - description = detail.bookData.description, - imageRes = detail.bookData.imageRes + title = detail.bookTitle, + author = detail.authorName, + publisher = detail.publisher, + description = detail.bookDescription, + imageUrl = detail.bookImageUrl ) // 추천 모임방이 있을 때만 표시 - if (detail.recommendations.isNotEmpty()) { + if (detail.recommendRooms.isNotEmpty()) { Text( modifier = Modifier.padding(top = 40.dp), text = stringResource(R.string.group_recommend), @@ -310,13 +358,15 @@ fun GroupRoomRecruitScreen( .fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(20.dp) ) { - items(detail.recommendations) { rec -> + items(detail.recommendRooms) { rec -> + // RecommendRoomResponse에서 데이터 추출 + val daysLeft = DateUtils.extractDaysFromDeadline(rec.recruitEndDate) CardItemRoomSmall( - title = rec.title, - participants = rec.participants, - maxParticipants = rec.maxParticipants, - endDate = rec.endDate, - imageRes = rec.imageRes, + title = rec.roomName, + participants = rec.memberCount, + maxParticipants = rec.recruitCount, + endDate = daysLeft, + imageUrl = rec.roomImageUrl, onClick = { onRecommendationClick(rec) } ) } @@ -327,69 +377,57 @@ fun GroupRoomRecruitScreen( } // 하단 버튼 - val buttonText = when (currentButtonType) { - GroupBottomButtonType.JOIN -> stringResource(R.string.group_room_screen_participant) - GroupBottomButtonType.CANCEL -> stringResource(R.string.group_room_screen_cancel) - GroupBottomButtonType.CLOSE -> stringResource(R.string.group_room_screen_end) - } + val buttonType = uiState.currentButtonType + if (buttonType != null) { + val buttonText = when (buttonType) { + GroupBottomButtonType.JOIN -> stringResource(R.string.group_room_screen_participant) + GroupBottomButtonType.CANCEL -> stringResource(R.string.group_room_screen_cancel) + GroupBottomButtonType.CLOSE -> stringResource(R.string.group_room_screen_end) + } - Button( - onClick = { - when (currentButtonType) { - GroupBottomButtonType.JOIN -> { - onParticipation() // 외부 콜백 호출 - showToast = true - toastMessage = context.getString(R.string.group_participant_complete_alarm) - currentButtonType = GroupBottomButtonType.CANCEL - } + Button( + onClick = { + when (buttonType) { + GroupBottomButtonType.JOIN -> { + onParticipationClick() + } - GroupBottomButtonType.CANCEL -> { - dialogTitle = context.getString(R.string.group_participant_cancel_popup) - dialogDescription = - context.getString(R.string.group_participant_cancel_comment) - pendingAction = { - onCancelParticipation() - showToast = true - toastMessage = - context.getString(R.string.group_participant_cancel_alarm) - currentButtonType = GroupBottomButtonType.JOIN + GroupBottomButtonType.CANCEL -> { + onCancelParticipationClick( + context.getString(R.string.group_participant_cancel_popup), + context.getString(R.string.group_participant_cancel_comment) + ) } - showDialog = true - } - GroupBottomButtonType.CLOSE -> { - dialogTitle = context.getString(R.string.group_participant_close_popup) - dialogDescription = - context.getString(R.string.group_participant_close_comment) - pendingAction = { - onCloseRecruitment() - showToast = true - toastMessage = context.getString(R.string.group_participant_close_alarm) + GroupBottomButtonType.CLOSE -> { + onCloseRecruitmentClick( + context.getString(R.string.group_participant_close_popup), + context.getString(R.string.group_participant_close_comment) + ) } - showDialog = true } - } - }, - colors = ButtonDefaults.buttonColors( - containerColor = colors.Purple - ), - modifier = Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - .height(50.dp), - shape = RoundedCornerShape(0.dp) - ) { - Text( - text = buttonText, - style = typography.smalltitle_sb600_s18_h24, - color = colors.White - ) + }, + colors = ButtonDefaults.buttonColors( + containerColor = colors.Purple + ), + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .height(50.dp), + shape = RoundedCornerShape(0.dp) + ) { + Text( + text = buttonText, + style = typography.smalltitle_sb600_s18_h24, + color = colors.White + ) + } } // 토스트 팝업 - if (showToast) { + if (uiState.showToast && !uiState.shouldNavigateToGroupScreen) { ToastWithDate( - message = toastMessage, + message = uiState.toastMessage, modifier = Modifier .align(Alignment.TopCenter) .padding(horizontal = 20.dp, vertical = 16.dp) @@ -397,7 +435,7 @@ fun GroupRoomRecruitScreen( ) } - if (showDialog) { + if (uiState.showDialog) { Box( modifier = Modifier .fillMaxSize() @@ -406,251 +444,86 @@ fun GroupRoomRecruitScreen( contentAlignment = Alignment.Center ) { DialogPopup( - title = dialogTitle, - description = dialogDescription, - onConfirm = { - showDialog = false - pendingAction?.invoke() - }, - onCancel = { - showDialog = false - pendingAction = null - } + title = uiState.dialogTitle, + description = uiState.dialogDescription, + onConfirm = onDialogConfirm, + onCancel = onDialogCancel ) } } } - // 토스트 3초 - LaunchedEffect(showToast) { - if (showToast) { + // 토스트 3초 후 자동 숨김 (GroupScreen으로 네비게이션 시에는 GroupScreen에서 관리) + LaunchedEffect(uiState.showToast, uiState.shouldNavigateToGroupScreen) { + if (uiState.showToast && !uiState.shouldNavigateToGroupScreen) { delay(3000) - showToast = false + onHideToast() } } } @Preview(name = "참여 버튼 상태") @Composable -fun GroupRoomRecruitScreenPreviewJoin() { +fun GroupRoomRecruitScreenPreview() { ThipTheme { - val recommendations = listOf( - GroupCardItemRoomData( - id = 1, - title = "일본 소설 좋아하는 사람들 일본 소설 좋아하는 사람들", - participants = 19, - maxParticipants = 25, - isRecruiting = true, - endDate = 2, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 2, - title = "일본 소설 좋아하는 사람들 일본 소설 좋아하는 사람들", - participants = 12, - maxParticipants = 16, - isRecruiting = true, - endDate = 6, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 3, - title = "일본 소설 좋아하는 사람들 일본 소설 좋아하는 사람들", - participants = 30, - maxParticipants = 30, - isRecruiting = false, - endDate = 0, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 4, - title = "일본 소설 좋아하는 사람들 일본 소설 좋아하는 사람들", - participants = 10, - maxParticipants = 12, - isRecruiting = true, - endDate = 8, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 5, - title = "에세이 나눔방", - participants = 14, - maxParticipants = 20, - isRecruiting = true, - endDate = 4, - genreIndex = 0 - ) - ) - - val bookData = GroupBookData( - title = "심장보다 단단한 토마토 한 알", - author = "고선지", - publisher = "푸른출판사", - description = "'시집만 읽는 사람들' 3월 모임에서 읽는 시집. 상처받고 단단해진 마음을 담은 감동적인 시와 해설이 어우러진 책으로, 읽는 이로 하여금 자신의 이야기를 투영하게 하는 힘이 있다.", - imageRes = R.drawable.bookcover_sample - ) - - val detailJoin = GroupRoomData( - id = 1, - title = "시집만 읽는 사람들 3월", - isSecret = true, - description = "'시집만 읽는 사람들' 3월 모임입니다. 이번 달 모임에서는 심장보다 단단한 토마토 한 알을 함께 읽어요.", - startDate = "2025.01.12", - endDate = "2025.02.12", - members = 22, - maxMembers = 30, - daysLeft = 4, - genre = "문학", - bookData = bookData, - recommendations = recommendations - ) - - GroupRoomRecruitScreen( - detail = detailJoin, - buttonType = GroupBottomButtonType.JOIN, - onRecommendationClick = {}, - onParticipation = {}, - onCancelParticipation = {}, - onCloseRecruitment = {}, - onBackClick = {} - ) - } -} - -@Preview(name = "참여 취소 버튼 상태") -@Composable -fun GroupRoomRecruitScreenPreviewCancel() { - ThipTheme { - val recommendations = listOf( - GroupCardItemRoomData( - id = 6, - title = "일본 소설 좋아하는 사람들 일본 소설 좋아하는 사람들", - participants = 19, - maxParticipants = 25, - isRecruiting = true, - endDate = 2, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 7, - title = "일본 소설 좋아하는 사람들 일본 소설 좋아하는 사람들", - participants = 12, - maxParticipants = 16, - isRecruiting = true, - endDate = 6, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 8, - title = "에세이 나눔방", - participants = 14, - maxParticipants = 20, - isRecruiting = true, - endDate = 4, - genreIndex = 0 + GroupRoomRecruitContent( + uiState = GroupRoomRecruitUiState( + isLoading = false, + roomDetail = com.texthip.thip.data.model.group.response.RoomRecruitingResponse( + isHost = false, + isJoining = false, + roomId = 1, + roomName = "🌙 미드나이트 라이브러리 함께읽기", + roomImageUrl = "https://picsum.photos/400/600?1", + isPublic = false, + progressStartDate = "2025.02.01", + progressEndDate = "2025.02.28", + recruitEndDate = "D-5", + category = "문학", + roomDescription = "매트 헤이그의 미드나이트 라이브러리를 함께 읽으며 인생의 가능성과 선택에 대해 이야기해요. 각자의 삶에서 후회했던 순간들을 공유하고, 서로 위로하며 성장하는 시간을 가져보아요. 따뜻한 마음으로 서로의 이야기를 들어주실 분들과 함께하고 싶습니다.", + memberCount = 18, + recruitCount = 20, + isbn = "9788937477263", + bookImageUrl = "https://picsum.photos/300/400?book1", + bookTitle = "미드나이트 라이브러리", + authorName = "매트 헤이그", + bookDescription = "삶과 죽음 사이, 후회와 가능성 사이에서 펼쳐지는 놀라운 이야기. 인생의 무한한 가능성을 탐험하는 감동적인 소설", + publisher = "인플루엔셜", + recommendRooms = listOf( + RecommendRoomResponse( + roomId = 2, + roomImageUrl = "https://picsum.photos/300/400?rec1", + roomName = "📚 현대문학 깊이 탐구하기", + memberCount = 12, + recruitCount = 15, + recruitEndDate = "D-3" + ), + RecommendRoomResponse( + roomId = 3, + roomImageUrl = "https://picsum.photos/300/400?rec2", + roomName = "✨ 철학 소설로 삶을 되돌아보기", + memberCount = 8, + recruitCount = 12, + recruitEndDate = "D-7" + ), + RecommendRoomResponse( + roomId = 4, + roomImageUrl = "https://picsum.photos/300/400?rec3", + roomName = "🎭 인간 심리를 다룬 소설 읽기", + memberCount = 15, + recruitCount = 18, + recruitEndDate = "D-2" + ) + ) + ), + currentButtonType = GroupBottomButtonType.JOIN, + showDialog = false, + showToast = false, + toastMessage = "", + dialogTitle = "", + dialogDescription = "", + shouldNavigateToGroupScreen = false ) ) - - val bookData = GroupBookData( - title = "심장보다 단단한 토마토 한 알", - author = "고선지", - publisher = "푸른출판사", - description = "'시집만 읽는 사람들' 3월 모임에서 읽는 시집. 상처받고 단단해진 마음을 담은 감동적인 시와 해설이 어우러진 책으로, 읽는 이로 하여금 자신의 이야기를 투영하게 하는 힘이 있다.", - imageRes = R.drawable.bookcover_sample - ) - - val detailCancel = GroupRoomData( - id = 2, - title = "시집만 읽는 사람들 3월", - isSecret = true, - description = "'시집만 읽는 사람들' 3월 모임입니다. 이번 달 모임에서는 심장보다 단단한 토마토 한 알을 함께 읽어요.", - startDate = "2025.01.12", - endDate = "2025.02.12", - members = 23, // 참여 후 인원 증가 - maxMembers = 30, - daysLeft = 4, - genre = "고전 문학", - bookData = bookData, - recommendations = recommendations - ) - - GroupRoomRecruitScreen( - detail = detailCancel, - buttonType = GroupBottomButtonType.CANCEL, - onRecommendationClick = {}, - onParticipation = {}, - onCancelParticipation = {}, - onCloseRecruitment = {}, - onBackClick = {} - ) } } - -@Preview(name = "모집 마감 버튼 상태") -@Composable -fun GroupRoomRecruitScreenClose() { - ThipTheme { - val recommendations = listOf( - GroupCardItemRoomData( - id = 9, - title = "일본 소설 좋아하는 사람들 일본 소설 좋아하는 사람들", - participants = 19, - maxParticipants = 25, - isRecruiting = true, - endDate = 2, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 10, - title = "일본 소설 좋아하는 사람들 일본 소설 좋아하는 사람들", - participants = 12, - maxParticipants = 16, - isRecruiting = true, - endDate = 6, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 11, - title = "미스터리 소설 탐구", - participants = 8, - maxParticipants = 15, - isRecruiting = true, - endDate = 3, - genreIndex = 0 - ) - ) - - val bookData = GroupBookData( - title = "심장보다 단단한 토마토 한 알", - author = "고선지", - publisher = "푸른출판사", - description = "'시집만 읽는 사람들' 3월 모임에서 읽는 시집. 상처받고 단단해진 마음을 담은 감동적인 시와 해설이 어우러진 책으로, 읽는 이로 하여금 자신의 이야기를 투영하게 하는 힘이 있다.", - imageRes = R.drawable.bookcover_sample - ) - - val detailClose = GroupRoomData( - id = 3, - title = "시집만 읽는 사람들 3월", - isSecret = false, // 오픈방으로 변경 - description = "'시집만 읽는 사람들' 3월 모임입니다. 이번 달 모임에서는 심장보다 단단한 토마토 한 알을 함께 읽어요. 모임장이 모집을 마감할 수 있는 상태입니다.", - startDate = "2025.01.12", - endDate = "2025.02.12", - members = 15, // 적절한 인원 - maxMembers = 30, - daysLeft = 7, // 마감일이 조금 더 남음 - genre = "문학", - bookData = bookData, - recommendations = recommendations - ) - - GroupRoomRecruitScreen( - detail = detailClose, - buttonType = GroupBottomButtonType.CLOSE, - onRecommendationClick = {}, - onParticipation = {}, - onCancelParticipation = {}, - onCloseRecruitment = {}, - onBackClick = {} - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitUiState.kt b/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitUiState.kt new file mode 100644 index 00000000..8c3bd88b --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitUiState.kt @@ -0,0 +1,18 @@ +package com.texthip.thip.ui.group.room.viewmodel + +import com.texthip.thip.ui.group.myroom.mock.GroupBottomButtonType +import com.texthip.thip.data.model.group.response.RoomRecruitingResponse + +data class GroupRoomRecruitUiState( + val roomDetail: RoomRecruitingResponse? = null, + val isLoading: Boolean = false, + val currentButtonType: GroupBottomButtonType? = null, + val showToast: Boolean = false, + val toastMessage: String = "", + val showDialog: Boolean = false, + val dialogTitle: String = "", + val dialogDescription: String = "", + val shouldNavigateToGroupScreen: Boolean = false +) { + val hasRoomDetail: Boolean get() = roomDetail != null +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt new file mode 100644 index 00000000..9012d0ba --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomRecruitViewModel.kt @@ -0,0 +1,147 @@ +package com.texthip.thip.ui.group.room.viewmodel + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.texthip.thip.R +import com.texthip.thip.data.repository.GroupRepository +import com.texthip.thip.ui.group.myroom.mock.GroupBottomButtonType +import com.texthip.thip.ui.group.room.mock.RoomAction +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class GroupRoomRecruitViewModel @Inject constructor( + private val repository: GroupRepository, + @param:ApplicationContext private val context: Context +) : ViewModel() { + + private val _uiState = MutableStateFlow(GroupRoomRecruitUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private var pendingAction: (() -> Unit)? = null + + private fun updateState(update: (GroupRoomRecruitUiState) -> GroupRoomRecruitUiState) { + _uiState.value = update(_uiState.value) + } + + fun loadRoomDetail(roomId: Int) { + viewModelScope.launch { + updateState { it.copy(isLoading = true) } + + repository.getRoomRecruiting(roomId) + .onSuccess { data -> + // RoomRecruitingResponse에서 buttonType 결정 + val buttonType = when { + data.isHost -> GroupBottomButtonType.CLOSE + data.isJoining -> GroupBottomButtonType.CANCEL + else -> GroupBottomButtonType.JOIN + } + updateState { + it.copy( + roomDetail = data, + currentButtonType = buttonType, + isLoading = false + ) + } + } + .onFailure { error -> + updateState { it.copy(isLoading = false) } + } + } + } + + fun onParticipationClick() { + viewModelScope.launch { + val roomId = uiState.value.roomDetail?.roomId ?: return@launch + + repository.joinOrCancelRoom(roomId, RoomAction.JOIN.value) + .onSuccess { + updateState { it.copy(currentButtonType = GroupBottomButtonType.CANCEL) } + showToastMessage(context.getString(R.string.success_participation_complete)) + } + .onFailure { error -> + showToastMessage(context.getString(R.string.error_participation_failed, error.message ?: "")) + } + } + } + + fun onCancelParticipationClick(dialogTitle: String, dialogDescription: String) { + updateState { + it.copy( + dialogTitle = dialogTitle, + dialogDescription = dialogDescription, + showDialog = true + ) + } + pendingAction = { + viewModelScope.launch { + val roomId = uiState.value.roomDetail?.roomId ?: return@launch + + repository.joinOrCancelRoom(roomId, RoomAction.CANCEL.value) + .onSuccess { + updateState { + it.copy( + currentButtonType = GroupBottomButtonType.JOIN, + toastMessage = context.getString(R.string.success_participation_cancelled), + showToast = true, + shouldNavigateToGroupScreen = true + ) + } + } + .onFailure { error -> + showToastMessage(context.getString(R.string.error_participation_cancel_failed)) + } + } + } + } + + fun onCloseRecruitmentClick(dialogTitle: String, dialogDescription: String) { + updateState { + it.copy( + dialogTitle = dialogTitle, + dialogDescription = dialogDescription, + showDialog = true + ) + } + pendingAction = { + viewModelScope.launch { + // TODO: 실제 모집 마감 API 호출 + showToastMessage(context.getString(R.string.success_recruitment_closed)) + } + } + } + + fun onDialogConfirm() { + updateState { it.copy(showDialog = false) } + pendingAction?.invoke() + pendingAction = null + } + + fun onDialogCancel() { + updateState { it.copy(showDialog = false) } + pendingAction = null + } + + fun hideToast() { + updateState { it.copy(showToast = false) } + } + + fun onNavigatedToGroupScreen() { + updateState { it.copy(shouldNavigateToGroupScreen = false) } + } + + private fun showToastMessage(message: String) { + updateState { + it.copy( + toastMessage = message, + showToast = true + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupDoneScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/screen/GroupDoneScreen.kt deleted file mode 100644 index 205b0423..00000000 --- a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupDoneScreen.kt +++ /dev/null @@ -1,143 +0,0 @@ -package com.texthip.thip.ui.group.screen - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.texthip.thip.R -import com.texthip.thip.ui.common.cards.CardItemRoom -import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar -import com.texthip.thip.ui.group.myroom.mock.GroupCardItemRoomData -import com.texthip.thip.ui.theme.ThipTheme -import com.texthip.thip.ui.theme.ThipTheme.colors -import com.texthip.thip.ui.theme.ThipTheme.typography - -@Composable -fun GroupDoneScreen( - name: String, - allDataList: List, - onNavigateBack: () -> Unit = {} -) { - val doneList = remember(allDataList) { - allDataList.filter { !it.isRecruiting } - } - - Column( - Modifier - .fillMaxSize() - ) { - DefaultTopAppBar( - title = stringResource(R.string.group_done_title), - onLeftClick = onNavigateBack, - ) - Column( - Modifier - .background(colors.Black) - .fillMaxSize() - .padding(horizontal = 20.dp) - ) { - - LazyColumn( - verticalArrangement = Arrangement.spacedBy(20.dp), - contentPadding = PaddingValues(bottom = 20.dp), - modifier = Modifier - .fillMaxSize() - .padding(top = 16.dp) - ) { - - item { - Text ( - text = stringResource(R.string.group_done_user_comment, name), - color = colors.White, - style = typography.menu_r400_s14_h24 - ) - } - - items(doneList) { item -> - CardItemRoom( - title = item.title, - participants = item.participants, - maxParticipants = item.maxParticipants, - isRecruiting = item.isRecruiting, - imageRes = item.imageRes, - onClick = { /* 완료된 모임방은 클릭 불가 */ } - ) - } - } - } - } -} - - - -@Preview -@Composable -fun MyGroupListFilterScreenPreview() { - ThipTheme { - val dataList = listOf( - GroupCardItemRoomData( - id = 1, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = false, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 2, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = false, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 3, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = false, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 4, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = false, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 5, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = false, - genreIndex = 0 - ), - GroupCardItemRoomData( - id = 6, - title = "모임방 이름입니다. 모임방...", - participants = 22, - maxParticipants = 30, - isRecruiting = false, - genreIndex = 0 - ) - ) - - GroupDoneScreen( - name = "rbqks529", - allDataList = dataList) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt index ff2fb15d..c49d0723 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt @@ -10,26 +10,35 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.compose.ui.zIndex +import androidx.hilt.navigation.compose.hiltViewModel import com.texthip.thip.R +import com.texthip.thip.data.model.group.response.RoomMainList import com.texthip.thip.ui.common.buttons.FloatingButton +import com.texthip.thip.ui.common.modal.ToastWithDate import com.texthip.thip.ui.common.topappbar.LogoTopAppBar import com.texthip.thip.ui.group.myroom.component.GroupMySectionHeader import com.texthip.thip.ui.group.myroom.component.GroupPager import com.texthip.thip.ui.group.myroom.component.GroupRoomDeadlineSection import com.texthip.thip.ui.group.myroom.component.GroupSearchTextField +import com.texthip.thip.ui.group.viewmodel.GroupUiState import com.texthip.thip.ui.group.viewmodel.GroupViewModel import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors +import kotlinx.coroutines.delay +@OptIn(ExperimentalMaterial3Api::class) @Composable fun GroupScreen( onNavigateToMakeRoom: () -> Unit = {}, @@ -39,20 +48,61 @@ fun GroupScreen( onNavigateToGroupMy: () -> Unit = {}, // 내 모임방 화면으로 이동 onNavigateToGroupRecruit: (Int) -> Unit = {}, // 모집 중인 모임방 화면으로 이동 onNavigateToGroupRoom: (Int) -> Unit = {}, // 기록장 화면으로 이동 - viewModel: GroupViewModel = viewModel() + viewModel: GroupViewModel = hiltViewModel() +) { + // 화면 재진입 시 데이터 새로고침 + LaunchedEffect(Unit) { + viewModel.refreshDataOnScreenEnter() + } + val uiState by viewModel.uiState.collectAsState() + + GroupContent( + uiState = uiState, + onNavigateToMakeRoom = onNavigateToMakeRoom, + onNavigateToGroupDone = onNavigateToGroupDone, + onNavigateToAlarm = onNavigateToAlarm, + onNavigateToGroupSearch = onNavigateToGroupSearch, + onNavigateToGroupMy = onNavigateToGroupMy, + onNavigateToGroupRecruit = onNavigateToGroupRecruit, + onNavigateToGroupRoom = onNavigateToGroupRoom, + onRefreshGroupData = { viewModel.refreshGroupData() }, + onCardVisible = { cardIndex -> viewModel.onCardVisible(cardIndex) }, + onSelectGenre = { genreIndex -> viewModel.selectGenre(genreIndex) }, + onHideToast = { viewModel.hideToast() } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun GroupContent( + uiState: GroupUiState, + onNavigateToMakeRoom: () -> Unit = {}, + onNavigateToGroupDone: () -> Unit = {}, + onNavigateToAlarm: () -> Unit = {}, + onNavigateToGroupSearch: () -> Unit = {}, + onNavigateToGroupMy: () -> Unit = {}, + onNavigateToGroupRecruit: (Int) -> Unit = {}, + onNavigateToGroupRoom: (Int) -> Unit = {}, + onRefreshGroupData: () -> Unit = {}, + onCardVisible: (Int) -> Unit = {}, + onSelectGenre: (Int) -> Unit = {}, + onHideToast: () -> Unit = {} ) { - val myGroups by viewModel.myGroups.collectAsState() - val roomSections by viewModel.roomSections.collectAsState() val scrollState = rememberScrollState() Box( modifier = Modifier.fillMaxSize() ) { - Column( - Modifier - .fillMaxSize() - .verticalScroll(scrollState) + PullToRefreshBox( + isRefreshing = uiState.isRefreshing, + onRefresh = onRefreshGroupData, + modifier = Modifier.fillMaxSize() ) { + Column( + Modifier + .fillMaxSize() + .verticalScroll(scrollState) + ) { // 상단바 LogoTopAppBar( leftIcon = painterResource(R.drawable.ic_done), @@ -74,10 +124,12 @@ fun GroupScreen( Spacer(Modifier.height(20.dp)) GroupPager( - groupCards = myGroups, - onCardClick = { groupCard -> - onNavigateToGroupRoom(groupCard.id) - } + groupCards = uiState.myJoinedRooms, + userName = uiState.userName, + onCardClick = { joinedRoom -> + onNavigateToGroupRoom(joinedRoom.roomId) + }, + onCardVisible = onCardVisible ) Spacer(Modifier.height(32.dp)) @@ -91,22 +143,41 @@ fun GroupScreen( // 마감 임박한 독서 모임방 GroupRoomDeadlineSection( - roomSections = roomSections, + roomMainList = uiState.roomMainList, + selectedGenreIndex = uiState.selectedGenreIndex, + errorMessage = uiState.roomSectionsError, + onGenreSelect = onSelectGenre, onRoomClick = { room -> - if (room.isRecruiting) { - onNavigateToGroupRecruit(room.id) - } else { - onNavigateToGroupRoom(room.id) - } + onNavigateToGroupRecruit(room.roomId) } ) Spacer(Modifier.height(102.dp)) + } } // 오른쪽 하단 FAB FloatingButton( icon = painterResource(id = R.drawable.ic_makegroup), onClick = onNavigateToMakeRoom ) + + // 토스트 팝업 + if (uiState.showToast) { + ToastWithDate( + message = uiState.toastMessage, + modifier = Modifier + .align(Alignment.TopCenter) + .padding(horizontal = 20.dp, vertical = 16.dp) + .zIndex(2f) + ) + } + } + + // 토스트 3초 후 자동 숨김 - showToast가 true가 된 시점부터 카운트 + LaunchedEffect(uiState.showToast) { + if (uiState.showToast) { + delay(3000L) + onHideToast() + } } } @@ -115,7 +186,94 @@ fun GroupScreen( @Composable fun PreviewGroupScreen() { ThipTheme { - val previewViewModel = remember { GroupViewModel() } - GroupScreen(viewModel = previewViewModel) + GroupContent( + uiState = GroupUiState( + userName = "김독서", + myJoinedRooms = listOf( + com.texthip.thip.data.model.group.response.JoinedRoomResponse( + roomId = 1, + bookImageUrl = "https://picsum.photos/300/400?joined1", + bookTitle = "미드나이트 라이브러리", + memberCount = 18, + userPercentage = 75 + ), + com.texthip.thip.data.model.group.response.JoinedRoomResponse( + roomId = 2, + bookImageUrl = "https://picsum.photos/300/400?joined2", + bookTitle = "코스모스", + memberCount = 25, + userPercentage = 42 + ), + com.texthip.thip.data.model.group.response.JoinedRoomResponse( + roomId = 3, + bookImageUrl = "https://picsum.photos/300/400?joined3", + bookTitle = "사피엔스", + memberCount = 15, + userPercentage = 88 + ) + ), + roomMainList = RoomMainList( + deadlineRoomList = listOf( + com.texthip.thip.data.model.group.response.RoomMainResponse( + roomId = 4, + bookImageUrl = "https://picsum.photos/300/400?deadline1", + roomName = "🌙 미드나이트 라이브러리 함께읽기", + recruitCount = 20, + memberCount = 18, + deadlineDate = "D-2" + ), + com.texthip.thip.data.model.group.response.RoomMainResponse( + roomId = 5, + bookImageUrl = "https://picsum.photos/300/400?deadline2", + roomName = "📚 현대문학 깊이 탐구하기", + recruitCount = 15, + memberCount = 12, + deadlineDate = "D-3" + ), + com.texthip.thip.data.model.group.response.RoomMainResponse( + roomId = 6, + bookImageUrl = "https://picsum.photos/300/400?deadline3", + roomName = "🔬 과학책으로 세상 이해하기", + recruitCount = 30, + memberCount = 25, + deadlineDate = "D-5" + ) + ), + popularRoomList = listOf( + com.texthip.thip.data.model.group.response.RoomMainResponse( + roomId = 7, + bookImageUrl = "https://picsum.photos/300/400?popular1", + roomName = "✨ 철학 고전 함께 읽기", + recruitCount = 12, + memberCount = 10, + deadlineDate = "D-7" + ), + com.texthip.thip.data.model.group.response.RoomMainResponse( + roomId = 8, + bookImageUrl = "https://picsum.photos/300/400?popular2", + roomName = "🎨 예술과 문학의 만남", + recruitCount = 20, + memberCount = 16, + deadlineDate = "D-10" + ), + com.texthip.thip.data.model.group.response.RoomMainResponse( + roomId = 9, + bookImageUrl = "https://picsum.photos/300/400?popular3", + roomName = "💭 심리학 도서 탐험대", + recruitCount = 18, + memberCount = 14, + deadlineDate = "D-12" + ) + ) + ), + selectedGenreIndex = 2, + isRefreshing = false, + hasMoreMyGroups = true, + isLoadingMoreMyGroups = false, + roomSectionsError = null, + showToast = false, + toastMessage = "" + ) + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/search/component/GroupFilteredSearchResult.kt b/app/src/main/java/com/texthip/thip/ui/group/search/component/GroupFilteredSearchResult.kt index ea039d5d..72aec70c 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/search/component/GroupFilteredSearchResult.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/search/component/GroupFilteredSearchResult.kt @@ -78,7 +78,7 @@ fun GroupFilteredSearchResult( participants = room.participants, maxParticipants = room.maxParticipants, endDate = room.endDate, - imageRes = room.imageRes, + imageUrl = room.imageUrl, isWide = true, isSecret = room.isSecret, onClick = { onRoomClick(room) } @@ -121,8 +121,7 @@ fun GroupFilteredSearchResultPreview() { maxParticipants = 10, isRecruiting = true, endDate = 7, - imageRes = R.drawable.bookcover_sample, - genreIndex = 1, + imageUrl = null, isSecret = false ), GroupCardItemRoomData( id = 2, @@ -131,8 +130,7 @@ fun GroupFilteredSearchResultPreview() { maxParticipants = 12, isRecruiting = false, endDate = 3, - imageRes = R.drawable.bookcover_sample, - genreIndex = 1, + imageUrl = null, isSecret = true ) ) diff --git a/app/src/main/java/com/texthip/thip/ui/group/search/component/GroupLiveSearchResult.kt b/app/src/main/java/com/texthip/thip/ui/group/search/component/GroupLiveSearchResult.kt index 7674e39e..e3ccac6f 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/search/component/GroupLiveSearchResult.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/search/component/GroupLiveSearchResult.kt @@ -1,7 +1,6 @@ package com.texthip.thip.ui.group.search.component import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -13,7 +12,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.texthip.thip.R import com.texthip.thip.ui.common.cards.CardItemRoomSmall import com.texthip.thip.ui.group.myroom.mock.GroupCardItemRoomData import com.texthip.thip.ui.theme.ThipTheme @@ -31,7 +29,7 @@ fun GroupLiveSearchResult( participants = room.participants, maxParticipants = room.maxParticipants, endDate = room.endDate, - imageRes = room.imageRes, + imageUrl = room.imageUrl, isWide = true, isSecret = room.isSecret, onClick = { onRoomClick(room) } @@ -66,8 +64,7 @@ fun GroupLiveSearchResultPreview() { maxParticipants = 10, isRecruiting = true, endDate = 7, - imageRes = R.drawable.bookcover_sample, - genreIndex = 0, + imageUrl = null, isSecret = false ), GroupCardItemRoomData( @@ -77,8 +74,7 @@ fun GroupLiveSearchResultPreview() { maxParticipants = 12, isRecruiting = false, endDate = 3, - imageRes = R.drawable.bookcover_sample, - genreIndex = 1, + imageUrl = null, isSecret = true ), GroupCardItemRoomData( @@ -88,8 +84,7 @@ fun GroupLiveSearchResultPreview() { maxParticipants = 8, isRecruiting = true, endDate = null, - imageRes = R.drawable.bookcover_sample, - genreIndex = 2, + imageUrl = null, isSecret = false ) ) diff --git a/app/src/main/java/com/texthip/thip/ui/group/search/screen/GroupSearchScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/search/screen/GroupSearchScreen.kt index bfcaedb8..50487fbf 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/search/screen/GroupSearchScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/search/screen/GroupSearchScreen.kt @@ -113,8 +113,7 @@ fun GroupSearchScreen( if (!isSearched) emptyList() else { val filtered = roomList.filter { room -> - (searchText.isBlank() || room.title.contains(searchText, ignoreCase = true)) && - (selectedGenreIndex == -1 || room.genreIndex == selectedGenreIndex) + (searchText.isBlank() || room.title.contains(searchText, ignoreCase = true)) } when (selectedSortOptionIndex) { 0 -> filtered.sortedBy { it.endDate } // 마감임박순 @@ -242,13 +241,36 @@ fun PreviewGroupSearchScreen() { ThipTheme { GroupSearchScreen( roomList = listOf( - GroupCardItemRoomData(1, "aaa", 22, 30, true, 3, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(2, "abc", 15, 20, true, 7, R.drawable.bookcover_sample, 1, true), - GroupCardItemRoomData(3, "abcd", 10, 15, true, 5, R.drawable.bookcover_sample, 2, true), - GroupCardItemRoomData(4, "abcde", 8, 12, false, 2, R.drawable.bookcover_sample, 3, true), - GroupCardItemRoomData(5, "abcdef", 18, 25, true, 4, R.drawable.bookcover_sample, 4), - GroupCardItemRoomData(6, "abcdefg", 12, 20, true, 1, R.drawable.bookcover_sample, 0), - GroupCardItemRoomData(7, "abcdefgh", 10, 14, true, 6, R.drawable.bookcover_sample, 1) + GroupCardItemRoomData( + id = 1, + title = "aaa", + participants = 22, + maxParticipants = 30, + isRecruiting = true, + endDate = 3, + imageUrl = null, + isSecret = false + ), + GroupCardItemRoomData( + id = 2, + title = "abc", + participants = 15, + maxParticipants = 20, + isRecruiting = true, + endDate = 7, + imageUrl = null, + isSecret = true + ), + GroupCardItemRoomData( + id = 3, + title = "abcd", + participants = 10, + maxParticipants = 15, + isRecruiting = true, + endDate = 5, + imageUrl = null, + isSecret = true + ) ) ) } diff --git a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupUiState.kt b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupUiState.kt new file mode 100644 index 00000000..82f5ab41 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupUiState.kt @@ -0,0 +1,20 @@ +package com.texthip.thip.ui.group.viewmodel + +import com.texthip.thip.data.model.group.response.JoinedRoomResponse +import com.texthip.thip.data.model.group.response.RoomMainList + +data class GroupUiState( + val myJoinedRooms: List = emptyList(), + val hasMoreMyGroups: Boolean = true, + val isRefreshing: Boolean = false, + val isLoadingMoreMyGroups: Boolean = false, + val roomMainList: RoomMainList? = null, + val roomSectionsError: String? = null, + val userName: String = "", + val selectedGenreIndex: Int = 0, + val showToast: Boolean = false, + val toastMessage: String = "" +) { + val hasContent: Boolean get() = myJoinedRooms.isNotEmpty() || (roomMainList != null) + val canLoadMore: Boolean get() = hasMoreMyGroups && !isRefreshing && !isLoadingMoreMyGroups +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt index 561adffe..663e322b 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/viewmodel/GroupViewModel.kt @@ -1,41 +1,38 @@ package com.texthip.thip.ui.group.viewmodel +import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.texthip.thip.data.group.repository.GroupRepository -import com.texthip.thip.ui.group.myroom.mock.GroupCardData -import com.texthip.thip.ui.group.myroom.mock.GroupCardItemRoomData -import com.texthip.thip.ui.group.myroom.mock.GroupRoomSectionData -import com.texthip.thip.ui.group.myroom.mock.GroupRoomData +import com.texthip.thip.data.manager.Genre +import com.texthip.thip.data.repository.GroupRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import javax.inject.Inject -class GroupViewModel( - private val repository: GroupRepository = GroupRepository() +@HiltViewModel +class GroupViewModel @Inject constructor( + private val repository: GroupRepository, + @param:ApplicationContext private val context: Context ) : ViewModel() { - private val _myGroups = MutableStateFlow>(emptyList()) - val myGroups: StateFlow> = _myGroups.asStateFlow() - - private val _roomSections = MutableStateFlow>(emptyList()) - val roomSections: StateFlow> = _roomSections.asStateFlow() - - private val _userName = MutableStateFlow("") - val userName: StateFlow = _userName.asStateFlow() - - private val _doneGroups = MutableStateFlow>(emptyList()) - val doneGroups: StateFlow> = _doneGroups.asStateFlow() - - private val _myRoomGroups = MutableStateFlow>(emptyList()) - val myRoomGroups: StateFlow> = _myRoomGroups.asStateFlow() - - private val _searchGroups = MutableStateFlow>(emptyList()) - val searchGroups: StateFlow> = _searchGroups.asStateFlow() - - private val _genres = MutableStateFlow>(emptyList()) - val genres: StateFlow> = _genres.asStateFlow() + private val _uiState = MutableStateFlow(GroupUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private var currentMyGroupsPage = 1 + private var loadedPagesCount = 0 + private val pagesPerBatch = 3 + private val preloadThreshold = 2 + private var isBatchLoading = false + + private fun updateState(update: (GroupUiState) -> GroupUiState) { + _uiState.value = update(_uiState.value) + } init { loadInitialData() @@ -45,72 +42,165 @@ class GroupViewModel( loadUserName() loadMyGroups() loadRoomSections() - loadDoneGroups() - loadMyRoomGroups() - loadSearchGroups() } - + private fun loadUserName() { viewModelScope.launch { repository.getUserName() .onSuccess { userName -> - _userName.value = userName + updateState { it.copy(userName = userName) } } } } - - private fun loadMyGroups() { - viewModelScope.launch { - repository.getMyGroups() - .onSuccess { groups -> - _myGroups.value = groups - } + + fun loadMyGroups(reset: Boolean = false) = viewModelScope.launch { + if (reset) { + resetMyGroupsData() + updateState { it.copy(isRefreshing = true) } + } + try { + loadPageBatchSuspend() + } finally { + updateState { it.copy(isRefreshing = false) } } } - + + private suspend fun loadPageBatchSuspend() { + if (!uiState.value.hasMoreMyGroups || isBatchLoading) return + + try { + isBatchLoading = true + updateState { it.copy(isLoadingMoreMyGroups = true) } + + val currentBatchStart = currentMyGroupsPage + val batchEndPage = currentBatchStart + pagesPerBatch - 1 + + for (page in currentBatchStart..batchEndPage) { + if (!uiState.value.hasMoreMyGroups) break + + repository.getMyJoinedRooms(page) + .onSuccess { joinedRoomsResponse -> + joinedRoomsResponse?.let { response -> + updateState { + it.copy( + myJoinedRooms = it.myJoinedRooms + response.roomList, + hasMoreMyGroups = !response.last + ) + } + loadedPagesCount++ + currentMyGroupsPage = page + 1 + } + } + .onFailure { + break + } + } + } finally { + isBatchLoading = false + updateState { it.copy(isLoadingMoreMyGroups = false) } + } + } + + private fun loadPageBatch() = viewModelScope.launch { + loadPageBatchSuspend() + } + + fun onCardVisible(cardIndex: Int) { + val currentPageEquivalent = (cardIndex / 3) + 1 + + if (currentPageEquivalent >= loadedPagesCount - preloadThreshold && + uiState.value.hasMoreMyGroups && !isBatchLoading + ) { + loadPageBatch() + } + } + private fun loadRoomSections() { viewModelScope.launch { - repository.getRoomSections() - .onSuccess { sections -> - _roomSections.value = sections + 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 loadDoneGroups() { - viewModelScope.launch { - repository.getDoneGroups() - .onSuccess { groups -> - _doneGroups.value = groups - } + + 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() + } } } - - private fun loadMyRoomGroups() { + + + + fun refreshGroupData() { viewModelScope.launch { - repository.getMyRoomGroups() - .onSuccess { groups -> - _myRoomGroups.value = groups - } + updateState { it.copy(isRefreshing = true) } + try { + val jobs = listOf( + async { loadUserName() }, + async { + resetMyGroupsData() + loadPageBatchSuspend() + }, + async { loadRoomSections() }, + ) + + jobs.awaitAll() + } finally { + updateState { it.copy(isRefreshing = false) } + } } } - - private fun loadSearchGroups() { - viewModelScope.launch { - repository.getSearchGroups() - .onSuccess { groups -> - _searchGroups.value = groups - } + + private fun resetMyGroupsData() { + currentMyGroupsPage = 1 + loadedPagesCount = 0 + updateState { + it.copy( + myJoinedRooms = emptyList(), + hasMoreMyGroups = true + ) } } - - fun refreshGroupData() { - loadInitialData() + + fun showToastMessage(message: String) { + updateState { + it.copy( + toastMessage = message, + showToast = true + ) + } } - - - suspend fun getRoomDetail(roomId: Int): GroupRoomData? { - return repository.getRoomDetail(roomId).getOrNull() + + fun hideToast() { + updateState { it.copy(showToast = false) } + } + + fun refreshDataOnScreenEnter() { + refreshGroupData() } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/mypage/component/MypageSaveBook.kt b/app/src/main/java/com/texthip/thip/ui/mypage/component/MypageSaveBook.kt index bafb3dbf..f3cdd68f 100644 --- a/app/src/main/java/com/texthip/thip/ui/mypage/component/MypageSaveBook.kt +++ b/app/src/main/java/com/texthip/thip/ui/mypage/component/MypageSaveBook.kt @@ -18,7 +18,7 @@ fun BookContent(viewModel: SavedBookViewModel = viewModel()) { CardBookList( title = book.title, author = book.author, - imageRes = null, + imageUrl = null, publisher = book.publisher, isBookmarked = book.isSaved, onBookmarkClick = { viewModel.toggleBookmark(book.id) } diff --git a/app/src/main/java/com/texthip/thip/ui/mypage/component/SavedFeedCard.kt b/app/src/main/java/com/texthip/thip/ui/mypage/component/SavedFeedCard.kt index fbda07d5..d576a627 100644 --- a/app/src/main/java/com/texthip/thip/ui/mypage/component/SavedFeedCard.kt +++ b/app/src/main/java/com/texthip/thip/ui/mypage/component/SavedFeedCard.kt @@ -166,7 +166,7 @@ private fun SavedFeedCardPrev() { commentCount = 5, isLiked = false, isSaved = true, - imageUrls = listOf(R.drawable.bookcover_sample,R.drawable.bookcover_sample,R.drawable.bookcover_sample) + imageUrls = listOf(R.drawable.img_book_cover_sample,R.drawable.img_book_cover_sample,R.drawable.bookcover_sample) ) val scrollState = rememberScrollState() diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt index e6693f61..3c98046d 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt @@ -4,26 +4,23 @@ import android.annotation.SuppressLint import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.compose.composable import androidx.navigation.toRoute +import com.texthip.thip.ui.group.done.screen.GroupDoneScreen import com.texthip.thip.ui.group.makeroom.screen.GroupMakeRoomScreen import com.texthip.thip.ui.group.makeroom.viewmodel.GroupMakeRoomViewModel -import com.texthip.thip.ui.group.myroom.mock.GroupBottomButtonType -import com.texthip.thip.ui.group.myroom.mock.GroupRoomData import com.texthip.thip.ui.group.myroom.screen.GroupMyScreen import com.texthip.thip.ui.group.room.screen.GroupRoomMatesScreen import com.texthip.thip.ui.group.room.screen.GroupRoomRecruitScreen import com.texthip.thip.ui.group.room.screen.GroupRoomScreen -import com.texthip.thip.ui.group.screen.GroupDoneScreen import com.texthip.thip.ui.group.screen.GroupScreen import com.texthip.thip.ui.group.search.screen.GroupSearchScreen import com.texthip.thip.ui.group.viewmodel.GroupViewModel +import com.texthip.thip.ui.group.myroom.viewmodel.GroupMyViewModel +import com.texthip.thip.ui.group.myroom.mock.RoomType import com.texthip.thip.ui.navigator.extensions.navigateToAlarm import com.texthip.thip.ui.navigator.extensions.navigateToGroupDone import com.texthip.thip.ui.navigator.extensions.navigateToGroupMakeRoom @@ -44,9 +41,17 @@ fun NavGraphBuilder.groupNavigation( ) { // 메인 Group 화면 composable { backStackEntry -> - val groupViewModel: GroupViewModel = viewModel( - viewModelStoreOwner = backStackEntry - ) + val groupViewModel: GroupViewModel = hiltViewModel() + + // 네비게이션 파라미터로 전달된 토스트 메시지가 있는지 확인 + LaunchedEffect(backStackEntry) { + val toastMessage = backStackEntry.savedStateHandle.get("toast_message") + + toastMessage?.let { message -> + backStackEntry.savedStateHandle.remove("toast_message") + groupViewModel.showToastMessage(message) + } + } GroupScreen( viewModel = groupViewModel, @@ -76,7 +81,7 @@ fun NavGraphBuilder.groupNavigation( // Group MakeRoom 화면 composable { - val viewModel: GroupMakeRoomViewModel = viewModel() + val viewModel: GroupMakeRoomViewModel = hiltViewModel() GroupMakeRoomScreen( viewModel = viewModel, onNavigateBack = { @@ -90,24 +95,7 @@ fun NavGraphBuilder.groupNavigation( // Group Done 화면 composable { - val parentEntry = remember(navController) { - try { - navController.getBackStackEntry(MainTabRoutes.Group) - } catch (e: Exception) { - null - } - } - val groupViewModel: GroupViewModel = if (parentEntry != null) { - viewModel(viewModelStoreOwner = parentEntry) - } else { - viewModel() - } - val userName by groupViewModel.userName.collectAsState() - val doneGroups by groupViewModel.doneGroups.collectAsState() - GroupDoneScreen( - name = userName, - allDataList = doneGroups, onNavigateBack = { navigateBack() } @@ -116,27 +104,16 @@ fun NavGraphBuilder.groupNavigation( // Group My 화면 composable { - val parentEntry = remember(navController) { - try { - navController.getBackStackEntry(MainTabRoutes.Group) - } catch (e: Exception) { - null - } - } - val groupViewModel: GroupViewModel = if (parentEntry != null) { - viewModel(viewModelStoreOwner = parentEntry) - } else { - viewModel() - } - val myRoomGroups by groupViewModel.myRoomGroups.collectAsState() + val groupMyViewModel: GroupMyViewModel = hiltViewModel() GroupMyScreen( - allDataList = myRoomGroups, + viewModel = groupMyViewModel, onCardClick = { room -> - if (room.isRecruiting) { - navController.navigateToGroupRecruit(room.id) + val isRecruiting = room.type == RoomType.RECRUITING.value + if (isRecruiting) { + navController.navigateToGroupRecruit(room.roomId) } else { - navController.navigateToGroupRoom(room.id) + navController.navigateToGroupRoom(room.roomId) } }, onNavigateBack = { @@ -147,30 +124,21 @@ fun NavGraphBuilder.groupNavigation( // Group Search 화면 composable { - val parentEntry = remember(navController) { - try { - navController.getBackStackEntry(MainTabRoutes.Group) - } catch (e: Exception) { - null - } - } - val groupViewModel: GroupViewModel = if (parentEntry != null) { - viewModel(viewModelStoreOwner = parentEntry) - } else { - viewModel() - } - val searchGroups by groupViewModel.searchGroups.collectAsState() + val groupViewModel: GroupViewModel = hiltViewModel() + val uiState by groupViewModel.uiState.collectAsState() GroupSearchScreen( - roomList = searchGroups, + roomList = emptyList(), //TODO: RoomMainResponse -> GroupCardItemRoomData 변환 필요 onNavigateBack = { navigateBack() }, onRoomClick = { room -> if (room.isRecruiting) { - navController.navigateToGroupRecruit(room.id) + // TODO: GroupCardItemRoomData -> RoomMainResponse 변환 후 roomId 사용 + // navController.navigateToGroupRecruit(room.roomId) } else { - navController.navigateToGroupRoom(room.id) + // TODO: GroupCardItemRoomData -> RoomMainResponse 변환 후 roomId 사용 + // navController.navigateToGroupRoom(room.roomId) } } ) @@ -180,48 +148,22 @@ fun NavGraphBuilder.groupNavigation( composable { backStackEntry -> val route = backStackEntry.toRoute() val roomId = route.roomId - val parentEntry = remember(navController) { - try { - navController.getBackStackEntry(MainTabRoutes.Group) - } catch (e: Exception) { - null - } - } - val groupViewModel: GroupViewModel = if (parentEntry != null) { - viewModel(viewModelStoreOwner = parentEntry) - } else { - viewModel() - } - - // suspend 함수를 위한 LaunchedEffect 사용 - var roomDetail by remember { mutableStateOf(null) } - LaunchedEffect(roomId) { - roomDetail = groupViewModel.getRoomDetail(roomId) - } - roomDetail?.let { detail -> - GroupRoomRecruitScreen( - detail = detail, - buttonType = GroupBottomButtonType.JOIN, // 기본값, 실제로는 사용자 상태에 따라 결정 - onRecommendationClick = { recommendation -> - navController.navigateToRecommendedGroupRecruit(recommendation.id) - }, - onParticipation = { - // 참여 로직 - }, - onCancelParticipation = { - // 참여 취소 로직 - }, - onCloseRecruitment = { - // 모집 마감 로직 - }, - onBackClick = { - navigateBack() - } - ) - } ?: run { - // 로딩 중이거나 데이터를 찾을 수 없는 경우 - } + GroupRoomRecruitScreen( + roomId = roomId, + onRecommendationClick = { recommendation -> + navController.navigateToRecommendedGroupRecruit(recommendation.roomId) + }, + onNavigateToGroupScreen = { toastMessage -> + // GroupScreen에 토스트 메시지 전달 + val groupEntry = navController.getBackStackEntry(MainTabRoutes.Group) + groupEntry.savedStateHandle["toast_message"] = toastMessage + navController.popBackStack(MainTabRoutes.Group, false) + }, + onBackClick = { + navigateBack() + } + ) } // Group Room 화면 diff --git a/app/src/main/java/com/texthip/thip/ui/search/component/SearchActiveField.kt b/app/src/main/java/com/texthip/thip/ui/search/component/SearchActiveField.kt index 088b582b..72d6f269 100644 --- a/app/src/main/java/com/texthip/thip/ui/search/component/SearchActiveField.kt +++ b/app/src/main/java/com/texthip/thip/ui/search/component/SearchActiveField.kt @@ -27,7 +27,7 @@ fun SearchActiveField( title = book.title, author = book.author, publisher = book.publisher, - imageRes = book.imageRes + imageUrl = book.imageUrl ) if (index < bookList.size - 1) { Spacer( diff --git a/app/src/main/java/com/texthip/thip/ui/search/component/SearchBookFilteredResult.kt b/app/src/main/java/com/texthip/thip/ui/search/component/SearchBookFilteredResult.kt index d3b130d0..95d150b4 100644 --- a/app/src/main/java/com/texthip/thip/ui/search/component/SearchBookFilteredResult.kt +++ b/app/src/main/java/com/texthip/thip/ui/search/component/SearchBookFilteredResult.kt @@ -63,7 +63,7 @@ fun SearchBookFilteredResult( title = book.title, author = book.author, publisher = book.publisher, - imageRes = book.imageRes + imageUrl = book.imageUrl ) if (index < bookList.size - 1) { Spacer( diff --git a/app/src/main/java/com/texthip/thip/ui/search/component/SearchRecentBook.kt b/app/src/main/java/com/texthip/thip/ui/search/component/SearchRecentBook.kt index 18db248c..c35cddc9 100644 --- a/app/src/main/java/com/texthip/thip/ui/search/component/SearchRecentBook.kt +++ b/app/src/main/java/com/texthip/thip/ui/search/component/SearchRecentBook.kt @@ -118,7 +118,7 @@ fun SearchRecentBook( CardBookSearch( number = index + 1, title = book.title, - imageRes = book.imageRes, + imageUrl = book.imageUrl, onClick = { onBookClick(book) } ) if (index < popularBooks.size - 1) { @@ -149,19 +149,19 @@ fun PreviewBookRecentSearch() { title = "이기적 유전자", author = "리처드 도킨스", publisher = "을유문화사", - imageRes = R.drawable.bookcover_sample + imageUrl = null ), BookData( title = "코스모스", author = "칼 세이건", publisher = "사이언스북스", - imageRes = R.drawable.bookcover_sample + imageUrl = null ), BookData( title = "총, 균, 쇠", author = "재레드 다이아몬드", publisher = "문학사상사", - imageRes = R.drawable.bookcover_sample + imageUrl = null ) ), popularBookDate = "01.12", diff --git a/app/src/main/java/com/texthip/thip/ui/search/mock/BookData.kt b/app/src/main/java/com/texthip/thip/ui/search/mock/BookData.kt index 0534fc31..b4610c61 100644 --- a/app/src/main/java/com/texthip/thip/ui/search/mock/BookData.kt +++ b/app/src/main/java/com/texthip/thip/ui/search/mock/BookData.kt @@ -6,5 +6,5 @@ data class BookData( val title: String, val author: String = "", val publisher: String = "", - val imageRes: Int = R.drawable.bookcover_sample + val imageUrl: String? = null ) diff --git a/app/src/main/java/com/texthip/thip/ui/search/mock/DetailBookData.kt b/app/src/main/java/com/texthip/thip/ui/search/mock/DetailBookData.kt index b30df944..dee82adb 100644 --- a/app/src/main/java/com/texthip/thip/ui/search/mock/DetailBookData.kt +++ b/app/src/main/java/com/texthip/thip/ui/search/mock/DetailBookData.kt @@ -7,7 +7,7 @@ data class DetailBookData( val author: String, val publisher: String, val description: String, - val coverImageRes: Int? = R.drawable.bookcover_sample, + val coverImageRes: Int? = R.drawable.img_book_cover_sample, val participantsCount: Int = 0, val recruitingRoomCount: Int = 0 ) diff --git a/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookDetailScreen.kt b/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookDetailScreen.kt index 29bf101c..59acf186 100644 --- a/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookDetailScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookDetailScreen.kt @@ -333,7 +333,7 @@ fun PreviewBookDetailScreen() { description = "인터내셔널 북커상, 산클레멘테 문학상 수상작. 전세계가 주목한 인간의 역작을 다시 만나다.2016년 인터내셔널 북커상을 수상하며 한국문학의 입지를 한단계 확장시킨 한강의 명단소설 『채식주의자』. 15년 만에 새로운 장정과 판형으로 출간된다. 식물화로 건설해온 극단적이며 실재적인 상상력의 강렬한 결실로 고통과 구속의 피안에 존재하는 인간의 본성에 다가간 작품." + "인터내셔널 북커상, 산클레멘테 문학상 수상작. 전세계가 주목한 인간의 역작을 다시 만나다. \n\n2016년 인터내셔널 북커상을 수상하며 한국문학의 입지를 한단계 확장시킨 한강의 명단소설 『채식주의자』. 15년 만에 새로운 장정과 판형으로 출간된다. 식물화로 건설해온 극단적이며 실재적인 상상력의 강렬한 결실로 고통과 구속의 피안에 존재하는 인간의 본성에 다가간 작품.", - coverImageRes = R.drawable.bookcover_sample, + coverImageRes = R.drawable.img_book_cover_sample, participantsCount = 210, recruitingRoomCount = 4 ), diff --git a/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookGroupScreen.kt b/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookGroupScreen.kt index c181a487..778622cc 100644 --- a/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookGroupScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookGroupScreen.kt @@ -109,7 +109,7 @@ fun SearchBookGroupScreen( maxParticipants = item.maxParticipants, isRecruiting = item.isRecruiting, endDate = item.endDate, - imageRes = item.imageRes, + imageUrl = item.imageUrl, onClick = { onCardClick(item) } ) } @@ -151,7 +151,6 @@ fun GroupRecruitingScreenPreview() { maxParticipants = 30, endDate = 3, isRecruiting = true, - genreIndex = 0 ), GroupCardItemRoomData( id = 2, @@ -160,7 +159,6 @@ fun GroupRecruitingScreenPreview() { maxParticipants = 30, endDate = 3, isRecruiting = true, - genreIndex = 0 ), GroupCardItemRoomData( id = 3, @@ -169,7 +167,6 @@ fun GroupRecruitingScreenPreview() { maxParticipants = 30, endDate = 3, isRecruiting = true, - genreIndex = 0 ), GroupCardItemRoomData( id = 4, @@ -178,7 +175,6 @@ fun GroupRecruitingScreenPreview() { maxParticipants = 30, endDate = 3, isRecruiting = true, - genreIndex = 0 ), GroupCardItemRoomData( id = 5, @@ -187,7 +183,6 @@ fun GroupRecruitingScreenPreview() { maxParticipants = 30, endDate = 3, isRecruiting = true, - genreIndex = 0 ), GroupCardItemRoomData( id = 6, @@ -196,7 +191,6 @@ fun GroupRecruitingScreenPreview() { maxParticipants = 30, endDate = 3, isRecruiting = true, - genreIndex = 0 ), GroupCardItemRoomData( id = 7, @@ -205,7 +199,6 @@ fun GroupRecruitingScreenPreview() { maxParticipants = 30, endDate = 3, isRecruiting = true, - genreIndex = 0 ), GroupCardItemRoomData( id = 8, @@ -214,7 +207,6 @@ fun GroupRecruitingScreenPreview() { maxParticipants = 30, endDate = 3, isRecruiting = true, - genreIndex = 0 ) ) diff --git a/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookScreen.kt b/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookScreen.kt index 7062aa3a..2eeb0f16 100644 --- a/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookScreen.kt @@ -198,20 +198,20 @@ fun PreviewBookSearchScreen_Default() { ThipTheme { SearchBookScreen( bookList = listOf( - BookData("aaa", "리처드 도킨스", "을유문화사", R.drawable.bookcover_sample), - BookData("abc", "마틴 셀리그만", "물푸레", R.drawable.bookcover_sample), - BookData("abcd", "빅터 프랭클", "청림출판", R.drawable.bookcover_sample), - BookData("abcde", "칼 융", "문학과지성사", R.drawable.bookcover_sample), - BookData("abcdef", "에릭 프롬", "까치글방", R.drawable.bookcover_sample), - BookData("abcedfg", "알베르 카뮈", "민음사", R.drawable.bookcover_sample), - BookData("abcdefgh", "장 폴 사르트르", "문학동네", R.drawable.bookcover_sample), + BookData(title = "aaa", author = "리처드 도킨스", publisher = "을유문화사", imageUrl = null), + BookData(title = "abc", author = "마틴 셀리그만", publisher = "물푸레", imageUrl = null), + BookData(title = "abcd", author = "빅터 프랭클", publisher = "청림출판", imageUrl = null), + BookData(title = "abcde", author = "칼 융", publisher = "문학과지성사", imageUrl = null), + BookData(title = "abcdef", author = "에릭 프롬", publisher = "까치글방", imageUrl = null), + BookData(title = "abcedfg", author = "알베르 카뮈", publisher = "민음사", imageUrl = null), + BookData(title = "abcdefgh", author = "장 폴 사르트르", publisher = "문학동네", imageUrl = null), ), popularBooks = listOf( - BookData("단 한번의 삶", "리처드 도킨스", "을유문화사", R.drawable.bookcover_sample), - BookData("사랑", "마틴 셀리그만", "물푸레", R.drawable.bookcover_sample), - BookData("호모 사피엔스", "빅터 프랭클", "청림출판", R.drawable.bookcover_sample), - BookData("코스모스 실버", "칼 융", "문학과지성사", R.drawable.bookcover_sample), - BookData("오만과 편견", "에릭 프롬", "까치글방", R.drawable.bookcover_sample), + BookData(title = "단 한번의 삶", author = "리처드 도킨스", publisher = "을유문화사", imageUrl = null), + BookData(title = "사랑", author = "마틴 셀리그만", publisher = "물푸레", imageUrl = null), + BookData(title = "호모 사피엔스", author = "빅터 프랭클", publisher = "청림출판", imageUrl = null), + BookData(title = "코스모스 실버", author = "칼 융", publisher = "문학과지성사", imageUrl = null), + BookData(title = "오만과 편견", author = "에릭 프롬", publisher = "까치글방", imageUrl = null), ) ) } @@ -223,13 +223,13 @@ fun PreviewBookSearchScreen_EmptyPopular() { ThipTheme { SearchBookScreen( bookList = listOf( - BookData("aaa", "리처드 도킨스", "을유문화사", R.drawable.bookcover_sample), - BookData("abc", "마틴 셀리그만", "물푸레", R.drawable.bookcover_sample), - BookData("abcd", "빅터 프랭클", "청림출판", R.drawable.bookcover_sample), - BookData("abcde", "칼 융", "문학과지성사", R.drawable.bookcover_sample), - BookData("abcdef", "에릭 프롬", "까치글방", R.drawable.bookcover_sample), - BookData("abcedfg", "알베르 카뮈", "민음사", R.drawable.bookcover_sample), - BookData("abcdefgh", "장 폴 사르트르", "문학동네", R.drawable.bookcover_sample), + BookData(title = "aaa", author = "리처드 도킨스", publisher = "을유문화사", imageUrl = null), + BookData(title = "abc", author = "마틴 셀리그만", publisher = "물푸레", imageUrl = null), + BookData(title = "abcd", author = "빅터 프랭클", publisher = "청림출판", imageUrl = null), + BookData(title = "abcde", author = "칼 융", publisher = "문학과지성사", imageUrl = null), + BookData(title = "abcdef", author = "에릭 프롬", publisher = "까치글방", imageUrl = null), + BookData(title = "abcedfg", author = "알베르 카뮈", publisher = "민음사", imageUrl = null), + BookData(title = "abcdefgh", author = "장 폴 사르트르", publisher = "문학동네", imageUrl = null), ), popularBooks = emptyList() ) diff --git a/app/src/main/java/com/texthip/thip/utils/rooms/DateUtils.kt b/app/src/main/java/com/texthip/thip/utils/rooms/DateUtils.kt new file mode 100644 index 00000000..f5579e11 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/utils/rooms/DateUtils.kt @@ -0,0 +1,13 @@ +package com.texthip.thip.utils.rooms + + +object DateUtils { + fun extractDaysFromDeadline(dateString: String): Int { + return when { + dateString.contains("일 뒤") -> { + dateString.replace("일 뒤", "").trim().toIntOrNull() ?: 0 + } + else -> 0 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/utils/rooms/GenreExtensions.kt b/app/src/main/java/com/texthip/thip/utils/rooms/GenreExtensions.kt new file mode 100644 index 00000000..bd6f2ccb --- /dev/null +++ b/app/src/main/java/com/texthip/thip/utils/rooms/GenreExtensions.kt @@ -0,0 +1,26 @@ +package com.texthip.thip.utils.rooms + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.texthip.thip.R +import com.texthip.thip.data.manager.Genre + +/** + * Genre enum을 UI에서 사용하기 위한 확장 함수들 + */ + +@Composable +fun Genre.toDisplayString(): String { + return when (this) { + Genre.LITERATURE -> stringResource(R.string.literature) + Genre.SCIENCE_IT -> stringResource(R.string.science_it) + Genre.SOCIAL_SCIENCE -> stringResource(R.string.social_science) + Genre.HUMANITIES -> stringResource(R.string.humanities) + Genre.ART -> stringResource(R.string.art) + } +} + +@Composable +fun List.toDisplayStrings(): List { + return this.map { it.toDisplayString() } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/utils/rooms/RoomUtils.kt b/app/src/main/java/com/texthip/thip/utils/rooms/RoomUtils.kt new file mode 100644 index 00000000..e206136f --- /dev/null +++ b/app/src/main/java/com/texthip/thip/utils/rooms/RoomUtils.kt @@ -0,0 +1,18 @@ +package com.texthip.thip.utils.rooms + + +object RoomUtils { + fun isRecruitingByType(type: String): Boolean { + return when (type) { + "recruiting" -> true + "playingAndRecruiting" -> false + "playing" -> false + "expired" -> false + else -> false + } + } + + fun getEndDateInDays(endDate: String): Int { + return DateUtils.extractDaysFromDeadline(endDate) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bookcover_sample.png b/app/src/main/res/drawable/img_book_cover_sample.png similarity index 100% rename from app/src/main/res/drawable/bookcover_sample.png rename to app/src/main/res/drawable/img_book_cover_sample.png diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 33dcceb3..d0b7aa4b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -348,4 +348,30 @@ 직접 모임방을 만들어보세요! 모임방 만들기 + + 마감 임박한 독서 모임방 + 인기 있는 독서 모임방 + 인플루언서·작가 독서 모임방 + + + 데이터를 불러올 수 없습니다 + 네트워크 연결을 확인해주세요 + + + + 입력 정보를 확인해주세요 + 책 정보가 올바르지 않습니다 + 모임방 생성에 실패했습니다: %1$s + 네트워크 오류가 발생했습니다: %1$s + + + 참여가 완료되었습니다 + 참여 처리 중 오류가 발생했습니다: %1$s + 모임방 참여가 취소되었어요! 다른 방을 찾아보세요. + 참여 취소 중 오류가 발생했습니다: %1$s + 모집이 마감되었습니다 + + + 과학/IT + \ No newline at end of file