Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9f70b41
✨ Feat: 온보딩 커플 연결 화면 일러스트 교체
chanho0908 Mar 6, 2026
ae5dfa1
♻️ Refactor: 온보딩 리소스 `design-system` 모듈로 이동
chanho0908 Mar 6, 2026
139b42b
✨ Feat: 온보딩 커플 연결 화면 뒤로가기 버튼 추가
chanho0908 Mar 6, 2026
5ffa4dd
♻️ Refactor: 커플연결 토스트 메시지 처리 로직 공통화
chanho0908 Mar 6, 2026
31f0849
♻️ Refactor: 온보딩 초대 코드 조회 로직 개선 및 자동화
chanho0908 Mar 6, 2026
5f1f98d
♻️ Refactor: 온보딩 토스트 메시지 처리 로직 공통화
chanho0908 Mar 6, 2026
899f984
♻️ Refactor: `OnBoardingSideEffect` 내 이동 관련 네이밍 변경
chanho0908 Mar 6, 2026
ef3ad22
♻️ Refactor: OnBoarding 관련 MVI 클래스 패키지 위치 변경 (`model` -> `contract`)
chanho0908 Mar 6, 2026
ec5d644
♻️ Refactor: 직접 연결하기 텍스트 스타일 수정
chanho0908 Mar 6, 2026
d81c68a
✨ Feat: 해지한 커플 복구 버튼 UI 개선
chanho0908 Mar 6, 2026
306ef3d
✨ Feat: 해지한 커플 바텀시트 디자인 수정
chanho0908 Mar 6, 2026
9d0589a
✨ Feat: 초대 코드 텍스트필드 자동 활성화 제거
chanho0908 Mar 6, 2026
1cb423d
♻️ Refactor: `OnBoardingUiState` 내 상태 업데이트 편의 메서드 제거 및 로직 viewModel로 이동
chanho0908 Mar 6, 2026
fcfaced
✨ Feat: 초대 코드 입력 시 공백 제거 로직 추가
chanho0908 Mar 6, 2026
d24bfa8
✨ Feat: 초대 코드 입력 시 자동 대문자 키보드 설정 추가
chanho0908 Mar 6, 2026
6790ac6
✨ Feat: 초대 코드 복사 로직 개선 및 SideEffect 처리 분리
chanho0908 Mar 6, 2026
4a91aec
♻️ Refactor: StatsDetailScreen 패딩 및 간격 조정
chanho0908 Mar 10, 2026
a7a713b
✨ Feat: `ToastHost`에 `imePadding` 적용
chanho0908 Mar 10, 2026
dda1ac6
♻️ Refactor: `InviteCodeTextField` 로직 단순화 및 커서 애니메이션 제거
chanho0908 Mar 10, 2026
aa7374e
♻️ Refactor: `OnBoardingViewModel` 내 미사용 `onError` 블록 제거
chanho0908 Mar 10, 2026
39ea35c
♻️ Refactor: 초대 코드 화면 수정
chanho0908 Mar 10, 2026
c6aad64
✨ Feat: CoupleConnectRoute 세로 스크롤 적용
chanho0908 Mar 10, 2026
33396ed
✨ Feat: `InviteCodeScreen` 스크롤시 gradient 효과 추가
chanho0908 Mar 10, 2026
59e4ef2
♻️ Refactor: 불필요한 리소스 임포트 별칭 제거
chanho0908 Mar 10, 2026
0e085ba
♻️ Refactor: CoupleConnectRoute 스크롤 적용 위치 변경
chanho0908 Mar 10, 2026
f766466
♻️ Refactor: `ConnectButton` 레이아웃 및 텍스트 정렬 개선
chanho0908 Mar 10, 2026
9cc29f6
♻️ Refactor: LoginButton login 모듈로 이동
chanho0908 Mar 10, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ 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.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
Expand Down Expand Up @@ -87,7 +88,10 @@ fun ToastHost(
}

Box(
modifier = modifier.fillMaxSize(),
modifier =
modifier
.fillMaxSize()
.imePadding(),
contentAlignment = Alignment.BottomCenter,
) {
AnimatedVisibility(
Expand Down
13 changes: 13 additions & 0 deletions core/design-system/src/main/res/drawable/ic_arrow_left.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="44dp"
android:height="44dp"
android:viewportWidth="44"
android:viewportHeight="44">
<path
android:pathData="M20.504,31H27.5C28.605,31 29.5,29.849 29.5,28.429V15.57C29.5,14.151 28.605,13 27.5,13H20.5M18,25.5L14.5,22L18,18.5M24.5,21.996H14.5"
android:strokeLineJoin="round"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#171717"
android:strokeLineCap="round"/>
</vector>
450 changes: 450 additions & 0 deletions core/design-system/src/main/res/drawable/ic_invite.xml

Large diffs are not rendered by default.

33 changes: 33 additions & 0 deletions core/design-system/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@
<string name="toast_camera_permission_request">인증샷 촬영을 위해서 카메라 권한이 필요해요.</string>
<string name="toast_fetch_notification_failed">알림 목록 조회에 실패했습니다.</string>
<string name="toast_poke_goal_failed">찌르기에 실패했습니다.</string>
<string name="toast_already_used_invite_code_message">이미 사용된 초대 코드입니다.</string>
<string name="toast_invite_code_copy">초대 코드가 복사되었어요</string>
<string name="toast_invalid_invite_code">잘못된 초대 코드입니다</string>

<!-- 다이얼로그 -->
<string name="dialog_end_goal_title">%s\n목표를 이루셨나요?</string>
Expand Down Expand Up @@ -160,4 +163,34 @@
<string name="fetch_onboarding_status_fail_message">온보딩 정보를 불러오는 데 실패했습니다. 다시 시도해주세요</string>
<string name="login_title_message">함께니까 멈추지 않아요.\n지금 바로 키피럽 시작하기!</string>

<!-- 온보딩 화면 -->
<string name="onboarding_name_placeholder">닉네임을 입력해 주세요.</string>
<string name="onboarding_name_helper">닉네임 2~8자</string>
<string name="onboarding_profile_title">짝꿍에게 보일\n내 이름을 입력해 주세요</string>
<string name="onboarding_profile_placeholder">닉네임을 입력해 주세요.</string>
<string name="onboarding_profile_helper">닉네임 2~8자</string>
<string name="onboarding_profile_button_title">완료</string>
<string name="onboarding_profile_invalid_name_length_toast">2자에서 8자 이내로 닉네임을 입력해주세요.</string>
<string name="onboarding_profile_setup_fail">프로필 설정 요청에 실패했습니다.</string>

<string name="onboarding_couple_connect_description">짝꿍과 연결하고\n함께 키피럽 시작하세요</string>
<string name="onboarding_couple_connect_send_invitation">초대장 보내기</string>
<string name="onboarding_couple_connect_direct_description">짝꿍에게 코드를 받았다면?</string>
<string name="onboarding_couple_direct_connect_button_title">직접 연결하기</string>
<string name="onboarding_couple_restore">해지한 커플 복구하려면?</string>
<string name="onboarding_couple_restore_bottom_sheet_content">아래 내용을 포함하여 문의해 주시기 바랍니다.\n고객센터 메일 - ttwixteamm@gmail.com</string>
<string name="onboarding_couple_restore_content_my_email">본인 로그인 계정 메일</string>
<string name="onboarding_couple_restore_content_partner_email">짝꿍의 로그인 계정 메일</string>
<string name="onboarding_couple_restore_content_restore_date">해지 일시</string>
<string name="onboarding_couple_fetch_my_invite_code_fail">내 조회코드 조회에 실패했습니다.</string>
<string name="onboarding_couple_connection_fail">커플 연결 요청에 실패했어요</string>

<string name="onboarding_invite_code_plz_write_invite_code">짝꿍에게 받은\n초대 코드를 써주세요</string>
<string name="onboarding_invite_code_my_invite_code">내 초대 코드</string>
<string name="onboarding_invite_code_write_invite_code">받은 코드 쓰기</string>

<string name="onboarding_dday_plz_set_dday">우리의 기념일을 등록해 주세요</string>
<string name="onboarding_dday_placeholder">YYYY-MM-DD</string>
<string name="onboarding_dday_setup_fail">기념일 설정에 실패했습니다.</string>

</resources>
65 changes: 17 additions & 48 deletions feature/login/src/main/java/com/twix/login/LoginScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,23 @@ package com.twix.login
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
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.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.boundsInParent
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
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.IntOffset
import androidx.compose.ui.unit.dp
import com.twix.designsystem.R
import com.twix.designsystem.components.button.LoginButton
import com.twix.designsystem.components.text.AppText
import com.twix.designsystem.components.toast.ToastManager
import com.twix.designsystem.components.toast.model.ToastData
Expand All @@ -39,6 +29,7 @@ import com.twix.designsystem.theme.TwixTheme
import com.twix.domain.model.OnboardingStatus
import com.twix.domain.model.enums.AppTextStyle
import com.twix.domain.model.enums.LoginType
import com.twix.login.component.LoginButton
import com.twix.login.contract.LoginIntent
import com.twix.login.contract.LoginSideEffect
import com.twix.ui.base.ObserveAsEvents
Expand Down Expand Up @@ -82,10 +73,6 @@ fun LoginRoute(

@Composable
private fun LoginScreen(onClickLogin: (LoginType) -> Unit) {
var imageBottomPx by remember { mutableFloatStateOf(0f) }
val density = LocalDensity.current
val offsetPx = with(density) { 34.dp.toPx() }

Column(
modifier =
Modifier
Expand All @@ -111,41 +98,23 @@ private fun LoginScreen(onClickLogin: (LoginType) -> Unit) {

Spacer(Modifier.height(27.dp))

Box(modifier = Modifier.fillMaxSize()) {
Image(
imageVector = ImageVector.vectorResource(R.drawable.ic_singing),
contentDescription = null,
modifier =
Modifier
.onGloballyPositioned { coordinates ->
imageBottomPx = coordinates.boundsInParent().bottom
},
)
Image(
imageVector = ImageVector.vectorResource(R.drawable.ic_singing),
contentDescription = null,
)

if (imageBottomPx != 0f) {
Column(
modifier =
Modifier
.padding(horizontal = 20.dp)
.offset {
IntOffset(
x = 0,
/**
* singing 이미지 하단 기준으로 로그인 버튼을 배치하고
* 이미지와 버튼이 겹치는 만큼(34dp) 상단으로 이동
* */
y = (imageBottomPx - offsetPx).toInt(),
)
},
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
LoginType.entries.forEach { type ->
LoginButton(
type = type,
onClickLogin = onClickLogin,
)
}
}
Column(
modifier =
Modifier
.padding(horizontal = 20.dp)
.padding(top = 29.dp, bottom = 27.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
LoginType.entries.forEach { type ->
LoginButton(
type = type,
onClickLogin = onClickLogin,
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.twix.designsystem.components.button
package com.twix.login.component

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
Expand Down Expand Up @@ -27,7 +27,7 @@ import com.twix.domain.model.enums.LoginType
import com.twix.ui.extension.noRippleClickable

@Composable
fun LoginButton(
internal fun LoginButton(
type: LoginType,
onClickLogin: (LoginType) -> Unit,
modifier: Modifier = Modifier,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,7 @@ object LoginNavGraph : NavGraphContributor {
OnboardingStatus.COMPLETED -> return@LoginRoute
}

navController.navigate(destination) {
popUpTo(NavRoutes.LoginGraph.route) {
inclusive = true
}
}
navController.navigate(destination)
},
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package com.twix.onboarding

import androidx.lifecycle.viewModelScope
import com.twix.designsystem.R
import com.twix.designsystem.components.toast.model.ToastType
import com.twix.domain.model.OnboardingStatus
import com.twix.domain.model.invitecode.InviteCode
import com.twix.domain.repository.NotificationRepository
import com.twix.domain.repository.OnBoardingRepository
import com.twix.onboarding.model.OnBoardingIntent
import com.twix.onboarding.model.OnBoardingSideEffect
import com.twix.onboarding.model.OnBoardingUiState
import com.twix.onboarding.contract.OnBoardingIntent
import com.twix.onboarding.contract.OnBoardingSideEffect
import com.twix.onboarding.contract.OnBoardingUiState
import com.twix.result.AppError
import com.twix.ui.base.BaseViewModel
import kotlinx.coroutines.launch
Expand All @@ -16,11 +19,26 @@ class OnBoardingViewModel(
private val onBoardingRepository: OnBoardingRepository,
private val notificationRepository: NotificationRepository,
) : BaseViewModel<OnBoardingUiState, OnBoardingIntent, OnBoardingSideEffect>(OnBoardingUiState()) {
fun fetchMyInviteCode() {
init {
fetchMyInviteCode()
}

private fun fetchMyInviteCode() {
launchResult(
block = { onBoardingRepository.fetchInviteCode() },
onSuccess = { reduce { updateMyInviteCode(it.value) } },
onError = { emitSideEffect(OnBoardingSideEffect.CoupleConnection.ShowFetchMyInviteCodeFailToast) },
onSuccess = { fetchedInviteCode ->
reduce {
copy(
inviteCode =
inviteCode.copy(
myInviteCode = fetchedInviteCode.value,
),
)
}
},
onError = {
showToast(R.string.onboarding_couple_fetch_my_invite_code_fail, ToastType.ERROR)
},
)
}

Expand All @@ -30,8 +48,7 @@ class OnBoardingViewModel(
is OnBoardingIntent.WriteInviteCode -> reduceInviteCode(intent.value)
OnBoardingIntent.ConnectCouple -> connectCouple()
OnBoardingIntent.CopyInviteCode ->
emitSideEffect(OnBoardingSideEffect.InviteCode.ShowCopyInviteCodeSuccessToast)

emitSideEffect(OnBoardingSideEffect.InviteCode.CopyInviteCode(currentState.inviteCode.myInviteCode))
// 프로필 설정 화면
is OnBoardingIntent.WriteNickName -> reduceNickName(intent.value)
OnBoardingIntent.SubmitNickName -> handleSubmitNickname()
Expand All @@ -41,12 +58,26 @@ class OnBoardingViewModel(
OnBoardingIntent.SubmitDday -> anniversarySetup()

is OnBoardingIntent.SubmitMarketingConsent ->
initNotificationSettings(intent.isPushEnabled, intent.isMarketingEnabled, intent.isNightMarketingEnabled)
initNotificationSettings(
intent.isPushEnabled,
intent.isMarketingEnabled,
intent.isNightMarketingEnabled,
)
}
}

private fun reduceInviteCode(value: String) {
reduce { updatePartnerInviteCode(value) }
val isValidInviteCode = InviteCode.create(value).isSuccess

reduce {
copy(
inviteCode =
inviteCode.copy(
partnerInviteCode = value,
isValid = isValidInviteCode,
),
)
}
}

private fun connectCouple() {
Expand All @@ -70,28 +101,28 @@ class OnBoardingViewModel(
* 초대 코드를 잘못 입력한 경우
* */
if (error.message == INVALID_INVITE_CODE_MESSAGE) {
emitSideEffect(OnBoardingSideEffect.InviteCode.ShowInvalidInviteCodeToast)
showToast(R.string.toast_invalid_invite_code, ToastType.ERROR)
} else if (error.message == ALREADY_USED_INVITE_CODE_MESSAGE) {
/**
* 상대방이 이미 연결한 경우
* */
emitSideEffect(OnBoardingSideEffect.InviteCode.NavigateToNext)
} else {
emitSideEffect(OnBoardingSideEffect.InviteCode.ShowConnectCoupleConnectFailToast)
showToast(R.string.onboarding_couple_connection_fail, ToastType.ERROR)
}
}
}

private fun reduceNickName(value: String) {
reduce { updateNickName(value) }
reduce { copy(profile = profile.updateNickname(value)) }
}

private fun handleSubmitNickname() {
if (currentState.isValidNickName) {
profileSetup()
} else {
viewModelScope.launch {
emitSideEffect(OnBoardingSideEffect.ProfileSetting.ShowInvalidNickNameToast)
showToast(R.string.onboarding_profile_invalid_name_length_toast, ToastType.ERROR)
}
}
}
Expand All @@ -100,7 +131,7 @@ class OnBoardingViewModel(
launchResult(
block = { onBoardingRepository.profileSetup(currentState.profile.nickname) },
onSuccess = { fetchOnboardingStatus() },
onError = { emitSideEffect(OnBoardingSideEffect.ProfileSetting.ShowProfileSetupFailToast) },
onError = { showToast(R.string.onboarding_profile_setup_fail, ToastType.ERROR) },
)
}

Expand All @@ -112,7 +143,7 @@ class OnBoardingViewModel(
val sideEffect =
when (onboardingStatus) {
OnboardingStatus.ANNIVERSARY_SETUP ->
OnBoardingSideEffect.ProfileSetting.NavigateToDDaySetting
OnBoardingSideEffect.ProfileSetting.NavigateToNext

OnboardingStatus.COMPLETED ->
OnBoardingSideEffect.ProfileSetting.NavigateToHome
Expand All @@ -122,14 +153,15 @@ class OnBoardingViewModel(
emitSideEffect(sideEffect)
}
},
onError = {
// 에러처리 추가
},
)
}

private fun reduceDday(value: LocalDate) {
reduce { updateDday(value) }
reduce {
copy(
dDay = dDay.updateAnniversaryDate(value),
)
}
}

private fun anniversarySetup() {
Expand All @@ -139,7 +171,7 @@ class OnBoardingViewModel(
viewModelScope.launch { emitSideEffect(OnBoardingSideEffect.DdaySetting.NavigateToHome) }
},
onError = {
emitSideEffect(OnBoardingSideEffect.DdaySetting.ShowAnniversarySetupFailToast)
showToast(R.string.onboarding_dday_setup_fail, ToastType.ERROR)
},
)
}
Expand All @@ -161,6 +193,13 @@ class OnBoardingViewModel(
)
}

private suspend fun showToast(
message: Int,
type: ToastType,
) {
emitSideEffect(OnBoardingSideEffect.ShowToast(message, type))
}

companion object {
private const val ALREADY_USED_INVITE_CODE_MESSAGE = "이미 사용된 초대 코드입니다."
private const val INVALID_INVITE_CODE_MESSAGE = "유효하지 않은 초대 코드입니다."
Expand Down
Loading