From fe0b51146af7895303548805d6849943473d1e55 Mon Sep 17 00:00:00 2001 From: Choi Sang Rok Date: Fri, 12 May 2023 22:25:25 +0900 Subject: [PATCH] =?UTF-8?q?=EC=8B=9C=ED=97=98=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=B1=8C=EB=A6=B0=EC=A7=80(?= =?UTF-8?q?=ED=80=B4=EC=A6=88)=EC=99=80=20=EB=8D=95=EB=A0=A5=EA=B3=A0?= =?UTF-8?q?=EC=82=AC=20=EB=B6=84=EA=B8=B0=20=EC=B2=98=EB=A6=AC=20(#475)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: ExamData에 quiz 관련 property 추가 * feat: Detail Screen 덕퀴즈인지, 일반 시험인지에 따라 분기할 수 있도록 재정비 * feat: 현재 quiz 정보인지에 대한 상태 추가 * feat: 새로운 왕관 아이콘 추가 * feat: String Resource 추가 * feat: ExamDetail에 대한 컴포저블 분리, 추후 시험 메타데이터 제공할 수 있도록 TODO 작성 * refactor: lint 검출 사항 수정 * feat: ranking empty일 때 케이스 고려 --- .../app/android/data/exam/mapper/mapper.kt | 13 + .../app/android/data/exam/model/ExamData.kt | 6 + .../data/exam/model/QuizInfoResponse.kt | 24 + .../app/android/domain/exam/model/Exam.kt | 5 + .../app/android/domain/quiz/model/QuizInfo.kt | 20 + .../feature/ui/detail/common/BottomContent.kt | 69 +++ .../feature/ui/detail/common/DetailContent.kt | 230 ++++++++++ .../feature/ui/detail/common/TopCustomBar.kt | 63 +++ .../feature/ui/detail/screen/DetailScreen.kt | 433 ++---------------- .../feature/ui/detail/screen/MeasurePolicy.kt | 66 +++ .../ui/detail/screen/exam/ExamDetailScreen.kt | 38 ++ .../ui/detail/screen/quiz/QuizDetailScreen.kt | 168 +++++++ .../ui/detail/viewmodel/state/DetailState.kt | 3 + .../src/main/res/values/strings.xml | 1 + .../android/shared/ui/compose/DuckieIcon.kt | 2 + .../src/main/res/drawable/ic_crown_12.xml | 12 + 16 files changed, 754 insertions(+), 399 deletions(-) create mode 100644 data/src/main/kotlin/team/duckie/app/android/data/exam/model/QuizInfoResponse.kt create mode 100644 domain/src/main/kotlin/team/duckie/app/android/domain/quiz/model/QuizInfo.kt create mode 100644 feature-ui-detail/src/main/kotlin/team/duckie/app/android/feature/ui/detail/common/BottomContent.kt create mode 100644 feature-ui-detail/src/main/kotlin/team/duckie/app/android/feature/ui/detail/common/DetailContent.kt create mode 100644 feature-ui-detail/src/main/kotlin/team/duckie/app/android/feature/ui/detail/common/TopCustomBar.kt create mode 100644 feature-ui-detail/src/main/kotlin/team/duckie/app/android/feature/ui/detail/screen/MeasurePolicy.kt create mode 100644 feature-ui-detail/src/main/kotlin/team/duckie/app/android/feature/ui/detail/screen/exam/ExamDetailScreen.kt create mode 100644 feature-ui-detail/src/main/kotlin/team/duckie/app/android/feature/ui/detail/screen/quiz/QuizDetailScreen.kt create mode 100644 shared-ui-compose/src/main/res/drawable/ic_crown_12.xml diff --git a/data/src/main/kotlin/team/duckie/app/android/data/exam/mapper/mapper.kt b/data/src/main/kotlin/team/duckie/app/android/data/exam/mapper/mapper.kt index 272d2cd79..2f5fbf553 100644 --- a/data/src/main/kotlin/team/duckie/app/android/data/exam/mapper/mapper.kt +++ b/data/src/main/kotlin/team/duckie/app/android/data/exam/mapper/mapper.kt @@ -24,6 +24,7 @@ import team.duckie.app.android.data.exam.model.ImageChoiceData import team.duckie.app.android.data.exam.model.ProblemData import team.duckie.app.android.data.exam.model.ProfileExamData import team.duckie.app.android.data.exam.model.QuestionData +import team.duckie.app.android.data.exam.model.QuizInfoResponse import team.duckie.app.android.data.heart.mapper.toDomain import team.duckie.app.android.data.tag.mapper.toDomain import team.duckie.app.android.data.tag.model.TagData @@ -42,6 +43,7 @@ import team.duckie.app.android.domain.exam.model.ImageChoiceModel import team.duckie.app.android.domain.exam.model.Problem import team.duckie.app.android.domain.exam.model.ProfileExam import team.duckie.app.android.domain.exam.model.Question +import team.duckie.app.android.domain.quiz.model.QuizInfo import team.duckie.app.android.util.kotlin.AllowCyclomaticComplexMethod import team.duckie.app.android.util.kotlin.exception.duckieResponseFieldNpe import team.duckie.app.android.util.kotlin.fastMap @@ -66,6 +68,8 @@ internal fun ExamData.toDomain() = Exam( status = status, heart = heart?.toDomain(), heartCount = heartCount, + quizs = quizs?.fastMap(QuizInfoResponse::toDomain)?.toImmutableList(), + perfectScoreImageUrl = perfectScoreImageUrl, ) internal fun ExamsData.toDomain() = exams?.fastMap { examData -> examData.toDomain() } @@ -243,3 +247,12 @@ internal fun ProfileExamData.toDomain() = ProfileExam( heartCount = heartCount, user = user?.toDomain(), ) + +internal fun QuizInfoResponse.toDomain() = QuizInfo( + id = id ?: duckieResponseFieldNpe("${this::class.java.simpleName}.id"), + correctProblemCount = correctProblemCount + ?: duckieResponseFieldNpe("${this::class.java.simpleName}.correctProblemCount"), + score = score ?: duckieResponseFieldNpe("${this::class.java.simpleName}.score"), + user = user?.toDomain() ?: duckieResponseFieldNpe("${this::class.java.simpleName}.user"), + time = time ?: duckieResponseFieldNpe("${this::class.java.simpleName}.time"), +) diff --git a/data/src/main/kotlin/team/duckie/app/android/data/exam/model/ExamData.kt b/data/src/main/kotlin/team/duckie/app/android/data/exam/model/ExamData.kt index 2eb652b6b..524ae5c88 100644 --- a/data/src/main/kotlin/team/duckie/app/android/data/exam/model/ExamData.kt +++ b/data/src/main/kotlin/team/duckie/app/android/data/exam/model/ExamData.kt @@ -64,6 +64,12 @@ data class ExamData( @field:JsonProperty("heartCount") val heartCount: Int? = null, + + @field:JsonProperty("challenges") + val quizs: List? = null, + + @field:JsonProperty("perfectScoreImageUrl") + val perfectScoreImageUrl: String? = null, ) data class ExamsData( diff --git a/data/src/main/kotlin/team/duckie/app/android/data/exam/model/QuizInfoResponse.kt b/data/src/main/kotlin/team/duckie/app/android/data/exam/model/QuizInfoResponse.kt new file mode 100644 index 000000000..780e08b3b --- /dev/null +++ b/data/src/main/kotlin/team/duckie/app/android/data/exam/model/QuizInfoResponse.kt @@ -0,0 +1,24 @@ +/* + * Designed and developed by Duckie Team, 2022 + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE + */ + +package team.duckie.app.android.data.exam.model + +import com.fasterxml.jackson.annotation.JsonProperty +import team.duckie.app.android.data.user.model.UserResponse + +data class QuizInfoResponse( + @JsonProperty("id") + val id: Int? = null, + @JsonProperty("correctProblemCount") + val correctProblemCount: Int? = null, + @JsonProperty("score") + val score: Int? = null, + @JsonProperty("time") + val time: Int? = null, + @JsonProperty("user") + val user: UserResponse? = null, +) diff --git a/domain/src/main/kotlin/team/duckie/app/android/domain/exam/model/Exam.kt b/domain/src/main/kotlin/team/duckie/app/android/domain/exam/model/Exam.kt index 33733632d..c2946acda 100644 --- a/domain/src/main/kotlin/team/duckie/app/android/domain/exam/model/Exam.kt +++ b/domain/src/main/kotlin/team/duckie/app/android/domain/exam/model/Exam.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.Immutable import kotlinx.collections.immutable.ImmutableList import team.duckie.app.android.domain.category.model.Category import team.duckie.app.android.domain.heart.model.Heart +import team.duckie.app.android.domain.quiz.model.QuizInfo import team.duckie.app.android.domain.tag.model.Tag import team.duckie.app.android.domain.user.model.User @@ -35,6 +36,8 @@ data class Exam( val status: String?, val heart: Heart?, val heartCount: Int?, + val quizs: ImmutableList?, + val perfectScoreImageUrl: String?, ) { companion object { /* @@ -59,6 +62,8 @@ data class Exam( status = null, heart = null, heartCount = null, + quizs = null, + perfectScoreImageUrl = null, ) } } diff --git a/domain/src/main/kotlin/team/duckie/app/android/domain/quiz/model/QuizInfo.kt b/domain/src/main/kotlin/team/duckie/app/android/domain/quiz/model/QuizInfo.kt new file mode 100644 index 000000000..9d5351dc9 --- /dev/null +++ b/domain/src/main/kotlin/team/duckie/app/android/domain/quiz/model/QuizInfo.kt @@ -0,0 +1,20 @@ +/* + * Designed and developed by Duckie Team, 2022 + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE + */ + +package team.duckie.app.android.domain.quiz.model + +import androidx.compose.runtime.Immutable +import team.duckie.app.android.domain.user.model.User + +@Immutable +data class QuizInfo( + val id: Int, + val correctProblemCount: Int, + val score: Int, + val user: User, + val time: Int, +) diff --git a/feature-ui-detail/src/main/kotlin/team/duckie/app/android/feature/ui/detail/common/BottomContent.kt b/feature-ui-detail/src/main/kotlin/team/duckie/app/android/feature/ui/detail/common/BottomContent.kt new file mode 100644 index 000000000..5fd261e04 --- /dev/null +++ b/feature-ui-detail/src/main/kotlin/team/duckie/app/android/feature/ui/detail/common/BottomContent.kt @@ -0,0 +1,69 @@ +/* + * Designed and developed by Duckie Team, 2022 + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE + */ + +package team.duckie.app.android.feature.ui.detail.common + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import team.duckie.app.android.feature.ui.detail.viewmodel.state.DetailState +import team.duckie.app.android.shared.ui.compose.Spacer +import team.duckie.quackquack.ui.R +import team.duckie.quackquack.ui.component.QuackDivider +import team.duckie.quackquack.ui.component.QuackImage +import team.duckie.quackquack.ui.component.QuackSmallButton +import team.duckie.quackquack.ui.component.QuackSmallButtonType + +/** 상세 화면 최하단 Layout */ +@Composable +internal fun DetailBottomLayout( + modifier: Modifier, + state: DetailState.Success, + onHeartClick: () -> Unit, + onChallengeClick: () -> Unit, +) { + Column(modifier = modifier) { + Spacer(space = 20.dp) + // 구분선 + QuackDivider() + // 버튼 모음 Layout + // TODO(riflockle7): 추후 Layout 을 활용해 처리하기 + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 9.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + // 좋아요 버튼 + QuackImage( + src = if (state.isHeart) { + R.drawable.quack_ic_heart_filled_24 + } else { + R.drawable.quack_ic_heart_24 + }, + size = DpSize(24.dp, 24.dp), + onClick = onHeartClick, + ) + + // 버튼 + QuackSmallButton( + text = state.buttonTitle, + type = QuackSmallButtonType.Fill, + enabled = true, + onClick = onChallengeClick, + ) + } + } +} diff --git a/feature-ui-detail/src/main/kotlin/team/duckie/app/android/feature/ui/detail/common/DetailContent.kt b/feature-ui-detail/src/main/kotlin/team/duckie/app/android/feature/ui/detail/common/DetailContent.kt new file mode 100644 index 000000000..ab809f74e --- /dev/null +++ b/feature-ui-detail/src/main/kotlin/team/duckie/app/android/feature/ui/detail/common/DetailContent.kt @@ -0,0 +1,230 @@ +/* + * Designed and developed by Duckie Team, 2022 + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE + */ + +package team.duckie.app.android.feature.ui.detail.common + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import team.duckie.app.android.feature.ui.detail.R +import team.duckie.app.android.feature.ui.detail.viewmodel.state.DetailState +import team.duckie.app.android.shared.ui.compose.DefaultProfile +import team.duckie.app.android.util.compose.GetHeightRatioW328H240 +import team.duckie.quackquack.ui.color.QuackColor +import team.duckie.quackquack.ui.component.QuackBody2 +import team.duckie.quackquack.ui.component.QuackBody3 +import team.duckie.quackquack.ui.component.QuackDivider +import team.duckie.quackquack.ui.component.QuackHeadLine2 +import team.duckie.quackquack.ui.component.QuackImage +import team.duckie.quackquack.ui.component.QuackSingeLazyRowTag +import team.duckie.quackquack.ui.component.QuackTagType +import team.duckie.quackquack.ui.icon.QuackIcon +import team.duckie.quackquack.ui.modifier.quackClickable +import team.duckie.quackquack.ui.shape.SquircleShape + +/** 상세 화면 컨텐츠 Layout */ +@Suppress("MagicNumber") +@Composable +internal fun DetailContentLayout( + modifier: Modifier = Modifier, + state: DetailState.Success, + tagItemClick: (String) -> Unit, + moreButtonClick: () -> Unit, + followButtonClick: () -> Unit, + profileClick: (Int) -> Unit, + additionalInfo: (@Composable () -> Unit), +) { + Column( + modifier = modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()), + ) { + // 그림 + AsyncImage( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(ratio = GetHeightRatioW328H240) + .padding( + top = 16.dp, + start = 16.dp, + end = 16.dp, + ) + .clip(RoundedCornerShape(size = 8.dp)), + model = state.exam.thumbnailUrl, + contentDescription = null, + contentScale = ContentScale.FillBounds, + ) + // 공백 + Spacer(modifier = Modifier.height(12.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + // 제목 + QuackHeadLine2(text = state.exam.title) + // 더보기 아이콘 + QuackImage( + src = QuackIcon.More, + size = DpSize(width = 24.dp, height = 24.dp), + onClick = moreButtonClick, + ) + } + + // 공백 + Spacer(modifier = Modifier.height(8.dp)) + // 내용 + state.exam.description?.run { + QuackBody2( + modifier = Modifier.padding(horizontal = 16.dp), + text = this, + ) + } + // 공백 + Spacer(modifier = Modifier.height(12.dp)) + // 태그 목록 + QuackSingeLazyRowTag( + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 4.dp), + horizontalSpace = 4.dp, + items = state.tagNames, + tagType = QuackTagType.Grayscale(""), + onClick = { index -> tagItemClick(state.tagNames[index]) }, + ) + // 공백 + Spacer(modifier = Modifier.height(24.dp)) + // 구분선 + QuackDivider() + // 프로필 Layout + DetailProfileLayout( + state = state, + followButtonClick = followButtonClick, + profileClick = profileClick, + ) + // 구분선 + QuackDivider() + additionalInfo() + // 점수 분포도 Layout + // TODO(riflockle7): 기획 정해질 시 활성화 + // DetailScoreDistributionLayout(state) + } +} + +/** + * 상세 화면 프로필 Layout + * TODO(riflockle7): 추후 공통화하기 + */ +@Composable +private fun DetailProfileLayout( + state: DetailState.Success, + followButtonClick: () -> Unit, + profileClick: (Int) -> Unit, +) { + val isFollowed = remember(state.isFollowing) { state.isFollowing } + + Row( + modifier = Modifier.padding( + horizontal = 16.dp, + vertical = 12.dp, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + // 작성자 프로필 이미지 + if (state.profileImageUrl.isNotEmpty()) { + QuackImage( + src = state.profileImageUrl, + shape = SquircleShape, + size = DpSize(32.dp, 32.dp), + ) + } else { + Image( + modifier = Modifier + .size(DpSize(32.dp, 32.dp)) + .clip(SquircleShape), + painter = painterResource(id = QuackIcon.DefaultProfile), + contentDescription = null, + ) + } + + // 공백 + Spacer(modifier = Modifier.width(8.dp)) + // 닉네임, 응시자, 일자 Layout + Column( + modifier = Modifier.quackClickable( + onClick = { profileClick(state.exam.user?.id ?: 0) }, + rippleEnabled = false, + ), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + // 댓글 작성자 닉네임 + QuackBody3( + text = state.nickname, + color = QuackColor.Black, + ) + + // 공백 + Spacer(modifier = Modifier.width(4.dp)) + } + + // 공백 + Spacer(modifier = Modifier.height(2.dp)) + + // 덕티어 + 퍼센트, 태그 + QuackBody3( + text = stringResource( + R.string.detail_tier_tag, + state.exam.user?.duckPower?.tier ?: "", + state.exam.user?.duckPower?.tag?.name ?: "", + ), + color = QuackColor.Gray2, + ) + } + // 공백 + Spacer(modifier = Modifier.weight(1f)) + + // 팔로우 버튼 + QuackBody2( + padding = PaddingValues( + top = 8.dp, + bottom = 8.dp, + ), + text = stringResource( + if (isFollowed) { + R.string.detail_following + } else { + R.string.detail_follow + }, + ), + color = if (isFollowed) QuackColor.Gray2 else QuackColor.DuckieOrange, + onClick = followButtonClick, + ) + } +} diff --git a/feature-ui-detail/src/main/kotlin/team/duckie/app/android/feature/ui/detail/common/TopCustomBar.kt b/feature-ui-detail/src/main/kotlin/team/duckie/app/android/feature/ui/detail/common/TopCustomBar.kt new file mode 100644 index 000000000..33b5fd363 --- /dev/null +++ b/feature-ui-detail/src/main/kotlin/team/duckie/app/android/feature/ui/detail/common/TopCustomBar.kt @@ -0,0 +1,63 @@ +/* + * Designed and developed by Duckie Team, 2022 + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE + */ + +package team.duckie.app.android.feature.ui.detail.common + +import android.app.Activity +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import team.duckie.app.android.feature.ui.detail.viewmodel.state.DetailState +import team.duckie.quackquack.ui.color.QuackColor +import team.duckie.quackquack.ui.component.QuackCircleTag +import team.duckie.quackquack.ui.component.QuackImage +import team.duckie.quackquack.ui.icon.QuackIcon + +/** 상세 화면에서 사용하는 TopAppBar */ +@Composable +internal fun TopAppCustomBar( + modifier: Modifier, + state: DetailState.Success, + onTagClick: (String) -> Unit, +) { + val activity = LocalContext.current as Activity + Row( + modifier = modifier + .fillMaxWidth() + .background(QuackColor.White.composeColor) + .padding( + vertical = 6.dp, + horizontal = 10.dp, + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + QuackImage( + padding = PaddingValues(6.dp), + src = QuackIcon.ArrowBack, + size = DpSize(24.dp, 24.dp), + rippleEnabled = false, + onClick = { activity.finish() }, + ) + + QuackCircleTag( + text = state.mainTagNames, + trailingIcon = QuackIcon.ArrowRight, + isSelected = false, + onClick = { onTagClick(state.mainTagNames) }, + ) + } +} diff --git a/feature-ui-detail/src/main/kotlin/team/duckie/app/android/feature/ui/detail/screen/DetailScreen.kt b/feature-ui-detail/src/main/kotlin/team/duckie/app/android/feature/ui/detail/screen/DetailScreen.kt index 3d54c8965..348131d18 100644 --- a/feature-ui-detail/src/main/kotlin/team/duckie/app/android/feature/ui/detail/screen/DetailScreen.kt +++ b/feature-ui-detail/src/main/kotlin/team/duckie/app/android/feature/ui/detail/screen/DetailScreen.kt @@ -5,29 +5,12 @@ * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE */ -@file:AllowMagicNumber @file:OptIn(ExperimentalMaterialApi::class) package team.duckie.app.android.feature.ui.detail.screen -import android.app.Activity -import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.rememberModalBottomSheetState @@ -37,59 +20,26 @@ 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.draw.clip -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.layoutId import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage import kotlinx.coroutines.launch import org.orbitmvi.orbit.compose.collectAsState -import team.duckie.app.android.feature.ui.detail.R +import team.duckie.app.android.feature.ui.detail.common.DetailBottomLayout +import team.duckie.app.android.feature.ui.detail.common.TopAppCustomBar +import team.duckie.app.android.feature.ui.detail.screen.exam.ExamDetailContentLayout +import team.duckie.app.android.feature.ui.detail.screen.quiz.QuizDetailContentLayout import team.duckie.app.android.feature.ui.detail.viewmodel.DetailViewModel import team.duckie.app.android.feature.ui.detail.viewmodel.state.DetailState -import team.duckie.app.android.shared.ui.compose.DefaultProfile import team.duckie.app.android.shared.ui.compose.ErrorScreen import team.duckie.app.android.shared.ui.compose.LoadingScreen -import team.duckie.app.android.shared.ui.compose.Spacer import team.duckie.app.android.shared.ui.compose.dialog.ReportBottomSheetDialog import team.duckie.app.android.shared.ui.compose.dialog.ReportDialog import team.duckie.app.android.util.android.network.NetworkUtil -import team.duckie.app.android.util.compose.GetHeightRatioW328H240 import team.duckie.app.android.util.compose.activityViewModel -import team.duckie.app.android.util.compose.asLoose -import team.duckie.app.android.util.kotlin.AllowMagicNumber -import team.duckie.app.android.util.kotlin.fastFirstOrNull -import team.duckie.app.android.util.kotlin.npe -import team.duckie.app.android.util.kotlin.percents import team.duckie.quackquack.ui.color.QuackColor -import team.duckie.quackquack.ui.component.QuackBody2 -import team.duckie.quackquack.ui.component.QuackBody3 -import team.duckie.quackquack.ui.component.QuackCircleTag -import team.duckie.quackquack.ui.component.QuackDivider -import team.duckie.quackquack.ui.component.QuackHeadLine2 -import team.duckie.quackquack.ui.component.QuackImage -import team.duckie.quackquack.ui.component.QuackSingeLazyRowTag -import team.duckie.quackquack.ui.component.QuackSmallButton -import team.duckie.quackquack.ui.component.QuackSmallButtonType -import team.duckie.quackquack.ui.component.QuackTagType -import team.duckie.quackquack.ui.component.internal.QuackText -import team.duckie.quackquack.ui.icon.QuackIcon -import team.duckie.quackquack.ui.modifier.quackClickable -import team.duckie.quackquack.ui.shape.SquircleShape -import team.duckie.quackquack.ui.textstyle.QuackTextStyle -private const val DetailScreenTopAppBarLayoutId = "DetailScreenTopAppBar" -private const val DetailScreenContentLayoutId = "DetailScreenContent" -private const val DetailScreenBottomBarLayoutId = "DetailScreenBottomBar" - -/** 상세 화면 Screen */ @Composable internal fun DetailScreen( modifier: Modifier, @@ -133,7 +83,7 @@ internal fun DetailScreen( }, onReport = { viewModel.report(state.exam.id) }, ) { - DetailSuccessScreen( + ExamDetailScreen( modifier = modifier, viewModel = viewModel, openBottomSheet = { @@ -156,7 +106,7 @@ internal fun DetailScreen( /** 데이터 성공적으로 받은[DetailState.Success] 상세 화면 */ @Composable -private fun DetailSuccessScreen( +internal fun ExamDetailScreen( viewModel: DetailViewModel, modifier: Modifier, openBottomSheet: () -> Unit, @@ -171,358 +121,43 @@ private fun DetailSuccessScreen( state = state, onTagClick = viewModel::goToSearch, ) - // content Layout - DetailContentLayout( - state = state, - tagItemClick = viewModel::goToSearch, - moreButtonClick = openBottomSheet, - followButtonClick = viewModel::followUser, - profileClick = viewModel::goToProfile, - ) + when (state.isQuiz) { + true -> { + QuizDetailContentLayout( + modifier = Modifier.layoutId(DetailScreenContentLayoutId), + state = state, + tagItemClick = viewModel::goToSearch, + moreButtonClick = openBottomSheet, + followButtonClick = viewModel::followUser, + profileClick = viewModel::goToProfile, + ) + } + + false -> { + ExamDetailContentLayout( + modifier = Modifier.layoutId(DetailScreenContentLayoutId), + state = state, + tagItemClick = viewModel::goToSearch, + moreButtonClick = openBottomSheet, + followButtonClick = viewModel::followUser, + profileClick = viewModel::goToProfile, + ) + } + } // 최하단 Layout DetailBottomLayout( modifier = Modifier .layoutId(DetailScreenBottomBarLayoutId) .background(color = QuackColor.White.composeColor), - state, + state = state, onHeartClick = viewModel::heartExam, onChallengeClick = viewModel::startExam, ) }, - ) { measurableItems, constraints -> - // 1. topAppBar, bottomBar 높이값 측정 - val looseConstraints = constraints.asLoose() - - val topAppBarMeasurable = measurableItems.fastFirstOrNull { measureItem -> - measureItem.layoutId == DetailScreenTopAppBarLayoutId - }?.measure(looseConstraints) ?: npe() - val topAppBarHeight = topAppBarMeasurable.height - - val bottomBarMeasurable = measurableItems.fastFirstOrNull { measurable -> - measurable.layoutId == DetailScreenBottomBarLayoutId - }?.measure(looseConstraints) ?: npe() - val bottomBarHeight = bottomBarMeasurable.height - - // 2. content 제약 설정 및 content 높이값 측정 - val contentHeight = constraints.maxHeight - topAppBarHeight - bottomBarHeight - val contentConstraints = constraints.copy( - minHeight = contentHeight, - maxHeight = contentHeight, - ) - val contentMeasurable = measurableItems.fastFirstOrNull { measurable -> - measurable.layoutId == DetailScreenContentLayoutId - }?.measure(contentConstraints) ?: npe() - - // 3. 위에서 추출한 값들을 활용해 레이아웃 위치 처리 - layout( - width = constraints.maxWidth, - height = constraints.maxHeight, - ) { - topAppBarMeasurable.place( - x = 0, - y = 0, - ) - contentMeasurable.place( - x = 0, - y = topAppBarHeight, - ) - bottomBarMeasurable.place( - x = 0, - y = topAppBarHeight + contentHeight, - ) - } - } -} - -/** 상세 화면 컨텐츠 Layout */ -@Suppress("MagicNumber") -@Composable -private fun DetailContentLayout( - state: DetailState.Success, - tagItemClick: (String) -> Unit, - moreButtonClick: () -> Unit, - followButtonClick: () -> Unit, - profileClick: (Int) -> Unit, -) { - Column( - modifier = Modifier - .layoutId(DetailScreenContentLayoutId) - .fillMaxWidth() - .verticalScroll(rememberScrollState()), - ) { - // 그림 - AsyncImage( - modifier = Modifier - .fillMaxWidth() - .aspectRatio(ratio = GetHeightRatioW328H240) - .padding( - top = 16.dp, - start = 16.dp, - end = 16.dp, - ) - .clip(RoundedCornerShape(size = 8.dp)), - model = state.exam.thumbnailUrl, - contentDescription = null, - contentScale = ContentScale.FillBounds, - ) - // 공백 - Spacer(modifier = Modifier.height(12.dp)) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - // 제목 - QuackHeadLine2(text = state.exam.title) - // 더보기 아이콘 - QuackImage( - src = QuackIcon.More, - size = DpSize(width = 24.dp, height = 24.dp), - onClick = moreButtonClick, - ) - } - - // 공백 - Spacer(modifier = Modifier.height(8.dp)) - // 내용 - state.exam.description?.run { - QuackBody2( - modifier = Modifier.padding(horizontal = 16.dp), - text = this, - ) - } - // 공백 - Spacer(modifier = Modifier.height(12.dp)) - // 태그 목록 - QuackSingeLazyRowTag( - contentPadding = PaddingValues(horizontal = 16.dp, vertical = 4.dp), - horizontalSpace = 4.dp, - items = state.tagNames, - tagType = QuackTagType.Grayscale(""), - onClick = { index -> tagItemClick(state.tagNames[index]) }, - ) - // 공백 - Spacer(modifier = Modifier.height(24.dp)) - // 구분선 - QuackDivider() - // 프로필 Layout - DetailProfileLayout( - state = state, - followButtonClick = followButtonClick, - profileClick = profileClick, - ) - // 구분선 - QuackDivider() - // 공백 - Spacer(modifier = Modifier.height(24.dp)) - // 점수 분포도 Layout - // TODO(riflockle7): 기획 정해질 시 활성화 - // DetailScoreDistributionLayout(state) - } -} - -/** - * 상세 화면 프로필 Layout - * TODO(riflockle7): 추후 공통화하기 - */ -@Composable -private fun DetailProfileLayout( - state: DetailState.Success, - followButtonClick: () -> Unit, - profileClick: (Int) -> Unit, -) { - val isFollowed = remember(state.isFollowing) { state.isFollowing } - - Row( - modifier = Modifier.padding( - horizontal = 16.dp, - vertical = 12.dp, + measurePolicy = screenMeasurePolicy( + topLayoutId = DetailScreenTopAppBarLayoutId, + contentLayoutId = DetailScreenContentLayoutId, + bottomLayoutId = DetailScreenBottomBarLayoutId, ), - verticalAlignment = Alignment.CenterVertically, - ) { - // 작성자 프로필 이미지 - if (state.profileImageUrl.isNotEmpty()) { - QuackImage( - src = state.profileImageUrl, - shape = SquircleShape, - size = DpSize(32.dp, 32.dp), - ) - } else { - Image( - modifier = Modifier - .size(DpSize(32.dp, 32.dp)) - .clip(SquircleShape), - painter = painterResource(id = QuackIcon.DefaultProfile), - contentDescription = null, - ) - } - - // 공백 - Spacer(modifier = Modifier.width(8.dp)) - // 닉네임, 응시자, 일자 Layout - Column( - modifier = Modifier.quackClickable( - onClick = { profileClick(state.exam.user?.id ?: 0) }, - rippleEnabled = false, - ), - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - // 댓글 작성자 닉네임 - QuackBody3( - text = state.nickname, - color = QuackColor.Black, - ) - - // 공백 - Spacer(modifier = Modifier.width(4.dp)) - } - - // 공백 - Spacer(modifier = Modifier.height(2.dp)) - - // 덕티어 + 퍼센트, 태그 - QuackBody3( - text = stringResource( - R.string.detail_tier_tag, - state.exam.user?.duckPower?.tier ?: "", - state.exam.user?.duckPower?.tag?.name ?: "", - ), - color = QuackColor.Gray2, - ) - } - // 공백 - Spacer(modifier = Modifier.weight(1f)) - - // 팔로우 버튼 - QuackBody2( - padding = PaddingValues( - top = 8.dp, - bottom = 8.dp, - ), - text = stringResource( - if (isFollowed) { - R.string.detail_following - } else { - R.string.detail_follow - }, - ), - color = if (isFollowed) QuackColor.Gray2 else QuackColor.DuckieOrange, - onClick = followButtonClick, - ) - } -} - -/** 상세 화면 점수 분포도 Layout */ -@Composable -@Suppress("unused") -private fun DetailScoreDistributionLayout(state: DetailState.Success) { - // 제목 Layout - Row( - modifier = Modifier.padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - // 제목 - QuackText(text = "점수 분포도", style = QuackTextStyle.Title2) - // 공백 - Spacer(modifier = Modifier.weight(1f)) - // 정답률 텍스트 - state.exam.answerRate?.percents?.run { - QuackText( - text = stringResource( - R.string.detail_right_percent, - this, - ), - style = QuackTextStyle.Body2, - ) - } - } - // 공백 - Spacer(modifier = Modifier.height(8.dp)) - // 분포도 레퍼 - QuackText( - modifier = Modifier.padding(horizontal = 16.dp), - text = "분포도 레퍼 필요", - style = QuackTextStyle.Body2, ) } - -/** 상세 화면 최하단 Layout */ -@Composable -private fun DetailBottomLayout( - modifier: Modifier, - state: DetailState.Success, - onHeartClick: () -> Unit, - onChallengeClick: () -> Unit, -) { - Column(modifier = modifier) { - Spacer(space = 20.dp) - // 구분선 - QuackDivider() - // 버튼 모음 Layout - // TODO(riflockle7): 추후 Layout 을 활용해 처리하기 - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 9.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - // 좋아요 버튼 - QuackImage( - src = if (state.isHeart) { - team.duckie.quackquack.ui.R.drawable.quack_ic_heart_filled_24 - } else { - team.duckie.quackquack.ui.R.drawable.quack_ic_heart_24 - }, - size = DpSize(24.dp, 24.dp), - onClick = onHeartClick, - ) - - // 버튼 - QuackSmallButton( - text = state.buttonTitle, - type = QuackSmallButtonType.Fill, - enabled = true, - onClick = onChallengeClick, - ) - } - } -} - -/** 상세 화면에서 사용하는 TopAppBar */ -@Composable -private fun TopAppCustomBar( - modifier: Modifier, - state: DetailState.Success, - onTagClick: (String) -> Unit, -) { - val activity = LocalContext.current as Activity - Row( - modifier = modifier - .fillMaxWidth() - .background(QuackColor.White.composeColor) - .padding( - vertical = 6.dp, - horizontal = 10.dp, - ), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - QuackImage( - padding = PaddingValues(6.dp), - src = QuackIcon.ArrowBack, - size = DpSize(24.dp, 24.dp), - rippleEnabled = false, - onClick = { activity.finish() }, - ) - - QuackCircleTag( - text = state.mainTagNames, - trailingIcon = QuackIcon.ArrowRight, - isSelected = false, - onClick = { onTagClick(state.mainTagNames) }, - ) - } -} diff --git a/feature-ui-detail/src/main/kotlin/team/duckie/app/android/feature/ui/detail/screen/MeasurePolicy.kt b/feature-ui-detail/src/main/kotlin/team/duckie/app/android/feature/ui/detail/screen/MeasurePolicy.kt new file mode 100644 index 000000000..e3afca962 --- /dev/null +++ b/feature-ui-detail/src/main/kotlin/team/duckie/app/android/feature/ui/detail/screen/MeasurePolicy.kt @@ -0,0 +1,66 @@ +/* + * Designed and developed by Duckie Team, 2022 + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE + */ + +package team.duckie.app.android.feature.ui.detail.screen + +import androidx.compose.ui.layout.MeasurePolicy +import androidx.compose.ui.layout.layoutId +import team.duckie.app.android.util.compose.asLoose +import team.duckie.app.android.util.kotlin.fastFirstOrNull +import team.duckie.app.android.util.kotlin.npe + +internal const val DetailScreenTopAppBarLayoutId = "DetailScreenTopAppBar" +internal const val DetailScreenContentLayoutId = "DetailScreenContent" +internal const val DetailScreenBottomBarLayoutId = "DetailScreenBottomBar" + +internal fun screenMeasurePolicy( + topLayoutId: String, + contentLayoutId: String, + bottomLayoutId: String, +) = MeasurePolicy { measurableItems, constraints -> + // 1. topAppBar, bottomBar 높이값 측정 + val looseConstraints = constraints.asLoose() + + val topAppBarMeasurable = measurableItems.fastFirstOrNull { measureItem -> + measureItem.layoutId == topLayoutId + }?.measure(looseConstraints) ?: npe() + val topAppBarHeight = topAppBarMeasurable.height + + val bottomBarMeasurable = measurableItems.fastFirstOrNull { measurable -> + measurable.layoutId == bottomLayoutId + }?.measure(looseConstraints) ?: npe() + val bottomBarHeight = bottomBarMeasurable.height + + // 2. content 제약 설정 및 content 높이값 측정 + val contentHeight = constraints.maxHeight - topAppBarHeight - bottomBarHeight + val contentConstraints = constraints.copy( + minHeight = contentHeight, + maxHeight = contentHeight, + ) + val contentMeasurable = measurableItems.fastFirstOrNull { measurable -> + measurable.layoutId == contentLayoutId + }?.measure(contentConstraints) ?: npe() + + // 3. 위에서 추출한 값들을 활용해 레이아웃 위치 처리 + layout( + width = constraints.maxWidth, + height = constraints.maxHeight, + ) { + topAppBarMeasurable.place( + x = 0, + y = 0, + ) + contentMeasurable.place( + x = 0, + y = topAppBarHeight, + ) + bottomBarMeasurable.place( + x = 0, + y = topAppBarHeight + contentHeight, + ) + } +} diff --git a/feature-ui-detail/src/main/kotlin/team/duckie/app/android/feature/ui/detail/screen/exam/ExamDetailScreen.kt b/feature-ui-detail/src/main/kotlin/team/duckie/app/android/feature/ui/detail/screen/exam/ExamDetailScreen.kt new file mode 100644 index 000000000..ef2bd3d92 --- /dev/null +++ b/feature-ui-detail/src/main/kotlin/team/duckie/app/android/feature/ui/detail/screen/exam/ExamDetailScreen.kt @@ -0,0 +1,38 @@ +/* + * Designed and developed by Duckie Team, 2022 + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE + */ + +@file:AllowMagicNumber + +package team.duckie.app.android.feature.ui.detail.screen.exam + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import team.duckie.app.android.feature.ui.detail.common.DetailContentLayout +import team.duckie.app.android.feature.ui.detail.viewmodel.state.DetailState +import team.duckie.app.android.util.kotlin.AllowMagicNumber + +@Composable +internal fun ExamDetailContentLayout( + modifier: Modifier = Modifier, + state: DetailState.Success, + tagItemClick: (String) -> Unit, + moreButtonClick: () -> Unit, + followButtonClick: () -> Unit, + profileClick: (Int) -> Unit, +) { + DetailContentLayout( + modifier = modifier, + state = state, + tagItemClick = tagItemClick, + moreButtonClick = moreButtonClick, + followButtonClick = followButtonClick, + profileClick = profileClick, + additionalInfo = { + // TODO(EvergreenTree97) 시험 정보 추후 삽입 + }, + ) +} diff --git a/feature-ui-detail/src/main/kotlin/team/duckie/app/android/feature/ui/detail/screen/quiz/QuizDetailScreen.kt b/feature-ui-detail/src/main/kotlin/team/duckie/app/android/feature/ui/detail/screen/quiz/QuizDetailScreen.kt new file mode 100644 index 000000000..97d146687 --- /dev/null +++ b/feature-ui-detail/src/main/kotlin/team/duckie/app/android/feature/ui/detail/screen/quiz/QuizDetailScreen.kt @@ -0,0 +1,168 @@ +/* + * Designed and developed by Duckie Team, 2022 + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE + */ + +package team.duckie.app.android.feature.ui.detail.screen.quiz + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.Divider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import team.duckie.app.android.domain.quiz.model.QuizInfo +import team.duckie.app.android.feature.ui.detail.R +import team.duckie.app.android.feature.ui.detail.common.DetailContentLayout +import team.duckie.app.android.feature.ui.detail.viewmodel.state.DetailState +import team.duckie.app.android.shared.ui.compose.Crown +import team.duckie.app.android.shared.ui.compose.Spacer +import team.duckie.app.android.util.kotlin.fastForEachIndexed +import team.duckie.quackquack.ui.color.QuackColor +import team.duckie.quackquack.ui.component.QuackBody2 +import team.duckie.quackquack.ui.component.QuackImage +import team.duckie.quackquack.ui.component.QuackSubtitle +import team.duckie.quackquack.ui.component.QuackTitle2 +import team.duckie.quackquack.ui.icon.QuackIcon +import team.duckie.quackquack.ui.modifier.quackClickable +import team.duckie.quackquack.ui.shape.SquircleShape +import team.duckie.quackquack.ui.util.DpSize + +@Composable +internal fun QuizDetailContentLayout( + modifier: Modifier = Modifier, + state: DetailState.Success, + tagItemClick: (String) -> Unit, + moreButtonClick: () -> Unit, + followButtonClick: () -> Unit, + profileClick: (Int) -> Unit, +) { + DetailContentLayout( + modifier = modifier, + state = state, + tagItemClick = tagItemClick, + moreButtonClick = moreButtonClick, + followButtonClick = followButtonClick, + profileClick = profileClick, + additionalInfo = { + if (state.exam.quizs.isNullOrEmpty().not()) { + RankingSection( + state = state, + userContentClick = profileClick, + ) + } + }, + ) +} + +@Composable +private fun RankingSection( + state: DetailState.Success, + userContentClick: (Int) -> Unit, +) { + val quizRankings = remember { checkNotNull(state.exam.quizs) } + Column( + modifier = Modifier + .fillMaxSize() + .padding(top = 28.dp), + ) { + QuackTitle2( + modifier = Modifier.padding(horizontal = 16.dp), + text = stringResource(id = R.string.quiz_ranking_title, state.mainTagNames), + ) + Spacer(space = 8.dp) + quizRankings.fastForEachIndexed { index, item -> + RankingContent( + rank = index + 1, + quizInfo = item, + onClick = userContentClick, + ) + } + } +} + +@Composable +private fun RankingContent( + rank: Int, + quizInfo: QuizInfo, + onClick: (Int) -> Unit, +) = with(quizInfo) { + val textColor = if (rank == 1) { + QuackColor.DuckieOrange + } else { + QuackColor.Black + } + + Column { + Row( + modifier = Modifier + .fillMaxWidth() + .quackClickable { + onClick(user.id) + }, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Row( + modifier = Modifier + .padding(vertical = 12.dp) + .padding( + start = 16.dp, + end = 8.dp, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Column( + modifier = Modifier.widthIn(min = 28.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + if (rank == 1) { + QuackImage( + src = QuackIcon.Crown, + size = DpSize(all = 12.dp), + ) + } + QuackSubtitle( + text = "${rank}등", + color = textColor, + ) + } + Spacer(space = 12.dp) + QuackImage( + src = user.profileImageUrl, + shape = SquircleShape, + size = DpSize(all = 44.dp), + ) + Spacer(space = 8.dp) + QuackTitle2( + text = user.nickname, + color = textColor, + ) + } + Row( + modifier = Modifier + .padding(vertical = 12.dp) + .padding(end = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + user.duckPower?.tier?.let { + QuackBody2(text = it) + QuackBody2(text = "|") + } + QuackBody2(text = "${time}초") + } + } + Divider(color = QuackColor.Gray4.composeColor) + } +} diff --git a/feature-ui-detail/src/main/kotlin/team/duckie/app/android/feature/ui/detail/viewmodel/state/DetailState.kt b/feature-ui-detail/src/main/kotlin/team/duckie/app/android/feature/ui/detail/viewmodel/state/DetailState.kt index 352704a72..7e4e34f31 100644 --- a/feature-ui-detail/src/main/kotlin/team/duckie/app/android/feature/ui/detail/viewmodel/state/DetailState.kt +++ b/feature-ui-detail/src/main/kotlin/team/duckie/app/android/feature/ui/detail/viewmodel/state/DetailState.kt @@ -59,6 +59,9 @@ sealed class DetailState { val isHeart: Boolean get() = exam.heart?.id != null + + val isQuiz: Boolean + get() = exam.quizs != null } /** diff --git a/feature-ui-detail/src/main/res/values/strings.xml b/feature-ui-detail/src/main/res/values/strings.xml index b05706bdf..95ac13fa6 100644 --- a/feature-ui-detail/src/main/res/values/strings.xml +++ b/feature-ui-detail/src/main/res/values/strings.xml @@ -17,4 +17,5 @@ 신고하기 확인 + %s 덕퀴즈 랭킹 diff --git a/shared-ui-compose/src/main/kotlin/team/duckie/app/android/shared/ui/compose/DuckieIcon.kt b/shared-ui-compose/src/main/kotlin/team/duckie/app/android/shared/ui/compose/DuckieIcon.kt index 18138697f..121630fbd 100644 --- a/shared-ui-compose/src/main/kotlin/team/duckie/app/android/shared/ui/compose/DuckieIcon.kt +++ b/shared-ui-compose/src/main/kotlin/team/duckie/app/android/shared/ui/compose/DuckieIcon.kt @@ -13,3 +13,5 @@ val QuackIcon.Companion.DefaultProfile get() = R.drawable.ic_default_profile val QuackIcon.Companion.Notice get() = R.drawable.ic_notice_24 val QuackIcon.Companion.Create get() = R.drawable.ic_create_24 + +val QuackIcon.Companion.Crown get() = R.drawable.ic_crown_12 diff --git a/shared-ui-compose/src/main/res/drawable/ic_crown_12.xml b/shared-ui-compose/src/main/res/drawable/ic_crown_12.xml new file mode 100644 index 000000000..dc019a517 --- /dev/null +++ b/shared-ui-compose/src/main/res/drawable/ic_crown_12.xml @@ -0,0 +1,12 @@ + + +