Skip to content
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

[TNT-190] 트레이니 연결 요청 팝업 기능 구현 #110

Merged
merged 23 commits into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d493159
[TNT-190] design: TnTCheckToggleDialog 텍스트 클릭 추가
SeonJeongk Feb 7, 2025
8fab3ad
[TNT-190] feat: 트레이니 홈화면 연결 dialog 띄우기 구현
SeonJeongk Feb 7, 2025
2cfbe83
[TNT-190] feat: 3일간 보지 않기 설정한 날짜 dataStore에 저장
SeonJeongk Feb 7, 2025
3e7635a
[TNT-190] fix: rebase 이후 오류 수정
SeonJeongk Feb 14, 2025
17ceef2
[TNT-000] refactor: 회원가입 이름 입력 에러 문구 수정 반영
SeonJeongk Feb 14, 2025
525664b
[TNT-190] refactor: uiResource -> coreR로 수정
SeonJeongk Feb 14, 2025
091f472
[TNT-000] refactor: 회원가입 달력 초기 노출 날짜 수정
SeonJeongk Feb 14, 2025
e2a5316
[TNT-000] refactor: viewModel에서 context 사용하지 않도록 개선
SeonJeongk Feb 14, 2025
7eb3704
[TNT-000] refactor: 회원가입 문구 수정
SeonJeongk Feb 14, 2025
140c097
[TNT-190] feat: isConnected 설정 추가 및 datasource 역할에 맞게 이름 변경
SeonJeongk Feb 14, 2025
611d3f1
[TNT-190] feat: isConnected 저장 및 삭제 구현
SeonJeongk Feb 14, 2025
91109c9
[TNT-190] feat: ConnectRepository에 localDatasource 추가
SeonJeongk Feb 14, 2025
31fc284
[TNT-000] fix: 완료 회차 입력 조건 및 회원 기본 프로필 사진 수정
SeonJeongk Feb 14, 2025
5a95597
[TNT-190] refactor: 연결 다이얼로그 뒤로가기 막기
SeonJeongk Feb 14, 2025
4ea6a54
[TNT-190] fix: 연결여부 저장하지 않도록 수정
SeonJeongk Feb 14, 2025
44b6e39
[TNT-190] feat: 트레이니 연결 촉구 Dialog 기능 구현
SeonJeongk Feb 14, 2025
5f471cb
[TNT-000] fix: 에러 이미지 추가
SeonJeongk Feb 14, 2025
cd6b6fd
[TNT-190] fix: 네트워크 에러 핸들링
SeonJeongk Feb 14, 2025
93c5c2c
[TNT-190] refactor: 네이밍 수정
SeonJeongk Feb 14, 2025
20efdde
[TNT-190] fix: onEach 삭제 및 네이밍 수정
SeonJeongk Feb 14, 2025
8241263
[TNT-190] refactor: 네이밍 변경
SeonJeongk Feb 14, 2025
c7e63ad
[TNT-190] refactor: 시간 상수 선언
SeonJeongk Feb 14, 2025
9b6a12e
[TNT-190] Merge branch 'develop' into feature/TNT-190
SeonJeongk Feb 14, 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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package co.kr.tnt.ui.component

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
Expand Down Expand Up @@ -39,11 +40,16 @@ fun TnTCheckToggleDialog(
modifier: Modifier = Modifier,
onLeftButtonClick: () -> Unit,
onRightButtonClick: () -> Unit,
cancelable: Boolean = false,
onCheckClick: () -> Unit,
onDismiss: () -> Unit,
) {
Dialog(
onDismissRequest = { onDismiss() },
onDismissRequest = {
if (cancelable) {
onDismiss()
}
},
properties = DialogProperties(usePlatformDefaultWidth = false),
) {
Card(
Expand Down Expand Up @@ -88,7 +94,9 @@ fun TnTCheckToggleDialog(
text = checkToggleText,
style = TnTTheme.typography.body2Medium,
color = TnTTheme.colors.neutralColors.Neutral500,
modifier = Modifier.padding(start = 4.dp),
modifier = Modifier
.padding(start = 4.dp)
.clickable(onClick = onCheckClick),
)
}
Row(
Expand Down
6 changes: 5 additions & 1 deletion core/ui/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
<string name="ok">확인</string>
<string name="move_to_setting">설정으로 이동</string>

<string name="error_server_request_failed">서버 요청에 실패했어요</string>
<string name="entered_wrong_text">잘못된 수치를 입력했어요</string>
<string name="text_length_and_format_warning">%s자 미만의 한글 또는 영문으로 입력해주세요</string>

<string name="trainee">트레이니</string>
<string name="trainer">트레이너</string>
Expand All @@ -27,6 +27,7 @@
<string name="start">시작하기</string>
<string name="skip">건너뛰기</string>
<string name="cancel">취소</string>
<string name="next_time">다음에</string>

<string name="name">이름</string>
<string name="age_label">나이</string>
Expand All @@ -39,6 +40,9 @@
<string name="notification">알림</string>
<string name="no_recent_notifications">최근 받은 알림이 없어요</string>

<!-- Dialog -->
<string name="do_not_see_for_three_days">3일 동안 보지 않기</string>

<!-- Meal -->
<string name="meal_breakfast">아침</string>
<string name="meal_lunch">점심</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ import kotlinx.serialization.Serializable
@Serializable
data class CheckSessionResponse(
val memberType: MemberType,
val isConnected: Boolean,
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,23 @@ package co.kr.data.repository
import co.kr.data.network.model.ConnectRequest
import co.kr.data.network.model.toDomain
import co.kr.data.network.source.ConnectRemoteDataSource
import co.kr.data.storage.source.ConnectLocalDataSource
import co.kr.tnt.domain.model.ConnectRequestResult
import co.kr.tnt.domain.model.ConnectedResult
import co.kr.tnt.domain.model.InviteCodeResult
import co.kr.tnt.domain.repository.ConnectRepository
import co.kr.tnt.domain.utils.DateFormatter
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import java.time.LocalDateTime
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
internal class ConnectRepositoryImpl @Inject constructor(
private val connectRemoteDataSource: ConnectRemoteDataSource,
private val connectLocalDataSource: ConnectLocalDataSource,
private val dateFormatter: DateFormatter,
) : ConnectRepository {
override suspend fun getInviteCode(): InviteCodeResult {
val response = connectRemoteDataSource.getInviteCode()
Expand Down Expand Up @@ -59,4 +66,17 @@ internal class ConnectRepositoryImpl @Inject constructor(
)
return response.toDomain()
}

override suspend fun getHomeDialogHiddenDate(): Flow<LocalDateTime?> =
connectLocalDataSource.explicitDeniedConnectDate.map { dateString ->
dateString?.let {
runCatching { dateFormatter.parseDateTime(it) }
.getOrDefault(LocalDateTime.MIN)
}
}

override suspend fun updateHomeDialogHiddenDate(date: LocalDateTime) {
val formattedDate = dateFormatter.format(date)
connectLocalDataSource.updateExplicitDeniedConnectDate(formattedDate)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import co.kr.data.network.model.LoginRequest
import co.kr.data.network.model.enum.toDomain
import co.kr.data.network.model.toDomain
import co.kr.data.network.source.LoginRemoteDataSource
import co.kr.data.storage.source.ConnectLocalDataSource
import co.kr.data.storage.source.SessionLocalDataSource
import co.kr.tnt.domain.model.AuthType
import co.kr.tnt.domain.model.LoginResult
Expand All @@ -16,9 +17,13 @@ import javax.inject.Singleton
internal class LoginRepositoryImpl @Inject constructor(
private val loginRemoteDataSource: LoginRemoteDataSource,
private val sessionLocalDataSource: SessionLocalDataSource,
private val connectLocalDataSource: ConnectLocalDataSource,
) : LoginRepository {
override suspend fun getUserType(): UserType =
loginRemoteDataSource.getCheckSession().memberType.toDomain()
override suspend fun getUserType(): UserType {
val response = loginRemoteDataSource.getCheckSession()

return response.memberType.toDomain()
}

override suspend fun login(
authType: AuthType,
Expand All @@ -41,10 +46,12 @@ internal class LoginRepositoryImpl @Inject constructor(
override suspend fun logout() {
loginRemoteDataSource.postLogout()
sessionLocalDataSource.removeSessionId()
connectLocalDataSource.clearExplicitDeniedConnectDate()
}

override suspend fun withdraw() {
loginRemoteDataSource.postWithdraw()
sessionLocalDataSource.removeSessionId()
connectLocalDataSource.clearExplicitDeniedConnectDate()
}
}
10 changes: 10 additions & 0 deletions data/storage/src/main/java/co/kr/data/storage/di/StorageModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ internal object StorageModule {
const val SETTING_STORAGE_NAME = "SETTING_STORAGE"
private val Context.settingDataStore by preferencesDataStore(name = SETTING_STORAGE_NAME)

const val CONNECT_STORAGE_NAME = "HOME_STORAGE"
private val Context.connectDataStore by preferencesDataStore(name = CONNECT_STORAGE_NAME)

@Provides
@Singleton
@Named(SESSION_STORAGE_NAME)
Expand All @@ -34,4 +37,11 @@ internal object StorageModule {
fun provideSettingDataStore(
@ApplicationContext context: Context,
): DataStore<Preferences> = context.settingDataStore

@Provides
@Singleton
@Named(CONNECT_STORAGE_NAME)
fun provideConnectDataStore(
@ApplicationContext context: Context,
): DataStore<Preferences> = context.connectDataStore
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package co.kr.data.storage.source

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import co.kr.data.storage.di.StorageModule.CONNECT_STORAGE_NAME
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Singleton

@Singleton
class ConnectLocalDataSource @Inject constructor(
@Named(CONNECT_STORAGE_NAME) private val connectPreferences: DataStore<Preferences>,
) {
val explicitDeniedConnectDate: Flow<String?> = connectPreferences.data.map { preferences ->
preferences[EXPLICIT_DENIED_CONNECT_DATE] ?: ""
}

suspend fun updateExplicitDeniedConnectDate(startDate: String) {
connectPreferences.edit { preferences ->
preferences[EXPLICIT_DENIED_CONNECT_DATE] = startDate
}
}

suspend fun clearExplicitDeniedConnectDate() {
connectPreferences.edit { preferences ->
preferences.remove(EXPLICIT_DENIED_CONNECT_DATE)
}
}

companion object {
private val EXPLICIT_DENIED_CONNECT_DATE = stringPreferencesKey("EXPLICIT_DENIED_CONNECT_DATE")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package co.kr.tnt.domain.repository
import co.kr.tnt.domain.model.ConnectRequestResult
import co.kr.tnt.domain.model.ConnectedResult
import co.kr.tnt.domain.model.InviteCodeResult
import kotlinx.coroutines.flow.Flow
import java.time.LocalDateTime

interface ConnectRepository {
suspend fun getInviteCode(): InviteCodeResult
Expand All @@ -22,4 +24,8 @@ interface ConnectRepository {
trainerId: String,
traineeId: String,
): ConnectedResult

suspend fun getHomeDialogHiddenDate(): Flow<LocalDateTime?>

suspend fun updateHomeDialogHiddenDate(date: LocalDateTime)
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,14 @@ internal fun PTSessionFormPage(
* 1. 모든 항목(완료된 회차, 총 등록 회차, 시작일)이 입력되어야 한다
* 2. 완료된 회차가 총 등록 회차보다 크지 않아야 한다
*/
val isFormValid = completedSessionCount.isNotEmpty() &&
totalSessionCount.isNotEmpty() && sessionStartDate != null &&
(completedSessionCount.toIntOrNull() ?: 0) < (totalSessionCount.toIntOrNull() ?: 0)
val isFormValid = sessionStartDate != null &&
isInvalidInput(completedSessionCount, allowZero = true).not() &&
isInvalidInput(totalSessionCount, allowZero = false).not() &&
isCompletedSessionInvalid(completedSessionCount, totalSessionCount).not()

val showWarning = completedSessionCount.isNotEmpty() && totalSessionCount.isNotEmpty() &&
(completedSessionCount.toIntOrNull() ?: 0) >= (totalSessionCount.toIntOrNull() ?: 0)
val showTotalSessionWarning = isInvalidInput(totalSessionCount, allowZero = false)
val showCompletedSessionWarning = isInvalidInput(completedSessionCount, allowZero = true) ||
isCompletedSessionInvalid(completedSessionCount, totalSessionCount)

Scaffold(
topBar = {
Expand Down Expand Up @@ -137,19 +139,21 @@ internal fun PTSessionFormPage(
isSingleLine = true,
isRequired = true,
keyboardType = KeyboardType.Number,
showWarning = showWarning,
showWarning = showCompletedSessionWarning || showTotalSessionWarning,
trailingComponent = {
UnitLabel(R.string.count_unit)
},
onValueChange = { newValue ->
if (validateInput(newValue)) {
onChangeCompletedSessionCount(newValue)
}
onChangeCompletedSessionCount(newValue)
},
modifier = Modifier.fillMaxWidth(),
)
Text(
text = if (showWarning) stringResource(uiResource.string.entered_wrong_text) else "",
text = if (showCompletedSessionWarning || showTotalSessionWarning) {
stringResource(uiResource.string.entered_wrong_text)
} else {
""
},
style = TnTTheme.typography.body2Medium,
color = TnTTheme.colors.redColors.Red500,
modifier = Modifier.padding(top = 4.dp),
Expand All @@ -171,19 +175,21 @@ internal fun PTSessionFormPage(
isSingleLine = true,
isRequired = true,
keyboardType = KeyboardType.Number,
showWarning = showWarning,
showWarning = showTotalSessionWarning || showCompletedSessionWarning,
trailingComponent = {
UnitLabel(R.string.count_unit)
},
onValueChange = { newValue ->
if (validateInput(newValue)) {
onChangeTotalSessionCount(newValue)
}
onChangeTotalSessionCount(newValue)
},
modifier = Modifier.fillMaxWidth(),
)
Text(
text = if (showWarning) stringResource(uiResource.string.entered_wrong_text) else "",
text = if (showTotalSessionWarning || showCompletedSessionWarning) {
stringResource(uiResource.string.entered_wrong_text)
} else {
""
},
style = TnTTheme.typography.body2Medium,
color = TnTTheme.colors.redColors.Red500,
modifier = Modifier.padding(top = 4.dp),
Expand All @@ -205,6 +211,27 @@ internal fun PTSessionFormPage(
}
}

/**
* 입력이 유효하지 않은 경우 true 리턴
* @param input 입력 값
* @param allowZero `true`일 경우 0 입력 가능 (현재 완료된 회차)
* @return `true`면 경고 필요, `false`면 정상 입력
*/
private fun isInvalidInput(input: String, allowZero: Boolean = false): Boolean {
val num = input.toIntOrNull() ?: return false

return if (allowZero) {
(num !in 0..MAX_COUNT) || (input.length > 1 && input.startsWith("0"))
} else {
(num !in 1..MAX_COUNT) || (input.length > 1 && input.startsWith("0"))
}
}

private fun isCompletedSessionInvalid(completedSession: String, totalSession: String): Boolean {
if (completedSession.isEmpty() || totalSession.isEmpty()) return false
return (completedSession.toIntOrNull() ?: 0) >= (totalSession.toIntOrNull() ?: 0)
}

@Composable
private fun DatePicker(
modifier: Modifier = Modifier,
Expand Down Expand Up @@ -265,14 +292,6 @@ private fun UnitLabel(stringResId: Int) {
)
}

/**
* 유효한 입력 값인지 검사
* 형식: 99 이하의 정수
*/
private fun validateInput(input: String): Boolean {
return input.isEmpty() || (input.toIntOrNull() != null && !input.startsWith("0") && input.toInt() <= MAX_COUNT)
}

@Preview
@Composable
private fun PTSessionFormPagePreview() {
Expand Down
Loading