Skip to content

feat: 마이페이지 공동 카테고리 초대 관리 #803 #867

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 163 commits into from
Jun 6, 2025

Conversation

Junyoung-WON
Copy link
Contributor

@Junyoung-WON Junyoung-WON commented Jun 2, 2025

⭐️ Issue Number


🚩 Summary

받은 초대 수락 받은 초대 거절 받은 초대 취소
영상은 서버의 에러가 해결되면 올리겠습니다!

주요 구현 내용

  • 마이페이지 화면 : '카테고리 초대 관리' 버튼과 구분선 추가
    • "카테고리 초대 관리" 버튼에 Indicator : 받은 초대가 있을 경우
  • 카테고리 초대 화면
    • 받은 초대 / 보낸 초대 선택 메뉴
    • 받은 초대 화면
    • 보낸 초대 화면
    • 초대 거절, 취소 시 AlertDialog 띄우기
    • 초대 완료 시 Toast 띄우기
  • 기본 컴포넌트
    • DefaultNavigationTopBar
    • DefaultTextButton
    • DefaultAlertDialog
    • DefaultEmptyView

카테고리 초대 관리 버튼의 Notification

image

"카테고리 초대 관리" 버튼의 우측 상단에 Notification을 띄우는 기능을 추가했습니다.

BadgedBoxBadge를 이용하여 구현했습니다.
MyPageMenuButton의 텍스트를 BadgedBox로 감싸고, Badge가 hasNotification 상태에 따라 설정되도록 하였습니다.

// MyPageMenuButton

Row(
    modifier = modifier.fillMaxWidth(),
    horizontalArrangement = Arrangement.SpaceBetween,
    verticalAlignment = Alignment.CenterVertically,
) {
    BadgedBox(
        badge = { if (hasNotification) Badge(containerColor = Accents4) },
    ) {
        Text(
            text = menuTitle,
            style = Title3,
            color = StaccatoBlack,
        )
    }
    Icon(
        imageVector = ImageVector.vectorResource(id = R.drawable.icon_arrow_right),
        contentDescription = stringResource(id = R.string.mypage_menu_navigation_icon_description),
        modifier = modifier,
        tint = Gray2,
    )
}

Indicator를 나타내는 Badge는 사이즈를 설정하지 않으면 기본적으로 6dp의 크기를 갖습니다.
현재 피그마에 작성된 크기는 5dp*5dp이지만, 실 기기에서 조금 더 작게 보이기에 기본 크기를 적용하였습니다!
image image

받은 초대 / 보낸 초대 선택 메뉴 : PrimaryTabRow, 커스텀 Tab

PrimaryTabRow 와 직접 작성한 MenuTab을 활용하여 초대 관리 화면의 메뉴를 구성했습니다.

  • 기존 PrimaryTabRow 와 Tabs
    image

  • PrimaryTabRow와 MenuTab을 이용해 만든 메뉴 화면
    image

TabRow와 함께 사용 가능한 Tab을 사용하고자 했으나, indicator의 커스텀이 어렵고 Ripple 애니메이션을 제거할 수 없다는 단점이 있어, 별도의 MenuTab을 만들었습니다.

받은 초대, 보낸 초대 Item : ConstraintLayout

너무 많은 컴포저블의 중첩으로 인한 장풍 현상을 줄이고자, ConstraintLayout을 이용해 Item을 만들었습니다.
또한 카테고리 제목이 너무 긴 경우를 처리하기 위해, Ellipse 설정과 Constraint 설정을 추가했습니다.

  • 카테고리 제목 컴포넌트 : overflow = Ellipse 설정
    @Composable
    fun CategoryTitle(
        title: String,
        modifier: Modifier = Modifier,
        style: TextStyle = Title3,
        color: Color = StaccatoBlack,
    ) {
        Text(
            text = title,
            modifier = modifier,
            style = style,
            color = color,
            fontWeight = FontWeight.SemiBold,
            overflow = TextOverflow.Ellipsis,
            maxLines = 1,
        )
    }
  • 받은 초대의 Constraint 설정 : Dimension.fillConstraints
    • 카테고리 제목의 너비가 제약 조건의 너비를 따르도록 설정했습니다.
      CategoryTitle(
          modifier =
              modifier.constrainAs(categoryTitle) {
                  // 시작, 끝 부분 제약 조건 설정
                  start.linkTo(profileImage.start)
                  end.linkTo(parent.end, margin = 32.dp)
                  width = Dimension.fillToConstraints  // 너비가 제약 조건 안에서 최대 너비를 갖도록 제한
              },
          title = categoryInvitation.categoryTitle,
      )
  • 보낸 초대의 Constraint 설정 : 체이닝과 Dimension.prefferedWrapContent
    • 보낸 초대는 카테고리 제목과 가이드 문구가 함께 붙어있고, 제목이 너무 긴 경우 카테고리 제목만을 Ellips 처리하고자 했습니다.
    • 체이닝을 생성하여 카테고리 제목과 가이드 문구가 함께 붙어있도록 설정했습니다.
      val titleChain = createHorizontalChain(titleRef, suffixRef, chainStyle = ChainStyle.Packed(bias = 0f))
      constrain(titleChain) {
          start.linkTo(profileImageRef.end, margin = 10.dp)
          end.linkTo(cancelButtonRef.start, margin = 22.dp)
      }
    • 카테고리 제목이 제약 조건의 너비에 맞춰 설정되도록 Dimension.preferredWrapContent를 사용했습니다.
      CategoryTitle(
          modifier =
              modifier.constrainAs(titleRef) {
                  end.linkTo(suffixRef.start)
                  width = Dimension.preferredWrapContent  // 제약 조건을 따라 최소한의 너비를 갖도록 설정
              },
          title = categoryInvitation.categoryTitle,
      )

초대 거절, 취소 시 사용자에게 경고 다이얼로그 출력

초대를 거절하거나 취소할 경우, 사용자가 바로 한번 더 확인할 수 있도록 다이얼로그를 띄웁니다.

  • DefaultAlertDialog : AlertDialog 활용하여 구현했습니다.
    sealed class InvitationDialogState {
        data object None : InvitationDialogState()
    
        data class Reject(
            val invitationId: Long,
            val onConfirm: () -> Unit,
        ) : InvitationDialogState()
    
        data class Cancel(
            val invitationId: Long,
            val onConfirm: () -> Unit,
        ) : InvitationDialogState()
    }
  • InvitationDialogState : 거절, 취소 요청 상태에 따라 다이얼로그를 분기합니다.
    sealed class InvitationDialogState {
        // 어떠한 요청도 없는 상태
        data object None : InvitationDialogState()
    
        // 거절 요청 상태
        data class Reject(
            val invitationId: Long,
            val onConfirm: () -> Unit,
        ) : InvitationDialogState()
    
        // 취소 요청 상태
        data class Cancel(
            val invitationId: Long,
            val onConfirm: () -> Unit,
        ) : InvitationDialogState()
    }
    
    // ViewModel
    private val _dialogState: MutableState<InvitationDialogState> = mutableStateOf(None)
    val dialogState: State<InvitationDialogState> = _dialogState
  • InvitationDialogs : InvitationDialogState에 따라 다이얼로그를 출력하는 컴포넌트입니다.
    @Composable
    fun InvitationDialogs(
        state: InvitationDialogState,
        onDismiss: () -> Unit,
    ) {
        when (state) {
            is None -> Unit
    
            is Reject -> {
                // 거절 다이얼로그
            }
    
            is Cancel -> {
                // 취소 다이얼로그
            }
        }
    }
    해당 다이얼로그는 InvitationManagementScreen에 위치합니다.
    // InvitationManagementScreen
    val dialogState by invitationViewModel.dialogState
    
    Scaffold() {
        // 초대 관리 화면
    
        InvitationDialogs(
            state = dialogState,
            onDismiss = { invitationViewModel.dismissDialog() },
        )
    }

초대 수락, 서버 에러 및 예외 발생 시 Toast 출력

초대를 수락하거나 서버 에러 발생, 예외 발생 시 Toast를 출력합니다.
단, 초대 수락과 예외 발생의 경우 StringResource를 사용하고, 서버 에러 문구는 문자열을 사용합니다.
이를 뷰모델에서 하나의 이벤트로 관리하고 UI 로직에서 편리하게 분기할 수 있도록 ToastMessage를 작성했습니다.

sealed interface ToastMessage {
    data class FromResource(
        @StringRes val messageId: Int,
    ) : ToastMessage

    data class Plain(val errorMessage: String) : ToastMessage
}

// ViewModel
private val _toastMessage = MutableSharedFlow<ToastMessage>()
val toastMessage: SharedFlow<ToastMessage> get() = _toastMessage

InvitationManagementScreen에서 LaunchedEffect로 collect하여 Toast를 띄웁니다.

    val context = LocalContext.current

    LaunchedEffect(Unit) {
        invitationViewModel.toastMessage.collect {
            val message =
                when (it) {
                    is ToastMessage.FromResource -> context.getString(it.messageId)
                    is ToastMessage.Plain -> it.errorMessage
                }
            context.showToast(message)
        }
    }

기본 컴포넌트 작성

공용으로 사용할 수 있는 컴포넌트를 작성했습니다.



🙂 To Reviewer

코드 양이 좀 많은 관계로, 아래 사항들을 위주로 리뷰해주시면 감사하겠습니다🙂

  • 카테고리 초대 관리 기능이 잘 동작하는지 확인해주세요!

    • UX/UI에서 어색한 부분
    • 버그 발생 여부
    • 에러 발생 시 처리: 우선 스낵바 대신 토스트로 구현했습니다. 나중에 리팩터링할 때 스낵바를 추가 구현하겠습니다.
  • 작성한 기본 컴포넌트가 사용에 편리할지, 개선할 부분이 있을지 확인해주세요!

    • DefaultNavigationTopBar, DefaultTextButton, DefaultAlertDialog, DefaultEmptyView
  • @hxeyexn

  • @s6m1n



📋 To Do

  • InvitationDataSource 제거
  • develop 병합
  • Empty View 적용하기
  • PR 설명 작성하기
  • 디자인 컨펌
  • 리뷰 반영

DividerStyle에서 중간 구분선 스타일 Middle을 추가한다
 - 높이 10dp
 - 색상 gray1
우선 xml로 구현 후 Compose로 마이그레이션 예정
- .editorconfig 파일 생성
- ktlint_function_naming_ignore_when_annotated_with=Composable 옵션 추가
- 중간 구분선 구현
- Preview 추가
activity_mypage.xml
- 중간 구분선을 ComposeView로 변경

MyPageActivity.kt
- dividerMypageMiddle을 MiddleDivider로 setContent
- PreviewParameterProvider 활용
- CenterAlignedTopAppBar, TopAppBar 활용
- menuId: 메뉴에 대한 색인
- title: 메뉴 이름
- iconResId: 메뉴 아이콘 리소스 ID
- iconContentDescription: 아이콘 ContentDescription
- 열거 아이템: 받은 초대, 보낸 초대
- Divider -> DefaultDivider
- 기본 두께: 0.5dp -> 1dp로 변경
- modifier: fillMaxWidth() 제거
- component 패키지 분리
- MiddleDivider 파일 분리
- MenuButton 파일 분리
  - 색상, 패딩 변수 private 설정
  - PreviewParameterProvider 위치 수정(컨벤션)
- 파일명 TopBar -> DefaultNavigationTopBar 로 변경
- preview 메서드 private 설정
- icon_receive_box
- icon_send
- ClickableWithoutRipple: 리플 애니메이션을 제거하는 Modifier 속성
- DefaultAsyncImage: 기본 이미지 비동기 로딩 Composable
- Manifest 수정: InvitationManagementActivity 추가, 일부 개행 수정
- Button에서 Box로 변경
- Modifier에 clickableWithoutRipple 속성 추가
- 텍스트 스타일, 색상 적용
- TopAppBarColors 설정
- Navigation 버튼을 IconButton에서 Icon으로 변경
- Modifier에 clickableWithoutRipple 속성 추가
- InvitationMenuItems -> InvitationSelectionMenuItems
- clickableWithoutRipple 활용
- CategoryTitle: 카테고리 제목
- NicknameText: 초대한 사람/초대 받은 사람 닉네임
- ProfileImage: 사용자 프로필 이미지
- 생명주기가 Started 이후일 때 마다 Notification 존재 여부를 가져옴
- MyPageMenuButton에 Badge 적용
- DefaultBasicDialogPreview, TopBarComponentsPreview, DisabledDefaultTextButtonPreview, MenuTapPreview
- DefaultBasicDialogPreview -> DefaultAlertDialogPreview
- TopBarComponentsPreview -> DefaultNavigationTopBarPreview
불필요한 배경 제거
- ReceivedInvitationPreview
- SentInvitationPreview

배경 색상 설정
- EmptyReceivedInvitationPreview : 흰색 배경
- EmptySentInvitationPreview : 흰색 배경
- DefaultEmptyViewPreview : 흰색 배경
nullable에서 투명 테두리를 기본 인자 값으로 변경
- title을 nullable 처리
  - 기본 인자 값: null
  - null이 아닌 경우 제목을 Compose
- Preview 세분화
  - 제목이 없는 경우
  - 제목 중앙 정렬
  - 제목 왼쪽 정렬
  - 제목과 부제목 중앙 정렬
  - 제목과 부제목 왼쪽 정렬
- InvitationSelectionMenuUiModel -> InvitationTabMenu
- 보낸 초대 탭 변경 시 깜빡거림 현상 제거
- ConstraintLayout에 padding 설정으로 Constrain의 margin 제거
- contentDescription 리소스화
- 다이얼로그 제목, 설명 문자열 리소스화
- 선택된 상태, 선택되지 않은 상태 분리
- 디자인 QA 피드백 반영: 초대 관리 화면의 메뉴 바의 내부 간격이 너무 넓다는 피드백
- 디자인 QA 피드백 반영
  - 간격, 위치가 부자연스러운 아이콘 변경: 정 중앙에 위치하는 아이콘으로 교체
  - 탭 메뉴 내부 여백을 미세하게 조정: 내부 여백 1dp 추가
- 다크모드 시에도 라이트모드와 동일한 UI를 보여주도록 변경
- 다크모드 대응 여부는 추후에 결정
- develop 브랜치와 병합
@hxeyexn hxeyexn merged commit 437f2ee into develop Jun 6, 2025
7 checks passed
@hxeyexn hxeyexn deleted the feature/#803-mypage-managing-invitation branch June 6, 2025 05:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
android We are android>< feat 기능 (새로운 기능)
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

feat: 마이페이지 공동 카테고리 초대 관리 기능
3 participants