diff --git a/app/src/main/java/com/keyme/app/ui/KeymeApp.kt b/app/src/main/java/com/keyme/app/ui/KeymeApp.kt index 3647358..5152416 100644 --- a/app/src/main/java/com/keyme/app/ui/KeymeApp.kt +++ b/app/src/main/java/com/keyme/app/ui/KeymeApp.kt @@ -18,8 +18,8 @@ import com.keyme.presentation.alarm.ui.AlarmDestination import com.keyme.presentation.alarm.ui.alarmGraph import com.keyme.presentation.designsystem.theme.KeymeTheme import com.keyme.presentation.feed.ui.feedGraph -import com.keyme.presentation.myprofile.ui.KeymeTestResultDetailDestination -import com.keyme.presentation.myprofile.ui.keymeTestResultDetailGraph +import com.keyme.presentation.myprofile.ui.KeymeQuestionResultDestination +import com.keyme.presentation.myprofile.ui.keymeQuestionResultGraph import com.keyme.presentation.myprofile.ui.myProfileGraph import com.keyme.presentation.nickname.NicknameDestination import com.keyme.presentation.nickname.nicknameGraph @@ -62,9 +62,14 @@ fun KeymeApp() { }, ) myProfileGraph( - navigateToDetail = { appState.navigate(KeymeTestResultDetailDestination) }, + navigateToQuestionResult = { question -> + appState.navigate( + KeymeQuestionResultDestination, + question.questionId, + ) + }, nestedGraphs = { - keymeTestResultDetailGraph(onBackClick = appState::onBackClick) + keymeQuestionResultGraph(onBackClick = appState::onBackClick) }, ) } diff --git a/app/src/main/java/com/keyme/app/ui/KeymeAppState.kt b/app/src/main/java/com/keyme/app/ui/KeymeAppState.kt index 662fc6e..67c8a4f 100644 --- a/app/src/main/java/com/keyme/app/ui/KeymeAppState.kt +++ b/app/src/main/java/com/keyme/app/ui/KeymeAppState.kt @@ -66,6 +66,18 @@ class KeymeAppState( } } + fun navigate(destination: KeymeNavigationDestination, args: Any) { + if (destination is TopLevelDestination) { + navController.navigate("${destination.route}/$args") { + popUpTo(navController.graph.findStartDestination().id) { saveState = true } + launchSingleTop = true + restoreState = true + } + } else { + navController.navigate("${destination.route}/$args") + } + } + fun onBackClick() { navController.popBackStack() } diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 24758ec..d953378 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -31,7 +31,6 @@ object Dependencies { private val composeNavigation = "androidx.navigation:navigation-compose:${Versions.COMPOSE_NAVIGATION}" private val runtime_compose = "androidx.lifecycle:lifecycle-runtime-compose:2.6.1" - // ViewModel private val viewModelKtx = "androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.LIFECYCLE}" @@ -44,6 +43,11 @@ object Dependencies { // Image private val glide = "com.github.bumptech.glide:glide:${Versions.GLIDE}" + // Coil + private val coil = "io.coil-kt:coil:${Versions.coil}" + private val coil_compose = "io.coil-kt:coil-compose:${Versions.coil}" + private val coil_gif = "io.coil-kt:coil-gif:${Versions.coil}" + // Network private val retrofit = "com.squareup.retrofit2:retrofit:${Versions.RETROFIT}" private val gsonConverter = "com.squareup.retrofit2:converter-gson:${Versions.RETROFIT}" @@ -61,6 +65,11 @@ object Dependencies { private val hiltCompiler = "com.google.dagger:hilt-android-compiler:${Versions.HILT}" private val javaXInject = "javax.inject:javax.inject:${Versions.JAVA_X_INJECT}" + // Paging + private val paging_runtime = "androidx.paging:paging-runtime-ktx:${Versions.paging}" + private val paging_common = "androidx.paging:paging-common-ktx:${Versions.paging}" + private val paging_compose = "androidx.paging:paging-compose:${Versions.paging_compose}" + // Test private val junit = "junit:junit:${Versions.JUNIT}" @@ -117,6 +126,9 @@ object Dependencies { fun DependencyHandler.setImageDependencies() { implementation(glide) + implementation(coil) + implementation(coil_compose) + implementation(coil_gif) } fun DependencyHandler.setNetworkDependencies() { @@ -147,6 +159,12 @@ object Dependencies { testImplementation(junit) } + fun DependencyHandler.setPagingDependencies() { + implementation(paging_compose) + implementation(paging_compose) + implementation(paging_runtime) + } + fun DependencyHandler.setAndroidTestDependencies() { androidTestImplementation(androidJunit) androidTestImplementation(espressoCore) diff --git a/buildSrc/src/main/java/Versions.kt b/buildSrc/src/main/java/Versions.kt index c85095c..c612192 100644 --- a/buildSrc/src/main/java/Versions.kt +++ b/buildSrc/src/main/java/Versions.kt @@ -32,6 +32,9 @@ object Versions { // Image const val GLIDE = "4.15.1" + // Coil + const val coil = "2.4.0" + // Network const val RETROFIT = "2.9.0" const val OKHTTP_INTERCEPTOR = "4.10.0" @@ -43,6 +46,10 @@ object Versions { const val HILT = "2.44" const val JAVA_X_INJECT = "1" + // Paging + const val paging = "3.1.1" + const val paging_compose = "3.2.0" + // Test const val JUNIT = "4.13.2" diff --git a/data/src/main/java/com/keyme/data/remote/api/KeymeApi.kt b/data/src/main/java/com/keyme/data/remote/api/KeymeApi.kt index 8ea6954..51c4c36 100644 --- a/data/src/main/java/com/keyme/data/remote/api/KeymeApi.kt +++ b/data/src/main/java/com/keyme/data/remote/api/KeymeApi.kt @@ -1,12 +1,17 @@ package com.keyme.data.remote.api import com.keyme.domain.entity.request.SignInRequest +import com.keyme.domain.entity.response.MemberStatistics +import com.keyme.domain.entity.response.MemberStatisticsResponse +import com.keyme.domain.entity.response.QuestionStatisticsResponse import com.keyme.domain.entity.response.SignInResponse +import com.keyme.domain.entity.response.SolvedScoreListResponse import com.keyme.domain.entity.response.keymetest.KeymeTestResultStatisticsResponse import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST import retrofit2.http.Path +import retrofit2.http.Query interface KeymeApi { @@ -19,4 +24,30 @@ interface KeymeApi { suspend fun getKeymeTestResultStatistics( @Path("id") questionId: String, ): KeymeTestResultStatisticsResponse + + @GET("members/{memberId}/statistics") + suspend fun getMemberStatistics( + @Path("memberId") memberId: String, + @Query("type") type: MemberStatistics.StatisticsType, + ): MemberStatisticsResponse + +// @GET("questions/{id}/score") +// suspend fun getMyQuestionScore( +// @Path("id") id: String, +// @Query("ownerId") ownerId: String, +// ): MyQuestionScoreResponse + + @GET("questions/{id}/statistics") + suspend fun getQuestionStatistics( + @Path("id") id: String, + @Query("ownerId") ownerId: Int, + ): QuestionStatisticsResponse + + @GET("questions/{id}/solved-scores") + suspend fun getSolvedScoreList( + @Query("cursor") cursor: Int?, + @Path("id") id: String, + @Query("limit") limit: Int = 20, + @Query("ownerId") ownerId: Int, + ): SolvedScoreListResponse } diff --git a/data/src/main/java/com/keyme/data/remote/di/ApiModule.kt b/data/src/main/java/com/keyme/data/remote/di/ApiModule.kt index 7ccfece..85b8454 100644 --- a/data/src/main/java/com/keyme/data/remote/di/ApiModule.kt +++ b/data/src/main/java/com/keyme/data/remote/di/ApiModule.kt @@ -1,6 +1,8 @@ package com.keyme.data.remote.di +import com.keyme.data.BuildConfig import com.keyme.data.remote.api.KeymeApi +import com.keyme.domain.usecase.GetMyMemberTokenUseCase import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -18,30 +20,39 @@ class ApiModule { @Provides @Singleton - fun provideKeymeApi(): KeymeApi { - return getRetrofit().create() + fun provideKeymeApi(getMyMemberTokenUseCase: GetMyMemberTokenUseCase): KeymeApi { + return getRetrofit(getMyMemberTokenUseCase()).create() } - private fun getRetrofit(): Retrofit { + private fun getRetrofit(memberToken: String): Retrofit { return Retrofit.Builder() .baseUrl("https://api.keyme.space") - .client(getOkHttpClient()) + .client(getOkHttpClient(memberToken)) .addConverterFactory(GsonConverterFactory.create()) .build() } - private fun getOkHttpClient(): OkHttpClient { - return OkHttpClient.Builder() - .addInterceptor( + private fun getOkHttpClient(memberToken: String): OkHttpClient { + val builder = OkHttpClient.Builder() + + builder.addInterceptor { chain -> + val origin = chain.request() + chain.request().newBuilder() + .header("Content-Type", "application/json;charset=UTF-8") + .header("Authorization", "Bearer $memberToken") + .method(origin.method, origin.body) + .build() + .let(chain::proceed) + } + + if (BuildConfig.DEBUG) { + builder.addInterceptor( HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }, ) - .addInterceptor { chain -> - chain.request().newBuilder() - .build() - .let(chain::proceed) - } - .build() + } + + return builder.build() } } diff --git a/data/src/main/java/com/keyme/data/remote/di/RepositoryModule.kt b/data/src/main/java/com/keyme/data/remote/di/RepositoryModule.kt index 796d740..8f2c0f2 100644 --- a/data/src/main/java/com/keyme/data/remote/di/RepositoryModule.kt +++ b/data/src/main/java/com/keyme/data/remote/di/RepositoryModule.kt @@ -1,8 +1,12 @@ package com.keyme.data.remote.di +import com.keyme.data.remote.repositoryimpl.MemberRepositoryImpl +import com.keyme.data.remote.repositoryimpl.QuestionRepositoryImpl import com.keyme.data.remote.repositoryimpl.ResultCircleRepositoryImpl import com.keyme.data.remote.repositoryimpl.SignInRepositoryImpl import com.keyme.data.remote.repositoryimpl.keymetest.KeymeTestResultRepositoryImpl +import com.keyme.domain.repository.MemberRepository +import com.keyme.domain.repository.QuestionRepository import com.keyme.domain.repository.ResultCircleRepository import com.keyme.domain.repository.SignInRepository import com.keyme.domain.repository.keymetest.KeymeTestResultRepository @@ -23,4 +27,10 @@ abstract class RepositoryModule { @Binds abstract fun bindKeymeTestResultRepository(impl: KeymeTestResultRepositoryImpl): KeymeTestResultRepository + + @Binds + abstract fun bindMemberRepository(impl: MemberRepositoryImpl): MemberRepository + + @Binds + abstract fun bindQuestionRepository(impl: QuestionRepositoryImpl): QuestionRepository } diff --git a/data/src/main/java/com/keyme/data/remote/repositoryimpl/MemberRepositoryImpl.kt b/data/src/main/java/com/keyme/data/remote/repositoryimpl/MemberRepositoryImpl.kt new file mode 100644 index 0000000..4d2c7e9 --- /dev/null +++ b/data/src/main/java/com/keyme/data/remote/repositoryimpl/MemberRepositoryImpl.kt @@ -0,0 +1,18 @@ +package com.keyme.data.remote.repositoryimpl + +import com.keyme.data.remote.api.KeymeApi +import com.keyme.domain.entity.response.MemberStatistics +import com.keyme.domain.entity.response.MemberStatisticsResponse +import com.keyme.domain.repository.MemberRepository +import javax.inject.Inject + +class MemberRepositoryImpl @Inject constructor( + private val keymeApi: KeymeApi, +) : MemberRepository { + override suspend fun getStatistics( + memberId: String, + type: MemberStatistics.StatisticsType, + ): MemberStatisticsResponse { + return keymeApi.getMemberStatistics(memberId = memberId, type = type) + } +} diff --git a/data/src/main/java/com/keyme/data/remote/repositoryimpl/QuestionRepositoryImpl.kt b/data/src/main/java/com/keyme/data/remote/repositoryimpl/QuestionRepositoryImpl.kt new file mode 100644 index 0000000..fb9e952 --- /dev/null +++ b/data/src/main/java/com/keyme/data/remote/repositoryimpl/QuestionRepositoryImpl.kt @@ -0,0 +1,24 @@ +package com.keyme.data.remote.repositoryimpl + +import com.keyme.data.remote.api.KeymeApi +import com.keyme.domain.entity.response.QuestionStatisticsResponse +import com.keyme.domain.entity.response.SolvedScoreListResponse +import com.keyme.domain.repository.QuestionRepository +import javax.inject.Inject + +class QuestionRepositoryImpl @Inject constructor( + private val keymeApi: KeymeApi, +) : QuestionRepository { + override suspend fun getStatistics(questionId: String): QuestionStatisticsResponse { + return keymeApi.getQuestionStatistics(id = questionId, ownerId = 1) + } + + override suspend fun getSolvedScoreList( + cursor: Int?, + questionId: String, + limit: Int, + ownerId: Int, + ): SolvedScoreListResponse { + return keymeApi.getSolvedScoreList(cursor, questionId, limit, ownerId) + } +} diff --git a/domain/src/main/java/com/keyme/domain/entity/member/Member.kt b/domain/src/main/java/com/keyme/domain/entity/member/Member.kt new file mode 100644 index 0000000..573cb82 --- /dev/null +++ b/domain/src/main/java/com/keyme/domain/entity/member/Member.kt @@ -0,0 +1,13 @@ +package com.keyme.domain.entity.member + +data class Member( + val friendCode: String, + val id: Int, + val nickname: String, + val profileImage: String, + val profileThumbnail: String, +) { + companion object { + val EMPTY = Member(friendCode = "", id = 0, nickname = "", profileImage = "", profileThumbnail = "") + } +} diff --git a/domain/src/main/java/com/keyme/domain/entity/response/Category.kt b/domain/src/main/java/com/keyme/domain/entity/response/Category.kt new file mode 100644 index 0000000..e6f5463 --- /dev/null +++ b/domain/src/main/java/com/keyme/domain/entity/response/Category.kt @@ -0,0 +1,7 @@ +package com.keyme.domain.entity.response + +data class Category( + val color: String, + val iconUrl: String, + val name: String, +) diff --git a/domain/src/main/java/com/keyme/domain/entity/response/Coordinate.kt b/domain/src/main/java/com/keyme/domain/entity/response/Coordinate.kt new file mode 100644 index 0000000..370b3be --- /dev/null +++ b/domain/src/main/java/com/keyme/domain/entity/response/Coordinate.kt @@ -0,0 +1,7 @@ +package com.keyme.domain.entity.response + +data class Coordinate( + val r: Double, + val x: Double, + val y: Double, +) diff --git a/domain/src/main/java/com/keyme/domain/entity/response/MemberStatisticsResponse.kt b/domain/src/main/java/com/keyme/domain/entity/response/MemberStatisticsResponse.kt new file mode 100644 index 0000000..06eb964 --- /dev/null +++ b/domain/src/main/java/com/keyme/domain/entity/response/MemberStatisticsResponse.kt @@ -0,0 +1,14 @@ +package com.keyme.domain.entity.response + +import com.keyme.domain.entity.BaseResponse + +class MemberStatisticsResponse : BaseResponse() + +data class MemberStatistics( + val memberId: Int = 0, + val results: List = listOf(), +) { + enum class StatisticsType { + SIMILAR, DIFFERENT + } +} diff --git a/domain/src/main/java/com/keyme/domain/entity/response/MyQuestionScoreResponse.kt b/domain/src/main/java/com/keyme/domain/entity/response/MyQuestionScoreResponse.kt new file mode 100644 index 0000000..6cc709d --- /dev/null +++ b/domain/src/main/java/com/keyme/domain/entity/response/MyQuestionScoreResponse.kt @@ -0,0 +1,3 @@ +package com.keyme.domain.entity.response + +// class MyQuestionScoreResponse: BaseResponse<>() diff --git a/domain/src/main/java/com/keyme/domain/entity/response/QuestionStatisticsResponse.kt b/domain/src/main/java/com/keyme/domain/entity/response/QuestionStatisticsResponse.kt new file mode 100644 index 0000000..bd28bb6 --- /dev/null +++ b/domain/src/main/java/com/keyme/domain/entity/response/QuestionStatisticsResponse.kt @@ -0,0 +1,13 @@ +package com.keyme.domain.entity.response + +import com.keyme.domain.entity.BaseResponse + +class QuestionStatisticsResponse : BaseResponse() + +data class Question( + val avgScore: Int, + val category: Category, + val keyword: String, + val questionId: Int, + val title: String, +) diff --git a/domain/src/main/java/com/keyme/domain/entity/response/Result.kt b/domain/src/main/java/com/keyme/domain/entity/response/Result.kt new file mode 100644 index 0000000..1674c56 --- /dev/null +++ b/domain/src/main/java/com/keyme/domain/entity/response/Result.kt @@ -0,0 +1,6 @@ +package com.keyme.domain.entity.response + +data class Result( + val coordinate: Coordinate, + val question: Question, +) diff --git a/domain/src/main/java/com/keyme/domain/entity/response/SolvedScoreListResponse.kt b/domain/src/main/java/com/keyme/domain/entity/response/SolvedScoreListResponse.kt new file mode 100644 index 0000000..d721023 --- /dev/null +++ b/domain/src/main/java/com/keyme/domain/entity/response/SolvedScoreListResponse.kt @@ -0,0 +1,15 @@ +package com.keyme.domain.entity.response + +import com.keyme.domain.entity.BaseResponse + +class SolvedScoreListResponse : BaseResponse() + +data class SolvedScoreList( + val count: Int, + val results: List, +) + +data class SolvedScore( + val createAt: String, + val score: Int, +) diff --git a/domain/src/main/java/com/keyme/domain/entity/response/keymetest/KeymeTestResultStatistics.kt b/domain/src/main/java/com/keyme/domain/entity/response/keymetest/KeymeTestResultStatistics.kt index 71d4e8a..ef55121 100644 --- a/domain/src/main/java/com/keyme/domain/entity/response/keymetest/KeymeTestResultStatistics.kt +++ b/domain/src/main/java/com/keyme/domain/entity/response/keymetest/KeymeTestResultStatistics.kt @@ -5,9 +5,9 @@ import com.keyme.domain.entity.BaseResponse class KeymeTestResultStatisticsResponse : BaseResponse() data class KeymeTestResultStatistics( - val averageRate: Int, - val questionsStatistics: List, - val solvedCount: Int, + val averageRate: Int = 0, + val questionsStatistics: List = listOf(), + val solvedCount: Int = 0, ) data class Category( diff --git a/domain/src/main/java/com/keyme/domain/repository/MemberRepository.kt b/domain/src/main/java/com/keyme/domain/repository/MemberRepository.kt new file mode 100644 index 0000000..e65fcc7 --- /dev/null +++ b/domain/src/main/java/com/keyme/domain/repository/MemberRepository.kt @@ -0,0 +1,12 @@ +package com.keyme.domain.repository + +import com.keyme.domain.entity.response.MemberStatistics +import com.keyme.domain.entity.response.MemberStatisticsResponse + +interface MemberRepository { + + suspend fun getStatistics( + memberId: String, + type: MemberStatistics.StatisticsType, + ): MemberStatisticsResponse +} diff --git a/domain/src/main/java/com/keyme/domain/repository/QuestionRepository.kt b/domain/src/main/java/com/keyme/domain/repository/QuestionRepository.kt new file mode 100644 index 0000000..f722bc7 --- /dev/null +++ b/domain/src/main/java/com/keyme/domain/repository/QuestionRepository.kt @@ -0,0 +1,12 @@ +package com.keyme.domain.repository + +import com.keyme.domain.entity.response.QuestionStatisticsResponse +import com.keyme.domain.entity.response.SolvedScoreListResponse + +interface QuestionRepository { +// suspend fun getMyScore(ownerId: String): MyQuestionScoreResponse + + suspend fun getStatistics(questionId: String): QuestionStatisticsResponse + + suspend fun getSolvedScoreList(cursor: Int?, questionId: String, limit: Int = 20, ownerId: Int): SolvedScoreListResponse +} diff --git a/domain/src/main/java/com/keyme/domain/usecase/GetMyCharacterUseCase.kt b/domain/src/main/java/com/keyme/domain/usecase/GetMyCharacterUseCase.kt new file mode 100644 index 0000000..25a994d --- /dev/null +++ b/domain/src/main/java/com/keyme/domain/usecase/GetMyCharacterUseCase.kt @@ -0,0 +1,10 @@ +package com.keyme.domain.usecase + +import com.keyme.domain.entity.member.Member +import javax.inject.Inject + +class GetMyCharacterUseCase @Inject constructor() { + operator fun invoke(): Member { + return Member(friendCode = "", id = 1, nickname = "키미미미", profileImage = "", profileThumbnail = "") + } +} diff --git a/domain/src/main/java/com/keyme/domain/usecase/GetMyMemberTokenUseCase.kt b/domain/src/main/java/com/keyme/domain/usecase/GetMyMemberTokenUseCase.kt new file mode 100644 index 0000000..3ba0e6f --- /dev/null +++ b/domain/src/main/java/com/keyme/domain/usecase/GetMyMemberTokenUseCase.kt @@ -0,0 +1,10 @@ +package com.keyme.domain.usecase + +import javax.inject.Inject + +class GetMyMemberTokenUseCase @Inject constructor() { + + operator fun invoke(): String { + return "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhY2Nlc3NUb2tlbiIsImlhdCI6MTY5MTg0MjM1NiwiZXhwIjoxNjk0NDM0MzU2LCJtZW1iZXJJZCI6Miwicm9sZSI6IlJPTEVfVVNFUiJ9.bLUl_ObvXr2pkLGNBZYWbJgLZLo3P0xB2pawckRGYZM" + } +} diff --git a/domain/src/main/java/com/keyme/domain/usecase/GetMyStatisticsUseCase.kt b/domain/src/main/java/com/keyme/domain/usecase/GetMyStatisticsUseCase.kt new file mode 100644 index 0000000..b290428 --- /dev/null +++ b/domain/src/main/java/com/keyme/domain/usecase/GetMyStatisticsUseCase.kt @@ -0,0 +1,13 @@ +package com.keyme.domain.usecase + +import com.keyme.domain.entity.apiResult +import com.keyme.domain.entity.response.MemberStatistics +import com.keyme.domain.repository.MemberRepository +import javax.inject.Inject + +class GetMyStatisticsUseCase @Inject constructor( + private val memberRepository: MemberRepository, +) { + suspend operator fun invoke(type: MemberStatistics.StatisticsType) = + apiResult { memberRepository.getStatistics(memberId = "1", type = type) } +} diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts index 5e07ecb..520bbf6 100644 --- a/presentation/build.gradle.kts +++ b/presentation/build.gradle.kts @@ -8,6 +8,7 @@ import Dependencies.setKakaoSignInDependencies import Dependencies.setKotlinStdLibDependencies import Dependencies.setLoggerDependencies import Dependencies.setLottieDependencies +import Dependencies.setPagingDependencies import Dependencies.setTestDependencies import Dependencies.setViewModelDependencies @@ -61,6 +62,7 @@ dependencies { setImageDependencies() setHiltDependencies() + setPagingDependencies() setTestDependencies() setAndroidTestDependencies() diff --git a/presentation/src/main/java/com/keyme/presentation/BaseViewModel.kt b/presentation/src/main/java/com/keyme/presentation/BaseViewModel.kt index 849201b..55f46f9 100644 --- a/presentation/src/main/java/com/keyme/presentation/BaseViewModel.kt +++ b/presentation/src/main/java/com/keyme/presentation/BaseViewModel.kt @@ -21,10 +21,12 @@ abstract class BaseViewModel : ViewModel() { @Inject lateinit var uiEventManager: UiEventManager - fun apiCall(apiRequest: suspend () -> ApiResult, action: (T) -> Unit) { + fun apiCall(apiRequest: suspend () -> ApiResult, action: suspend (T) -> Unit) { baseViewModelScope.launch { apiRequest().onSuccess { - action(it) + baseViewModelScope.launch { + action(it) + } }.onApiError { code, message -> baseViewModelScope.launch { uiEventManager.onEvent(UiEvent.Toast("($code) $message")) diff --git a/presentation/src/main/java/com/keyme/presentation/designsystem/component/BottomSheetHandle.kt b/presentation/src/main/java/com/keyme/presentation/designsystem/component/BottomSheetHandle.kt new file mode 100644 index 0000000..fef9aac --- /dev/null +++ b/presentation/src/main/java/com/keyme/presentation/designsystem/component/BottomSheetHandle.kt @@ -0,0 +1,37 @@ +package com.keyme.presentation.designsystem.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +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.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +@Composable +fun BottomSheetHandle(modifier: Modifier) { + Box( + modifier = modifier.bottomSheetHandle(), + contentAlignment = Alignment.Center, + ) { + Box( + modifier = Modifier + .width(40.dp) + .height(4.dp) + .background(color = Color(0x4DFFFFFF), shape = RoundedCornerShape(size = 2.dp)), + ) + } +} + +@Composable +fun Modifier.bottomSheetHandle() = composed { + Modifier + .fillMaxWidth() + .padding(14.dp) +} diff --git a/presentation/src/main/java/com/keyme/presentation/myprofile/KeymeTestResultDetailViewModel.kt b/presentation/src/main/java/com/keyme/presentation/myprofile/KeymeQuestionResultViewModel.kt similarity index 71% rename from presentation/src/main/java/com/keyme/presentation/myprofile/KeymeTestResultDetailViewModel.kt rename to presentation/src/main/java/com/keyme/presentation/myprofile/KeymeQuestionResultViewModel.kt index ff68fd8..4d06656 100644 --- a/presentation/src/main/java/com/keyme/presentation/myprofile/KeymeTestResultDetailViewModel.kt +++ b/presentation/src/main/java/com/keyme/presentation/myprofile/KeymeQuestionResultViewModel.kt @@ -5,24 +5,27 @@ import com.keyme.domain.entity.onSuccess import com.keyme.domain.entity.response.keymetest.KeymeTestResultStatistics import com.keyme.domain.usecase.keymetest.GetKeymeTestResultStatisticsUseCase import com.keyme.presentation.BaseViewModel -import com.keyme.presentation.myprofile.ui.KeymeTestResultDetailDestination +import com.keyme.presentation.myprofile.ui.KeymeQuestionResultDestination import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @HiltViewModel -class KeymeTestResultDetailViewModel @Inject constructor( +class KeymeQuestionResultViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val getKeymeTestResultStatistics: GetKeymeTestResultStatisticsUseCase, ) : BaseViewModel() { - private val questionId: String? = savedStateHandle[KeymeTestResultDetailDestination.Argument.questionIdName] + private val questionId: String? = savedStateHandle[KeymeQuestionResultDestination.Argument.questionIdName] - private val _statisticsState = MutableStateFlow(null) + private val _statisticsState = MutableStateFlow(KeymeTestResultStatistics()) val statisticsState = _statisticsState.asStateFlow() init { + Timber.d("questionId: $questionId") + baseViewModelScope.launch { questionId?.let { getKeymeTestResultStatistics(questionId).onSuccess { diff --git a/presentation/src/main/java/com/keyme/presentation/myprofile/MyProfileViewModel.kt b/presentation/src/main/java/com/keyme/presentation/myprofile/MyProfileViewModel.kt index 2ee290a..e62a8e4 100644 --- a/presentation/src/main/java/com/keyme/presentation/myprofile/MyProfileViewModel.kt +++ b/presentation/src/main/java/com/keyme/presentation/myprofile/MyProfileViewModel.kt @@ -1,33 +1,44 @@ package com.keyme.presentation.myprofile -import com.keyme.domain.entity.onFailure -import com.keyme.domain.entity.onSuccess -import com.keyme.domain.entity.response.Circle -import com.keyme.domain.usecase.GetResultCircleUseCase +import com.keyme.domain.entity.member.Member +import com.keyme.domain.entity.response.MemberStatistics +import com.keyme.domain.usecase.GetMyCharacterUseCase +import com.keyme.domain.usecase.GetMyStatisticsUseCase import com.keyme.presentation.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class MyProfileViewModel @Inject constructor( - private val resultCircleUseCase: GetResultCircleUseCase, + private val getMyCharacterUseCase: GetMyCharacterUseCase, + private val getMyStatisticsUseCase: GetMyStatisticsUseCase, ) : BaseViewModel() { - private val _resultCircleState = MutableStateFlow>(listOf()) - val resultCircleState = _resultCircleState.asStateFlow() + private val _mySimilarStatisticsState = MutableStateFlow(MemberStatistics()) + val mySimilarStatisticsState = _mySimilarStatisticsState.asStateFlow() + + private val _myDifferentStatisticsState = MutableStateFlow(MemberStatistics()) + val myDifferentStatisticsState = _myDifferentStatisticsState.asStateFlow() + + private val _myCharacterState = MutableStateFlow(Member.EMPTY) + val myCharacterState = _myCharacterState.asStateFlow() init { - getResultCircle() + loadMyStatistics() + loadMyCharacter() } - private fun getResultCircle() { - baseViewModelScope.launch { - resultCircleUseCase().onSuccess { - _resultCircleState.value = it - }.onFailure { - } + private fun loadMyCharacter() { + _myCharacterState.value = getMyCharacterUseCase() + } + + private fun loadMyStatistics() { + apiCall(apiRequest = { getMyStatisticsUseCase.invoke(type = MemberStatistics.StatisticsType.SIMILAR) }) { + _mySimilarStatisticsState.value = it + } + apiCall(apiRequest = { getMyStatisticsUseCase.invoke(type = MemberStatistics.StatisticsType.DIFFERENT) }) { + _myDifferentStatisticsState.value = it } } } diff --git a/presentation/src/main/java/com/keyme/presentation/myprofile/chart/BubbleChartState.kt b/presentation/src/main/java/com/keyme/presentation/myprofile/chart/BubbleChartState.kt index 1ba5ff1..848fce8 100644 --- a/presentation/src/main/java/com/keyme/presentation/myprofile/chart/BubbleChartState.kt +++ b/presentation/src/main/java/com/keyme/presentation/myprofile/chart/BubbleChartState.kt @@ -1,140 +1,64 @@ package com.keyme.presentation.myprofile.chart -import android.graphics.Bitmap -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.center -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.compose.ui.viewinterop.AndroidView -import androidx.core.view.drawToBitmap -import com.keyme.domain.entity.response.Circle +import com.keyme.domain.entity.response.Question +import com.keyme.domain.entity.response.Result import com.keyme.presentation.utils.scale -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch import timber.log.Timber @Composable fun rememberBubbleChartState( - circles: List, + results: List, containerSize: Size, - onBubbleClick: () -> Unit, + onBubbleClick: (BubbleItem) -> Unit, ): BubbleChartState { - val coroutineScope = rememberCoroutineScope() - return remember(Unit) { - BubbleChartState(coroutineScope, circles, containerSize, onBubbleClick) + val density = LocalDensity.current.density + return remember(results, containerSize) { + BubbleChartState(density, results, containerSize, onBubbleClick) } } -enum class BubbleChartInitialState { - Init, Loading, Finish; - - fun isInit() = this == Init - - fun isFinish() = this == Finish -} - class BubbleChartState( - private val coroutineScope: CoroutineScope, - private val circles: List, + private val density: Float, + private val results: List, private val containerSize: Size, - private val onBubbleClick: () -> Unit, + private val onBubbleClick: (BubbleItem) -> Unit, ) { - val scale = containerSize.width.toInt() - val bubbleRectList = circles.map { circle -> - Rect( + private val scale = containerSize.width.toInt() + val bubbleItems = results.map { it.toBubbleItem() } + + private fun Result.toBubbleItem(): BubbleItem { + val rect = Rect( center = Offset( - x = circle.x.scale(scale) + containerSize.center.x, - y = circle.y.scale(scale) + containerSize.center.y, + x = coordinate.x.scale(scale) + containerSize.center.x, + y = coordinate.y.scale(scale) + containerSize.center.y, ), - radius = circle.r.scale(scale), + radius = coordinate.r.scale(scale), ) - } - - var bubbleChartInitialState by mutableStateOf(BubbleChartInitialState.Init) - val bubbleChartItemBitmaps = mutableListOf() + val offsetX = rect.topLeft.x / density + val offsetY = rect.topLeft.y / density + val dpSize = (rect.size.width / density).dp - @Composable - fun init() { - bubbleChartInitialState = BubbleChartInitialState.Loading - - bubbleRectList.forEachIndexed { index, _ -> - val snapShot = CaptureBitmap(content = { BubbleChartItem() }) - coroutineScope.launch { - bubbleChartItemBitmaps.add(snapShot.invoke()) - if (index == bubbleRectList.lastIndex) { - bubbleChartInitialState = BubbleChartInitialState.Finish - Timber.d("bubbleChartInitialState: $bubbleChartInitialState, bitmaps: ${bubbleChartItemBitmaps.size}") - } - } - } + return BubbleItem(question, Offset(offsetX, offsetY), dpSize) } - fun onBubbleItemClick(item: Rect) { + fun onBubbleItemClick(item: BubbleItem) { Timber.d("onBubbleItemClick: $item") - onBubbleClick() - } -} - -@Composable -private fun CaptureBitmap( - content: @Composable () -> Unit, -): () -> Bitmap { - val context = LocalContext.current - val composeView = remember { ComposeView(context) } - - fun captureBitmap(): Bitmap { - return composeView.drawToBitmap() + onBubbleClick(item) } - - AndroidView( - factory = { - composeView.apply { - setContent { - content.invoke() - } - } - }, - ) - - return ::captureBitmap } -@Composable -private fun BubbleChartItem() { - Box( - modifier = Modifier - .border( - width = 1.dp, - color = Color.White, - shape = CircleShape, - ) - .background( - color = Color(0x17171799), - shape = CircleShape, - ) - .padding(20.dp), - contentAlignment = Alignment.Center, - ) { - Text("표현력\n4.0점", textAlign = TextAlign.Center) - } -} +data class BubbleItem( + val question: Question, + val offSet: Offset, + val size: Dp, +) diff --git a/presentation/src/main/java/com/keyme/presentation/myprofile/chart/ui/BubbleChart.kt b/presentation/src/main/java/com/keyme/presentation/myprofile/chart/ui/BubbleChart.kt index 40333f3..7957487 100644 --- a/presentation/src/main/java/com/keyme/presentation/myprofile/chart/ui/BubbleChart.kt +++ b/presentation/src/main/java/com/keyme/presentation/myprofile/chart/ui/BubbleChart.kt @@ -1,7 +1,5 @@ package com.keyme.presentation.myprofile.chart.ui -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.BoxWithConstraintsScope import androidx.compose.foundation.layout.fillMaxSize @@ -9,16 +7,21 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Size -import androidx.compose.ui.zIndex -import com.keyme.domain.entity.response.Circle -import com.keyme.presentation.designsystem.theme.keyme_black +import com.keyme.domain.entity.response.Result +import com.keyme.presentation.myprofile.chart.BubbleItem import com.keyme.presentation.myprofile.chart.rememberBubbleChartState +import timber.log.Timber @Composable -fun BubbleChart(circles: List, onBubbleClick: () -> Unit) { +fun BubbleChart( + results: List, + onBubbleClick: (BubbleItem) -> Unit, +) { + Timber.d("resultSize: ${results.size}") + BubbleChartContainer { val bubbleChartState = rememberBubbleChartState( - circles = circles, + results = results, containerSize = Size( constraints.maxWidth.toFloat(), constraints.maxHeight.toFloat(), @@ -26,18 +29,7 @@ fun BubbleChart(circles: List, onBubbleClick: () -> Unit) { onBubbleClick = onBubbleClick, ) - if (bubbleChartState.bubbleChartInitialState.isInit()) bubbleChartState.init() - - if (bubbleChartState.bubbleChartInitialState.isFinish()) { - BubbleChartCanvas(bubbleChartState = bubbleChartState) - } else { - Box( - modifier = Modifier - .fillMaxSize() - .background(color = keyme_black) - .zIndex(1f), - ) - } + BubbleChartCanvas(state = bubbleChartState) } } diff --git a/presentation/src/main/java/com/keyme/presentation/myprofile/chart/ui/BubbleChartCanvas.kt b/presentation/src/main/java/com/keyme/presentation/myprofile/chart/ui/BubbleChartCanvas.kt index 25599f2..56075ed 100644 --- a/presentation/src/main/java/com/keyme/presentation/myprofile/chart/ui/BubbleChartCanvas.kt +++ b/presentation/src/main/java/com/keyme/presentation/myprofile/chart/ui/BubbleChartCanvas.kt @@ -1,60 +1,76 @@ package com.keyme.presentation.myprofile.chart.ui -import android.graphics.Bitmap -import androidx.compose.foundation.Canvas +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Path -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import com.keyme.presentation.designsystem.component.KeymeText +import com.keyme.presentation.designsystem.component.KeymeTextType +import com.keyme.presentation.designsystem.theme.panchang import com.keyme.presentation.myprofile.chart.BubbleChartState +import com.keyme.presentation.myprofile.chart.BubbleItem +import com.keyme.presentation.utils.textDp @Composable fun BubbleChartCanvas( - bubbleChartState: BubbleChartState, + state: BubbleChartState, ) { - Canvas( - modifier = Modifier.bubbleChart(bubbleChartState), - onDraw = { - repeat(bubbleChartState.bubbleRectList.size) { index -> - val rect = bubbleChartState.bubbleRectList[index] - val itemBitmap = bubbleChartState.bubbleChartItemBitmaps[index] - - drawResultBubble(rect, colors[index]) - drawResultItem(rect, itemBitmap) - } - }, - ) + Box(modifier = Modifier.bubbleChart()) { + state.bubbleItems.forEach { + BubbleChartItem(bubbleItem = it, onClick = { state.onBubbleItemClick(it) }) + } + } } -private val colors = listOf( - Color.Blue, - Color.Green, - Color.Gray, - Color.Red, - Color.LightGray, - Color.Green, - Color.Cyan, - Color.Yellow, - Color.Red, - Color.LightGray, -) - -private fun DrawScope.drawResultBubble( - bubbleRect: Rect, - color: Color, +@Composable +fun BubbleChartItem( + bubbleItem: BubbleItem, + onClick: () -> Unit, ) { - val circlePath = Path().apply { addOval(bubbleRect) } - drawPath(circlePath, color) -} + Box( + modifier = Modifier + .bubbleChartItem(bubbleItem) + .clickable { onClick() }, + contentAlignment = Alignment.Center, + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + AsyncImage( + modifier = Modifier.size(48.dp), + model = ImageRequest.Builder(LocalContext.current) + .data(bubbleItem.question.category.iconUrl) + .build(), + contentDescription = "", + contentScale = ContentScale.Crop, + ) + + Spacer(modifier = Modifier.height(6.dp)) -private fun DrawScope.drawResultItem(bubbleRect: Rect, item: Bitmap) { - val targetX = bubbleRect.center.x - (item.width / 2) - val targetY = bubbleRect.center.y - (item.height / 2) - val targetOffset = Offset(targetX, targetY) + KeymeText( + text = bubbleItem.question.category.name, + keymeTextType = KeymeTextType.BODY_3_SEMIBOLD, + color = Color.White, + ) - drawImage(image = item.asImageBitmap(), topLeft = targetOffset) + Text( + text = bubbleItem.question.avgScore.toString(), + fontFamily = panchang, + fontWeight = FontWeight.ExtraBold, + fontSize = 18.textDp(), + color = Color.White, + ) + } + } } diff --git a/presentation/src/main/java/com/keyme/presentation/myprofile/chart/ui/BubbleChartModifier.kt b/presentation/src/main/java/com/keyme/presentation/myprofile/chart/ui/BubbleChartModifier.kt index 2965b46..03a7963 100644 --- a/presentation/src/main/java/com/keyme/presentation/myprofile/chart/ui/BubbleChartModifier.kt +++ b/presentation/src/main/java/com/keyme/presentation/myprofile/chart/ui/BubbleChartModifier.kt @@ -1,51 +1,52 @@ package com.keyme.presentation.myprofile.chart.ui import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.rememberTransformableState import androidx.compose.foundation.gestures.transformable import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.composed +import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.dp import com.keyme.presentation.designsystem.theme.keyme_black -import com.keyme.presentation.myprofile.chart.BubbleChartState -import timber.log.Timber +import com.keyme.presentation.myprofile.chart.BubbleItem +import com.keyme.presentation.utils.ColorUtil -fun Modifier.bubbleChart(bubbleChartState: BubbleChartState) = composed { +fun Modifier.bubbleChart() = composed { var offset by remember { mutableStateOf(Offset.Zero) } - val state = rememberTransformableState { _, offsetChange, _ -> + var scale by remember { mutableStateOf(1f) } + val state = rememberTransformableState { zoomChange, offsetChange, _ -> offset += offsetChange + scale = (scale * zoomChange).coerceIn(0.6f, 1.8f) } Modifier .fillMaxSize() .background(color = keyme_black) .clipToBounds() - .pointerInput(Unit) { - detectTapGestures( - onTap = { tapOffset -> - Timber.d("offset: $offset, tapOffset: $tapOffset") - - val tapOffset = tapOffset - offset - bubbleChartState.bubbleRectList - .find { it.contains(tapOffset) } - ?.let { - bubbleChartState.onBubbleItemClick(it) - } - }, - ) - } .transformable(state) .graphicsLayer( translationX = offset.x, translationY = offset.y, + scaleX = scale, + scaleY = scale, ) } + +fun Modifier.bubbleChartItem(item: BubbleItem) = composed { + Modifier + .offset(item.offSet.x.dp, item.offSet.y.dp) + .size(item.size) + .clip(CircleShape) + .background(color = ColorUtil.hexStringToColor(item.question.category.color), shape = CircleShape) +} diff --git a/presentation/src/main/java/com/keyme/presentation/myprofile/ui/KeymeMemberStatisticsScreen.kt b/presentation/src/main/java/com/keyme/presentation/myprofile/ui/KeymeMemberStatisticsScreen.kt new file mode 100644 index 0000000..542fd23 --- /dev/null +++ b/presentation/src/main/java/com/keyme/presentation/myprofile/ui/KeymeMemberStatisticsScreen.kt @@ -0,0 +1,19 @@ +package com.keyme.presentation.myprofile.ui + +import androidx.compose.runtime.Composable +import com.keyme.domain.entity.response.MemberStatistics +import com.keyme.domain.entity.response.Question +import com.keyme.presentation.myprofile.chart.ui.BubbleChart + +@Composable +fun KeymeMemberStatisticsScreen( + memberStatistics: MemberStatistics, + onQuestionClick: (Question) -> Unit, +) { + BubbleChart( + results = memberStatistics.results, + onBubbleClick = { + onQuestionClick(it.question) + }, + ) +} diff --git a/presentation/src/main/java/com/keyme/presentation/myprofile/ui/KeymeQuestionResultRoute.kt b/presentation/src/main/java/com/keyme/presentation/myprofile/ui/KeymeQuestionResultRoute.kt new file mode 100644 index 0000000..18a5973 --- /dev/null +++ b/presentation/src/main/java/com/keyme/presentation/myprofile/ui/KeymeQuestionResultRoute.kt @@ -0,0 +1,59 @@ +package com.keyme.presentation.myprofile.ui + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import com.keyme.domain.entity.response.keymetest.Category +import com.keyme.domain.entity.response.keymetest.QuestionsStatistic +import com.keyme.presentation.myprofile.KeymeQuestionResultViewModel +import com.keyme.presentation.navigation.KeymeNavigationDestination + +object KeymeQuestionResultDestination : KeymeNavigationDestination { + override val route: String = "keyme_test_result_detail_route" + override val destination: String = "keyme_test_result_detail_destination" + + object Argument { + val questionIdName: String = "questionId" + } +} + +fun NavGraphBuilder.keymeQuestionResultGraph(onBackClick: () -> Unit) { + composable( + route = "${KeymeQuestionResultDestination.route}/{${KeymeQuestionResultDestination.Argument.questionIdName}}", + arguments = listOf( + navArgument(KeymeQuestionResultDestination.Argument.questionIdName) { + type = NavType.StringType + }, + ), + ) { + KeymeQuestionResultRoute(onBackClick = onBackClick) + } +} + +@Composable +fun KeymeQuestionResultRoute( + keymeQuestionResultViewModel: KeymeQuestionResultViewModel = hiltViewModel(), + onBackClick: () -> Unit, +) { + val statisticsState by keymeQuestionResultViewModel.statisticsState.collectAsStateWithLifecycle() + + KeymeQuestionResultScreen( + statistics = QuestionsStatistic( + averageScore = 0, + category = Category( + color = "", + imageUrl = "", + name = "", + ), + description = "", + keyword = "", + questionId = 0, + ), + onBackClick = onBackClick, + ) +} diff --git a/presentation/src/main/java/com/keyme/presentation/myprofile/ui/KeymeQuestionResultScreen.kt b/presentation/src/main/java/com/keyme/presentation/myprofile/ui/KeymeQuestionResultScreen.kt new file mode 100644 index 0000000..5f6622a --- /dev/null +++ b/presentation/src/main/java/com/keyme/presentation/myprofile/ui/KeymeQuestionResultScreen.kt @@ -0,0 +1,268 @@ +package com.keyme.presentation.myprofile.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.gestures.detectVerticalDragGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.keyme.domain.entity.response.keymetest.Category +import com.keyme.domain.entity.response.keymetest.QuestionsStatistic +import com.keyme.presentation.R +import com.keyme.presentation.designsystem.component.BottomSheetHandle +import com.keyme.presentation.designsystem.component.KeymeText +import com.keyme.presentation.designsystem.component.KeymeTextType +import com.keyme.presentation.designsystem.theme.keyme_black +import com.keyme.presentation.designsystem.theme.panchang +import com.keyme.presentation.utils.clickableRippleEffect +import com.keyme.presentation.utils.getUploadTimeString +import com.keyme.presentation.utils.textDp +import timber.log.Timber + +@Composable +fun KeymeQuestionResultScreen( + statistics: QuestionsStatistic, + onBackClick: () -> Unit, +) { + Box(modifier = Modifier.fillMaxSize()) { + Icon( + modifier = Modifier + .padding(top = 20.dp, start = 16.dp) + .clickableRippleEffect(bounded = false) { onBackClick() }, + painter = painterResource(id = R.drawable.icon_nav_back), + contentDescription = "", + tint = Color.White, + ) + + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + KeymeQuestionStatisticsInfo(statistics) + + KeymeQuestionStatisticsCircle() + + KeymeQuestionScoreListContainer { + KeymeQuestionInfo(title = "키미미미미미님의 애정 표현정도는?", solvedCount = 10) + + Spacer(modifier = Modifier.height(12.dp)) + + Divider(modifier = Modifier.fillMaxWidth(), thickness = 1.dp, color = Color(0x1AFFFFFF)) + + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + items(100) { + KeymeQuestionScoreItem("5", System.currentTimeMillis()) + } + } + } + } + } +} + +@Composable +fun ColumnScope.KeymeQuestionStatisticsCircle() { + Box( + modifier = Modifier + .padding(top = 12.dp, start = 30.dp, end = 30.dp, bottom = 30.dp) + .weight(1f) + .aspectRatio(1f) + .shadow( + elevation = 20.dp, + spotColor = Color(0x00000000), + ambientColor = Color(0x00000000), + ) + .border( + width = 1.dp, + color = Color(0x4DFFFFFF), + shape = RoundedCornerShape(size = 320.00003.dp), + ) + .background(color = Color(0x4DFFFFFF), shape = RoundedCornerShape(size = 320.00003.dp)), + ) +} + +@Composable +fun ColumnScope.KeymeQuestionScoreListContainer(content: @Composable ColumnScope.() -> Unit) { + var bottomWeightValue by remember { mutableStateOf(1f) } + + Column( + modifier = Modifier + .fillMaxWidth() + .background( + color = Color(0xFF232323), + RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), + ) + .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)) + .weight(bottomWeightValue), + ) { + BottomSheetHandle( + modifier = Modifier + .pointerInput(Unit) { + detectVerticalDragGestures { _, dragAmount -> + bottomWeightValue = (bottomWeightValue - dragAmount / 200).coerceIn(1f, 5f) + Timber.d("bottomWeightValue: $bottomWeightValue") + } + }, + ) + + content() + } +} + +@Composable +fun KeymeQuestionInfo( + title: String, + solvedCount: Int, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = title, + style = androidx.compose.ui.text.TextStyle( + fontSize = 20.sp, + lineHeight = 26.sp, + fontWeight = FontWeight(600), + color = Color(0xFFFFFFFF), + ), + ) + + Text( + text = "응답자 수 ${solvedCount}명", + style = androidx.compose.ui.text.TextStyle( + fontSize = 16.sp, + lineHeight = 19.2.sp, + fontWeight = FontWeight(400), + color = Color(0x99FFFFFF), + ), + ) + } +} + +@Composable +private fun KeymeQuestionScoreItem( + score: String, + timeStamp: Long, +) { + Box( + modifier = Modifier + .fillMaxWidth() + .background(color = Color(0xFF303030), shape = RoundedCornerShape(size = 16.dp)) + .padding(vertical = 16.dp, horizontal = 20.dp), + ) { + KeymeText( + modifier = Modifier.align(Alignment.Center), + text = "${score}점", + keymeTextType = KeymeTextType.BODY_3_REGULAR, + ) + KeymeText( + modifier = Modifier.align(Alignment.CenterEnd), + text = timeStamp.getUploadTimeString(), + keymeTextType = KeymeTextType.BODY_3_REGULAR, + ) + } +} + +@Composable +private fun KeymeQuestionStatisticsInfo( + questionsStatistic: QuestionsStatistic, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 4.dp, bottom = 12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + KeymeText( + modifier = Modifier + .border( + width = 0.5.dp, + color = Color(0xFFFFFFFF), + shape = RoundedCornerShape(size = 16.dp), + ) + .background(color = Color(0x33FFFFFF), shape = RoundedCornerShape(size = 16.dp)) + .padding(start = 10.dp, top = 5.dp, end = 10.dp, bottom = 5.dp), + text = questionsStatistic.category.name, + keymeTextType = KeymeTextType.BODY_3_REGULAR, + ) + + Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + text = questionsStatistic.averageScore.toString(), + fontFamily = panchang, + fontWeight = FontWeight.ExtraBold, + fontSize = 40.textDp(), + color = Color.White.copy(alpha = 0.6f), + ) + KeymeText( + modifier = Modifier + .align(Alignment.Bottom) + .padding(bottom = 10.dp), + text = "점", + keymeTextType = KeymeTextType.BODY_3_REGULAR, + color = Color.White.copy(alpha = 0.6f), + ) + } + } +} + +@Preview(showBackground = true, backgroundColor = 0x000000) +@Composable +private fun KeymeQuestionDetailScreenPreview() { + Box( + modifier = Modifier + .fillMaxSize() + .background(color = keyme_black), + ) { + KeymeQuestionResultScreen( + statistics = QuestionsStatistic( + averageScore = 0, + category = Category( + color = "", + imageUrl = "", + name = "", + ), + description = "", + keyword = "", + questionId = 0, + ), + onBackClick = {}, + ) + } +} diff --git a/presentation/src/main/java/com/keyme/presentation/myprofile/ui/KeymeTestResultDetailRoute.kt b/presentation/src/main/java/com/keyme/presentation/myprofile/ui/KeymeTestResultDetailRoute.kt deleted file mode 100644 index 13d90a0..0000000 --- a/presentation/src/main/java/com/keyme/presentation/myprofile/ui/KeymeTestResultDetailRoute.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.keyme.presentation.myprofile.ui - -import androidx.compose.runtime.Composable -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavType -import androidx.navigation.compose.composable -import androidx.navigation.navArgument -import com.keyme.presentation.navigation.KeymeNavigationDestination - -object KeymeTestResultDetailDestination : KeymeNavigationDestination { - override val route: String = "keyme_test_result_detail_route/{${Argument.questionIdName}}" - override val destination: String = "keyme_test_result_detail_destination" - - object Argument { - val questionIdName: String = "questionId" - } -} - -fun NavGraphBuilder.keymeTestResultDetailGraph(onBackClick: () -> Unit) { - composable( - route = KeymeTestResultDetailDestination.route, - arguments = listOf( - navArgument(KeymeTestResultDetailDestination.Argument.questionIdName) { - type = NavType.StringType - }, - ), - ) { - KeymeTestResultDetailRoute(onBackClick = onBackClick) - } -} - -@Composable -fun KeymeTestResultDetailRoute(onBackClick: () -> Unit) { - KeymeTestResultDetailScreen(onBackClick = onBackClick) -} diff --git a/presentation/src/main/java/com/keyme/presentation/myprofile/ui/KeymeTestResultDetailScreen.kt b/presentation/src/main/java/com/keyme/presentation/myprofile/ui/KeymeTestResultDetailScreen.kt deleted file mode 100644 index 1a5d297..0000000 --- a/presentation/src/main/java/com/keyme/presentation/myprofile/ui/KeymeTestResultDetailScreen.kt +++ /dev/null @@ -1,209 +0,0 @@ -package com.keyme.presentation.myprofile.ui - -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.gestures.detectVerticalDragGestures -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.keyme.presentation.R -import com.keyme.presentation.designsystem.theme.keyme_black -import com.keyme.presentation.designsystem.theme.panchang -import com.keyme.presentation.utils.clickableRippleEffect -import com.keyme.presentation.utils.textDp -import timber.log.Timber - -@Composable -fun KeymeTestResultDetailScreen(onBackClick: () -> Unit) { - Box(modifier = Modifier.fillMaxSize()) { - Icon( - modifier = Modifier - .padding(top = 20.dp, start = 16.dp) - .clickableRippleEffect(bounded = false) { onBackClick() }, - painter = painterResource(id = R.drawable.icon_nav_back), - contentDescription = "", - tint = Color.White, - ) - - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - KeymeTestInfo() - - Box( - modifier = Modifier - .padding(top = 12.dp, start = 30.dp, end = 30.dp, bottom = 30.dp) - .weight(1f) - .aspectRatio(1f) - .shadow( - elevation = 20.dp, - spotColor = Color(0x00000000), - ambientColor = Color(0x00000000), - ) - .border( - width = 1.dp, - color = Color(0x4DFFFFFF), - shape = RoundedCornerShape(size = 320.00003.dp), - ) - .background(color = Color(0x4DFFFFFF), shape = RoundedCornerShape(size = 320.00003.dp)), - ) - - var bottomWeightValue by remember { - mutableStateOf(1f) - } - - Column( - modifier = Modifier - .fillMaxWidth() - .background( - color = Color(0xFF232323), - RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), - ) - .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)) - .weight(bottomWeightValue), - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(14.dp) - .pointerInput(Unit) { - detectVerticalDragGestures { _, dragAmount -> - bottomWeightValue = (bottomWeightValue - dragAmount / 200).coerceIn(1f, 5f) - Timber.d("bottomWeightValue: $bottomWeightValue") - } - }, - contentAlignment = Alignment.Center, - ) { - Box( - modifier = Modifier - .width(40.dp) - .height(4.dp) - .background(color = Color(0x4DFFFFFF), shape = RoundedCornerShape(size = 2.dp)), - ) - } - - Text( - text = "키미미미미미님의 애정 표현정도는?", - style = androidx.compose.ui.text.TextStyle( - fontSize = 20.sp, - lineHeight = 26.sp, - fontWeight = FontWeight(600), - color = Color(0xFFFFFFFF), - ), - ) - Text( - text = "응답자 수 16명", - style = androidx.compose.ui.text.TextStyle( - fontSize = 16.sp, - lineHeight = 19.2.sp, - fontWeight = FontWeight(400), - color = Color(0x99FFFFFF), - ), - ) - - LazyColumn( - modifier = Modifier.fillMaxWidth(), - ) { - items(100) { - Item() - } - } - } - } - } -} - -@Composable -private fun Item() { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp, horizontal = 20.dp), - ) { - Text(modifier = Modifier.align(Alignment.Center), text = "5점") - Text(modifier = Modifier.align(Alignment.CenterEnd), text = "2시간전") - } -} - -@Composable -private fun KeymeTestInfo() { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(top = 4.dp, bottom = 12.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - modifier = Modifier - .border( - width = 0.5.dp, - color = Color(0xFFFFFFFF), - shape = RoundedCornerShape(size = 16.dp), - ) - .background(color = Color(0x33FFFFFF), shape = RoundedCornerShape(size = 16.dp)) - .padding(start = 10.dp, top = 5.dp, end = 10.dp, bottom = 5.dp), - text = "표현력", - ) - - Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { - Text( - text = "4.0", - fontFamily = panchang, - fontWeight = FontWeight.ExtraBold, - fontSize = 40.textDp(), - color = Color.White.copy(alpha = 0.6f), - ) - Text( - modifier = Modifier - .align(Alignment.Bottom) - .padding(bottom = 10.dp), - text = "점", - fontSize = 12.textDp(), - fontWeight = FontWeight.Medium, - color = Color.White.copy(alpha = 0.6f), - ) - } - } -} - -@Preview(showBackground = true, backgroundColor = 0x000000) -@Composable -private fun KeymeTestDetailScreenPreview() { - Box( - modifier = Modifier - .fillMaxSize() - .background(color = keyme_black), - ) { - KeymeTestResultDetailScreen( - onBackClick = {}, - ) - } -} diff --git a/presentation/src/main/java/com/keyme/presentation/myprofile/ui/KeymeTestResultScreen.kt b/presentation/src/main/java/com/keyme/presentation/myprofile/ui/KeymeTestResultScreen.kt deleted file mode 100644 index 4c5d12d..0000000 --- a/presentation/src/main/java/com/keyme/presentation/myprofile/ui/KeymeTestResultScreen.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.keyme.presentation.myprofile.ui - -import androidx.compose.runtime.Composable -import com.keyme.domain.entity.response.Circle -import com.keyme.presentation.myprofile.chart.ui.BubbleChart - -@Composable -fun KeymeTestStatisticsScreen( - circles: List, - onTestItemClick: () -> Unit, -) { - BubbleChart(circles = circles, onBubbleClick = { - onTestItemClick() - },) -} diff --git a/presentation/src/main/java/com/keyme/presentation/myprofile/ui/MyProfileRoute.kt b/presentation/src/main/java/com/keyme/presentation/myprofile/ui/MyProfileRoute.kt index ba53c30..07b59fb 100644 --- a/presentation/src/main/java/com/keyme/presentation/myprofile/ui/MyProfileRoute.kt +++ b/presentation/src/main/java/com/keyme/presentation/myprofile/ui/MyProfileRoute.kt @@ -1,14 +1,12 @@ package com.keyme.presentation.myprofile.ui -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable +import com.keyme.domain.entity.response.Question import com.keyme.presentation.myprofile.MyProfileViewModel import com.keyme.presentation.navigation.KeymeNavigationDestination @@ -18,11 +16,11 @@ object MyProfileDestination : KeymeNavigationDestination { } fun NavGraphBuilder.myProfileGraph( - navigateToDetail: () -> Unit, + navigateToQuestionResult: (Question) -> Unit, nestedGraphs: NavGraphBuilder.() -> Unit, ) { composable(route = MyProfileDestination.route) { - MyProfileRoute(navigateToDetail = navigateToDetail) + MyProfileRoute(navigateToQuestionResult = navigateToQuestionResult) } nestedGraphs() } @@ -30,14 +28,16 @@ fun NavGraphBuilder.myProfileGraph( @Composable fun MyProfileRoute( myProfileViewModel: MyProfileViewModel = hiltViewModel(), - navigateToDetail: () -> Unit, + navigateToQuestionResult: (Question) -> Unit, ) { - val resultCircle by myProfileViewModel.resultCircleState.collectAsStateWithLifecycle() + val myCharacter by myProfileViewModel.myCharacterState.collectAsStateWithLifecycle() + val mySimilarStatistics by myProfileViewModel.mySimilarStatisticsState.collectAsStateWithLifecycle() + val myDifferentStatistics by myProfileViewModel.myDifferentStatisticsState.collectAsStateWithLifecycle() - Box(modifier = Modifier.fillMaxSize()) { - KeymeTestStatisticsScreen( - circles = resultCircle, - onTestItemClick = { navigateToDetail() }, - ) - } + MyProfileScreen( + myCharacter = myCharacter, + mySimilarStatistics = mySimilarStatistics, + myDifferentStatistics = myDifferentStatistics, + onQuestionClick = navigateToQuestionResult, + ) } diff --git a/presentation/src/main/java/com/keyme/presentation/myprofile/ui/MyProfileScreen.kt b/presentation/src/main/java/com/keyme/presentation/myprofile/ui/MyProfileScreen.kt new file mode 100644 index 0000000..1300423 --- /dev/null +++ b/presentation/src/main/java/com/keyme/presentation/myprofile/ui/MyProfileScreen.kt @@ -0,0 +1,199 @@ +package com.keyme.presentation.myprofile.ui + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import com.keyme.domain.entity.member.Member +import com.keyme.domain.entity.response.MemberStatistics +import com.keyme.domain.entity.response.Question +import com.keyme.presentation.R +import com.keyme.presentation.designsystem.component.KeymeText +import com.keyme.presentation.designsystem.component.KeymeTextType +import com.keyme.presentation.utils.clickableRippleEffect +import kotlinx.coroutines.launch + +enum class MyProfileTab(val title: String) { + Similar("가장 비슷한"), Different("가장 차이나는") +} + +private val myProfileTabs = listOf(MyProfileTab.Similar, MyProfileTab.Different) + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun MyProfileScreen( + myCharacter: Member, + mySimilarStatistics: MemberStatistics, + myDifferentStatistics: MemberStatistics, + onQuestionClick: (Question) -> Unit, +) { + Box(modifier = Modifier.fillMaxSize()) { + val pagerState = rememberPagerState(initialPage = 0) + val coroutineScope = rememberCoroutineScope() + + MyProfileTopContainer( + myCharacter, + pagerState.currentPage, + myProfileTabs, + onTabSelected = { + coroutineScope.launch { + pagerState.scrollToPage(it) + } + }, + ) + + HorizontalPager( + pageCount = myProfileTabs.size, + state = pagerState, + ) { + when (myProfileTabs[it]) { + MyProfileTab.Similar -> KeymeMemberStatisticsScreen( + memberStatistics = mySimilarStatistics, + onQuestionClick = { question -> onQuestionClick(question) }, + ) + + MyProfileTab.Different -> KeymeMemberStatisticsScreen( + memberStatistics = myDifferentStatistics, + onQuestionClick = { question -> onQuestionClick(question) }, + ) + } + } + } +} + +@Composable +private fun MyProfileTitle() { + Row( + modifier = Modifier.padding(vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + KeymeText( + text = "마이", + keymeTextType = KeymeTextType.BODY_3_SEMIBOLD, + color = Color(0xFFF8F8F8), + ) + Icon( + painter = painterResource(id = R.drawable.info_circle), + contentDescription = "", + tint = Color.White, + ) + } +} + +@Composable +private fun MyProfileTopContainer( + myCharacter: Member, + selectedTabIndex: Int, + myProfileTabs: List, + onTabSelected: (Int) -> Unit, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .zIndex(1f), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + MyProfileTitle() + + Spacer(modifier = Modifier.height(10.dp)) + + MyProfileTabRow( + selectedTabIndex = selectedTabIndex, + tabs = myProfileTabs, + onTabSelected = onTabSelected, + ) + + Spacer(modifier = Modifier.height(18.dp)) + + KeymeText( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 18.dp), + text = "친구들이 생각하는\n${myCharacter.nickname}님의 성격은?", + keymeTextType = KeymeTextType.HEADING_1, + color = Color(0xFFFFFFFF), + ) + } +} + +@Composable +private fun MyProfileTabRow( + selectedTabIndex: Int, + tabs: List, + onTabSelected: (Int) -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(50.dp) + .padding(horizontal = 16.dp) + .border(width = 1.dp, color = Color(0xFF3C3C3C), shape = RoundedCornerShape(size = 16.dp)) + .background(color = Color(0x80363636), shape = RoundedCornerShape(size = 16.dp)), + verticalAlignment = Alignment.CenterVertically, + ) { + tabs.forEachIndexed { index, tab -> + MyProfileTabItem( + modifier = Modifier.weight(1f), + text = tab.title, + isSelected = selectedTabIndex == index, + onClick = { + onTabSelected(index) + }, + ) + } + } +} + +@Composable +private fun MyProfileTabItem( + modifier: Modifier = Modifier, + text: String, + isSelected: Boolean, + onClick: () -> Unit, +) { + Box( + modifier = modifier + .fillMaxHeight() + .then( + if (isSelected) { + Modifier + .padding(4.dp) + .background(color = Color(0xFFFFFFFF), shape = RoundedCornerShape(size = 16.dp)) + } else { + Modifier + }, + ) + .clipToBounds() + .clickableRippleEffect(bounded = false) { onClick() }, + contentAlignment = Alignment.Center, + ) { + KeymeText( + text = text, + keymeTextType = KeymeTextType.BODY_3_SEMIBOLD, + color = if (isSelected) Color(0xFF171717) else Color.White, + ) + } +} diff --git a/presentation/src/main/java/com/keyme/presentation/utils/ColorUtil.kt b/presentation/src/main/java/com/keyme/presentation/utils/ColorUtil.kt new file mode 100644 index 0000000..cb6ea6f --- /dev/null +++ b/presentation/src/main/java/com/keyme/presentation/utils/ColorUtil.kt @@ -0,0 +1,24 @@ +package com.keyme.presentation.utils + +import androidx.compose.ui.graphics.Color +import timber.log.Timber + +object ColorUtil { + fun hexStringToColor(value: String): Color { + require(value.length == 6) + + val alpha = 255 + val red = value.substring(0, 2).toInt(16) + val green = value.substring(2, 4).toInt(16) + val blue = value.substring(4, 6).toInt(16) + + val argbColor = (alpha and 0xFF shl 24) or + (red and 0xFF shl 16) or + (green and 0xFF shl 8) or + (blue and 0xFF) + + Timber.d("argbColor = $argbColor") + + return Color(argbColor) + } +} diff --git a/presentation/src/main/java/com/keyme/presentation/utils/TimeUtil.kt b/presentation/src/main/java/com/keyme/presentation/utils/TimeUtil.kt new file mode 100644 index 0000000..1c389c9 --- /dev/null +++ b/presentation/src/main/java/com/keyme/presentation/utils/TimeUtil.kt @@ -0,0 +1,23 @@ +package com.keyme.presentation.utils + +import java.text.SimpleDateFormat +import java.util.Date +import java.util.concurrent.TimeUnit + +fun Long.getUploadTimeString(): String { + val uploadInterval = System.currentTimeMillis() - this + + val minutesUnit = TimeUnit.MINUTES.toMillis(1) + val hourUnit = TimeUnit.HOURS.toMillis(1) + val dayUnit = TimeUnit.DAYS.toMillis(1) + + val format = SimpleDateFormat("aa hh:mm") + + return when (uploadInterval) { + in 0L until minutesUnit -> "방금 전" + in minutesUnit until hourUnit -> "${uploadInterval / minutesUnit}분 전" + in hourUnit until dayUnit -> "${uploadInterval / hourUnit}시간 전" + in dayUnit until dayUnit * 2 -> "어제 ${format.format(Date(this))}" + else -> "엊그제" + } +} diff --git a/presentation/src/main/res/drawable/info_circle.xml b/presentation/src/main/res/drawable/info_circle.xml new file mode 100644 index 0000000..b0161e8 --- /dev/null +++ b/presentation/src/main/res/drawable/info_circle.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/presentation/src/test/java/com/keyme/presentation/TimeUtil.kt b/presentation/src/test/java/com/keyme/presentation/TimeUtil.kt new file mode 100644 index 0000000..1ccd489 --- /dev/null +++ b/presentation/src/test/java/com/keyme/presentation/TimeUtil.kt @@ -0,0 +1,22 @@ +package com.keyme.presentation + +import com.keyme.presentation.utils.getUploadTimeString +import org.junit.Assert.assertEquals +import org.junit.Test +import java.util.concurrent.TimeUnit + +class TimeUtil { + @Test + fun uploadTimeString() { + val now = System.currentTimeMillis() + + val minutesUnit = TimeUnit.MINUTES.toMillis(1) + val hourUnit = TimeUnit.HOURS.toMillis(1) + val dayUnit = TimeUnit.DAYS.toMillis(1) + + assertEquals("방금 전", (now - (minutesUnit - 100L)).getUploadTimeString()) + assertEquals("3분 전", (now - (minutesUnit * 3)).getUploadTimeString()) + assertEquals("3시간 전", (now - (hourUnit * 3)).getUploadTimeString()) + assertEquals("엊그제", (now - (dayUnit * 3)).getUploadTimeString()) + } +}