Skip to content

feat: 공동 카테고리 함께하는 사람들, 친구 초대 구현 #783 #825

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 51 commits into from
May 29, 2025
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
91da369
ui: 카테고리 조회 화면의 함께하는 사람들 UI 구현
s6m1n May 7, 2025
34eec8e
feat: 닉네임으로 멤버를 검색하는 API 추가
s6m1n May 14, 2025
57e5587
ui: 컴포즈 기본 컴포넌트 추가
s6m1n May 14, 2025
7b50804
feat: 카테고리 프래그먼트에 컴포즈 뷰 연결
s6m1n May 14, 2025
cab580e
ui: 초대 화면에 필요한 이미지, 스트링 리소스 추가
s6m1n May 14, 2025
79c42aa
refactor: 공통 컴포넌트 컨벤션에 맞게 수정
s6m1n May 14, 2025
a861161
feat: 카테고리 멤버 초대를 위한 모델 및 테스트 추가
s6m1n May 14, 2025
fdbf751
ui: 초대 다이얼로그 상단바 구현
s6m1n May 14, 2025
ba85efd
ui: 사용자 검색을 위한 TextField 구현
s6m1n May 14, 2025
603916c
ui: 선택된 멤버 리스트 ui 구현
s6m1n May 14, 2025
75aba4b
ui: 검색된 멤버 리스트 ui 구현
s6m1n May 14, 2025
28f6ec9
feat: 멤버 초대 다이얼로그 구현
s6m1n May 14, 2025
f579ccc
Merge branch 'refs/heads/develop' into feature/#783-mate-invite
s6m1n May 14, 2025
1537a88
style: KtLint 컴포즈 설정
s6m1n May 14, 2025
7efeb1e
chore: 초대하기 선택 버튼 스트링 수정
s6m1n May 15, 2025
14460dc
refactor: updateCategory 중복 코루틴 제거
s6m1n May 15, 2025
1e29f61
test: 멤버 검색 및 선택 기능 ViewModel 테스트 구현
s6m1n May 15, 2025
0b4deec
test: 테스트 통과를 위해 CategoryFixture 수정
s6m1n May 15, 2025
ebd4880
feat: 공동 카테고리 참가자 Participant 객체 추가
s6m1n May 15, 2025
2bcb8b7
refactor: 카테고리 조회 API v3 및 Participant 적용
s6m1n May 15, 2025
9ff9e6f
ui: 함께하는 사람들 host ui 구현
s6m1n May 15, 2025
f3097fc
ui: 비어있는 카테고리 empty view 수정
s6m1n May 15, 2025
9f69340
test: test fixture 수정
s6m1n May 16, 2025
8cde712
ui: TopBar에 선택한 사용자 수 표시
s6m1n May 16, 2025
42b6337
ui: 코멘트 엠티뷰 수정 및 캐릭터 흑백 사진 해상도 개선
s6m1n May 16, 2025
59626aa
test: 학습 테스트 삭제
s6m1n May 16, 2025
5aa883f
feat: 카테고리 공유 여부에 따라 함께하는 사람들 표시
s6m1n May 16, 2025
80305b0
ui: DefaultDivider 컬러 변경
s6m1n May 21, 2025
aa1046f
feat: 사용자 역할에 따라 카테고리 초대, 수정, 삭제 권한 조정
s6m1n May 21, 2025
1333f7d
feat: 카테고리 멤버 초대 API 연결
s6m1n May 22, 2025
da2c451
feat: 카테고리 멤버 초대 기능 구현
s6m1n May 22, 2025
70a66db
Merge branch 'develop' into feature/#783-mate-invite
s6m1n May 22, 2025
af149c0
test: 테스트가 통과할 수 있도록 변경사항 반영
s6m1n May 22, 2025
100da19
chore: 띄어쓰기 수정
s6m1n May 28, 2025
99cc83e
refactor: category 프로퍼티 명 수정 member -> participants
s6m1n May 28, 2025
187f334
refactor: StaccatoSearchTextField -> SearchTextField 이름 변경
s6m1n May 28, 2025
9ead2cb
ui: 멤버 검색 결과 아이템 ui 긴 이름 대응
s6m1n May 28, 2025
7106520
ui: 사용자 검색 LazyColumn 애니메이션 제거
s6m1n May 28, 2025
59cdd6f
ui: 함께하는 사람들 리사이클러뷰 padding 수정 및 멤버 프로필 placeHolder 적용
s6m1n May 28, 2025
eca2d2d
ui: Material3 TextField에서 BasicTextField로 변경
s6m1n May 28, 2025
0f66b42
Merge branch 'develop' into feature/#783-mate-invite
s6m1n May 28, 2025
74916e3
ui: 폰트 weight 및 Preview 이름 수정
s6m1n May 28, 2025
173f41b
Merge branch 'develop' into feature/#783-mate-invite
s6m1n May 29, 2025
b557fe4
ui: 다이얼로그 내에서 실패 시 토스트로 띄우기
s6m1n May 29, 2025
98a74d1
ui: 다이얼로그 decorFitsSystemWindows 옵션 제거
s6m1n May 29, 2025
9b8e3f1
feat: 선택된 사람이 없으면 초대 요청을 보내지 않도록 수정
s6m1n May 29, 2025
4c3f669
style: 불필요한 개행 제거
s6m1n May 29, 2025
105741d
refactor: isGone 사용
s6m1n May 29, 2025
099bc59
chore: 오타 수정
s6m1n May 29, 2025
24e21b8
chore: preview 이름 수정
s6m1n May 29, 2025
ed7392a
chore: InviteDialog preview 중복 코드 개선
s6m1n May 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions android/Staccato_AN/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[*.{kt,kts}]
ktlint_function_naming_ignore_when_annotated_with=Composable
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.on.staccato.data.dto.member

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class MemberSearchResponse(
@SerialName("members") val members: List<MemberDto>,
)
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.on.staccato.data.member

import com.on.staccato.data.dto.member.MemberSearchResponse
import com.on.staccato.data.dto.member.RecoveryCodeResponse
import com.on.staccato.data.network.ApiResult
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Query

Expand All @@ -11,8 +13,15 @@ interface MemberApiService {
@Query(RECOVERY_CODE) recoveryCode: String,
): ApiResult<RecoveryCodeResponse>

@GET(MEMBERS_SEARCH_PATH)
suspend fun getMembersBy(
@Query(NICKNAME) nickname: String,
): ApiResult<MemberSearchResponse>

companion object {
private const val MEMBERS_PATH = "/members"
private const val RECOVERY_CODE = "code"
private const val NICKNAME = "nickname"
private const val MEMBERS_SEARCH_PATH = "$MEMBERS_PATH/search"
Comment on lines +16 to +25
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API Path를 동반객체의 문자열 상수로 깔끔하게 잘 표현해주셨네요! 👍

}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package com.on.staccato.data.member

import com.on.staccato.StaccatoApplication
import com.on.staccato.data.dto.mapper.toDomain
import com.on.staccato.data.network.ApiResult
import com.on.staccato.data.network.handle
import com.on.staccato.domain.model.Members
import com.on.staccato.domain.repository.MemberRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import javax.inject.Inject

class MemberDefaultRepository
Expand All @@ -12,5 +16,14 @@ class MemberDefaultRepository
private val memberApiService: MemberApiService,
) : MemberRepository {
override suspend fun fetchTokenWithRecoveryCode(recoveryCode: String): ApiResult<Unit> =
memberApiService.postRecoveryCode(recoveryCode).handle { StaccatoApplication.userInfoPrefsManager.setToken(it.token) }
memberApiService.postRecoveryCode(recoveryCode)
.handle { StaccatoApplication.userInfoPrefsManager.setToken(it.token) }

override suspend fun searchMembersBy(nickname: String): Flow<ApiResult<Members>> =
flow {
emit(
memberApiService.getMembersBy(nickname)
.handle { Members(it.members.map { it.toDomain() }) },
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,12 @@ data class Member(
val nickname: String,
val memberImage: String? = null,
)

val dummyMember =
Member(
memberId = 0L,
nickname = "빙티",
memberImage = "",
)

val longNameMember = dummyMember.copy(nickname = "엄청나게아주긴닉네임")
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.on.staccato.domain.model

data class Members(val members: List<Member>) {
fun addFirst(member: Member): Members = Members((listOf(member) + members).distinct())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

distinct 메서드로 중복을 제거해주셨네요!
중복 제거를 보니 Set을 사용하는 방법도 생각났지만, 사용자들을 정렬해야할 필요성도 있다고 한다면 지금 방식이 적절하겠네요!


fun filter(member: Member): Members = Members(members.filterNot { it.memberId == member.memberId })

fun contains(target: Member) = members.contains(target)
}

val emptyMembers = Members(emptyList())

val dummyMembers =
Members(
listOf(
dummyMember,
longNameMember.copy(memberId = 1L),
dummyMember.copy(memberId = 2L),
longNameMember.copy(memberId = 3L),
dummyMember.copy(memberId = 4L),
longNameMember.copy(memberId = 5L),
dummyMember.copy(memberId = 6L),
longNameMember.copy(memberId = 7L),
),
)
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package com.on.staccato.domain.repository

import com.on.staccato.data.network.ApiResult
import com.on.staccato.domain.model.Members
import kotlinx.coroutines.flow.Flow

interface MemberRepository {
suspend fun fetchTokenWithRecoveryCode(recoveryCode: String): ApiResult<Unit>

suspend fun searchMembersBy(nickname: String): Flow<ApiResult<Members>>
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import com.google.android.material.snackbar.Snackbar
import com.on.staccato.R
import com.on.staccato.databinding.FragmentCategoryBinding
import com.on.staccato.presentation.base.BindingFragment
import com.on.staccato.presentation.category.adapter.MatesAdapter
import com.on.staccato.presentation.category.adapter.MembersAdapter
import com.on.staccato.presentation.category.adapter.StaccatosAdapter
import com.on.staccato.presentation.category.invite.InviteScreen
import com.on.staccato.presentation.category.model.CategoryUiModel
import com.on.staccato.presentation.category.model.CategoryUiModel.Companion.DEFAULT_CATEGORY_ID
import com.on.staccato.presentation.category.viewmodel.CategoryViewModel
Expand Down Expand Up @@ -53,7 +54,12 @@ class CategoryFragment :
@Inject
lateinit var loggingManager: LoggingManager

private val matesAdapter by lazy { MatesAdapter() }
private val membersAdapter by lazy {
MembersAdapter {
viewModel.changeInviteMode(true)
}
}

private val staccatosAdapter by lazy { StaccatosAdapter(handler = this) }

override fun onViewCreated(
Expand Down Expand Up @@ -123,6 +129,7 @@ class CategoryFragment :
binding.viewModel = viewModel
binding.toolbarHandler = this
binding.categoryHandler = this
binding.cvMemberInvite.setContent { InviteScreen() }
observeIsPermissionCanceled()
}

Expand All @@ -139,13 +146,13 @@ class CategoryFragment :
}

private fun initAdapter() {
binding.rvCategoryMates.adapter = matesAdapter
binding.rvCategoryMates.adapter = membersAdapter
binding.rvCategoryStaccatos.adapter = staccatosAdapter
}

private fun observeCategory() {
viewModel.category.observe(viewLifecycleOwner) { category ->
matesAdapter.updateMates(category.mates)
membersAdapter.updateMembers(category.members)
staccatosAdapter.updateStaccatos(category.staccatos)
}
}
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.on.staccato.presentation.category.adapter

fun interface MemberInviteHandler {
fun onClicked()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.on.staccato.presentation.category.adapter

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import com.on.staccato.databinding.ItemMemberInviteBinding
import com.on.staccato.databinding.ItemMemberProfileBinding
import com.on.staccato.domain.model.Member
import com.on.staccato.presentation.category.adapter.MembersViewHolder.MemberInviteViewHolder
import com.on.staccato.presentation.category.adapter.MembersViewHolder.MemberProfileViewHolder
import com.on.staccato.presentation.category.adapter.MembersViewType.MEMBER_INVITE
import com.on.staccato.presentation.category.adapter.MembersViewType.MEMBER_PROFILE

class MembersAdapter(private val memberInviteHandler: MemberInviteHandler) :
ListAdapter<Member, MembersViewHolder>(diffUtil) {
init {
submitList(listOf(inviteButton))
}

override fun getItemViewType(position: Int): Int {
return if (position == INVITE_BUTTON_POSITION) {
MEMBER_INVITE.viewType
} else {
MEMBER_PROFILE.viewType
}
}

override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): MembersViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (MembersViewType.from(viewType)) {
MEMBER_INVITE -> {
val binding = ItemMemberInviteBinding.inflate(inflater, parent, false)
MemberInviteViewHolder(binding, memberInviteHandler)
}

MEMBER_PROFILE -> {
val binding = ItemMemberProfileBinding.inflate(inflater, parent, false)
MemberProfileViewHolder(binding)
}
}
}

override fun onBindViewHolder(
holder: MembersViewHolder,
position: Int,
) {
when (holder) {
is MemberInviteViewHolder -> holder.bind()
is MemberProfileViewHolder -> holder.bind(getItem(position))
}
}

fun updateMembers(members: List<Member>) {
submitList(listOf(inviteButton) + members)
}

companion object {
const val INVITE_BUTTON_POSITION = 0

val diffUtil =
object : DiffUtil.ItemCallback<Member>() {
override fun areItemsTheSame(
oldItem: Member,
newItem: Member,
): Boolean = oldItem.memberId == newItem.memberId

override fun areContentsTheSame(
oldItem: Member,
newItem: Member,
): Boolean = oldItem == newItem
}

val inviteButton by lazy {
Member(
0,
"",
null,
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.on.staccato.presentation.category.adapter

import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.on.staccato.databinding.ItemMemberInviteBinding
import com.on.staccato.databinding.ItemMemberProfileBinding
import com.on.staccato.domain.model.Member

sealed class MembersViewHolder(binding: ViewDataBinding) : ViewHolder(binding.root) {
class MemberInviteViewHolder(
private val binding: ItemMemberInviteBinding,
private val memberInviteHandler: MemberInviteHandler,
) : MembersViewHolder(binding) {
fun bind() {
binding.handler = memberInviteHandler
}
}

class MemberProfileViewHolder(
private val binding: ItemMemberProfileBinding,
) : MembersViewHolder(binding) {
fun bind(mate: Member) {
binding.member = mate
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.on.staccato.presentation.category.adapter

enum class MembersViewType(val viewType: Int) {
MEMBER_INVITE(0),
MEMBER_PROFILE(1),
;

companion object {
fun from(viewType: Int): MembersViewType {
return when (viewType) {
0 -> MEMBER_INVITE
1 -> MEMBER_PROFILE
else -> throw IllegalArgumentException("Invalid viewType: $viewType")
}
}
}
}
Loading
Loading