diff --git a/app/src/main/java/com/poti/android/core/common/extension/IntExt.kt b/app/src/main/java/com/poti/android/core/common/extension/IntExt.kt new file mode 100644 index 00000000..62e5385a --- /dev/null +++ b/app/src/main/java/com/poti/android/core/common/extension/IntExt.kt @@ -0,0 +1,8 @@ +package com.poti.android.core.common.extension + +import java.text.DecimalFormat + +fun Int.toMoneyString(): String { + val decimalFormat = DecimalFormat("#,###") + return decimalFormat.format(this) +} diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/display/PotiProfileSummary.kt b/app/src/main/java/com/poti/android/core/designsystem/component/display/PotiProfileSummary.kt index f7c873b2..bf32ecc9 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/display/PotiProfileSummary.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/display/PotiProfileSummary.kt @@ -8,20 +8,18 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import coil.compose.SubcomposeAsyncImage +import coil.compose.AsyncImage import com.poti.android.R import com.poti.android.core.designsystem.theme.PotiTheme import com.poti.android.core.designsystem.theme.PotiTheme.colors @@ -34,7 +32,7 @@ enum class PotiProfileSummarySize(val profilePicSize: Dp) { @Composable fun PotiProfileSummary( - profileImageUrl: String, + profileImageUrl: String?, nickname: String, sizeType: PotiProfileSummarySize, rating: String, @@ -46,25 +44,15 @@ fun PotiProfileSummary( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, ) { - SubcomposeAsyncImage( + AsyncImage( model = profileImageUrl, contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier .size(sizeType.profilePicSize) .clip(RoundedCornerShape(99.dp)), - // TODO: [천민재] 에셋 추가시 구현 - loading = { - // TODO: [천민재] 임시 이미지 - Icon( - painter = painterResource(id = R.drawable.ic_member), - tint = Color.Black, - contentDescription = null, - ) - }, - // TODO: [천민재] 에셋 추가시 구현 - error = { - }, + placeholder = painterResource(id = R.drawable.ic_member), + error = painterResource(id = R.drawable.ic_member), ) when (sizeType) { diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/FieldErrorMessage.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/FieldErrorMessage.kt deleted file mode 100644 index 611039e9..00000000 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/FieldErrorMessage.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.poti.android.core.designsystem.component.field - -import androidx.compose.foundation.layout.Row -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import com.poti.android.core.designsystem.theme.PotiTheme - -// TODO: [도연] Display>ErrorMessage 병합 시 삭제 -@Composable -fun FieldErrorMessage( - error: String, - modifier: Modifier = Modifier, -) { - if (error.isNotEmpty()) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = error, - color = PotiTheme.colors.sementicRed, - style = PotiTheme.typography.body14m, - ) - } - } -} diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiBasicField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiBasicField.kt index 31da0faf..a5f5fbd9 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiBasicField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiBasicField.kt @@ -44,7 +44,7 @@ internal fun PotiBasicField( onFocusChanged: (Boolean) -> Unit = {}, focusRequester: FocusRequester? = null, singleLine: Boolean = true, - trailingIcon: @Composable () -> Unit = {}, + trailingIcon: (@Composable () -> Unit)? = null, enabled: Boolean = true, ) { var isFocused by remember { mutableStateOf(false) } @@ -115,7 +115,8 @@ internal fun PotiBasicField( ) } } - trailingIcon() + + trailingIcon?.invoke() } }, ) diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiCountField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiCountField.kt index 6ced36c0..9ed5f226 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiCountField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiCountField.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.poti.android.core.designsystem.component.display.PotiErrorMessage import com.poti.android.core.designsystem.theme.PotiTheme /** @@ -105,8 +106,9 @@ fun PotiCountField( enabled = enabled, ) - // TODO: [도연] Display>ErrorMessage으로 대체 - FieldErrorMessage(error) + if (error.isNotBlank()) { + PotiErrorMessage(message = error) + } } } diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt index eb691b8e..cfc0bd51 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiDropdownField.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.window.PopupProperties import com.poti.android.R +import com.poti.android.core.designsystem.component.display.PotiErrorMessage import com.poti.android.core.designsystem.model.FieldMenuItem import com.poti.android.core.designsystem.theme.PotiTheme @@ -136,8 +137,9 @@ fun PotiDropdownField( enabled = false, ) - // TODO: [도연] Display>errorMessage로 대체 - FieldErrorMessage(error) + if (error.isNotBlank()) { + PotiErrorMessage(message = error) + } } PotiDropdownMenu( diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiLongTextField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiLongTextField.kt index fd7dfe90..38ad695c 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiLongTextField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiLongTextField.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.poti.android.core.designsystem.component.display.PotiErrorMessage import com.poti.android.core.designsystem.theme.PotiTheme /** @@ -84,7 +85,9 @@ fun PotiLongTextField( singleLine = false, ) - FieldErrorMessage(error) + if (error.isNotBlank()) { + PotiErrorMessage(message = error) + } } } diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt index bff7492e..3f72a113 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiSearchField.kt @@ -39,6 +39,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import com.poti.android.R +import com.poti.android.core.designsystem.component.display.PotiErrorMessage import com.poti.android.core.designsystem.model.FieldMenuItem import com.poti.android.core.designsystem.theme.PotiTheme import com.poti.android.core.designsystem.theme.White @@ -178,8 +179,9 @@ fun PotiSearchField( focusRequester = focusRequester, ) - // TODO: [도연] Display>errorMessage로 대체 - FieldErrorMessage(error) + if (error.isNotBlank()) { + PotiErrorMessage(message = error) + } } PotiDropdownMenu( diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiShortTextField.kt b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiShortTextField.kt index 68ee4789..eed781b5 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiShortTextField.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/field/PotiShortTextField.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.poti.android.core.designsystem.component.display.PotiErrorMessage import com.poti.android.core.designsystem.theme.PotiTheme /** @@ -32,6 +33,7 @@ import com.poti.android.core.designsystem.theme.PotiTheme * @param enabled 입력 및 포커스, 터치 이벤트 차단하고 싶다면 false로 설정합니다. 기본값 true입니다. * @param label 필드 상단에 표시됩니다. * @param error 에러가 emptyString이 아닌 경우에만 필드 하단에 표시되며, borderColor가 red로 변경됩니다. + * @param trailingIcon 필드 우측 표시되는 아이콘입니다. * @param imeAction 키보드 액션 타입으로, 기본값은 Done 입니다. Next 설정 시 아래 위치한 필드로 포커스 이동시킬 수 있습니다. * @param focusRequester 필드 포커스를 외부에서 제어하고 싶을 때 사용합니다. */ @@ -45,6 +47,7 @@ fun PotiShortTextField( enabled: Boolean = true, label: String = "", error: String = "", + trailingIcon: (@Composable () -> Unit)? = null, imeAction: ImeAction = ImeAction.Done, focusRequester: FocusRequester? = null, ) { @@ -87,9 +90,12 @@ fun PotiShortTextField( onFocusChanged = { isFocused = it }, focusRequester = focusRequester, singleLine = true, + trailingIcon = trailingIcon, ) - FieldErrorMessage(error) + if (error.isNotBlank()) { + PotiErrorMessage(message = error) + } } } diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/navigation/PotiBottomButton.kt b/app/src/main/java/com/poti/android/core/designsystem/component/navigation/PotiBottomButton.kt index 7b66498a..e8e6ea86 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/navigation/PotiBottomButton.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/navigation/PotiBottomButton.kt @@ -9,6 +9,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.poti.android.core.common.util.screenWidthDp import com.poti.android.core.designsystem.component.button.ActionButtonType import com.poti.android.core.designsystem.component.button.PotiActionButton import com.poti.android.core.designsystem.theme.PotiTheme @@ -24,7 +25,7 @@ fun PotiBottomButton( Row( modifier = modifier .background(PotiTheme.colors.white) - .padding(horizontal = 16.dp) + .padding(horizontal = screenWidthDp(16.dp)) .padding(top = 4.dp, bottom = 16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), ) { diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/navigation/PotiHeaderPage.kt b/app/src/main/java/com/poti/android/core/designsystem/component/navigation/PotiHeaderPage.kt index 07d14ef2..e2a367f3 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/navigation/PotiHeaderPage.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/navigation/PotiHeaderPage.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.em import com.poti.android.R +import com.poti.android.core.common.util.screenWidthDp import com.poti.android.core.designsystem.component.button.PotiIconButton import com.poti.android.core.designsystem.theme.PotiTheme @@ -27,7 +28,7 @@ fun PotiHeaderPage( Row( modifier = modifier .background(PotiTheme.colors.white) - .padding(4.dp), + .padding(horizontal = screenWidthDp(4.dp), vertical = 4.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), ) { diff --git a/app/src/main/java/com/poti/android/core/designsystem/component/navigation/PotiHeaderPrimary.kt b/app/src/main/java/com/poti/android/core/designsystem/component/navigation/PotiHeaderPrimary.kt index aec541e2..f3c16d3b 100644 --- a/app/src/main/java/com/poti/android/core/designsystem/component/navigation/PotiHeaderPrimary.kt +++ b/app/src/main/java/com/poti/android/core/designsystem/component/navigation/PotiHeaderPrimary.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.poti.android.R +import com.poti.android.core.common.util.screenWidthDp import com.poti.android.core.designsystem.component.button.PotiIconButton import com.poti.android.core.designsystem.theme.PotiTheme @@ -33,7 +34,7 @@ fun PotiHeaderPrimary( Row( modifier = modifier .background(PotiTheme.colors.white) - .padding(start = 20.dp, end = 4.dp) + .padding(start = screenWidthDp(20.dp), end = screenWidthDp(4.dp)) .padding(vertical = 4.dp), verticalAlignment = Alignment.CenterVertically, ) { diff --git a/app/src/main/java/com/poti/android/domain/model/delivery/DeliveryOption.kt b/app/src/main/java/com/poti/android/domain/model/delivery/DeliveryOption.kt new file mode 100644 index 00000000..710f8dc4 --- /dev/null +++ b/app/src/main/java/com/poti/android/domain/model/delivery/DeliveryOption.kt @@ -0,0 +1,7 @@ +package com.poti.android.domain.model.delivery + +data class DeliveryOption( + val deliveryId: Long, // 배송 방식 ID + val name: String, // 배송 방식 + val price: Int, // 배송비 +) diff --git a/app/src/main/java/com/poti/android/domain/model/party/Participant.kt b/app/src/main/java/com/poti/android/domain/model/party/Participant.kt new file mode 100644 index 00000000..37007ece --- /dev/null +++ b/app/src/main/java/com/poti/android/domain/model/party/Participant.kt @@ -0,0 +1,9 @@ +package com.poti.android.domain.model.party + +data class Participant( + val userId: Long, // 참여자 유저 ID + val nickname: String, // 참여자 닉네임 + val profileImage: String?, // 참여자 프로필 이미지 (없으면 null) + val rating: Double, // 참여자 평점 + val selectedMembers: List, // 선점한 멤버 목록 +) diff --git a/app/src/main/java/com/poti/android/domain/model/party/PartyDetail.kt b/app/src/main/java/com/poti/android/domain/model/party/PartyDetail.kt new file mode 100644 index 00000000..81472b37 --- /dev/null +++ b/app/src/main/java/com/poti/android/domain/model/party/PartyDetail.kt @@ -0,0 +1,24 @@ +package com.poti.android.domain.model.party + +import com.poti.android.domain.model.delivery.DeliveryOption +import com.poti.android.domain.model.user.UserSummary +import com.poti.android.domain.type.PartyStatusType + +data class PartyDetail( + val postId: Long, // 분철글 고유 ID + val isMyPost: Boolean, // 본인 작성 글 여부 + val status: PartyStatusType, // 모집 상태 + val artist: String, // 아티스트 그룹명 + val artistId: Long, // 아티스트 아이디 + val title: String, // 분철글 제목 + val price: Int, // 1인당 가격 (원) + val uploadTime: String, // 업로드 시간 (예: "4시간 전") + val deadline: String, // 모집 마감일 + val images: List, // 상품 이미지 리스트 + val content: String, // 분철글 본문 내용 + val deliveryOptions: List, // 배송 방법 리스트 + val userSummary: UserSummary, // 총대(작성자) 정보 + val currentCount: Int, // 현재 참여 인원 수 + val totalCount: Int, // 총 모집 인원 수 + val participants: List, // 참여자 정보 리스트 +) diff --git a/app/src/main/java/com/poti/android/domain/model/party/PartyImage.kt b/app/src/main/java/com/poti/android/domain/model/party/PartyImage.kt new file mode 100644 index 00000000..5b4ebe02 --- /dev/null +++ b/app/src/main/java/com/poti/android/domain/model/party/PartyImage.kt @@ -0,0 +1,6 @@ +package com.poti.android.domain.model.party + +data class PartyImage( + val sortOrder: Int, // 이미지 노출 순서 + val imageUrl: String, // 이미지 S3 URL +) diff --git a/app/src/main/java/com/poti/android/domain/model/user/UserSummary.kt b/app/src/main/java/com/poti/android/domain/model/user/UserSummary.kt new file mode 100644 index 00000000..30acf1bf --- /dev/null +++ b/app/src/main/java/com/poti/android/domain/model/user/UserSummary.kt @@ -0,0 +1,9 @@ +package com.poti.android.domain.model.user + +data class UserSummary( + val userId: Long, // 총대 유저 ID + val nickname: String, // 총대 닉네임 + val profileImage: String?, // 총대 프로필 이미지 (없으면 null) + val rating: Double, // 총대 매너 온도/평점 + val reviewCount: Int, // 총대가 받은 거래 후기 수 +) diff --git a/app/src/main/java/com/poti/android/domain/type/PartyStatusType.kt b/app/src/main/java/com/poti/android/domain/type/PartyStatusType.kt new file mode 100644 index 00000000..36e440ef --- /dev/null +++ b/app/src/main/java/com/poti/android/domain/type/PartyStatusType.kt @@ -0,0 +1,10 @@ +package com.poti.android.domain.type + +enum class PartyStatusType { + RECRUITING, // 모집중 + CLOSED, // 모집 완료 + PAYMENT_DONE, // 입금완료 + SHIPPING, // 배송시작 + DELIVERED, // 배송완료 + COMPLETED, // 거래 완료 +} diff --git a/app/src/main/java/com/poti/android/presentation/history/component/HistoryCardItem.kt b/app/src/main/java/com/poti/android/presentation/history/component/HistoryCardItem.kt new file mode 100644 index 00000000..e044956d --- /dev/null +++ b/app/src/main/java/com/poti/android/presentation/history/component/HistoryCardItem.kt @@ -0,0 +1,177 @@ +package com.poti.android.presentation.history.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight +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.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +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.text.TextStyle +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.poti.android.R +import com.poti.android.core.common.extension.noRippleClickable +import com.poti.android.core.designsystem.theme.PotiTheme +import com.poti.android.core.designsystem.theme.PotiTheme.colors +import com.poti.android.core.designsystem.theme.PotiTheme.typography + +enum class CardHistorySize { + SMALL, + LARGE, +} + +val CardHistorySize.artistStyle: TextStyle + @Composable get() = when (this) { + CardHistorySize.SMALL -> typography.caption12m + CardHistorySize.LARGE -> typography.body14m + } + +val CardHistorySize.titleStyle: TextStyle + @Composable get() = when (this) { + CardHistorySize.SMALL -> typography.body14m + CardHistorySize.LARGE -> typography.body16m + } + +@Composable +fun HistoryCardItem( + sizeType: CardHistorySize, + imageUrl: String, + artist: String, + title: String, + participantStageType: ParticipantStateLabelStage, + participantStatusType: ParticipantStateLabelStatus, + onClick: () -> Unit, + modifier: Modifier = Modifier, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, +) { + val isPressed by interactionSource.collectIsPressedAsState() + + Row( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .background(if (isPressed) colors.gray100 else colors.white) + .noRippleClickable( + interactionSource = interactionSource, + onClick = onClick, + ) + .padding(8.dp) + .height(IntrinsicSize.Min), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + AsyncImage( + model = imageUrl, + contentDescription = null, + modifier = Modifier + .fillMaxHeight() + .aspectRatio(1f) + .clip(RoundedCornerShape(8.dp)), + contentScale = ContentScale.Crop, + ) + + Column( + modifier = Modifier + .weight(1f) + .padding( + vertical = + if (sizeType == CardHistorySize.SMALL) 2.dp else 4.dp, + ), + ) { + Text( + text = artist, + style = sizeType.artistStyle, + color = colors.gray800, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + if (sizeType == CardHistorySize.SMALL) { + Spacer(modifier = Modifier.height(2.dp)) + } + + Text( + text = title, + style = sizeType.titleStyle, + color = colors.black, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + Spacer( + modifier = Modifier.height( + if (sizeType == CardHistorySize.SMALL) { + 12.dp + } else { + 22.dp + }, + ), + ) + + HistoryParticipantStateLabel( + sizeType = ParticipantStateLabelSize.SMALL, + stageType = participantStageType, + statusType = participantStatusType, + ) + } + + Icon( + painter = painterResource(id = R.drawable.ic_arrow_right_lg), + contentDescription = null, + tint = colors.gray700, + ) + } +} + +@Preview +@Composable +private fun HistoryCardItemPreview() { + PotiTheme { + Column( + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + HistoryCardItem( + modifier = Modifier.width(344.dp), + sizeType = CardHistorySize.SMALL, + imageUrl = "", + artist = "ive(아이브)", + title = "러브다이브 위드뮤", + participantStageType = ParticipantStateLabelStage.DELIVERY, + participantStatusType = ParticipantStateLabelStatus.WAIT, + onClick = {}, + ) + + HistoryCardItem( + modifier = Modifier.width(344.dp), + sizeType = CardHistorySize.LARGE, + imageUrl = "", + artist = "ive(아이브)", + title = "러브다이브 위드뮤", + participantStageType = ParticipantStateLabelStage.DEPOSIT, + participantStatusType = ParticipantStateLabelStatus.DONE, + onClick = {}, + ) + } + } +} diff --git a/app/src/main/java/com/poti/android/presentation/history/component/HistoryParticipantDetail.kt b/app/src/main/java/com/poti/android/presentation/history/component/HistoryParticipantDetail.kt new file mode 100644 index 00000000..34c87866 --- /dev/null +++ b/app/src/main/java/com/poti/android/presentation/history/component/HistoryParticipantDetail.kt @@ -0,0 +1,307 @@ +package com.poti.android.presentation.history.component + +import androidx.annotation.StringRes +import androidx.compose.foundation.background +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.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.poti.android.R +import com.poti.android.core.common.extension.toMoneyString +import com.poti.android.core.designsystem.component.button.PotiInlineButton +import com.poti.android.core.designsystem.component.display.PotiDivider +import com.poti.android.core.designsystem.component.display.PotiDividerStyle +import com.poti.android.core.designsystem.component.display.PotiItemOption +import com.poti.android.core.designsystem.component.display.PotiItemOptionSize +import com.poti.android.core.designsystem.component.display.PotiItemOptionType +import com.poti.android.core.designsystem.component.display.PotiListOptionPrice +import com.poti.android.core.designsystem.component.display.PotiListOptionPriceSize +import com.poti.android.core.designsystem.theme.PotiTheme +import com.poti.android.core.designsystem.theme.PotiTheme.colors +import com.poti.android.core.designsystem.theme.PotiTheme.typography + +data class DepositItem( + val type: PotiItemOptionType, + val name: String, + val price: Int, +) + +sealed interface DetailState { + val fields: List> + + @get:StringRes + val buttonLabelId: Int? + val onButtonClick: (() -> Unit)? + + data object Default : DetailState { + override val fields: List> = emptyList() + override val buttonLabelId: Int? = null + override val onButtonClick: (() -> Unit)? = null + } + + data class DepositCheck( + val deposit: String, + override val onButtonClick: () -> Unit, + ) : DetailState { + override val fields = listOf( + FieldType.DEPOSIT to deposit, + ) + override val buttonLabelId: Int = + R.string.history_participant_field_deposit_label + } + + data class Delivery( + val name: String, + val delivery: String, + val contact: String, + override val onButtonClick: () -> Unit, + ) : DetailState { + override val fields = listOf( + FieldType.NAME to name, + FieldType.DELIVERY to delivery, + FieldType.CONTACT to contact, + ) + override val buttonLabelId: Int = + R.string.history_participant_field_delivery_label + } + + data class AfterDelivery( + val name: String, + val delivery: String, + val contact: String, + val invoice: String, + ) : DetailState { + override val fields = listOf( + FieldType.NAME to name, + FieldType.DELIVERY to delivery, + FieldType.CONTACT to contact, + FieldType.INVOICE to invoice, + ) + override val onButtonClick: (() -> Unit)? = null + override val buttonLabelId: Int? = null + } + + data class Finished( + val invoice: String, + ) : DetailState { + override val fields = listOf( + FieldType.INVOICE to invoice, + ) + override val onButtonClick: (() -> Unit)? = null + override val buttonLabelId: Int? = null + } +} + +enum class FieldType( + @StringRes val labelId: Int, +) { + NAME(R.string.history_participant_field_type_name), + DEPOSIT(R.string.history_participant_field_type_deposit), + DELIVERY(R.string.history_participant_field_type_delivery), + CONTACT(R.string.history_participant_field_type_contact), + INVOICE(R.string.history_participant_field_type_invoice), +} + +@Composable +fun HistoryParticipantDetail( + userName: String, + userImageUrl: String, + depositItems: List, + totalPrice: Int, + detailState: DetailState, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .background(colors.gray100) + .padding(16.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + AsyncImage( + model = userImageUrl, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .size(24.dp) + .clip(CircleShape), + ) + + Text( + text = userName, + style = typography.body14m, + color = colors.black, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .weight(1f), + ) + } + + PotiDivider( + styleType = PotiDividerStyle.SMALL, + modifier = Modifier.padding(vertical = 16.dp), + ) + + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text( + text = stringResource(R.string.history_participant_detail_deposit_label), + style = typography.body14sb, + color = colors.black, + ) + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + depositItems.forEach { item -> + PotiListOptionPrice( + itemOptionType = item.type, + itemOptionText = item.name, + priceText = stringResource( + R.string.history_participant_detail_won_unit_format, + item.price.toMoneyString(), + ), + sizeType = PotiListOptionPriceSize.SMALL, + ) + } + + PotiDivider( + styleType = PotiDividerStyle.SMALL, + modifier = Modifier.fillMaxWidth(), + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + PotiItemOption( + optionType = PotiItemOptionType.PRICE, + sizeType = PotiItemOptionSize.SMALL, + text = stringResource(R.string.history_participant_detail_total_deposit_label), + ) + + Text( + text = stringResource( + R.string.history_participant_detail_won_unit_format, + totalPrice.toMoneyString(), + ), + style = typography.body16sb, + color = colors.black, + ) + } + } + } + + if (detailState !is DetailState.Default) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 32.dp), + verticalArrangement = Arrangement.spacedBy(24.dp), + ) { + detailState.fields.forEach { (field, value) -> + Column( + modifier = Modifier + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = stringResource(field.labelId), + style = typography.body14sb, + color = colors.black, + ) + Text( + text = value, + style = typography.body14m, + color = colors.black, + ) + } + } + } + + val onButtonClick = detailState.onButtonClick + val buttonLabelId = detailState.buttonLabelId + if (onButtonClick != null && buttonLabelId != null) { + PotiInlineButton( + text = stringResource(buttonLabelId), + onClick = onButtonClick, + showIcon = false, + modifier = Modifier + .fillMaxWidth(), + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun HistoryParticipantDetailPreview() { + val depositItems = listOf( + DepositItem( + type = PotiItemOptionType.PRICE, + name = "멤버1", + price = 2000000000, + ), + DepositItem( + type = PotiItemOptionType.PRICE, + name = "멤버2멤버2멤버2멤버2멤버2멤버2멤버2멤버2멤버2멤버2", + price = 10000, + ), + DepositItem( + type = PotiItemOptionType.PRICE, + name = "멤버2멤버2멤버2멤버2멤버2멤버2멤버2멤버2멤버2멤버2", + price = 320000000, + ), + DepositItem( + type = PotiItemOptionType.PRICE, + name = "멤버2멤버2멤버2멤버2멤버2멤버2멤버2멤버2멤버2멤버2", + price = 320000000, + ), + DepositItem( + type = PotiItemOptionType.DELIVERY, + name = "등기등기등기등기둥기둥", + price = 320000000, + ), + ) + + PotiTheme { + HistoryParticipantDetail( + userName = "닉네임", + userImageUrl = "", + depositItems = depositItems, + detailState = DetailState.AfterDelivery( + name = "이포티", + delivery = "(01234) 서울특별시 솝트구 다솝로 456", + contact = "010-1111-1111", + invoice = "우체국 37249720348093", + ), + totalPrice = depositItems.sumOf { it.price }, + modifier = Modifier.width(311.dp), + ) + } +} diff --git a/app/src/main/java/com/poti/android/presentation/history/component/HistoryParticipantDropdown.kt b/app/src/main/java/com/poti/android/presentation/history/component/HistoryParticipantDropdown.kt new file mode 100644 index 00000000..235fe759 --- /dev/null +++ b/app/src/main/java/com/poti/android/presentation/history/component/HistoryParticipantDropdown.kt @@ -0,0 +1,179 @@ +package com.poti.android.presentation.history.component + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.Crossfade +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +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.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.poti.android.R +import com.poti.android.core.common.extension.noRippleClickable +import com.poti.android.core.common.util.screenWidthDp +import com.poti.android.core.designsystem.component.display.PotiItemOptionType +import com.poti.android.core.designsystem.theme.PotiTheme + +@Composable +fun HistoryParticipantDropdown( + userName: String, + userImageUrl: String, + depositItems: List, + depositTotalPrice: Int, + detailState: DetailState, + stageType: ParticipantStateLabelStage, + statusType: ParticipantStateLabelStatus, + isExpanded: Boolean, + onToggle: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .background(PotiTheme.colors.white) + .padding( + vertical = 20.dp, + horizontal = screenWidthDp(16.dp), + ), + ) { + ParticipantDropdownHeader( + name = userName, + stageType = stageType, + statusType = statusType, + expanded = isExpanded, + onToggle = { onToggle() }, + ) + AnimatedVisibility(visible = isExpanded) { + HistoryParticipantDetail( + userName = userName, + userImageUrl = userImageUrl, + depositItems = depositItems, + detailState = detailState, + totalPrice = depositTotalPrice, + modifier = Modifier.padding(top = 20.dp), + ) + } + } +} + +@Composable +private fun ParticipantDropdownHeader( + name: String, + stageType: ParticipantStateLabelStage, + statusType: ParticipantStateLabelStatus, + expanded: Boolean, + onToggle: () -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .noRippleClickable(onClick = onToggle), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = name, + style = PotiTheme.typography.body16m, + color = PotiTheme.colors.black, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .weight(1f) + .padding(end = 12.dp), + ) + HistoryParticipantStateLabel( + sizeType = ParticipantStateLabelSize.SMALL, + stageType = stageType, + statusType = statusType, + modifier = Modifier.padding(vertical = 2.dp), + ) + Spacer(modifier = Modifier.width(8.dp)) + + Crossfade( + targetState = expanded, + ) { expand -> + Icon( + painter = painterResource( + id = if (expand) { + R.drawable.ic_arrow_up_lg + } else { + R.drawable.ic_arrow_down_lg + }, + ), + contentDescription = null, + tint = PotiTheme.colors.gray700, + ) + } + } +} + +@Preview +@Composable +fun HistoryParticipantDropdownPreview() { + val depositItems = listOf( + DepositItem( + type = PotiItemOptionType.PRICE, + name = "멤버1", + price = 2000000000, + ), + DepositItem( + type = PotiItemOptionType.PRICE, + name = "멤버2멤버2멤버2멤버2멤버2멤버2멤버2멤버2멤버2멤버2", + price = 10000, + ), + DepositItem( + type = PotiItemOptionType.PRICE, + name = "멤버2멤버2멤버2멤버2멤버2멤버2멤버2멤버2멤버2멤버2", + price = 320000000, + ), + DepositItem( + type = PotiItemOptionType.PRICE, + name = "멤버2멤버2멤버2멤버2멤버2멤버2멤버2멤버2멤버2멤버2", + price = 320000000, + ), + DepositItem( + type = PotiItemOptionType.DELIVERY, + name = "등기등기등기등기둥기둥", + price = 320000000, + ), + ) + var isExpanded by remember { mutableStateOf(false) } + + PotiTheme { + HistoryParticipantDropdown( + userName = "어쩌구저쩌구".repeat(20), + userImageUrl = "", + depositItems = depositItems, + depositTotalPrice = depositItems.sumOf { it.price }, + detailState = DetailState.AfterDelivery( + name = "어쩌구", + delivery = "(01234) 서울특별시 솝트구 다솝로 456", + contact = "010-xxxx-xxxx", + invoice = "우체국 37249720348093", + ), + stageType = ParticipantStateLabelStage.DELIVERY, + statusType = ParticipantStateLabelStatus.DONE, + isExpanded = isExpanded, + onToggle = { isExpanded = !isExpanded }, + modifier = Modifier + .width(375.dp) + .padding(20.dp), + ) + } +} diff --git a/app/src/main/java/com/poti/android/presentation/history/component/HistoryParticipantOverview.kt b/app/src/main/java/com/poti/android/presentation/history/component/HistoryParticipantOverview.kt new file mode 100644 index 00000000..9b48858b --- /dev/null +++ b/app/src/main/java/com/poti/android/presentation/history/component/HistoryParticipantOverview.kt @@ -0,0 +1,114 @@ +package com.poti.android.presentation.history.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.poti.android.R +import com.poti.android.core.common.extension.toMoneyString +import com.poti.android.core.common.util.screenHeightDp +import com.poti.android.core.designsystem.component.display.PotiItemOption +import com.poti.android.core.designsystem.component.display.PotiItemOptionSize +import com.poti.android.core.designsystem.component.display.PotiItemOptionType +import com.poti.android.core.designsystem.theme.PotiTheme +import com.poti.android.core.designsystem.theme.PotiTheme.colors + +@Composable +fun HistoryParticipantOverview( + memberList: String, + userInfo: String, + deliveryMethod: String, + price: Int, + participantStageType: ParticipantStateLabelStage, + participantStatusType: ParticipantStateLabelStatus, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxWidth() + .background(colors.white) + .padding( + horizontal = (16.dp), + vertical = screenHeightDp(16.dp), + ), + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalAlignment = Alignment.Start, + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = memberList, + style = PotiTheme.typography.body16m, + color = colors.black, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + Spacer(Modifier.width(12.dp)) + + HistoryParticipantStateLabel( + sizeType = ParticipantStateLabelSize.SMALL, + stageType = participantStageType, + statusType = participantStatusType, + ) + } + + Text( + text = userInfo, + style = PotiTheme.typography.body14m, + color = colors.gray800, + ) + + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + PotiItemOption( + optionType = PotiItemOptionType.DELIVERY, + sizeType = PotiItemOptionSize.SMALL, + text = deliveryMethod, + ) + PotiItemOption( + optionType = PotiItemOptionType.PRICE, + sizeType = PotiItemOptionSize.SMALL, + text = stringResource( + R.string.history_participant_detail_won_unit_format, + price.toMoneyString(), + ), + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun HistoryParticipantOverviewPreview() { + PotiTheme { + val members = listOf("멤버명", "멤버명", "멤버명", "멤버명", "멤버명", "멤버명", "멤버명") + val userName = "이포티" + val zipcode = "01234" + val address = "서울특별시 솝트구 다솝로 456" + val phone = "010-2345-2345" + + HistoryParticipantOverview( + memberList = members.joinToString(", "), + userInfo = "$userName\n($zipcode) $address\n$phone", + deliveryMethod = "준등기", + price = 12800, + participantStageType = ParticipantStateLabelStage.DEPOSIT, + participantStatusType = ParticipantStateLabelStatus.CHECK, + modifier = Modifier.width(375.dp), + ) + } +} diff --git a/app/src/main/java/com/poti/android/presentation/main/PotiNavHost.kt b/app/src/main/java/com/poti/android/presentation/main/PotiNavHost.kt index 4376b4fb..d39b6fa7 100644 --- a/app/src/main/java/com/poti/android/presentation/main/PotiNavHost.kt +++ b/app/src/main/java/com/poti/android/presentation/main/PotiNavHost.kt @@ -8,7 +8,6 @@ import androidx.navigation.compose.NavHost import com.poti.android.presentation.auth.navigation.authNavGraph import com.poti.android.presentation.history.navigation.historyNavGraph import com.poti.android.presentation.onboarding.navigation.onboardingNavGraph -import com.poti.android.presentation.party.goodsfilter.navigation.navigateToGoodsCategory import com.poti.android.presentation.party.partyNavGraph import com.poti.android.presentation.user.mypage.navigation.myPageNavGraph import com.poti.android.presentation.user.profile.navigation.profileNavGraph @@ -31,8 +30,8 @@ fun PotiNavHost( paddingValues = paddingValues, ) partyNavGraph( + navController = navigator.navController, paddingValues = paddingValues, - onNavigateToGoodsCategory = navigator.navController::navigateToGoodsCategory, ) historyNavGraph( paddingValues = paddingValues, diff --git a/app/src/main/java/com/poti/android/presentation/party/PartyNavigation.kt b/app/src/main/java/com/poti/android/presentation/party/PartyNavigation.kt index e9446072..8005a37d 100644 --- a/app/src/main/java/com/poti/android/presentation/party/PartyNavigation.kt +++ b/app/src/main/java/com/poti/android/presentation/party/PartyNavigation.kt @@ -1,6 +1,7 @@ package com.poti.android.presentation.party import androidx.compose.foundation.layout.PaddingValues +import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.navigation import com.poti.android.core.navigation.Route @@ -15,7 +16,7 @@ import kotlinx.serialization.Serializable object PartyGraph : Route fun NavGraphBuilder.partyNavGraph( - onNavigateToGoodsCategory: () -> Unit, + navController: NavController, paddingValues: PaddingValues, ) { navigation( @@ -23,13 +24,14 @@ fun NavGraphBuilder.partyNavGraph( ) { homeNavGraph( paddingValues = paddingValues, - onNavigateToGoodsCategory = onNavigateToGoodsCategory, + navController = navController, ) goodsFilterNavGraph( paddingValues = paddingValues, ) partyDetailNavGraph( paddingValues = paddingValues, + navController = navController, ) partyCreateNavGraph( paddingValues = paddingValues, diff --git a/app/src/main/java/com/poti/android/presentation/party/create/component/CreatePhotoUpload.kt b/app/src/main/java/com/poti/android/presentation/party/create/component/CreatePhotoUpload.kt new file mode 100644 index 00000000..19340f19 --- /dev/null +++ b/app/src/main/java/com/poti/android/presentation/party/create/component/CreatePhotoUpload.kt @@ -0,0 +1,173 @@ +package com.poti.android.presentation.party.create.component + +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +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.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.poti.android.R +import com.poti.android.core.common.util.screenWidthDp +import com.poti.android.core.designsystem.component.button.DeleteButtonType +import com.poti.android.core.designsystem.component.button.PotiDeleteButton +import com.poti.android.core.designsystem.theme.PotiTheme + +private const val MAX_ITEMS = 5 + +@Composable +fun CreatePhotoUpload( + imageUris: List, + onImageChanged: (List) -> Unit, + modifier: Modifier = Modifier, +) { + val lazyListState = rememberLazyListState() + + val remaining = MAX_ITEMS - imageUris.size + + val photoPickerLauncher = rememberLauncherForActivityResult( + contract = when { + remaining <= 1 -> ActivityResultContracts.PickVisualMedia() + else -> ActivityResultContracts.PickMultipleVisualMedia(remaining.coerceIn(2, 5)) + }, + ) { result -> + when (result) { + is Uri -> { + onImageChanged((imageUris + result).distinct()) + } + + is List<*> -> { + val uris = result.filterIsInstance() + onImageChanged((imageUris + uris).distinct()) + } + } + } + + LazyRow( + state = lazyListState, + modifier = modifier + .padding(vertical = 8.dp), + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + if (imageUris.size < MAX_ITEMS) { + item { + UploadButton( + onClick = { + photoPickerLauncher.launch( + PickVisualMediaRequest.Builder() + .setMediaType(ActivityResultContracts.PickVisualMedia.ImageOnly) + .build(), + ) + }, + ) + } + } + + itemsIndexed(imageUris) { index, uri -> + PhotoItem( + image = uri, + onXClick = { + val newUriList = imageUris.filterIndexed { i, _ -> i != index } + onImageChanged(newUriList) + }, + ) + } + } +} + +@Composable +private fun UploadButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, +) { + Box( + modifier = modifier + .clip(RoundedCornerShape(8.dp)) + .background(PotiTheme.colors.gray100) + .clickable( + onClick = onClick, + enabled = enabled, + ) + .size(screenWidthDp(90.dp)), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_plus), + contentDescription = null, + tint = PotiTheme.colors.gray700, + ) + } +} + +@Composable +private fun PhotoItem( + image: Uri, + onXClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .size(screenWidthDp(90.dp)) + .clip(RoundedCornerShape(8.dp)), + ) { + AsyncImage( + model = image, + contentDescription = null, + modifier = Modifier + .fillMaxSize() + .background(Color.Gray), + contentScale = ContentScale.Crop, + ) + + PotiDeleteButton( + onClick = onXClick, + modifier = Modifier + .align(Alignment.TopEnd) + .offset(x = 8.dp, y = (-8).dp), + type = DeleteButtonType.LIGHT, + ) + } +} + +@Preview +@Composable +private fun CreatePhotoUploadPreview() { + var imageUris by remember { mutableStateOf>((emptyList())) } + + PotiTheme { + CreatePhotoUpload( + imageUris = imageUris, + onImageChanged = { imageUris = it }, + modifier = Modifier + .padding(top = 100.dp), + ) + } +} diff --git a/app/src/main/java/com/poti/android/presentation/party/create/component/EditOptionPrice.kt b/app/src/main/java/com/poti/android/presentation/party/create/component/EditOptionPrice.kt new file mode 100644 index 00000000..d4abc6f7 --- /dev/null +++ b/app/src/main/java/com/poti/android/presentation/party/create/component/EditOptionPrice.kt @@ -0,0 +1,268 @@ +package com.poti.android.presentation.party.create.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.HorizontalDivider +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.focus.FocusDirection +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.rememberTextMeasurer +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.text.isDigitsOnly +import com.poti.android.R +import com.poti.android.core.common.extension.toMoneyString +import com.poti.android.core.designsystem.component.display.PotiCheckBox +import com.poti.android.core.designsystem.theme.PotiTheme + +private const val MAX_LENGTH = 9 + +@Composable +fun EditOptionPrice( + option: String, + value: String, + onValueChanged: (String) -> Unit, + modifier: Modifier = Modifier, + isChecked: Boolean? = null, + onCheckboxClick: (() -> Unit)? = null, + imeAction: ImeAction = ImeAction.Done, +) { + val density = LocalDensity.current + val measurer = rememberTextMeasurer() + + val textStyle = PotiTheme.typography.body16sb + val transformation = remember { PriceVisualTransformation() } + + val transformedText = remember(value) { + transformation.filter(AnnotatedString(value)).text.text + } + + val textWidth = remember(transformedText, textStyle) { + density.run { + measurer.measure(transformedText, textStyle).size.width.toDp() + 2.dp + } + } + + Row( + modifier = modifier.fillMaxWidth(), + ) { + isChecked?.let { + PotiCheckBox( + selected = isChecked, + onClick = onCheckboxClick, + ) + + Spacer(Modifier.width(8.dp)) + } + + Text( + text = option, + modifier = Modifier + .weight(1f), + color = PotiTheme.colors.black, + style = PotiTheme.typography.body16m, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + + Spacer(Modifier.width(12.dp)) + + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalAlignment = Alignment.End, + ) { + OptionTextField( + value = value, + onValueChanged = { newValue -> + if (!newValue.isDigitsOnly()) return@OptionTextField + if (newValue.length > MAX_LENGTH) return@OptionTextField + + onValueChanged(newValue) + }, + imeAction = imeAction, + transformation = transformation, + textStyle = textStyle, + modifier = Modifier.width(textWidth), + ) + + HorizontalDivider( + modifier = Modifier + .widthIn(min = 42.dp) + .width(textWidth) + .clip(CircleShape), + thickness = 2.dp, + color = PotiTheme.colors.gray300, + ) + } + + Spacer(Modifier.width(4.dp)) + + Text( + text = stringResource(R.string.create_label_won), + color = PotiTheme.colors.black, + style = PotiTheme.typography.body16m, + ) + } +} + +@Composable +private fun OptionTextField( + value: String, + onValueChanged: (String) -> Unit, + imeAction: ImeAction, + transformation: VisualTransformation, + textStyle: TextStyle, + modifier: Modifier = Modifier, +) { + val focusManager = LocalFocusManager.current + val keyboardController = LocalSoftwareKeyboardController.current + + BasicTextField( + value = value, + onValueChange = onValueChanged, + modifier = modifier, + textStyle = textStyle.copy( + color = PotiTheme.colors.black, + ), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + imeAction = imeAction, + ), + keyboardActions = KeyboardActions( + onDone = { + focusManager.clearFocus() + keyboardController?.hide() + }, + onNext = { + focusManager.moveFocus( + FocusDirection.Down, + ) + }, + ), + singleLine = true, + visualTransformation = transformation, + decorationBox = { innerTextField -> + innerTextField() + }, + ) +} + +private class PriceVisualTransformation : VisualTransformation { + override fun filter(text: AnnotatedString): TransformedText { + val text = text.text + + val textWithComma = when (text.length) { + 0 -> text + else -> text.toInt().toMoneyString() + } + + val offsetMapping = object : OffsetMapping { + override fun originalToTransformed(offset: Int): Int { + if (offset == text.length) { + return textWithComma.length + } + + val numbersAtferCursor = text.length - offset + + val commasAfterCursor = if (numbersAtferCursor % 3 == 0) { + numbersAtferCursor / 3 - 1 + } else { + numbersAtferCursor / 3 + } + + return textWithComma.length - numbersAtferCursor - commasAfterCursor + } + + override fun transformedToOriginal(offset: Int): Int { + var commasBeforeCursor = 0 + + textWithComma.forEachIndexed { index, char -> + if (index >= offset) return@forEachIndexed + + if (char == ',') { + commasBeforeCursor += 1 + } + } + + return offset - commasBeforeCursor + } + } + + return TransformedText( + text = AnnotatedString(textWithComma), + offsetMapping = offsetMapping, + ) + } +} + +@Preview +@Composable +private fun OptionTextFieldPreview() { + var text1 by remember { mutableStateOf("") } + var text2 by remember { mutableStateOf("") } + var text3 by remember { mutableStateOf("") } + + PotiTheme { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp, vertical = 40.dp), + verticalArrangement = Arrangement.SpaceAround, + ) { + EditOptionPrice( + option = "옵션", + value = text1, + onValueChanged = { text1 = it }, + imeAction = ImeAction.Next, + isChecked = true, + onCheckboxClick = {}, + ) + + EditOptionPrice( + option = "옵션", + value = text2, + onValueChanged = { text2 = it }, + imeAction = ImeAction.Next, + isChecked = false, + onCheckboxClick = {}, + ) + + EditOptionPrice( + option = "옵션".repeat(50), + value = text3, + onValueChanged = { text3 = it }, + imeAction = ImeAction.Done, + ) + } + } +} diff --git a/app/src/main/java/com/poti/android/presentation/party/create/component/HintTooltip.kt b/app/src/main/java/com/poti/android/presentation/party/create/component/HintTooltip.kt new file mode 100644 index 00000000..7e2c84fb --- /dev/null +++ b/app/src/main/java/com/poti/android/presentation/party/create/component/HintTooltip.kt @@ -0,0 +1,130 @@ +package com.poti.android.presentation.party.create.component + +import androidx.annotation.StringRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +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.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntRect +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Popup +import androidx.compose.ui.window.PopupPositionProvider +import androidx.compose.ui.window.PopupProperties +import com.poti.android.R +import com.poti.android.core.common.util.screenHeightDp +import com.poti.android.core.designsystem.component.button.PotiInlineButton +import com.poti.android.core.designsystem.theme.PotiTheme + +private const val VISUAL_GAP_PX = 1 + +@Composable +fun HintToolTip( + modifier: Modifier = Modifier, + @StringRes text: Int = R.string.create_msg_hint, + yOffset: Dp = 9.dp, +) { + val density = LocalDensity.current + + val popupPositionProvider = remember(yOffset, density) { + val offsetYPx = with(density) { yOffset.roundToPx() } + VISUAL_GAP_PX + + object : PopupPositionProvider { + override fun calculatePosition( + anchorBounds: IntRect, + windowSize: IntSize, + layoutDirection: LayoutDirection, + popupContentSize: IntSize, + ): IntOffset { + return IntOffset( + x = (windowSize.width - popupContentSize.width) / 2, + y = anchorBounds.top - popupContentSize.height - offsetYPx, + ) + } + } + } + + val popupProperties = remember { + PopupProperties( + dismissOnBackPress = false, + dismissOnClickOutside = false, + ) + } + + Popup( + popupPositionProvider = popupPositionProvider, + properties = popupProperties, + ) { + Box( + modifier = modifier.fillMaxWidth(), + contentAlignment = Alignment.TopCenter, + ) { + Image( + imageVector = ImageVector.vectorResource(id = R.drawable.img_create_hint), + contentDescription = null, + ) + + Text( + text = stringResource(text), + modifier = Modifier + .padding(top = screenHeightDp(11.dp)), + color = PotiTheme.colors.poti600, + style = PotiTheme.typography.body14sb, + ) + } + } +} + +@Preview +@Composable +private fun HintToolTipPreview() { + var showHint by remember { mutableStateOf(false) } + + PotiTheme { + Column( + modifier = Modifier + .fillMaxSize() + .padding(vertical = 100.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + PotiInlineButton( + text = "힌트 보여주기", + onClick = { showHint = true }, + modifier = Modifier.width(320.dp), + ) + + Box { + PotiInlineButton( + text = "힌트 닫기", + onClick = { showHint = false }, + modifier = Modifier.width(320.dp), + ) + + if (showHint) { + HintToolTip() + } + } + } + } +} diff --git a/app/src/main/java/com/poti/android/presentation/party/detail/DummyPartyDetailData.kt b/app/src/main/java/com/poti/android/presentation/party/detail/DummyPartyDetailData.kt new file mode 100644 index 00000000..99556b6c --- /dev/null +++ b/app/src/main/java/com/poti/android/presentation/party/detail/DummyPartyDetailData.kt @@ -0,0 +1,79 @@ +package com.poti.android.presentation.party.detail + +import com.poti.android.domain.model.delivery.DeliveryOption +import com.poti.android.domain.model.party.Participant +import com.poti.android.domain.model.party.PartyDetail +import com.poti.android.domain.model.party.PartyImage +import com.poti.android.domain.model.user.UserSummary +import com.poti.android.domain.type.PartyStatusType + +val dummyPartyDetail = PartyDetail( + postId = 1, + isMyPost = false, + status = PartyStatusType.RECRUITING, + artist = "IVE(아이브)", + artistId = 1, + title = "러브다이브 위드뮤", + price = 5000, + uploadTime = "4시간 전", + deadline = "2025-12-31", + content = "내용내용내용\n내용내용내용", + images = listOf( + PartyImage( + sortOrder = 1, + imageUrl = "https://i.pinimg.com/736x/ad/6f/c0/ad6fc0da5a240a59524a64f0d168659f.jpg", + ), + PartyImage( + sortOrder = 2, + imageUrl = "https://i.pinimg.com/736x/54/a1/f6/54a1f60741b33e99d574e81ccf4f5b9e.jpg", + ), + PartyImage( + sortOrder = 3, + imageUrl = "https://i.pinimg.com/1200x/61/73/3b/61733b2ae4023c9826ec7d303dab0ba0.jpg", + ), + ), + deliveryOptions = listOf( + DeliveryOption( + deliveryId = 1, + name = "택배", + price = 4000, + ), + DeliveryOption( + deliveryId = 2, + name = "준등기", + price = 1800, + ), + ), + userSummary = UserSummary( + userId = 1, + nickname = "닉네임", + profileImage = null, + rating = 4.8, + reviewCount = 14, + ), + participants = listOf( + Participant( + userId = 1, + nickname = "참여자1", + profileImage = null, + rating = 4.5, + selectedMembers = listOf("원영"), + ), + Participant( + userId = 1, + nickname = "참여자1", + profileImage = null, + rating = 4.5, + selectedMembers = listOf("이서"), + ), + Participant( + userId = 2, + nickname = "참여자2", + profileImage = null, + rating = 3.4, + selectedMembers = listOf("유진"), + ), + ), + currentCount = 2, + totalCount = 5, +) diff --git a/app/src/main/java/com/poti/android/presentation/party/detail/PartyDetailScreen.kt b/app/src/main/java/com/poti/android/presentation/party/detail/PartyDetailScreen.kt index a88f0d19..1b3f3025 100644 --- a/app/src/main/java/com/poti/android/presentation/party/detail/PartyDetailScreen.kt +++ b/app/src/main/java/com/poti/android/presentation/party/detail/PartyDetailScreen.kt @@ -1,15 +1,178 @@ package com.poti.android.presentation.party.detail +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil.compose.AsyncImage +import com.poti.android.R +import com.poti.android.core.common.state.ApiState +import com.poti.android.core.common.util.screenWidthDp +import com.poti.android.core.designsystem.component.display.PotiDivider +import com.poti.android.core.designsystem.component.display.PotiDividerStyle +import com.poti.android.core.designsystem.component.navigation.PotiBottomButton +import com.poti.android.core.designsystem.component.navigation.PotiHeaderPage +import com.poti.android.core.designsystem.theme.PotiTheme +import com.poti.android.domain.model.party.PartyDetail +import com.poti.android.presentation.party.detail.component.PartyDetailContent +import com.poti.android.presentation.party.detail.component.PartyDetailHeaderInfo +import com.poti.android.presentation.party.detail.component.PartyParticipantsInfo +import com.poti.android.presentation.party.detail.component.PartyUploaderInfo +import com.poti.android.presentation.party.detail.model.PartyDetailEffect +import com.poti.android.presentation.party.detail.model.PartyDetailIntent @Composable fun PartyDetailRoute( + onPopBackStack: () -> Unit, + onNavigateToJoin: () -> Unit, + onNavigateToProfile: (Long) -> Unit, modifier: Modifier = Modifier, + viewModel: PartyDetailViewModel = hiltViewModel(), ) { - PartyDetailScreen(modifier = modifier) + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + LaunchedEffect(viewModel.sideEffect) { + viewModel.sideEffect.collect { effect -> + when (effect) { + PartyDetailEffect.NavigateBack -> onPopBackStack() + PartyDetailEffect.NavigateToJoin -> onNavigateToJoin() + is PartyDetailEffect.NavigateToProfile -> onNavigateToProfile(effect.userId) + } + } + } + + when (val state = uiState.partyDetail) { + is ApiState.Success -> { + PartyDetailScreen( + partyDetail = state.data, + onBackClick = { viewModel.processIntent(PartyDetailIntent.OnBackClick) }, + onJoinClick = { viewModel.processIntent(PartyDetailIntent.OnJoinClick) }, + onUploaderClick = { viewModel.processIntent(PartyDetailIntent.OnUploaderClick(it)) }, + modifier = modifier, + ) + } + else -> {} + } +} + +@Composable +private fun PartyDetailScreen( + partyDetail: PartyDetail, + onBackClick: () -> Unit, + onJoinClick: () -> Unit, + onUploaderClick: (Long) -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + topBar = { + PotiHeaderPage( + onNavigationClick = onBackClick, + title = stringResource(R.string.party_detail_title, partyDetail.userSummary.nickname), + ) + }, + bottomBar = { + PotiBottomButton( + text = stringResource(R.string.party_detail_join_party), + onClick = onJoinClick, + ) + }, + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .verticalScroll(rememberScrollState()), + ) { + HorizontalPager( + state = rememberPagerState(pageCount = { partyDetail.images.size }), + modifier = Modifier + .fillMaxWidth() + .aspectRatio(375f / 268f), + ) { page -> + AsyncImage( + model = partyDetail.images[page].imageUrl, + contentDescription = null, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop, + ) + } + + PartyDetailHeaderInfo( + partyDetail = partyDetail, + onLikeClick = {}, + modifier = Modifier.padding(horizontal = screenWidthDp(16.dp), vertical = 20.dp), + ) + + PotiDivider( + styleType = PotiDividerStyle.SMALL, + modifier = Modifier.padding(horizontal = screenWidthDp(16.dp)), + ) + + PartyDetailContent( + partyDetail = partyDetail, + modifier = Modifier.padding(horizontal = screenWidthDp(16.dp), vertical = 20.dp), + ) + + PotiDivider(styleType = PotiDividerStyle.LARGE) + + PartyUploaderInfo( + userSummary = partyDetail.userSummary, + onClick = onUploaderClick, + modifier = Modifier.padding(start = screenWidthDp(16.dp), top = 20.dp, end = screenWidthDp(4.dp)), + ) + + PotiDivider( + styleType = PotiDividerStyle.SMALL, + modifier = Modifier.padding(horizontal = screenWidthDp(16.dp), vertical = 24.dp), + ) + + PartyParticipantsInfo( + partyDetail = partyDetail, + modifier = Modifier + .padding(bottom = 20.dp) + .padding(horizontal = screenWidthDp(16.dp)), + ) + + PotiDivider(styleType = PotiDividerStyle.LARGE) + + Text( + text = stringResource(R.string.party_detail_announcement), + style = PotiTheme.typography.caption12m, + color = PotiTheme.colors.gray800, + modifier = Modifier + .padding(horizontal = screenWidthDp(16.dp), vertical = 16.dp) + .padding(bottom = 40.dp), + ) + } + } } +@Preview @Composable -private fun PartyDetailScreen(modifier: Modifier = Modifier) { +private fun PartyDetailScreenPreview() { + PotiTheme { + PartyDetailScreen( + partyDetail = dummyPartyDetail, + onBackClick = {}, + onJoinClick = {}, + onUploaderClick = {}, + ) + } } diff --git a/app/src/main/java/com/poti/android/presentation/party/detail/PartyDetailViewModel.kt b/app/src/main/java/com/poti/android/presentation/party/detail/PartyDetailViewModel.kt index d11559d9..0f2c8ef9 100644 --- a/app/src/main/java/com/poti/android/presentation/party/detail/PartyDetailViewModel.kt +++ b/app/src/main/java/com/poti/android/presentation/party/detail/PartyDetailViewModel.kt @@ -1,9 +1,41 @@ package com.poti.android.presentation.party.detail -import androidx.lifecycle.ViewModel +import androidx.lifecycle.SavedStateHandle +import androidx.navigation.toRoute +import com.poti.android.core.base.BaseViewModel +import com.poti.android.core.common.state.ApiState +import com.poti.android.presentation.party.detail.model.PartyDetailEffect +import com.poti.android.presentation.party.detail.model.PartyDetailIntent +import com.poti.android.presentation.party.detail.model.PartyDetailUiState +import com.poti.android.presentation.party.detail.navigation.PartyDetailRoute import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @HiltViewModel -class PartyDetailViewModel @Inject constructor() : ViewModel() { +class PartyDetailViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, +) : BaseViewModel( + initialState = PartyDetailUiState(), + ) { + private val args = savedStateHandle.toRoute() + private val partyId = args.partyId + + init { + processIntent(PartyDetailIntent.LoadPartyDetail) + } + + override fun processIntent(intent: PartyDetailIntent) { + when (intent) { + PartyDetailIntent.LoadPartyDetail -> loadPartyDetail() + PartyDetailIntent.OnBackClick -> sendEffect(PartyDetailEffect.NavigateBack) + PartyDetailIntent.OnJoinClick -> sendEffect(PartyDetailEffect.NavigateToJoin) + is PartyDetailIntent.OnUploaderClick -> sendEffect(PartyDetailEffect.NavigateToProfile(intent.userId)) + } + } + + private fun loadPartyDetail() = launchScope { + updateState { copy(partyDetail = ApiState.Loading) } + // TODO: [지현] 나중에 서버 연결 + updateState { copy(partyDetail = ApiState.Success(dummyPartyDetail)) } + } } diff --git a/app/src/main/java/com/poti/android/presentation/party/detail/component/PartyDetailContent.kt b/app/src/main/java/com/poti/android/presentation/party/detail/component/PartyDetailContent.kt new file mode 100644 index 00000000..5755dc2b --- /dev/null +++ b/app/src/main/java/com/poti/android/presentation/party/detail/component/PartyDetailContent.kt @@ -0,0 +1,114 @@ +package com.poti.android.presentation.party.detail.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.poti.android.R +import com.poti.android.core.common.extension.toMoneyString +import com.poti.android.core.designsystem.theme.PotiTheme +import com.poti.android.domain.model.party.PartyDetail +import com.poti.android.presentation.party.detail.dummyPartyDetail + +@Composable +fun PartyDetailContent( + partyDetail: PartyDetail, + modifier: Modifier = Modifier, +) { + val density = LocalDensity.current + val textStyle = PotiTheme.typography.body14m + + val textHeightDp = with(density) { + if (textStyle.lineHeight.isSp) { + textStyle.lineHeight.toDp() + } else { + textStyle.fontSize.toDp() + } + } + + Column( + modifier = modifier.fillMaxWidth(), + ) { + Text( + text = partyDetail.content, + style = PotiTheme.typography.body16m, + color = PotiTheme.colors.black, + ) + + Spacer(modifier = Modifier.height(60.dp)) + + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Column( + modifier = Modifier.widthIn(min = 76.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = stringResource(R.string.party_recruit_deadline), + style = PotiTheme.typography.body14m, + color = PotiTheme.colors.gray800, + ) + Text( + text = stringResource(R.string.party_detail_shipping_price), + style = PotiTheme.typography.body14m, + color = PotiTheme.colors.gray800, + ) + } + + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = stringResource(R.string.party_detail_deadline, partyDetail.deadline), + style = PotiTheme.typography.body14m, + color = PotiTheme.colors.black, + ) + + FlowRow( + modifier = Modifier.height(IntrinsicSize.Min), + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + partyDetail.deliveryOptions.forEachIndexed { index, option -> + Text( + text = stringResource(R.string.party_detail_shipping_price_format, option.name, option.price.toMoneyString()), + style = PotiTheme.typography.body14m, + color = PotiTheme.colors.black, + ) + + if (index < partyDetail.deliveryOptions.lastIndex) { + VerticalDivider( + thickness = 1.dp, + color = PotiTheme.colors.gray800, + modifier = Modifier.height(textHeightDp), + ) + } + } + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PartyDetailContentPreview() { + PotiTheme { + PartyDetailContent( + partyDetail = dummyPartyDetail, + ) + } +} diff --git a/app/src/main/java/com/poti/android/presentation/party/detail/component/PartyDetailHeaderInfo.kt b/app/src/main/java/com/poti/android/presentation/party/detail/component/PartyDetailHeaderInfo.kt new file mode 100644 index 00000000..f8c47d42 --- /dev/null +++ b/app/src/main/java/com/poti/android/presentation/party/detail/component/PartyDetailHeaderInfo.kt @@ -0,0 +1,90 @@ +package com.poti.android.presentation.party.detail.component + +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.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.poti.android.R +import com.poti.android.core.common.extension.toMoneyString +import com.poti.android.core.designsystem.component.button.PotiIconButton +import com.poti.android.core.designsystem.theme.PotiTheme +import com.poti.android.domain.model.party.PartyDetail +import com.poti.android.presentation.party.detail.dummyPartyDetail + +@Composable +fun PartyDetailHeaderInfo( + partyDetail: PartyDetail, + onLikeClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Column { + Text( + text = partyDetail.artist, + style = PotiTheme.typography.body14m, + color = PotiTheme.colors.gray800, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(bottom = 4.dp), + ) + Text( + text = partyDetail.title, + style = PotiTheme.typography.title18sb, + color = PotiTheme.colors.black, + minLines = 2, + overflow = TextOverflow.Ellipsis, + ) + + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.Bottom, + modifier = Modifier.padding(top = 8.dp, bottom = 16.dp), + ) { + Text( + text = stringResource(R.string.party_detail_price, partyDetail.price.toMoneyString()), + style = PotiTheme.typography.display20b, + color = PotiTheme.colors.black, + ) + Text( + text = stringResource(R.string.party_detail_price_per_person), + style = PotiTheme.typography.body16m, + color = PotiTheme.colors.gray800, + ) + } + + Text( + text = partyDetail.uploadTime, + style = PotiTheme.typography.body14m, + color = PotiTheme.colors.gray800, + ) + } + + PotiIconButton( + iconRes = R.drawable.ic_heart, + onClick = onLikeClick, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun PartyDetailHeaderInfoPreview() { + PotiTheme { + PartyDetailHeaderInfo( + partyDetail = dummyPartyDetail, + onLikeClick = {}, + ) + } +} diff --git a/app/src/main/java/com/poti/android/presentation/party/detail/component/PartyParticipantsInfo.kt b/app/src/main/java/com/poti/android/presentation/party/detail/component/PartyParticipantsInfo.kt new file mode 100644 index 00000000..32a788ba --- /dev/null +++ b/app/src/main/java/com/poti/android/presentation/party/detail/component/PartyParticipantsInfo.kt @@ -0,0 +1,80 @@ +package com.poti.android.presentation.party.detail.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.poti.android.R +import com.poti.android.core.designsystem.component.display.PotiPrimaryTag +import com.poti.android.core.designsystem.component.display.PotiPrimaryTagColor +import com.poti.android.core.designsystem.component.display.PotiPrimaryTagSize +import com.poti.android.core.designsystem.component.display.PotiProfileSummary +import com.poti.android.core.designsystem.component.display.PotiProfileSummarySize +import com.poti.android.core.designsystem.theme.PotiTheme +import com.poti.android.domain.model.party.PartyDetail +import com.poti.android.presentation.party.detail.dummyPartyDetail + +@Composable +fun PartyParticipantsInfo( + partyDetail: PartyDetail, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(R.string.party_detail_participants), + style = PotiTheme.typography.body16sb, + color = PotiTheme.colors.black, + modifier = Modifier.weight(1f), + ) + Text( + text = stringResource(R.string.party_detail_participants_count, partyDetail.currentCount, partyDetail.totalCount), + style = PotiTheme.typography.body16sb, + color = PotiTheme.colors.poti600, + ) + } + + partyDetail.participants.forEach { participant -> + participant.selectedMembers.forEach { selectedMember -> + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + PotiProfileSummary( + profileImageUrl = participant.profileImage, + nickname = participant.nickname, + sizeType = PotiProfileSummarySize.LARGE, + rating = participant.rating.toString(), + modifier = Modifier.weight(1f), + ) + + PotiPrimaryTag( + text = selectedMember, + colorType = PotiPrimaryTagColor.GRAY, + sizeType = PotiPrimaryTagSize.LARGE, + ) + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PartyParticipantsInfoPreview() { + PotiTheme { + PartyParticipantsInfo( + partyDetail = dummyPartyDetail, + ) + } +} diff --git a/app/src/main/java/com/poti/android/presentation/party/detail/component/PartyUploaderInfo.kt b/app/src/main/java/com/poti/android/presentation/party/detail/component/PartyUploaderInfo.kt new file mode 100644 index 00000000..e18232a8 --- /dev/null +++ b/app/src/main/java/com/poti/android/presentation/party/detail/component/PartyUploaderInfo.kt @@ -0,0 +1,68 @@ +package com.poti.android.presentation.party.detail.component + +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.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.poti.android.R +import com.poti.android.core.designsystem.component.button.PotiIconButton +import com.poti.android.core.designsystem.component.display.PotiProfileSummary +import com.poti.android.core.designsystem.component.display.PotiProfileSummarySize +import com.poti.android.core.designsystem.theme.PotiTheme +import com.poti.android.domain.model.user.UserSummary +import com.poti.android.presentation.party.detail.dummyPartyDetail + +@Composable +fun PartyUploaderInfo( + userSummary: UserSummary, + onClick: (Long) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + Text( + text = stringResource(R.string.party_detail_uploader), + style = PotiTheme.typography.body16sb, + color = PotiTheme.colors.black, + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + PotiProfileSummary( + profileImageUrl = userSummary.profileImage, + nickname = userSummary.nickname, + sizeType = PotiProfileSummarySize.LARGE, + rating = userSummary.rating.toString(), + reviewText = stringResource(R.string.party_detail_review_count, userSummary.reviewCount), + modifier = Modifier.weight(1f), + ) + + PotiIconButton( + iconRes = R.drawable.ic_arrow_right_lg, + onClick = { onClick(userSummary.userId) }, + tint = PotiTheme.colors.gray700, + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PartyUploaderInfoPreview() { + PotiTheme { + PartyUploaderInfo( + userSummary = dummyPartyDetail.userSummary, + onClick = {}, + ) + } +} diff --git a/app/src/main/java/com/poti/android/presentation/party/detail/model/Contracts.kt b/app/src/main/java/com/poti/android/presentation/party/detail/model/Contracts.kt new file mode 100644 index 00000000..1b7c9421 --- /dev/null +++ b/app/src/main/java/com/poti/android/presentation/party/detail/model/Contracts.kt @@ -0,0 +1,30 @@ +@file:Suppress("ktlint:standard:filename") +package com.poti.android.presentation.party.detail.model + +import com.poti.android.core.base.UiEffect +import com.poti.android.core.base.UiIntent +import com.poti.android.core.base.UiState +import com.poti.android.core.common.state.ApiState +import com.poti.android.domain.model.party.PartyDetail + +data class PartyDetailUiState( + val partyDetail: ApiState = ApiState.Loading, +) : UiState + +sealed interface PartyDetailIntent : UiIntent { + data object OnBackClick : PartyDetailIntent + + data object OnJoinClick : PartyDetailIntent + + data class OnUploaderClick(val userId: Long) : PartyDetailIntent + + data object LoadPartyDetail : PartyDetailIntent +} + +sealed interface PartyDetailEffect : UiEffect { + data object NavigateBack : PartyDetailEffect + + data object NavigateToJoin : PartyDetailEffect + + data class NavigateToProfile(val userId: Long) : PartyDetailEffect +} diff --git a/app/src/main/java/com/poti/android/presentation/party/detail/navigation/PartyDetailNavigation.kt b/app/src/main/java/com/poti/android/presentation/party/detail/navigation/PartyDetailNavigation.kt index eb241a82..46392f21 100644 --- a/app/src/main/java/com/poti/android/presentation/party/detail/navigation/PartyDetailNavigation.kt +++ b/app/src/main/java/com/poti/android/presentation/party/detail/navigation/PartyDetailNavigation.kt @@ -9,18 +9,19 @@ import androidx.navigation.compose.composable import com.poti.android.core.navigation.Route import com.poti.android.presentation.party.detail.PartyDetailRoute import com.poti.android.presentation.party.detail.PartyJoinRoute +import com.poti.android.presentation.user.profile.navigation.navigateToProfile import kotlinx.serialization.Serializable sealed interface PartyDetailRoute : Route { @Serializable - data object Detail : PartyDetailRoute + data class Detail(val partyId: Long) : PartyDetailRoute @Serializable data object Join : PartyDetailRoute } -fun NavController.navigateToPartyDetail() { - navigate(PartyDetailRoute.Detail) +fun NavController.navigateToPartyDetail(partyId: Long) { + navigate(PartyDetailRoute.Detail(partyId)) } fun NavController.navigateToPartyJoin() { @@ -29,13 +30,19 @@ fun NavController.navigateToPartyJoin() { fun NavGraphBuilder.partyDetailNavGraph( paddingValues: PaddingValues, + navController: NavController, ) { composable { PartyDetailRoute( + onPopBackStack = navController::popBackStack, + onNavigateToJoin = navController::navigateToPartyJoin, + onNavigateToProfile = navController::navigateToProfile, modifier = Modifier.padding(paddingValues), ) } composable { - PartyJoinRoute(modifier = Modifier.padding(paddingValues)) + PartyJoinRoute( + modifier = Modifier.padding(paddingValues), + ) } } diff --git a/app/src/main/java/com/poti/android/presentation/party/goodsfilter/component/PotsCard.kt b/app/src/main/java/com/poti/android/presentation/party/goodsfilter/component/PotsCard.kt new file mode 100644 index 00000000..66f855bb --- /dev/null +++ b/app/src/main/java/com/poti/android/presentation/party/goodsfilter/component/PotsCard.kt @@ -0,0 +1,209 @@ +package com.poti.android.presentation.party.goodsfilter.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import com.poti.android.R +import com.poti.android.core.common.extension.noRippleClickable +import com.poti.android.core.designsystem.component.display.PotiDivider +import com.poti.android.core.designsystem.component.display.PotiDividerStyle +import com.poti.android.core.designsystem.component.display.PotiProfileSummary +import com.poti.android.core.designsystem.component.display.PotiProfileSummarySize +import com.poti.android.core.designsystem.theme.PotiTheme + +@Composable +fun PotsCard( + profileImageUrl: String, + nickname: String, + rating: String, + imageUrl: String, + members: String, + price: String, + currentCount: Int, + totalCount: Int, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val isClosed = totalCount - currentCount == 0 + val contentAlpha = if (isClosed) 0.5f else 1f + + Column( + modifier = modifier + .clip(RoundedCornerShape(12.dp)) + .background(PotiTheme.colors.white) + .border( + width = 1.dp, + color = PotiTheme.colors.gray300, + shape = RoundedCornerShape(12.dp), + ) + .noRippleClickable(onClick) + .padding(16.dp) + .alpha(contentAlpha), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.Top, + ) { + PotiProfileSummary( + profileImageUrl = profileImageUrl, + nickname = nickname, + sizeType = PotiProfileSummarySize.SMALL, + rating = rating, + ) + + Spacer(modifier = Modifier.weight(1f)) + + if (isClosed) { + Text( + text = stringResource(R.string.pots_card_closed), + color = PotiTheme.colors.gray800, + style = PotiTheme.typography.body16sb, + ) + } else { + Row( + verticalAlignment = Alignment.Bottom, + ) { + Text( + text = stringResource(R.string.pots_card_current_count, currentCount), + color = PotiTheme.colors.sementicRed, + style = PotiTheme.typography.display18b, + ) + Text( + text = stringResource(R.string.pots_card_total_count, totalCount), + color = PotiTheme.colors.sementicRed, + style = PotiTheme.typography.body16sb, + ) + } + } + } + + PotiDivider(styleType = PotiDividerStyle.SMALL) + + Row( + modifier = Modifier + .fillMaxWidth() + .height(IntrinsicSize.Min), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.Top, + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(end = 12.dp), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.Start, + ) { + Text( + text = members, + color = PotiTheme.colors.gray800, + overflow = TextOverflow.Ellipsis, + maxLines = 2, + style = PotiTheme.typography.caption12m, + ) + + Spacer(Modifier.height(14.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.Bottom, + ) { + Text( + text = price, + color = PotiTheme.colors.black, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = PotiTheme.typography.display18b, + ) + + Text( + text = stringResource(R.string.pots_card_per_person), + color = PotiTheme.colors.gray800, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = PotiTheme.typography.body14m, + ) + } + } + + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(imageUrl) + .crossfade(true) + .build(), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxHeight() + .aspectRatio(1f) + .clip(RoundedCornerShape(8.dp)) + .background(PotiTheme.colors.gray300), + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PotsCardPreview() { + PotiTheme { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + PotsCard( + profileImageUrl = "", + nickname = "닉네임", + rating = "4.8", + imageUrl = "", + members = "남은멤버1 | 남은멤버2 | 남은멤버3 | 남은멤버4 | 남은멤버6 | 남은멤버7 | 남은멤버8 | 남은멤버9 | 남은멤버10", + price = "5,000원~", + onClick = {}, + currentCount = 6, + totalCount = 7, + modifier = Modifier.fillMaxWidth(), + ) + + PotsCard( + profileImageUrl = "", + nickname = "닉네임", + rating = "4.8", + imageUrl = "", + members = "남은멤버1 | 남은멤버2 | 남은멤버3 | 남은멤버4 | 남은멤버6 | 남은멤버7", + price = "5,000원~", + onClick = {}, + currentCount = 7, + totalCount = 7, + modifier = Modifier.fillMaxWidth(), + ) + } + } +} diff --git a/app/src/main/java/com/poti/android/presentation/party/home/component/GoodsLargeCard.kt b/app/src/main/java/com/poti/android/presentation/party/home/component/GoodsLargeCard.kt new file mode 100644 index 00000000..0528b27a --- /dev/null +++ b/app/src/main/java/com/poti/android/presentation/party/home/component/GoodsLargeCard.kt @@ -0,0 +1,157 @@ +package com.poti.android.presentation.party.home.component + +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.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import com.poti.android.R +import com.poti.android.core.common.extension.noRippleClickable +import com.poti.android.core.designsystem.component.display.PotiPrimaryTag +import com.poti.android.core.designsystem.component.display.PotiPrimaryTagColor +import com.poti.android.core.designsystem.component.display.PotiPrimaryTagSize +import com.poti.android.core.designsystem.component.display.PotiSecondaryTag +import com.poti.android.core.designsystem.component.display.PotiTagSize +import com.poti.android.core.designsystem.theme.PotiTheme + +@Composable +fun GoodsLargeCard( + imageUrl: String, + artist: String, + goodsType: String, + partyCount: Int, + tag: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .clip(RoundedCornerShape(12.dp)) + .background(PotiTheme.colors.gray100) + .border( + width = 1.dp, + color = PotiTheme.colors.gray300, + shape = RoundedCornerShape(12.dp), + ) + .noRippleClickable(onClick), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(343f / 128f), + ) { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(imageUrl) + .crossfade(true) + .build(), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize(), + ) + + if (tag.isNotBlank()) { + PotiSecondaryTag( + text = tag, + sizeType = PotiTagSize.SMALL, + modifier = Modifier + .align(Alignment.TopStart) + .padding(12.dp), + ) + } + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.Top, + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(end = 8.dp), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.Start, + ) { + Text( + text = artist, + modifier = Modifier, + color = PotiTheme.colors.gray800, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = PotiTheme.typography.caption12m, + ) + + Text( + text = goodsType, + modifier = Modifier, + color = PotiTheme.colors.black, + overflow = TextOverflow.Ellipsis, + maxLines = 2, + minLines = 2, + style = PotiTheme.typography.body14m, + ) + } + + PotiPrimaryTag( + text = stringResource(R.string.goods_card_party_count, partyCount), + sizeType = PotiPrimaryTagSize.LARGE, + colorType = PotiPrimaryTagColor.WHITE, + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun GoodsLargeCardPreview() { + PotiTheme { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + GoodsLargeCard( + imageUrl = "", + artist = "아티스트명", + goodsType = "상품 종류명", + partyCount = 3, + tag = "인기", + onClick = {}, + modifier = Modifier.fillMaxWidth(), + ) + + GoodsLargeCard( + imageUrl = "", + artist = "아티스트명 ".repeat(10), + goodsType = "상품 종류명 ".repeat(10), + partyCount = 3, + tag = "인기", + onClick = {}, + modifier = Modifier.fillMaxWidth(), + ) + } + } +} diff --git a/app/src/main/java/com/poti/android/presentation/party/home/component/GoodsSmallCard.kt b/app/src/main/java/com/poti/android/presentation/party/home/component/GoodsSmallCard.kt new file mode 100644 index 00000000..d8f126c5 --- /dev/null +++ b/app/src/main/java/com/poti/android/presentation/party/home/component/GoodsSmallCard.kt @@ -0,0 +1,152 @@ +package com.poti.android.presentation.party.home.component + +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.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.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import com.poti.android.R +import com.poti.android.core.common.extension.noRippleClickable +import com.poti.android.core.common.util.screenWidthDp +import com.poti.android.core.designsystem.component.display.PotiPrimaryTag +import com.poti.android.core.designsystem.component.display.PotiPrimaryTagColor +import com.poti.android.core.designsystem.component.display.PotiPrimaryTagSize +import com.poti.android.core.designsystem.component.display.PotiSecondaryTag +import com.poti.android.core.designsystem.component.display.PotiTagSize +import com.poti.android.core.designsystem.theme.PotiTheme + +@Composable +fun GoodsSmallCard( + imageUrl: String, + artist: String, + goodsType: String, + partyCount: Int, + tag: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .width(screenWidthDp(192.dp)) + .clip(RoundedCornerShape(12.dp)) + .background(PotiTheme.colors.gray100) + .border( + width = 1.dp, + color = PotiTheme.colors.gray300, + shape = RoundedCornerShape(12.dp), + ) + .noRippleClickable(onClick), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.Start, + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(192f / 128f), + ) { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(imageUrl) + .crossfade(true) + .build(), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize(), + ) + + if (tag.isNotBlank()) { + PotiSecondaryTag( + text = tag, + sizeType = PotiTagSize.SMALL, + modifier = Modifier + .align(Alignment.TopStart) + .padding(12.dp), + ) + } + } + + Column( + modifier = Modifier.padding(12.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start, + ) { + Text( + text = artist, + modifier = Modifier.fillMaxWidth(), + color = PotiTheme.colors.gray800, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = PotiTheme.typography.caption12m, + ) + + Text( + text = goodsType, + modifier = Modifier.fillMaxWidth(), + color = PotiTheme.colors.black, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = PotiTheme.typography.body14m, + ) + + Spacer(Modifier.height(8.dp)) + + PotiPrimaryTag( + text = stringResource(R.string.goods_card_party_count, partyCount), + sizeType = PotiPrimaryTagSize.LARGE, + colorType = PotiPrimaryTagColor.WHITE, + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun GoodsSmallCardPreview() { + PotiTheme { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + GoodsSmallCard( + imageUrl = "", + artist = "아티스트명", + goodsType = "상품 종류명", + partyCount = 3, + tag = "인기", + onClick = {}, + modifier = Modifier, + ) + + GoodsSmallCard( + imageUrl = "", + artist = "아티스트명 아티스트명 아티스트명 아티스트명 ", + goodsType = "상품 종류명 상품 종류명 상품 종류명 ", + partyCount = 3, + tag = "인기", + onClick = {}, + modifier = Modifier, + ) + } + } +} diff --git a/app/src/main/java/com/poti/android/presentation/party/home/navigation/HomeNavigation.kt b/app/src/main/java/com/poti/android/presentation/party/home/navigation/HomeNavigation.kt index 56653ed8..0fb7ac38 100644 --- a/app/src/main/java/com/poti/android/presentation/party/home/navigation/HomeNavigation.kt +++ b/app/src/main/java/com/poti/android/presentation/party/home/navigation/HomeNavigation.kt @@ -7,6 +7,7 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import com.poti.android.core.navigation.Route +import com.poti.android.presentation.party.goodsfilter.navigation.navigateToGoodsCategory import com.poti.android.presentation.party.home.HomeRoute import kotlinx.serialization.Serializable @@ -21,11 +22,11 @@ fun NavController.navigateToHome() { fun NavGraphBuilder.homeNavGraph( paddingValues: PaddingValues, - onNavigateToGoodsCategory: () -> Unit, + navController: NavController, ) { composable { HomeRoute( - onNavigateToGoodsCategory = onNavigateToGoodsCategory, + onNavigateToGoodsCategory = navController::navigateToGoodsCategory, modifier = Modifier.padding(paddingValues), ) } diff --git a/app/src/main/java/com/poti/android/presentation/user/profile/navigation/ProfileNavigation.kt b/app/src/main/java/com/poti/android/presentation/user/profile/navigation/ProfileNavigation.kt index c9c76997..e3ec1495 100644 --- a/app/src/main/java/com/poti/android/presentation/user/profile/navigation/ProfileNavigation.kt +++ b/app/src/main/java/com/poti/android/presentation/user/profile/navigation/ProfileNavigation.kt @@ -9,11 +9,11 @@ import kotlinx.serialization.Serializable sealed interface ProfileRoute : Route { @Serializable - data object Profile : ProfileRoute + data class Profile(val userId: Long) : ProfileRoute } -fun NavController.navigateToProfile() { - navigate(ProfileRoute.Profile) +fun NavController.navigateToProfile(userId: Long) { + navigate(ProfileRoute.Profile(userId)) } fun NavGraphBuilder.profileNavGraph( diff --git a/app/src/main/res/drawable/img_create_hint.xml b/app/src/main/res/drawable/img_create_hint.xml new file mode 100644 index 00000000..c5c86908 --- /dev/null +++ b/app/src/main/res/drawable/img_create_hint.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4094abd1..6b1400eb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -10,26 +10,63 @@ 진행 중 종료 + + 다음 + 건너뛰기 + 시작하기 + 초기화 + 완료 + 계속 + 전체 선택 + + + 모집 기한 + + + %s의 팟 + %s원~ + / 인 + %s 까지 + 배송비 + %1$s %2$s원 + 모집자 + 참여자 + %d개의 평가 + %1$d/%2$d + 안내 사항\n어쩌구 저쩌구\n어쩌구 저쩌구\n어쩌구 저쩌구\n + 분철팟 참여하기 + 입금 배송 모집 - 대기 확인 중 시작 완료 - %1$s %2$s - 복사 - - - 다음 - 건너뛰기 - 시작하기 - 초기화 - 완료 - 계속 - 전체 선택 + 이름 + 입금 정보 + 배송 정보 + 연락처 + 송장 번호 + 입금 확인 + 송장 번호 입력 + 입금 금액 + 총 입금 금액 + %s원 + + + 팟 %1$d개 + 마감 + %1$d + /%1$d + / 인 + + + + 모집자 본인이 보유할 멤버는 꼭 제외해주세요! + +