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 eb8dd0fd..5c8d8132 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,7 +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.RecentSearchService import com.texthip.thip.data.service.RoomsService import com.texthip.thip.data.service.UserService import dagger.Module @@ -14,12 +14,6 @@ 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 @@ -36,4 +30,10 @@ object ServiceModule { fun provideUserService(retrofit: Retrofit): UserService { return retrofit.create(UserService::class.java) } + + @Provides + @Singleton + fun provideRecentSearchService(retrofit: Retrofit): RecentSearchService { + return retrofit.create(RecentSearchService::class.java) + } } \ 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 index 6abf7ff6..b64eea5c 100644 --- 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 @@ -11,9 +11,10 @@ data class BookListResponse( @Serializable data class BookSavedResponse( - @SerialName("isbn") val isbn: String = "", + @SerialName("bookId") val bookId: Int = 0, @SerialName("bookTitle") val bookTitle: String = "", @SerialName("authorName") val authorName: String = "", @SerialName("publisher") val publisher: String = "", - @SerialName("imageUrl") val imageUrl: String? = null + @SerialName("bookImageUrl") val bookImageUrl: String? = null, + @SerialName("isbn") val isbn: 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/rooms/request/CreateRoomRequest.kt similarity index 92% rename from app/src/main/java/com/texthip/thip/data/model/group/request/CreateRoomRequest.kt rename to app/src/main/java/com/texthip/thip/data/model/rooms/request/CreateRoomRequest.kt index b8b88fd8..c09b736a 100644 --- a/app/src/main/java/com/texthip/thip/data/model/group/request/CreateRoomRequest.kt +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/request/CreateRoomRequest.kt @@ -1,4 +1,4 @@ -package com.texthip.thip.data.model.group.request +package com.texthip.thip.data.model.rooms.request import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable 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/rooms/request/RoomJoinRequest.kt similarity index 77% rename from app/src/main/java/com/texthip/thip/data/model/group/request/RoomJoinRequest.kt rename to app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomJoinRequest.kt index 3e545262..f17bd436 100644 --- a/app/src/main/java/com/texthip/thip/data/model/group/request/RoomJoinRequest.kt +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomJoinRequest.kt @@ -1,4 +1,4 @@ -package com.texthip.thip.data.model.group.request +package com.texthip.thip.data.model.rooms.request import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomSecreteRoomRequest.kt b/app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomSecreteRoomRequest.kt new file mode 100644 index 00000000..68d4b964 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/request/RoomSecreteRoomRequest.kt @@ -0,0 +1,10 @@ +package com.texthip.thip.data.model.rooms.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + + +@Serializable +data class RoomSecreteRoomRequest( + @SerialName("password") val password: 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/rooms/response/CreateRoomResponse.kt similarity index 77% rename from app/src/main/java/com/texthip/thip/data/model/group/response/CreateRoomResponse.kt rename to app/src/main/java/com/texthip/thip/data/model/rooms/response/CreateRoomResponse.kt index a1c57cf1..5877eac0 100644 --- a/app/src/main/java/com/texthip/thip/data/model/group/response/CreateRoomResponse.kt +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/response/CreateRoomResponse.kt @@ -1,4 +1,4 @@ -package com.texthip.thip.data.model.group.response +package com.texthip.thip.data.model.rooms.response import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable 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/rooms/response/JoinedRoomListResponse.kt similarity index 93% rename from app/src/main/java/com/texthip/thip/data/model/group/response/JoinedRoomListResponse.kt rename to app/src/main/java/com/texthip/thip/data/model/rooms/response/JoinedRoomListResponse.kt index 9ad032ea..bb057aaf 100644 --- a/app/src/main/java/com/texthip/thip/data/model/group/response/JoinedRoomListResponse.kt +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/response/JoinedRoomListResponse.kt @@ -1,4 +1,4 @@ -package com.texthip.thip.data.model.group.response +package com.texthip.thip.data.model.rooms.response import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable 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/rooms/response/MyRoomListResponse.kt similarity index 93% rename from app/src/main/java/com/texthip/thip/data/model/group/response/MyRoomListResponse.kt rename to app/src/main/java/com/texthip/thip/data/model/rooms/response/MyRoomListResponse.kt index aa9c6a4d..a4283217 100644 --- a/app/src/main/java/com/texthip/thip/data/model/group/response/MyRoomListResponse.kt +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/response/MyRoomListResponse.kt @@ -1,4 +1,4 @@ -package com.texthip.thip.data.model.group.response +package com.texthip.thip.data.model.rooms.response import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomCloseResponse.kt b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomCloseResponse.kt new file mode 100644 index 00000000..35aa9ec3 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomCloseResponse.kt @@ -0,0 +1,9 @@ +package com.texthip.thip.data.model.rooms.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RoomCloseResponse( + @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/RoomJoinResponse.kt b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomJoinResponse.kt similarity index 80% rename from app/src/main/java/com/texthip/thip/data/model/group/response/RoomJoinResponse.kt rename to app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomJoinResponse.kt index 2af96a65..1345eb9d 100644 --- a/app/src/main/java/com/texthip/thip/data/model/group/response/RoomJoinResponse.kt +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomJoinResponse.kt @@ -1,4 +1,4 @@ -package com.texthip.thip.data.model.group.response +package com.texthip.thip.data.model.rooms.response import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable 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/rooms/response/RoomMainResponse.kt similarity index 92% rename from app/src/main/java/com/texthip/thip/data/model/group/response/RoomMainResponse.kt rename to app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomMainResponse.kt index a0b9bb51..a49b4d53 100644 --- a/app/src/main/java/com/texthip/thip/data/model/group/response/RoomMainResponse.kt +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomMainResponse.kt @@ -1,4 +1,4 @@ -package com.texthip.thip.data.model.group.response +package com.texthip.thip.data.model.rooms.response import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable 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/rooms/response/RoomRecruitingResponse.kt similarity index 93% rename from app/src/main/java/com/texthip/thip/data/model/group/response/RoomRecruitingResponse.kt rename to app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomRecruitingResponse.kt index 0d35628a..5c016aa9 100644 --- a/app/src/main/java/com/texthip/thip/data/model/group/response/RoomRecruitingResponse.kt +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomRecruitingResponse.kt @@ -1,4 +1,4 @@ -package com.texthip.thip.data.model.group.response +package com.texthip.thip.data.model.rooms.response import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -15,6 +15,7 @@ data class RoomRecruitingResponse( @SerialName("progressEndDate") val progressEndDate: String, @SerialName("recruitEndDate") val recruitEndDate: String, @SerialName("category") val category: String, + @SerialName("categoryColor") val categoryColor: String, @SerialName("roomDescription") val roomDescription: String, @SerialName("memberCount") val memberCount: Int, @SerialName("recruitCount") val recruitCount: Int, diff --git a/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomSecreteRoomResponse.kt b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomSecreteRoomResponse.kt new file mode 100644 index 00000000..40d8f11e --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomSecreteRoomResponse.kt @@ -0,0 +1,11 @@ +package com.texthip.thip.data.model.rooms.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + + +@Serializable +data class RoomSecreteRoomResponse( + @SerialName("matched") val matched: Boolean = false, + @SerialName("roomId") val roomId: Int = 0 +) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/provider/StringResourceProvider.kt b/app/src/main/java/com/texthip/thip/data/provider/StringResourceProvider.kt new file mode 100644 index 00000000..c6114fc4 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/provider/StringResourceProvider.kt @@ -0,0 +1,21 @@ +package com.texthip.thip.data.provider + +import android.content.Context +import androidx.annotation.StringRes +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class StringResourceProvider @Inject constructor( + @param:ApplicationContext private val context: Context +) { + + fun getString(@StringRes resId: Int): String { + return context.getString(resId) + } + + fun getString(@StringRes resId: Int, vararg formatArgs: Any): String { + return context.getString(resId, *formatArgs) + } +} \ 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 index 8e09405c..0fb2cd47 100644 --- a/app/src/main/java/com/texthip/thip/data/repository/BookRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/BookRepository.kt @@ -3,11 +3,10 @@ package com.texthip.thip.data.repository import com.texthip.thip.data.model.base.handleBaseResponse import com.texthip.thip.data.model.book.request.BookSaveRequest import com.texthip.thip.data.model.book.response.BookDetailResponse +import com.texthip.thip.data.model.book.response.BookListResponse import com.texthip.thip.data.model.book.response.BookSaveResponse -import com.texthip.thip.data.model.book.response.BookSavedResponse import com.texthip.thip.data.model.book.response.BookSearchResponse import com.texthip.thip.data.model.book.response.MostSearchedBooksResponse -import com.texthip.thip.data.model.book.response.RecentSearchResponse import com.texthip.thip.data.model.book.response.RecruitingRoomsResponse import com.texthip.thip.data.service.BookService import javax.inject.Inject @@ -19,11 +18,10 @@ class BookRepository @Inject constructor( ) { /** 저장된 책 또는 모임 책 목록 조회 */ - suspend fun getBooks(type: String): Result> = runCatching { + suspend fun getBooks(type: String): Result = runCatching { bookService.getBooks(type) .handleBaseResponse() .getOrThrow() - ?.bookList ?: emptyList() } /** 책 검색 */ @@ -40,20 +38,6 @@ class BookRepository @Inject constructor( .getOrThrow() } - /** 최근 검색어 조회 */ - suspend fun getRecentSearches(type: String = "BOOK"): Result = runCatching { - bookService.getRecentSearches(type) - .handleBaseResponse() - .getOrThrow() - } - - /** 최근 검색어 삭제 */ - suspend fun deleteRecentSearch(recentSearchId: Int): Result = runCatching { - bookService.deleteRecentSearch(recentSearchId) - .handleBaseResponse() - .getOrThrow() - } - /** 책 상세 조회 */ suspend fun getBookDetail(isbn: String): Result = runCatching { bookService.getBookDetail(isbn) 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 deleted file mode 100644 index e808a3c2..00000000 --- a/app/src/main/java/com/texthip/thip/data/repository/GroupRepository.kt +++ /dev/null @@ -1,90 +0,0 @@ -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() - ?: throw NoSuchElementException("모집중인 모임방 정보를 찾을 수 없습니다.") - } - - /** 새 모임방 생성 */ - suspend fun createRoom(request: CreateRoomRequest): Result = runCatching { - val response = groupService.createRoom(request) - .handleBaseResponse() - .getOrThrow() - ?: throw NoSuchElementException("모임방 생성 응답을 받을 수 없습니다.") - response.roomId - } - - /** 모임방 참여 또는 취소 */ - suspend fun joinOrCancelRoom(roomId: Int, type: String): Result = runCatching { - val request = RoomJoinRequest(type = type) - val response = groupService.joinOrCancelRoom(roomId, request) - .handleBaseResponse() - .getOrThrow() - ?: throw NoSuchElementException("모임방 참여/취소 응답을 받을 수 없습니다.") - response.type - } -} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/repository/RecentSearchRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/RecentSearchRepository.kt new file mode 100644 index 00000000..1b8e7067 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/repository/RecentSearchRepository.kt @@ -0,0 +1,28 @@ +package com.texthip.thip.data.repository + +import com.texthip.thip.data.model.base.handleBaseResponse +import com.texthip.thip.data.model.book.response.RecentSearchResponse +import com.texthip.thip.data.service.RecentSearchService +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class RecentSearchRepository @Inject constructor( + private val recentSearchService: RecentSearchService +) { + + /** 최근 검색어 조회 */ + suspend fun getRecentSearches(type: String): Result = runCatching { + recentSearchService.getRecentSearches(type) + .handleBaseResponse() + .getOrThrow() + } + + /** 최근 검색어 삭제 */ + suspend fun deleteRecentSearch(recentSearchId: Int): Result = runCatching { + recentSearchService.deleteRecentSearch(recentSearchId) + .handleBaseResponse() + .getOrThrow() + ?: Unit + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt index 8b16c7a2..5e33841c 100644 --- a/app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/RoomsRepository.kt @@ -1,11 +1,23 @@ package com.texthip.thip.data.repository +import com.texthip.thip.data.manager.Genre +import com.texthip.thip.data.manager.GenreManager +import com.texthip.thip.data.manager.UserDataManager import com.texthip.thip.data.model.base.handleBaseResponse +import com.texthip.thip.data.model.rooms.request.CreateRoomRequest +import com.texthip.thip.data.model.rooms.request.RoomJoinRequest +import com.texthip.thip.data.model.rooms.request.RoomSecreteRoomRequest import com.texthip.thip.data.model.rooms.request.RoomsCreateVoteRequest import com.texthip.thip.data.model.rooms.request.RoomsPostsLikesRequest import com.texthip.thip.data.model.rooms.request.RoomsRecordRequest import com.texthip.thip.data.model.rooms.request.RoomsVoteRequest import com.texthip.thip.data.model.rooms.request.VoteItem +import com.texthip.thip.data.model.rooms.response.JoinedRoomListResponse +import com.texthip.thip.data.model.rooms.response.MyRoomListResponse +import com.texthip.thip.data.model.rooms.response.RoomCloseResponse +import com.texthip.thip.data.model.rooms.response.RoomMainList +import com.texthip.thip.data.model.rooms.response.RoomRecruitingResponse +import com.texthip.thip.data.model.rooms.response.RoomSecreteRoomResponse import com.texthip.thip.data.service.RoomsService import javax.inject.Inject import javax.inject.Singleton @@ -13,7 +25,98 @@ import javax.inject.Singleton @Singleton class RoomsRepository @Inject constructor( private val roomsService: RoomsService, + 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 = roomsService.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) + + roomsService.getRooms(apiCategory) + .handleBaseResponse() + .getOrThrow() + } + + /** 타입별 내 모임방 목록 조회 */ + suspend fun getMyRoomsByType(type: String?, cursor: String? = null): Result = runCatching { + roomsService.getMyRooms(type, cursor) + .handleBaseResponse() + .getOrThrow() + } + + /** 모집중인 모임방 상세 정보 조회 */ + suspend fun getRoomRecruiting(roomId: Int): Result = runCatching { + roomsService.getRoomRecruiting(roomId) + .handleBaseResponse() + .getOrThrow() + } + + /** 새 모임방 생성 */ + suspend fun createRoom(request: CreateRoomRequest): Result = runCatching { + val response = roomsService.createRoom(request) + .handleBaseResponse() + .getOrThrow() + ?: throw NoSuchElementException("모임방 생성 응답을 받을 수 없습니다.") + response.roomId + } + + /** 모임방 참여 또는 취소 */ + suspend fun joinOrCancelRoom(roomId: Int, type: String): Result = runCatching { + val request = RoomJoinRequest(type = type) + val response = roomsService.joinOrCancelRoom(roomId, request) + .handleBaseResponse() + .getOrThrow() + ?: throw NoSuchElementException("모임방 참여/취소 응답을 받을 수 없습니다.") + response.type + } + + /** 비밀번호 입력 */ + suspend fun postParticipateSecreteRoom(roomId: Int, password: String): Result = runCatching { + val request = RoomSecreteRoomRequest(password = password) + val response = roomsService.postParticipateSecreteRoom(roomId, request) + .handleBaseResponse() + .getOrThrow() + ?: throw NoSuchElementException("비밀번호 입력 응답을 받을 수 없습니다.") + + response + } + + /** 모집 마감 */ + suspend fun closeRoom(roomId: Int): Result = runCatching { + val response = roomsService.closeRoom(roomId) + .handleBaseResponse() + .getOrThrow() + ?: throw NoSuchElementException("모집 마감 응답을 받을 수 없습니다.") + response + } + + + + /** 기록장 API들 */ suspend fun getRoomsPlaying( roomId: Int ) = runCatching { 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 index b674619a..a6351c91 100644 --- a/app/src/main/java/com/texthip/thip/data/service/BookService.kt +++ b/app/src/main/java/com/texthip/thip/data/service/BookService.kt @@ -7,10 +7,8 @@ import com.texthip.thip.data.model.book.response.BookListResponse import com.texthip.thip.data.model.book.response.BookSaveResponse import com.texthip.thip.data.model.book.response.BookSearchResponse import com.texthip.thip.data.model.book.response.MostSearchedBooksResponse -import com.texthip.thip.data.model.book.response.RecentSearchResponse import com.texthip.thip.data.model.book.response.RecruitingRoomsResponse import retrofit2.http.Body -import retrofit2.http.DELETE import retrofit2.http.GET import retrofit2.http.POST import retrofit2.http.Path @@ -19,7 +17,7 @@ import retrofit2.http.Query interface BookService { /** 저장된 책 또는 모임 책 목록 조회 */ - @GET("books") + @GET("books/selectable-list") suspend fun getBooks( @Query("type") type: String ): BaseResponse @@ -36,18 +34,6 @@ interface BookService { @GET("books/most-searched") suspend fun getMostSearchedBooks(): BaseResponse - /** 최근 검색어 조회 */ - @GET("recent-searches") - suspend fun getRecentSearches( - @Query("type") type: String - ): BaseResponse - - /** 최근 검색어 삭제 */ - @DELETE("recent-searches/{recentSearchId}") - suspend fun deleteRecentSearch( - @Path("recentSearchId") recentSearchId: Int - ): BaseResponse - /** 책 상세 조회 */ @GET("books/{isbn}") suspend fun getBookDetail( 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 deleted file mode 100644 index b02895c2..00000000 --- a/app/src/main/java/com/texthip/thip/data/service/GroupService.kt +++ /dev/null @@ -1,57 +0,0 @@ -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/data/service/RecentSearchService.kt b/app/src/main/java/com/texthip/thip/data/service/RecentSearchService.kt new file mode 100644 index 00000000..e4f9f041 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/service/RecentSearchService.kt @@ -0,0 +1,23 @@ +package com.texthip.thip.data.service + +import com.texthip.thip.data.model.base.BaseResponse +import com.texthip.thip.data.model.book.response.RecentSearchResponse +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Query + +interface RecentSearchService { + + /** 최근 검색어 조회 */ + @GET("recent-searches") + suspend fun getRecentSearches( + @Query("type") type: String + ): BaseResponse + + /** 최근 검색어 삭제 */ + @DELETE("recent-searches/{recentSearchId}") + suspend fun deleteRecentSearch( + @Path("recentSearchId") recentSearchId: Int + ): BaseResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/service/RoomsService.kt b/app/src/main/java/com/texthip/thip/data/service/RoomsService.kt index 3e8e05e0..3056b242 100644 --- a/app/src/main/java/com/texthip/thip/data/service/RoomsService.kt +++ b/app/src/main/java/com/texthip/thip/data/service/RoomsService.kt @@ -1,10 +1,21 @@ package com.texthip.thip.data.service import com.texthip.thip.data.model.base.BaseResponse +import com.texthip.thip.data.model.rooms.request.CreateRoomRequest +import com.texthip.thip.data.model.rooms.request.RoomJoinRequest +import com.texthip.thip.data.model.rooms.request.RoomSecreteRoomRequest import com.texthip.thip.data.model.rooms.request.RoomsCreateVoteRequest import com.texthip.thip.data.model.rooms.request.RoomsPostsLikesRequest import com.texthip.thip.data.model.rooms.request.RoomsRecordRequest import com.texthip.thip.data.model.rooms.request.RoomsVoteRequest +import com.texthip.thip.data.model.rooms.response.CreateRoomResponse +import com.texthip.thip.data.model.rooms.response.JoinedRoomListResponse +import com.texthip.thip.data.model.rooms.response.MyRoomListResponse +import com.texthip.thip.data.model.rooms.response.RoomJoinResponse +import com.texthip.thip.data.model.rooms.response.RoomRecruitingResponse +import com.texthip.thip.data.model.rooms.response.RoomMainList +import com.texthip.thip.data.model.rooms.response.RoomSecreteRoomResponse +import com.texthip.thip.data.model.rooms.response.RoomCloseResponse import com.texthip.thip.data.model.rooms.response.RoomsBookPageResponse import com.texthip.thip.data.model.rooms.response.RoomsCreateVoteResponse import com.texthip.thip.data.model.rooms.response.RoomsDeleteRecordResponse @@ -22,6 +33,59 @@ import retrofit2.http.Path import retrofit2.http.Query interface RoomsService { + + /** 참여 중인 모임방 목록 조회 */ + @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 + + /** 비밀번호 입력 */ + @POST("rooms/{roomId}/password") + suspend fun postParticipateSecreteRoom( + @Path("roomId") roomId: Int, + @Body request: RoomSecreteRoomRequest + ): BaseResponse + + /** 모집 마감 */ + @POST("rooms/{roomId}/close") + suspend fun closeRoom( + @Path("roomId") roomId: Int + ): BaseResponse + + + + /** 기록장 API들 */ @GET("rooms/{roomId}/playing") suspend fun getRoomsPlaying( @Path("roomId") roomId: Int 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 index 6bb1dcef..63c26da1 100644 --- 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 @@ -24,7 +24,7 @@ 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.data.model.rooms.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 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 index 8af6d4bc..df299ec1 100644 --- 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 @@ -1,6 +1,6 @@ package com.texthip.thip.ui.group.done.viewmodel -import com.texthip.thip.data.model.group.response.MyRoomResponse +import com.texthip.thip.data.model.rooms.response.MyRoomResponse data class GroupDoneUiState( val expiredRooms: List = emptyList(), 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 index 338cb28e..f6cc44ac 100644 --- 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 @@ -2,7 +2,7 @@ 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.data.repository.RoomsRepository import com.texthip.thip.ui.group.myroom.mock.RoomType import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -13,7 +13,7 @@ import javax.inject.Inject @HiltViewModel class GroupDoneViewModel @Inject constructor( - private val repository: GroupRepository + private val repository: RoomsRepository ) : ViewModel() { private val _uiState = MutableStateFlow(GroupDoneUiState()) 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 67d8afe9..ecf4fb75 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 @@ -1,18 +1,21 @@ package com.texthip.thip.ui.group.makeroom.component +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column 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.material3.CircularProgressIndicator import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable 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 @@ -33,9 +36,11 @@ fun GroupBookSearchBottomSheet( onRequestBook: () -> Unit, savedBooks: List, groupBooks: List, - isLoading: Boolean = false + searchResults: List = emptyList(), + isLoading: Boolean = false, + isSearching: Boolean = false, + onSearch: (String) -> Unit = {} ) { - val hasBooks = savedBooks.isNotEmpty() || groupBooks.isNotEmpty() var selectedTab by rememberSaveable { mutableIntStateOf(0) } val tabs = listOf( stringResource(R.string.group_saved_book), stringResource(R.string.group_book) @@ -45,21 +50,14 @@ fun GroupBookSearchBottomSheet( val currentBooks = if (selectedTab == 0) savedBooks else groupBooks - val filteredBooks by remember(currentBooks, searchText) { - derivedStateOf { - if (searchText.isEmpty()) { - currentBooks - } else { - currentBooks.filter { book -> - book.title.contains(searchText, ignoreCase = true) || - (book.author?.contains(searchText, ignoreCase = true) == true) - } - } - } + // 검색어가 있으면 검색 결과 사용, 없으면 탭별 도서 목록 사용 + val displayBooks = if (searchText.isNotEmpty()) { + searchResults + } else { + currentBooks } - // 검색 결과가 있는지 확인 - val hasSearchResults = searchText.isEmpty() || filteredBooks.isNotEmpty() + val showNoSearchResultsError = searchText.isNotEmpty() && displayBooks.isEmpty() && !isSearching CustomBottomSheet( onDismiss = onDismiss @@ -67,64 +65,50 @@ fun GroupBookSearchBottomSheet( Column( Modifier .fillMaxWidth() - .padding(start = 20.dp, end = 20.dp, top = 20.dp) + .padding(bottom = 90.dp) ) { - // 검색창 - SearchBookTextField( - hint = stringResource(R.string.group_book_search_hint), - text = searchText, - onValueChange = { searchText = it }, - onSearch = { /* 이미 실시간으로 필터링됨 */ }, - ) - Spacer(Modifier.height(20.dp)) - } - - // 책이 있고 검색 결과가 있을 때만 탭 표시 - if (hasBooks && hasSearchResults) { - HeaderMenuBarTab( - titles = tabs, - selectedTabIndex = selectedTab, - onTabSelected = { - selectedTab = it - }, - indicatorColor = ThipTheme.colors.White, - modifier = Modifier.fillMaxWidth() - ) - } - - Column( - Modifier - .fillMaxWidth() - .padding(start = 20.dp, end = 20.dp, bottom = 90.dp) - ) { - Spacer(Modifier.height(20.dp)) + Column(Modifier.padding(start = 20.dp, end = 20.dp, top = 20.dp)) { + SearchBookTextField( + hint = stringResource(R.string.group_book_search_hint), + text = searchText, + onValueChange = { + searchText = it + onSearch(it) + }, + onSearch = { onSearch(searchText) }, + ) + Spacer(Modifier.height(20.dp)) + } - when { - // 로딩 중 - isLoading -> { - EmptyBookSheetContent(onRequestBook = onRequestBook) - } - // 검색 결과가 없을 때 (검색어가 있지만 결과가 없음) - searchText.isNotEmpty() && filteredBooks.isEmpty() -> { - SearchEmptyContent( - searchText = searchText, - onRequestBook = onRequestBook + if (showNoSearchResultsError) { + EmptyBookSheetContent(onRequestBook) + } else { + // 검색어가 없을 때만 탭 표시 + if (searchText.isEmpty()) { + HeaderMenuBarTab( + titles = tabs, + selectedTabIndex = selectedTab, + onTabSelected = { selectedTab = it }, + indicatorColor = ThipTheme.colors.White, + modifier = Modifier.fillMaxWidth() ) + Spacer(Modifier.height(20.dp)) } - // 전체 책이 없을 때 - !hasBooks -> { - EmptyBookSheetContent(onRequestBook = onRequestBook) - } - // 현재 탭의 책이 없을 때 - currentBooks.isEmpty() -> { - EmptyBookSheetContent(onRequestBook = onRequestBook) - } - // 정상적으로 책 목록이 있을 때 - else -> { - GroupBookListWithScrollbar( - books = filteredBooks, - onBookClick = onBookSelect - ) + + when { + isLoading -> { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } + else -> { + Column(Modifier.padding(horizontal = 20.dp)) { + GroupBookListWithScrollbar( + books = displayBooks, + onBookClick = onBookSelect + ) + } + } } } } @@ -132,17 +116,6 @@ fun GroupBookSearchBottomSheet( } -// 검색 결과가 없을 때 표시할 컴포넌트 (필요시 구현) -@Composable -private fun SearchEmptyContent( - searchText: String, - onRequestBook: () -> Unit -) { - // 검색 결과가 없을 때의 UI - // 예: "'{searchText}'에 대한 검색 결과가 없습니다" 메시지와 책 요청 버튼 - EmptyBookSheetContent(onRequestBook = onRequestBook) -} - @Preview(showBackground = true) @Composable fun PreviewBookSearchBottomSheet_HasBooks() { diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupEmptyBookSheetContent.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupEmptyBookSheetContent.kt index 2f36b707..835fe44a 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupEmptyBookSheetContent.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/component/GroupEmptyBookSheetContent.kt @@ -6,12 +6,10 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -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 @@ -28,7 +26,7 @@ fun EmptyBookSheetContent( Column( modifier = Modifier .fillMaxWidth() - .padding(bottom = 30.dp), + .padding(top = 20.dp, bottom = 30.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Text( 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 5c86173b..090a5f29 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 @@ -33,9 +33,9 @@ import com.texthip.thip.ui.common.forms.WarningTextField import com.texthip.thip.ui.common.topappbar.InputTopAppBar import com.texthip.thip.ui.group.makeroom.component.GroupBookSearchBottomSheet import com.texthip.thip.ui.group.makeroom.component.GroupInputField +import com.texthip.thip.ui.group.makeroom.component.GroupMemberLimitPicker import com.texthip.thip.ui.group.makeroom.component.GroupRoomDurationPicker 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.viewmodel.GroupMakeRoomUiState @@ -49,7 +49,7 @@ import com.texthip.thip.utils.rooms.toDisplayStrings @Composable fun GroupMakeRoomScreen( onNavigateBack: () -> Unit, - onGroupCreated: () -> Unit, + onGroupCreated: (Int) -> Unit, // roomId 전달 modifier: Modifier = Modifier, viewModel: GroupMakeRoomViewModel = hiltViewModel() ) { @@ -65,16 +65,13 @@ fun GroupMakeRoomScreen( GroupMakeRoomContent( uiState = uiState, onNavigateBack = onNavigateBack, - onGroupCreated = onGroupCreated, - onCreateGroup = { + onCreateGroup = { viewModel.createGroup( onSuccess = { roomId -> - // TODO: 생성된 roomId를 사용하여 해당 방으로 이동할 수 있음 - onGroupCreated() + onGroupCreated(roomId) }, onError = { errorMessage -> - // TODO: 에러 메시지 표시 (토스트 메시지 등) - // 현재는 uiState.errorMessage를 통해 처리 + // TODO: 에러 메시지 표시 } ) }, @@ -87,6 +84,7 @@ fun GroupMakeRoomScreen( onSetMemberLimit = viewModel::setMemberLimit, onTogglePrivate = viewModel::togglePrivate, onUpdatePassword = viewModel::updatePassword, + onSearchBooks = viewModel::searchBooks, modifier = modifier ) } @@ -96,7 +94,6 @@ fun GroupMakeRoomContent( modifier: Modifier = Modifier, uiState: GroupMakeRoomUiState, onNavigateBack: () -> Unit = {}, - onGroupCreated: () -> Unit = {}, // 그룹이 만들어졌을때 로직 onCreateGroup: () -> Unit = {}, onSelectBook: (BookData) -> Unit = {}, onToggleBookSearchSheet: (Boolean) -> Unit = {}, @@ -106,7 +103,8 @@ fun GroupMakeRoomContent( onSetDateRange: (java.time.LocalDate, java.time.LocalDate) -> Unit = { _, _ -> }, onSetMemberLimit: (Int) -> Unit = {}, onTogglePrivate: (Boolean) -> Unit = {}, - onUpdatePassword: (String) -> Unit = {} + onUpdatePassword: (String) -> Unit = {}, + onSearchBooks: (String) -> Unit = {} ) { val scrollState = rememberScrollState() @@ -257,21 +255,12 @@ fun GroupMakeRoomContent( }, savedBooks = uiState.savedBooks, groupBooks = uiState.groupBooks, - isLoading = uiState.isLoadingBooks + searchResults = uiState.searchResults, + isLoading = uiState.isLoadingBooks, + isSearching = uiState.isSearching, + onSearch = onSearchBooks ) } - - // 로딩 인디케이터 - /*if (uiState.isLoading) { - Box( - modifier = Modifier - .fillMaxSize() - .background(colors.Black.copy(alpha = 0.5f)), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator(color = colors.NeonGreen) - } - }*/ } } diff --git a/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomUiState.kt b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomUiState.kt index 25104279..f95b5b31 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomUiState.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/makeroom/viewmodel/GroupMakeRoomUiState.kt @@ -22,6 +22,8 @@ data class GroupMakeRoomUiState( val savedBooks: List = emptyList(), val groupBooks: List = emptyList(), val isLoadingBooks: Boolean = false, + val searchResults: List = emptyList(), + val isSearching: Boolean = false, val genres: List = emptyList(), val isBookPreselected: Boolean = false ) { 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 04d910a3..275587ab 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,17 +1,19 @@ 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.model.book.response.BookSearchItem +import com.texthip.thip.data.model.rooms.request.CreateRoomRequest import com.texthip.thip.data.manager.Genre +import com.texthip.thip.data.provider.StringResourceProvider import com.texthip.thip.data.repository.BookRepository -import com.texthip.thip.data.repository.GroupRepository +import com.texthip.thip.data.repository.RoomsRepository import com.texthip.thip.ui.group.makeroom.mock.BookData import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -22,14 +24,16 @@ import javax.inject.Inject @HiltViewModel class GroupMakeRoomViewModel @Inject constructor( - private val groupRepository: GroupRepository, + private val roomsRepository: RoomsRepository, private val bookRepository: BookRepository, - @param:ApplicationContext private val context: Context + private val stringResourceProvider: StringResourceProvider ) : ViewModel() { private val _uiState = MutableStateFlow(GroupMakeRoomUiState()) val uiState: StateFlow = _uiState.asStateFlow() + private var searchJob: Job? = null + companion object { private val DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy.MM.dd") } @@ -44,7 +48,7 @@ class GroupMakeRoomViewModel @Inject constructor( private fun loadGenres() { viewModelScope.launch { - groupRepository.getGenres() + roomsRepository.getGenres() .onSuccess { genresList -> updateState { it.copy(genres = genresList) } } @@ -81,16 +85,16 @@ class GroupMakeRoomViewModel @Inject constructor( 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() }) } + val savedBooksResult = bookRepository.getBooks("SAVED") + savedBooksResult.onSuccess { response -> + updateState { it.copy(savedBooks = response?.bookList?.map { dto -> dto.toBookData() } ?: emptyList()) } }.onFailure { updateState { it.copy(savedBooks = emptyList()) } } - - val groupBooksResult = bookRepository.getBooks("joining") - groupBooksResult.onSuccess { bookDtos -> - updateState { it.copy(groupBooks = bookDtos.map { dto -> dto.toBookData() }) } + + val groupBooksResult = bookRepository.getBooks("JOINING") + groupBooksResult.onSuccess { response -> + updateState { it.copy(groupBooks = response?.bookList?.map { dto -> dto.toBookData() } ?: emptyList()) } }.onFailure { updateState { it.copy(groupBooks = emptyList()) } } @@ -105,11 +109,46 @@ class GroupMakeRoomViewModel @Inject constructor( private fun BookSavedResponse.toBookData(): BookData { return BookData( title = this.bookTitle, + imageUrl = this.bookImageUrl, + author = this.authorName, + isbn = this.isbn + ) + } + + private fun BookSearchItem.toBookData(): BookData { + return BookData( + title = this.title, imageUrl = this.imageUrl, author = this.authorName, isbn = this.isbn ) } + + fun searchBooks(query: String) { + searchJob?.cancel() + + if (query.isBlank()) { + updateState { it.copy(searchResults = emptyList(), isSearching = false) } + return + } + + searchJob = viewModelScope.launch { + delay(300) // 디바운싱 + updateState { it.copy(isSearching = true) } + + try { + val result = bookRepository.searchBooks(query, page = 1, isFinalized = false) + result.onSuccess { response -> + val searchResults = response?.searchResult?.map { it.toBookData() } ?: emptyList() + updateState { it.copy(searchResults = searchResults, isSearching = false) } + }.onFailure { + updateState { it.copy(searchResults = emptyList(), isSearching = false) } + } + } catch (e: Exception) { + updateState { it.copy(searchResults = emptyList(), isSearching = false) } + } + } + } fun selectGenre(index: Int) { updateState { it.copy(selectedGenreIndex = index) } @@ -153,13 +192,13 @@ class GroupMakeRoomViewModel @Inject constructor( val currentState = _uiState.value if (!currentState.isFormValid) { - onError(context.getString(R.string.error_form_validation)) + onError(stringResourceProvider.getString(R.string.error_form_validation)) return } val selectedBook = currentState.selectedBook if (selectedBook?.isbn == null) { - onError(context.getString(R.string.error_book_info_invalid)) + onError(stringResourceProvider.getString(R.string.error_book_info_invalid)) return } @@ -179,14 +218,14 @@ class GroupMakeRoomViewModel @Inject constructor( isPublic = !currentState.isPrivate ) - val result = groupRepository.createRoom(request) + val result = roomsRepository.createRoom(request) result.onSuccess { roomId -> onSuccess(roomId) }.onFailure { exception -> - onError(context.getString(R.string.error_room_creation_failed, exception.message ?: "")) + onError(stringResourceProvider.getString(R.string.error_room_creation_failed, exception.message ?: "")) } } catch (e: Exception) { - onError(context.getString(R.string.error_network_error, e.message ?: "")) + onError(stringResourceProvider.getString(R.string.error_network_error, e.message ?: "")) } finally { updateState { it.copy(isLoading = false) } } 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 20b791f2..69d81ff0 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 @@ -29,9 +29,9 @@ 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.data.model.group.response.RoomMainList +import com.texthip.thip.data.model.rooms.response.RoomMainList import com.texthip.thip.data.manager.Genre -import com.texthip.thip.data.model.group.response.RoomMainResponse +import com.texthip.thip.data.model.rooms.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 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 46ffe402..35924a31 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 @@ -32,7 +32,7 @@ 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.data.model.group.response.JoinedRoomResponse +import com.texthip.thip.data.model.rooms.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 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 4716683b..1ea642b1 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,7 +16,7 @@ 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.data.model.group.response.JoinedRoomResponse +import com.texthip.thip.data.model.rooms.response.JoinedRoomResponse import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors 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 b05f0d0e..59b70a28 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 @@ -27,7 +27,7 @@ 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.data.model.rooms.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 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 index 65f5a5b2..d48e308f 100644 --- 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 @@ -1,6 +1,6 @@ package com.texthip.thip.ui.group.myroom.viewmodel -import com.texthip.thip.data.model.group.response.MyRoomResponse +import com.texthip.thip.data.model.rooms.response.MyRoomResponse import com.texthip.thip.ui.group.myroom.mock.RoomType data class GroupMyUiState( 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 index f34aabdc..2de4c248 100644 --- 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 @@ -2,7 +2,7 @@ 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.data.repository.RoomsRepository import com.texthip.thip.ui.group.myroom.mock.RoomType import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -13,7 +13,7 @@ import javax.inject.Inject @HiltViewModel class GroupMyViewModel @Inject constructor( - private val repository: GroupRepository + private val repository: RoomsRepository ) : ViewModel() { private val _uiState = MutableStateFlow(GroupMyUiState()) 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 85cddd88..1c9005f1 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 @@ -1,7 +1,7 @@ package com.texthip.thip.ui.group.room.screen -import androidx.compose.foundation.Image import androidx.compose.foundation.background +import coil.compose.AsyncImage import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -29,6 +29,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource @@ -38,7 +39,8 @@ 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.data.model.rooms.response.RecommendRoomResponse +import com.texthip.thip.data.model.rooms.response.RoomRecruitingResponse import com.texthip.thip.ui.common.cards.CardItemRoomSmall import com.texthip.thip.ui.common.cards.CardRoomBook import com.texthip.thip.ui.common.modal.DialogPopup @@ -50,6 +52,7 @@ 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.color.hexToColor import com.texthip.thip.utils.rooms.DateUtils import kotlinx.coroutines.delay @@ -59,6 +62,9 @@ fun GroupRoomRecruitScreen( onRecommendationClick: (RecommendRoomResponse) -> Unit = {}, onNavigateToGroupScreen: (String) -> Unit = {}, // GroupScreen으로 네비게이션 + 토스트 메시지 onBackClick: () -> Unit = {}, // 뒤로가기 + onBookDetailClick: (String) -> Unit = {}, // 책 상세 화면으로 이동 + onNavigateToPasswordScreen: (Int) -> Unit = {}, // 비밀번호 입력 화면으로 이동 + onNavigateToRoomPlayingScreen: (Int) -> Unit = {}, // 기록장 화면으로 이동 viewModel: GroupRoomRecruitViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsState() @@ -76,11 +82,31 @@ fun GroupRoomRecruitScreen( } } + // 기록장 화면으로 네비게이션 + LaunchedEffect(uiState.shouldNavigateToRoomPlayingScreen, uiState.roomId) { + if (uiState.shouldNavigateToRoomPlayingScreen) { + val roomIdValue = uiState.roomId + if (roomIdValue != null) { + onNavigateToRoomPlayingScreen(roomIdValue) + viewModel.onNavigatedToRoomPlayingScreen() + } + } + } + GroupRoomRecruitContent( uiState = uiState, onRecommendationClick = onRecommendationClick, onBackClick = onBackClick, - onParticipationClick = { viewModel.onParticipationClick() }, + onBookDetailClick = onBookDetailClick, + onParticipationClick = { + // 비밀방이면 비밀번호 화면으로, 공개방이면 바로 참여 + val detail = uiState.roomDetail + if (detail != null && !detail.isPublic) { + onNavigateToPasswordScreen(roomId) + } else { + viewModel.onParticipationClick() + } + }, onCancelParticipationClick = { title, description -> viewModel.onCancelParticipationClick(title, description) }, onCloseRecruitmentClick = { title, description -> viewModel.onCloseRecruitmentClick(title, description) }, onDialogConfirm = { viewModel.onDialogConfirm() }, @@ -94,6 +120,7 @@ fun GroupRoomRecruitContent( uiState: GroupRoomRecruitUiState, onRecommendationClick: (RecommendRoomResponse) -> Unit = {}, onBackClick: () -> Unit = {}, + onBookDetailClick: (String) -> Unit = {}, onParticipationClick: () -> Unit = {}, onCancelParticipationClick: (String, String) -> Unit = { _, _ -> }, onCloseRecruitmentClick: (String, String) -> Unit = { _, _ -> }, @@ -119,9 +146,9 @@ fun GroupRoomRecruitContent( // 데이터가 없는 경우 val detail = uiState.roomDetail ?: return@Box - Image( - painter = painterResource(id = R.drawable.group_room_recruiting), - contentDescription = "배경 이미지", + AsyncImage( + model = detail.roomImageUrl, + contentDescription = "모임방 배경 이미지", modifier = Modifier .align(Alignment.TopCenter) .fillMaxWidth(), @@ -331,7 +358,7 @@ fun GroupRoomRecruitContent( Text( text = detail.category, style = typography.info_m500_s12, - color = colors.SocialScience + color = hexToColor(detail.categoryColor) ) } } @@ -343,7 +370,8 @@ fun GroupRoomRecruitContent( author = detail.authorName, publisher = detail.publisher, description = detail.bookDescription, - imageUrl = detail.bookImageUrl + imageUrl = detail.bookImageUrl, + onClick = { onBookDetailClick(detail.isbn) } ) // 추천 모임방이 있을 때만 표시 @@ -363,7 +391,6 @@ fun GroupRoomRecruitContent( horizontalArrangement = Arrangement.spacedBy(20.dp) ) { items(detail.recommendRooms) { rec -> - // RecommendRoomResponse에서 데이터 추출 val daysLeft = DateUtils.extractDaysFromDeadline(rec.recruitEndDate) CardItemRoomSmall( title = rec.roomName, @@ -473,7 +500,7 @@ fun GroupRoomRecruitScreenPreview() { GroupRoomRecruitContent( uiState = GroupRoomRecruitUiState( isLoading = false, - roomDetail = com.texthip.thip.data.model.group.response.RoomRecruitingResponse( + roomDetail = RoomRecruitingResponse( isHost = false, isJoining = false, roomId = 1, @@ -484,6 +511,7 @@ fun GroupRoomRecruitScreenPreview() { progressEndDate = "2025.02.28", recruitEndDate = "D-5", category = "문학", + categoryColor = "#8B7CF6", roomDescription = "매트 헤이그의 미드나이트 라이브러리를 함께 읽으며 인생의 가능성과 선택에 대해 이야기해요. 각자의 삶에서 후회했던 순간들을 공유하고, 서로 위로하며 성장하는 시간을 가져보아요. 따뜻한 마음으로 서로의 이야기를 들어주실 분들과 함께하고 싶습니다.", memberCount = 18, recruitCount = 20, @@ -504,7 +532,7 @@ fun GroupRoomRecruitScreenPreview() { ), RecommendRoomResponse( roomId = 3, - roomImageUrl = "https://picsum.photos/300/400?rec2", + roomImageUrl = "https://picsum.photos/300/400?rec2", roomName = "✨ 철학 소설로 삶을 되돌아보기", memberCount = 8, recruitCount = 12, diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomUnlockScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomUnlockScreen.kt index f6cf68d8..6dbe96c1 100644 --- a/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomUnlockScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/group/room/screen/GroupRoomUnlockScreen.kt @@ -11,7 +11,9 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect 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 @@ -26,9 +28,11 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign 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.ui.common.forms.SingleDigitBox import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar +import com.texthip.thip.ui.group.room.viewmodel.GroupRoomUnlockViewModel import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography @@ -36,10 +40,12 @@ import kotlinx.coroutines.delay @Composable fun GroupRoomUnlockScreen( + roomId: Int = 0, + viewModel: GroupRoomUnlockViewModel = hiltViewModel(), onBackClick: () -> Unit = {}, - onPasswordComplete: (String) -> Unit = {}, - correctPassword: String = "1234" // 실제로는 외부에서 받아올 값 + onSuccessNavigation: () -> Unit = {} ) { + val uiState by viewModel.uiState.collectAsState() var password by remember { mutableStateOf(arrayOf("", "", "", "")) } var showError by remember { mutableStateOf(false) } val focusRequesters = remember { List(4) { FocusRequester() } } @@ -48,25 +54,43 @@ fun GroupRoomUnlockScreen( LaunchedEffect(password.toList()) { val fullPassword = password.joinToString("") if (fullPassword.length == 4 && password.all { it.length == 1 }) { - if (fullPassword == correctPassword) { - showError = false - onPasswordComplete(fullPassword) - } else { + viewModel.checkPassword(roomId, fullPassword) + } + } + + LaunchedEffect(uiState.passwordMatched) { + when (uiState.passwordMatched) { + true -> { + // 비밀번호 일치: 성공 콜백 호출하여 네비게이션 처리 + onSuccessNavigation() + } + false -> { showError = true - delay(1000) + delay(1000) // 사용자에게 에러 메시지를 보여줄 시간 password = arrayOf("", "", "", "") showError = false focusRequesters[0].requestFocus() + viewModel.resetPasswordState() // ViewModel 상태 초기화 } - } else { - showError = false + null -> { } } } + // 화면 진입 시 상태 초기화 및 키보드 자동 표시 LaunchedEffect(Unit) { + viewModel.initializeState() // 상태 완전 초기화 + delay(300) // 화면 전환 애니메이션 후 키보드 표시 focusRequesters[0].requestFocus() keyboardController?.show() } + + // 화면 종료 시 리소스 정리 + DisposableEffect(Unit) { + onDispose { + // 키보드 숨기기 + keyboardController?.hide() + } + } Box( modifier = Modifier @@ -123,7 +147,6 @@ fun GroupRoomUnlockScreen( focusRequesters[index - 1].requestFocus() } }, - containerColor = colors.DarkGrey50, borderColor = if (showError) colors.Red else Color.Transparent, modifier = Modifier .size(44.dp) @@ -158,9 +181,7 @@ fun GroupRoomUnlockScreenPreview() { ThipTheme { GroupRoomUnlockScreen( onBackClick = {}, - onPasswordComplete = { password -> - }, - correctPassword = "1234" + onSuccessNavigation = {} ) } } \ 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 index 8c3bd88b..d1fc7bb5 100644 --- 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 @@ -1,7 +1,7 @@ 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 +import com.texthip.thip.data.model.rooms.response.RoomRecruitingResponse data class GroupRoomRecruitUiState( val roomDetail: RoomRecruitingResponse? = null, @@ -12,7 +12,9 @@ data class GroupRoomRecruitUiState( val showDialog: Boolean = false, val dialogTitle: String = "", val dialogDescription: String = "", - val shouldNavigateToGroupScreen: Boolean = false + val shouldNavigateToGroupScreen: Boolean = false, + val shouldNavigateToRoomPlayingScreen: Boolean = false, + val roomId: Int? = null ) { 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 index 9012d0ba..1875c59e 100644 --- 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 @@ -1,14 +1,13 @@ 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.data.provider.StringResourceProvider +import com.texthip.thip.data.repository.RoomsRepository 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 @@ -17,8 +16,8 @@ import javax.inject.Inject @HiltViewModel class GroupRoomRecruitViewModel @Inject constructor( - private val repository: GroupRepository, - @param:ApplicationContext private val context: Context + private val repository: RoomsRepository, + private val stringResourceProvider: StringResourceProvider ) : ViewModel() { private val _uiState = MutableStateFlow(GroupRoomRecruitUiState()) @@ -36,18 +35,25 @@ class GroupRoomRecruitViewModel @Inject constructor( 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 - ) + if (data != null) { + val buttonType = when { + data.isHost -> GroupBottomButtonType.CLOSE + data.isJoining -> GroupBottomButtonType.CANCEL + else -> GroupBottomButtonType.JOIN + } + updateState { + it.copy( + roomDetail = data, + currentButtonType = buttonType, + isLoading = false + ) + } + } else { + updateState { + it.copy( + isLoading = false + ) + } } } .onFailure { error -> @@ -63,10 +69,10 @@ class GroupRoomRecruitViewModel @Inject constructor( repository.joinOrCancelRoom(roomId, RoomAction.JOIN.value) .onSuccess { updateState { it.copy(currentButtonType = GroupBottomButtonType.CANCEL) } - showToastMessage(context.getString(R.string.success_participation_complete)) + showToastMessage(stringResourceProvider.getString(R.string.success_participation_complete)) } .onFailure { error -> - showToastMessage(context.getString(R.string.error_participation_failed, error.message ?: "")) + showToastMessage(stringResourceProvider.getString(R.string.error_participation_failed, error.message ?: "")) } } } @@ -88,14 +94,14 @@ class GroupRoomRecruitViewModel @Inject constructor( updateState { it.copy( currentButtonType = GroupBottomButtonType.JOIN, - toastMessage = context.getString(R.string.success_participation_cancelled), + toastMessage = stringResourceProvider.getString(R.string.success_participation_cancelled), showToast = true, shouldNavigateToGroupScreen = true ) } } .onFailure { error -> - showToastMessage(context.getString(R.string.error_participation_cancel_failed)) + showToastMessage(stringResourceProvider.getString(R.string.error_participation_cancel_failed)) } } } @@ -111,8 +117,31 @@ class GroupRoomRecruitViewModel @Inject constructor( } pendingAction = { viewModelScope.launch { - // TODO: 실제 모집 마감 API 호출 - showToastMessage(context.getString(R.string.success_recruitment_closed)) + val currentRoomId = _uiState.value.roomDetail?.roomId + if (currentRoomId != null) { + repository.closeRoom(currentRoomId) + .onSuccess { response -> + showToastMessage(stringResourceProvider.getString(R.string.success_recruitment_closed)) + // 마감 성공 시 기록장 화면으로 이동 + updateState { + it.copy( + shouldNavigateToRoomPlayingScreen = true, + roomId = response.roomId + ) + } + } + .onFailure { throwable -> + // 에러 처리 + val errorMessage = when { + throwable.message?.contains("140008") == true -> stringResourceProvider.getString(R.string.error_room_close_permission) + throwable.message?.contains("100004") == true -> stringResourceProvider.getString(R.string.error_room_expired) + else -> throwable.message ?: stringResourceProvider.getString(R.string.error_room_close_failed) + } + showToastMessage(errorMessage) + } + } else { + showToastMessage(stringResourceProvider.getString(R.string.error_room_info_not_found)) + } } } } @@ -136,6 +165,15 @@ class GroupRoomRecruitViewModel @Inject constructor( updateState { it.copy(shouldNavigateToGroupScreen = false) } } + fun onNavigatedToRoomPlayingScreen() { + updateState { + it.copy( + shouldNavigateToRoomPlayingScreen = false, + roomId = null + ) + } + } + private fun showToastMessage(message: String) { updateState { it.copy( diff --git a/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomUnlockViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomUnlockViewModel.kt new file mode 100644 index 00000000..57609c81 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/group/room/viewmodel/GroupRoomUnlockViewModel.kt @@ -0,0 +1,83 @@ +package com.texthip.thip.ui.group.room.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.texthip.thip.data.repository.RoomsRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +data class GroupRoomUnlockUiState( + val isLoading: Boolean = false, + val passwordMatched: Boolean? = null, + val errorMessage: String? = null +) + +@HiltViewModel +class GroupRoomUnlockViewModel @Inject constructor( + private val roomsRepository: RoomsRepository +) : ViewModel() { + + private val _uiState = MutableStateFlow(GroupRoomUnlockUiState()) + val uiState = _uiState.asStateFlow() + + private var passwordCheckJob: Job? = null + + fun checkPassword(roomId: Int, password: String) { + // 이전 요청이 있다면 취소 + passwordCheckJob?.cancel() + + // 이미 요청 중이면 중복 실행 방지 + if (_uiState.value.isLoading) return + + passwordCheckJob = viewModelScope.launch { + _uiState.update { it.copy(isLoading = true) } + + roomsRepository.postParticipateSecreteRoom(roomId, password) + .onSuccess { response -> + // API 호출은 성공했고, 응답 바디의 matched 값으로 상태 업데이트 + _uiState.update { + it.copy( + isLoading = false, + passwordMatched = response.matched + ) + } + } + .onFailure { throwable -> + // API 호출 자체 실패 (네트워크 오류 등) + _uiState.update { + it.copy( + isLoading = false, + passwordMatched = false, // 실패 시 불일치로 간주 + errorMessage = throwable.message ?: "알 수 없는 오류가 발생했습니다." + ) + } + } + } + } + + fun resetPasswordState() { + _uiState.update { it.copy(passwordMatched = null, errorMessage = null) } + } + + /** + * 화면 진입 시 상태 완전 초기화 + * Screen에서 LaunchedEffect로 호출 + */ + fun initializeState() { + passwordCheckJob?.cancel() + passwordCheckJob = null + _uiState.value = GroupRoomUnlockUiState() + } + + override fun onCleared() { + super.onCleared() + // 진행 중인 작업 취소 및 리소스 정리 + passwordCheckJob?.cancel() + passwordCheckJob = null + } +} \ 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 76bf820f..8cd4047a 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 @@ -24,7 +24,9 @@ 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.RoomMainList +import com.texthip.thip.data.model.rooms.response.JoinedRoomResponse +import com.texthip.thip.data.model.rooms.response.RoomMainList +import com.texthip.thip.data.model.rooms.response.RoomMainResponse import com.texthip.thip.ui.common.buttons.FloatingButton import com.texthip.thip.ui.common.modal.ToastWithDate import com.texthip.thip.ui.common.topappbar.LogoTopAppBar @@ -190,21 +192,21 @@ fun PreviewGroupScreen() { uiState = GroupUiState( userName = "김독서", myJoinedRooms = listOf( - com.texthip.thip.data.model.group.response.JoinedRoomResponse( + JoinedRoomResponse( roomId = 1, bookImageUrl = "https://picsum.photos/300/400?joined1", roomTitle = "미드나이트 라이브러리", memberCount = 18, userPercentage = 75 ), - com.texthip.thip.data.model.group.response.JoinedRoomResponse( + JoinedRoomResponse( roomId = 2, bookImageUrl = "https://picsum.photos/300/400?joined2", roomTitle = "코스모스", memberCount = 25, userPercentage = 42 ), - com.texthip.thip.data.model.group.response.JoinedRoomResponse( + JoinedRoomResponse( roomId = 3, bookImageUrl = "https://picsum.photos/300/400?joined3", roomTitle = "사피엔스", @@ -214,7 +216,7 @@ fun PreviewGroupScreen() { ), roomMainList = RoomMainList( deadlineRoomList = listOf( - com.texthip.thip.data.model.group.response.RoomMainResponse( + RoomMainResponse( roomId = 4, bookImageUrl = "https://picsum.photos/300/400?deadline1", roomName = "🌙 미드나이트 라이브러리 함께읽기", @@ -222,7 +224,7 @@ fun PreviewGroupScreen() { memberCount = 18, deadlineDate = "D-2" ), - com.texthip.thip.data.model.group.response.RoomMainResponse( + RoomMainResponse( roomId = 5, bookImageUrl = "https://picsum.photos/300/400?deadline2", roomName = "📚 현대문학 깊이 탐구하기", @@ -230,7 +232,7 @@ fun PreviewGroupScreen() { memberCount = 12, deadlineDate = "D-3" ), - com.texthip.thip.data.model.group.response.RoomMainResponse( + RoomMainResponse( roomId = 6, bookImageUrl = "https://picsum.photos/300/400?deadline3", roomName = "🔬 과학책으로 세상 이해하기", @@ -240,7 +242,7 @@ fun PreviewGroupScreen() { ) ), popularRoomList = listOf( - com.texthip.thip.data.model.group.response.RoomMainResponse( + RoomMainResponse( roomId = 7, bookImageUrl = "https://picsum.photos/300/400?popular1", roomName = "✨ 철학 고전 함께 읽기", @@ -248,7 +250,7 @@ fun PreviewGroupScreen() { memberCount = 10, deadlineDate = "D-7" ), - com.texthip.thip.data.model.group.response.RoomMainResponse( + RoomMainResponse( roomId = 8, bookImageUrl = "https://picsum.photos/300/400?popular2", roomName = "🎨 예술과 문학의 만남", @@ -256,7 +258,7 @@ fun PreviewGroupScreen() { memberCount = 16, deadlineDate = "D-10" ), - com.texthip.thip.data.model.group.response.RoomMainResponse( + RoomMainResponse( roomId = 9, bookImageUrl = "https://picsum.photos/300/400?popular3", roomName = "💭 심리학 도서 탐험대", 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 50487fbf..2ed38932 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 @@ -1,6 +1,5 @@ package com.texthip.thip.ui.group.search.screen -import android.content.Context import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -10,6 +9,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding 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.mutableIntStateOf @@ -21,11 +21,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager 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.ui.common.buttons.FilterButton import com.texthip.thip.ui.common.forms.SearchBookTextField @@ -35,46 +35,18 @@ import com.texthip.thip.ui.group.myroom.mock.GroupCardItemRoomData import com.texthip.thip.ui.group.search.component.GroupEmptyResult import com.texthip.thip.ui.group.search.component.GroupFilteredSearchResult import com.texthip.thip.ui.group.search.component.GroupLiveSearchResult +import com.texthip.thip.ui.group.search.viewmodel.GroupSearchViewModel import com.texthip.thip.ui.theme.ThipTheme -import kotlinx.serialization.json.Json -import androidx.core.content.edit -import kotlinx.serialization.builtins.ListSerializer -import kotlinx.serialization.builtins.serializer @Composable fun GroupSearchScreen( modifier: Modifier = Modifier, roomList: List, onNavigateBack: () -> Unit = {}, - onRoomClick: (GroupCardItemRoomData) -> Unit = {} + onRoomClick: (GroupCardItemRoomData) -> Unit = {}, + viewModel: GroupSearchViewModel = hiltViewModel() ) { - val context = LocalContext.current - val sharedPrefs = remember { - context.getSharedPreferences("group_search_prefs", Context.MODE_PRIVATE) - } - - var recentSearches by remember { - mutableStateOf( - try { - val jsonString = sharedPrefs.getString("recent_searches", "[]") ?: "[]" - Json.decodeFromString>(jsonString) - } catch (e: Exception) { - emptyList() - } - ) - } - - fun saveRecentSearches(searches: List) { - try { - val jsonString = Json.encodeToString(ListSerializer(String.serializer()), searches) - sharedPrefs.edit { - putString("recent_searches", jsonString) - } - recentSearches = searches - } catch (e: Exception) { - recentSearches = emptyList() - } - } + val uiState by viewModel.uiState.collectAsState() var searchText by rememberSaveable { mutableStateOf("") } var isSearched by rememberSaveable { mutableStateOf(false) } var selectedGenreIndex by rememberSaveable { mutableIntStateOf(-1) } @@ -158,18 +130,17 @@ fun GroupSearchScreen( isSearched = false }, onSearch = { query -> - if (query.isNotBlank() && !recentSearches.contains(query)) { - val newSearches = listOf(query) + recentSearches.take(9) // 최대 10개 유지 - saveRecentSearches(newSearches) - } + // 검색 실행 isSearched = true selectedGenreIndex = -1 + // 최근 검색어 새로고침 (서버에서 자동으로 추가됨) + viewModel.refreshData() } ) Spacer(modifier = Modifier.height(16.dp)) when { - searchText.isBlank() && !isSearched && recentSearches.isEmpty() -> { + searchText.isBlank() && !isSearched && uiState.recentSearches.isEmpty() -> { GroupRecentSearch( recentSearches = emptyList(), onSearchClick = {}, @@ -177,16 +148,15 @@ fun GroupSearchScreen( ) } - searchText.isBlank() && !isSearched && recentSearches.isNotEmpty() -> { + searchText.isBlank() && !isSearched && uiState.recentSearches.isNotEmpty() -> { GroupRecentSearch( - recentSearches = recentSearches, + recentSearches = uiState.recentSearches.map { it.searchTerm }, onSearchClick = { keyword -> searchText = keyword isSearched = true }, onRemove = { keyword -> - val updatedSearches = recentSearches.filterNot { it == keyword } - saveRecentSearches(updatedSearches) + viewModel.deleteRecentSearchByKeyword(keyword) } ) } diff --git a/app/src/main/java/com/texthip/thip/ui/group/search/viewmodel/GroupSearchViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/search/viewmodel/GroupSearchViewModel.kt new file mode 100644 index 00000000..c5914b96 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/group/search/viewmodel/GroupSearchViewModel.kt @@ -0,0 +1,104 @@ +package com.texthip.thip.ui.group.search.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.texthip.thip.data.model.book.response.RecentSearchItem +import com.texthip.thip.data.repository.RecentSearchRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +data class GroupSearchUiState( + val recentSearches: List = emptyList(), + val isLoading: Boolean = false, + val error: String? = null +) + +@HiltViewModel +class GroupSearchViewModel @Inject constructor( + private val recentSearchRepository: RecentSearchRepository +) : ViewModel() { + + private val _uiState = MutableStateFlow(GroupSearchUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + // Map 기반 빠른 최근 검색어 관리 + private val recentSearchMap = mutableMapOf() + + init { + loadRecentSearches() + } + + private fun updateState(update: (GroupSearchUiState) -> GroupSearchUiState) { + _uiState.update(update) + } + + fun loadRecentSearches() { + viewModelScope.launch { + updateState { it.copy(isLoading = true) } + + recentSearchRepository.getRecentSearches("ROOM") + .onSuccess { response -> + response?.let { recentSearchResponse -> + // Map에 최근 검색어 저장 (빠른 검색을 위해) + recentSearchMap.clear() + recentSearchResponse.recentSearchList.forEach { item -> + recentSearchMap[item.searchTerm] = item + } + + updateState { + it.copy( + recentSearches = recentSearchResponse.recentSearchList, + isLoading = false, + error = null + ) + } + } ?: run { + updateState { + it.copy( + recentSearches = emptyList(), + isLoading = false, + error = null + ) + } + } + } + .onFailure { throwable -> + updateState { + it.copy( + recentSearches = emptyList(), + isLoading = false, + error = throwable.message ?: "최근 검색어를 불러오는 중 오류가 발생했습니다." + ) + } + } + } + } + + fun deleteRecentSearch(recentSearchId: Int) { + viewModelScope.launch { + recentSearchRepository.deleteRecentSearch(recentSearchId) + .onSuccess { + loadRecentSearches() // 삭제 성공 시 목록 새로고침 + } + .onFailure { + // 삭제 실패는 조용히 처리 + } + } + } + + /** 키워드로 빠른 최근 검색어 삭제 (Map 기반) */ + fun deleteRecentSearchByKeyword(keyword: String) { + recentSearchMap[keyword]?.let { recentSearchItem -> + deleteRecentSearch(recentSearchItem.recentSearchId) + } + } + + fun refreshData() { + loadRecentSearches() + } +} \ No newline at end of file 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 index 82f5ab41..9facd3c7 100644 --- 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 @@ -1,7 +1,7 @@ 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 +import com.texthip.thip.data.model.rooms.response.JoinedRoomResponse +import com.texthip.thip.data.model.rooms.response.RoomMainList data class GroupUiState( val myJoinedRooms: List = emptyList(), 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 861dc8c1..5615c50d 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 @@ -4,7 +4,7 @@ import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.texthip.thip.data.manager.Genre -import com.texthip.thip.data.repository.GroupRepository +import com.texthip.thip.data.repository.RoomsRepository import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.async @@ -17,7 +17,7 @@ import javax.inject.Inject @HiltViewModel class GroupViewModel @Inject constructor( - private val repository: GroupRepository, + private val repository: RoomsRepository, @param:ApplicationContext private val context: Context ) : ViewModel() { diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/extensions/GroupNavigationExtensions.kt b/app/src/main/java/com/texthip/thip/ui/navigator/extensions/GroupNavigationExtensions.kt index 75dbf907..7812cf71 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/extensions/GroupNavigationExtensions.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/extensions/GroupNavigationExtensions.kt @@ -56,6 +56,11 @@ fun NavHostController.navigateToGroupRecruit(roomId: Int) { navigate(GroupRoutes.Recruit(roomId)) } +// 비밀번호 입력 화면으로 이동 +fun NavHostController.navigateToGroupRoomUnlock(roomId: Int) { + navigate(GroupRoutes.RoomUnlock(roomId)) +} + // 추천 모임방으로 이동 (현재 화면을 대체) fun NavHostController.navigateToRecommendedGroupRecruit(roomId: Int) { navigate(GroupRoutes.Recruit(roomId)) { 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 8470680c..9cadf111 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 @@ -23,10 +23,13 @@ import com.texthip.thip.ui.group.note.viewmodel.GroupNoteViewModel 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.room.screen.GroupRoomUnlockScreen +import com.texthip.thip.ui.group.room.viewmodel.GroupRoomRecruitViewModel 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.navigator.extensions.navigateToAlarm +import com.texthip.thip.ui.navigator.extensions.navigateToBookDetail import com.texthip.thip.ui.navigator.extensions.navigateToGroupDone import com.texthip.thip.ui.navigator.extensions.navigateToGroupMakeRoom import com.texthip.thip.ui.navigator.extensions.navigateToGroupMy @@ -35,13 +38,15 @@ import com.texthip.thip.ui.navigator.extensions.navigateToGroupNoteCreate import com.texthip.thip.ui.navigator.extensions.navigateToGroupRecruit import com.texthip.thip.ui.navigator.extensions.navigateToGroupRoom import com.texthip.thip.ui.navigator.extensions.navigateToGroupRoomMates +import com.texthip.thip.ui.navigator.extensions.navigateToGroupRoomUnlock import com.texthip.thip.ui.navigator.extensions.navigateToGroupSearch import com.texthip.thip.ui.navigator.extensions.navigateToGroupVoteCreate import com.texthip.thip.ui.navigator.extensions.navigateToRecommendedGroupRecruit import com.texthip.thip.ui.navigator.routes.GroupRoutes import com.texthip.thip.ui.navigator.routes.MainTabRoutes -// Group +private const val PARTICIPATION_APPROVED_KEY = "participation_approved_key" + @SuppressLint("UnrememberedGetBackStackEntry") fun NavGraphBuilder.groupNavigation( navController: NavHostController, @@ -95,8 +100,10 @@ fun NavGraphBuilder.groupNavigation( onNavigateBack = { navigateBack() }, - onGroupCreated = { - navigateBack() + onGroupCreated = { roomId -> + navController.navigate(GroupRoutes.Recruit(roomId)) { + popUpTo { inclusive = true } + } } ) } @@ -121,8 +128,11 @@ fun NavGraphBuilder.groupNavigation( onNavigateBack = { navigateBack() }, - onGroupCreated = { - navigateBack() + onGroupCreated = { roomId -> + // 생성된 방의 모집 화면으로 이동하고 백스택 제거 + navController.navigateToGroupRecruit(roomId) + // 백스택에서 MakeRoomWithBook 화면 제거 + navController.popBackStack(inclusive = true) } ) } @@ -182,7 +192,19 @@ fun NavGraphBuilder.groupNavigation( composable { backStackEntry -> val route = backStackEntry.toRoute() val roomId = route.roomId - + val viewModel: GroupRoomRecruitViewModel = hiltViewModel() + + val participationApproved by backStackEntry.savedStateHandle + .getStateFlow(PARTICIPATION_APPROVED_KEY, false) + .collectAsState() + + LaunchedEffect(participationApproved) { + if (participationApproved) { + viewModel.onParticipationClick() + backStackEntry.savedStateHandle[PARTICIPATION_APPROVED_KEY] = false + } + } + GroupRoomRecruitScreen( roomId = roomId, onRecommendationClick = { recommendation -> @@ -195,6 +217,42 @@ fun NavGraphBuilder.groupNavigation( navController.popBackStack(MainTabRoutes.Group, false) }, onBackClick = { + // MakeRoom에서 바로 온 경우를 확인하여 Group 홈으로 이동 + val canGoBack = navController.previousBackStackEntry != null + if (canGoBack) { + navigateBack() + } else { + // 백스택이 비어있으면 Group 홈으로 이동 (방금 생성된 방의 경우) + navController.popBackStack(MainTabRoutes.Group, false) + } + }, + onBookDetailClick = { isbn -> + navController.navigateToBookDetail(isbn) + }, + onNavigateToPasswordScreen = { roomId -> + navController.navigateToGroupRoomUnlock(roomId) + }, + onNavigateToRoomPlayingScreen = { roomId -> + navController.navigateToGroupRoom(roomId) + } + ) + } + + // Group Room Unlock 화면 (비밀번호 입력) + composable { backStackEntry -> + val route = backStackEntry.toRoute() + val roomId = route.roomId + + GroupRoomUnlockScreen( + roomId = roomId, + onBackClick = { + navigateBack() + }, + onSuccessNavigation = { + // 비밀번호가 맞았다는 '신호'만 이전 화면에 전달 + navController.previousBackStackEntry + ?.savedStateHandle + ?.set(PARTICIPATION_APPROVED_KEY, true) navigateBack() } ) @@ -206,8 +264,7 @@ fun NavGraphBuilder.groupNavigation( val roomId = route.roomId GroupRoomScreen( -// roomId = roomId, - roomId = 1, + roomId = roomId, onBackClick = { navigateBack() }, diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/routes/GroupRoutes.kt b/app/src/main/java/com/texthip/thip/ui/navigator/routes/GroupRoutes.kt index 77357989..1cc8056f 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/routes/GroupRoutes.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/routes/GroupRoutes.kt @@ -27,6 +27,9 @@ sealed class GroupRoutes : Routes() { @Serializable data class Recruit(val roomId: Int) : GroupRoutes() + @Serializable + data class RoomUnlock(val roomId: Int) : GroupRoutes() + @Serializable data class Room(val roomId: Int) : GroupRoutes() diff --git a/app/src/main/java/com/texthip/thip/ui/search/viewmodel/BookDetailViewModel.kt b/app/src/main/java/com/texthip/thip/ui/search/viewmodel/BookDetailViewModel.kt index bd85f629..839cae5b 100644 --- a/app/src/main/java/com/texthip/thip/ui/search/viewmodel/BookDetailViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/search/viewmodel/BookDetailViewModel.kt @@ -2,7 +2,9 @@ package com.texthip.thip.ui.search.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.texthip.thip.R import com.texthip.thip.data.model.book.response.BookDetailResponse +import com.texthip.thip.data.provider.StringResourceProvider import com.texthip.thip.data.repository.BookRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -13,7 +15,8 @@ import javax.inject.Inject @HiltViewModel class BookDetailViewModel @Inject constructor( - private val bookRepository: BookRepository + private val bookRepository: BookRepository, + private val stringResourceProvider: StringResourceProvider ) : ViewModel() { private val _uiState = MutableStateFlow(BookDetailUiState()) @@ -34,7 +37,7 @@ class BookDetailViewModel @Inject constructor( .onFailure { exception -> _uiState.value = _uiState.value.copy( isLoading = false, - error = exception.message ?: "책 정보를 불러오는데 실패했습니다." + error = exception.message ?: stringResourceProvider.getString(R.string.error_book_detail_load_failed) ) } } @@ -59,7 +62,7 @@ class BookDetailViewModel @Inject constructor( .onFailure { exception -> _uiState.value = _uiState.value.copy( isSaving = false, - error = exception.message ?: "책 저장에 실패했습니다." + error = exception.message ?: stringResourceProvider.getString(R.string.error_book_save_failed) ) } } diff --git a/app/src/main/java/com/texthip/thip/ui/search/viewmodel/SearchBookGroupViewModel.kt b/app/src/main/java/com/texthip/thip/ui/search/viewmodel/SearchBookGroupViewModel.kt index 82eb2c77..5cc6ba34 100644 --- a/app/src/main/java/com/texthip/thip/ui/search/viewmodel/SearchBookGroupViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/search/viewmodel/SearchBookGroupViewModel.kt @@ -2,8 +2,10 @@ package com.texthip.thip.ui.search.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.texthip.thip.R import com.texthip.thip.data.model.book.response.BookDetailResponse import com.texthip.thip.data.model.book.response.RecruitingRoomItem +import com.texthip.thip.data.provider.StringResourceProvider import com.texthip.thip.data.repository.BookRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job @@ -15,7 +17,8 @@ import javax.inject.Inject @HiltViewModel class SearchBookGroupViewModel @Inject constructor( - private val bookRepository: BookRepository + private val bookRepository: BookRepository, + private val stringResourceProvider: StringResourceProvider ) : ViewModel() { private val _uiState = MutableStateFlow(SearchBookGroupUiState()) @@ -82,7 +85,7 @@ class SearchBookGroupViewModel @Inject constructor( isLoading = false, isLoadingMore = false, hasMore = false, // null 응답 시 더 이상 로드할 수 없음을 명시 - error = if (cursor == null) "모집중인 방 정보를 찾을 수 없습니다." else null + error = if (cursor == null) stringResourceProvider.getString(R.string.error_recruiting_rooms_not_found) else null ) } } @@ -90,7 +93,7 @@ class SearchBookGroupViewModel @Inject constructor( _uiState.value = _uiState.value.copy( isLoading = false, isLoadingMore = false, - error = exception.message ?: "모집중인 방을 불러오는데 실패했습니다." + error = exception.message ?: stringResourceProvider.getString(R.string.error_recruiting_rooms_load_failed) ) } } diff --git a/app/src/main/java/com/texthip/thip/ui/search/viewmodel/SearchBookViewModel.kt b/app/src/main/java/com/texthip/thip/ui/search/viewmodel/SearchBookViewModel.kt index 73c7b29a..5efbe973 100644 --- a/app/src/main/java/com/texthip/thip/ui/search/viewmodel/SearchBookViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/search/viewmodel/SearchBookViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.texthip.thip.data.model.book.response.RecentSearchItem import com.texthip.thip.data.repository.BookRepository +import com.texthip.thip.data.repository.RecentSearchRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -16,7 +17,8 @@ import javax.inject.Inject @HiltViewModel class SearchBookViewModel @Inject constructor( - private val bookRepository: BookRepository + private val bookRepository: BookRepository, + private val recentSearchRepository: RecentSearchRepository ) : ViewModel() { private val _uiState = MutableStateFlow(SearchBookUiState()) @@ -199,7 +201,7 @@ class SearchBookViewModel @Inject constructor( private fun loadRecentSearches() { viewModelScope.launch { - bookRepository.getRecentSearches() + recentSearchRepository.getRecentSearches("BOOK") .onSuccess { response -> response?.let { recentSearchResponse -> // Map에 최근 검색어 저장 (빠른 검색을 위해) @@ -221,7 +223,7 @@ class SearchBookViewModel @Inject constructor( fun deleteRecentSearch(recentSearchId: Int) { viewModelScope.launch { - bookRepository.deleteRecentSearch(recentSearchId) + recentSearchRepository.deleteRecentSearch(recentSearchId) .onSuccess { loadRecentSearches() // 삭제 성공 시 목록 새로고침 } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 39ec0b0d..b3c88fc1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -384,11 +384,25 @@ 네트워크 오류가 발생했습니다: %1$s - 참여가 완료되었습니다 + 모임방 참여가 완료되었어요! 모집 마감 후 활동이 시작돼요. 참여 처리 중 오류가 발생했습니다: %1$s 모임방 참여가 취소되었어요! 다른 방을 찾아보세요. 참여 취소 중 오류가 발생했습니다: %1$s - 모집이 마감되었습니다 + 독서메이트 모집을 성공적으로 마감했어요. + 방 모집 마감 권한이 없습니다. + 이미 모집기간이 만료된 방입니다. + 모집 마감 중 오류가 발생했습니다. + 방 정보를 찾을 수 없습니다. + + \n 모집중인 방 정보를 찾을 수 없습니다. + 모집중인 방을 불러오는데 실패했습니다. + + + 책 정보를 불러오는데 실패했습니다. + 책 저장에 실패했습니다. + + + 알 수 없는 오류가 발생했습니다. 과학/IT