From 3d6ad26eb291f95087e7d1dab32b731636878a49 Mon Sep 17 00:00:00 2001 From: cnsvkf Date: Sat, 7 Nov 2026 02:56:15 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20[#272]=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=9D=B8=EC=A6=9D=EB=B2=88=ED=98=B8=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=95=88=EB=82=B4=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/AuthVerificationCodeInput.kt | 18 ++++-- .../heartguard/view/route/auth/AuthRoutes.kt | 26 ++++++++- .../view/screen/auth/AuthSignUpScreen.kt | 56 +++++++++++++++---- app/src/main/res/values/strings.xml | 6 +- docs/PRODUCT_BRIEF.md | 10 ++++ 5 files changed, 93 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/nativelap/heartguard/view/component/auth/AuthVerificationCodeInput.kt b/app/src/main/java/com/nativelap/heartguard/view/component/auth/AuthVerificationCodeInput.kt index b9595a7c..d822438a 100644 --- a/app/src/main/java/com/nativelap/heartguard/view/component/auth/AuthVerificationCodeInput.kt +++ b/app/src/main/java/com/nativelap/heartguard/view/component/auth/AuthVerificationCodeInput.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.error import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.KeyboardType @@ -42,6 +43,7 @@ internal fun AuthVerificationCodeInput( code: String, onCodeChange: (String) -> Unit, enabled: Boolean, + errorDescription: String? = null, modifier: Modifier = Modifier, ) { var isFocused by remember { mutableStateOf(false) } @@ -59,7 +61,10 @@ internal fun AuthVerificationCodeInput( .fillMaxWidth() .keyboardInputBounds() .onFocusChanged { focusState -> isFocused = focusState.isFocused } - .semantics { contentDescription = accessibilityDescription }, + .semantics { + contentDescription = accessibilityDescription + errorDescription?.let { description -> error(description) } + }, enabled = enabled, singleLine = true, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword), @@ -85,12 +90,12 @@ internal fun AuthVerificationCodeInput( color = MaterialTheme.colorScheme.surface, shape = RoundedCornerShape(CODE_CELL_CORNER_RADIUS), ).border( - width = if (isCurrentCell) 2.dp else 0.dp, + width = if (errorDescription != null || isCurrentCell) 2.dp else 0.dp, color = - if (isCurrentCell) { - MaterialTheme.extraColors.authPrimary - } else { - androidx.compose.ui.graphics.Color.Transparent + when { + errorDescription != null -> MaterialTheme.colorScheme.error + isCurrentCell -> MaterialTheme.extraColors.authPrimary + else -> androidx.compose.ui.graphics.Color.Transparent }, shape = RoundedCornerShape(CODE_CELL_CORNER_RADIUS), ).padding(horizontal = 4.dp), @@ -130,6 +135,7 @@ private fun AuthVerificationCodeInputPreview() { code = "123", onCodeChange = {}, enabled = true, + errorDescription = stringResource(R.string.auth_signup_invalid_code_error), ) } } diff --git a/app/src/main/java/com/nativelap/heartguard/view/route/auth/AuthRoutes.kt b/app/src/main/java/com/nativelap/heartguard/view/route/auth/AuthRoutes.kt index 85a01467..052c9cf7 100644 --- a/app/src/main/java/com/nativelap/heartguard/view/route/auth/AuthRoutes.kt +++ b/app/src/main/java/com/nativelap/heartguard/view/route/auth/AuthRoutes.kt @@ -180,6 +180,7 @@ internal fun HeartGuardSignUpRoute( val uiState by viewModel.uiState.collectAsStateWithLifecycle() val normalizedEmail = email.trim().lowercase(Locale.ROOT) val isCodeSentForCurrentEmail = uiState.emailForRequestedCode == normalizedEmail + val emailCodeRemainingSeconds = uiState.emailCodeRemainingSecondsByEmail[normalizedEmail] ?: 0 BackHandler( enabled = uiState.isSubmitting || uiState.isRequestingEmailCode || uiState.isVerifyingEmailCode || step > 0, @@ -275,9 +276,24 @@ internal fun HeartGuardSignUpRoute( stringResource(R.string.auth_signup_registration_error) } } + val isInvalidVerificationCode = + uiState.failure == AppAccountSignUpFailure.INVALID_VERIFICATION_CODE + val isExpiredVerificationCode = isInvalidVerificationCode && emailCodeRemainingSeconds <= 0 val statusMessage = - uiState.failureMessage?.takeIf { serverMessage -> serverMessage.isNotBlank() } - ?: fallbackStatusMessage + when { + isInvalidVerificationCode && !isExpiredVerificationCode -> { + fallbackStatusMessage + } + + isExpiredVerificationCode -> { + stringResource(R.string.auth_signup_expired_code_error) + } + + else -> { + uiState.failureMessage?.takeIf { serverMessage -> serverMessage.isNotBlank() } + ?: fallbackStatusMessage + } + } AuthSignUpScreen( name = name, @@ -289,10 +305,11 @@ internal fun HeartGuardSignUpRoute( isEmailCodeSent = isCodeSentForCurrentEmail, isEmailCodeResendBlocked = (uiState.emailCodeCooldownSecondsByEmail[normalizedEmail] ?: 0) > 0, - emailCodeRemainingSeconds = uiState.emailCodeRemainingSecondsByEmail[normalizedEmail] ?: 0, + emailCodeRemainingSeconds = emailCodeRemainingSeconds, isSubmitting = uiState.isSubmitting || uiState.isVerifyingEmailCode, statusMessage = statusMessage, isStatusError = uiState.failure != null, + isVerificationCodeError = isInvalidVerificationCode, step = step, onEvent = { event -> when (event) { @@ -311,6 +328,9 @@ internal fun HeartGuardSignUpRoute( } is AuthSignUpScreenEvent.VerificationCodeChanged -> { + if (uiState.failure == AppAccountSignUpFailure.INVALID_VERIFICATION_CODE) { + viewModel.clearEmailCodeVerification() + } verificationCode = event.verificationCode.filter { digit -> digit in '0'..'9' }.take(6) } diff --git a/app/src/main/java/com/nativelap/heartguard/view/screen/auth/AuthSignUpScreen.kt b/app/src/main/java/com/nativelap/heartguard/view/screen/auth/AuthSignUpScreen.kt index 91945e30..559010af 100644 --- a/app/src/main/java/com/nativelap/heartguard/view/screen/auth/AuthSignUpScreen.kt +++ b/app/src/main/java/com/nativelap/heartguard/view/screen/auth/AuthSignUpScreen.kt @@ -44,6 +44,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.LiveRegionMode +import androidx.compose.ui.semantics.liveRegion +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview @@ -76,6 +79,7 @@ internal fun AuthSignUpScreen( isSubmitting: Boolean, statusMessage: String?, isStatusError: Boolean, + isVerificationCodeError: Boolean, step: Int, onEvent: (AuthSignUpScreenEvent) -> Unit, modifier: Modifier = Modifier, @@ -251,8 +255,13 @@ internal fun AuthSignUpScreen( onEvent(AuthSignUpScreenEvent.VerificationCodeChanged(code)) }, enabled = !isSubmitting && !isSendingEmailCode && emailCodeRemainingSeconds > 0, + errorDescription = statusMessage.takeIf { isVerificationCodeError }, + ) + SignUpErrorMessage( + isError = isStatusError, + message = statusMessage, + isLiveRegion = isVerificationCodeError, ) - SignUpErrorMessage(isStatusError, statusMessage) } if (step == SIGN_UP_CODE_STEP) { @@ -261,16 +270,26 @@ internal fun AuthSignUpScreen( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - Text( - text = - stringResource( - R.string.auth_signup_code_remaining_time, - emailCodeRemainingSeconds / SECONDS_PER_MINUTE, - emailCodeRemainingSeconds % SECONDS_PER_MINUTE, - ), - color = MaterialTheme.extraColors.secondaryText, - style = MaterialTheme.typography.bodySmall, - ) + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(R.string.auth_signup_code_remaining_time_label), + color = MaterialTheme.extraColors.secondaryText, + style = MaterialTheme.typography.bodySmall, + ) + Text( + text = + stringResource( + R.string.auth_signup_code_remaining_time_value, + emailCodeRemainingSeconds / SECONDS_PER_MINUTE, + emailCodeRemainingSeconds % SECONDS_PER_MINUTE, + ), + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.bodySmall, + ) + } TextButton( onClick = { onEvent(AuthSignUpScreenEvent.RequestEmailCodeClicked) }, enabled = @@ -459,11 +478,23 @@ internal fun AuthSignUpScreen( private fun SignUpErrorMessage( isError: Boolean, message: String?, + isLiveRegion: Boolean = false, ) { if (isError && message != null) { Text( text = message, - modifier = Modifier.padding(top = HeartGuardSpacing.AuthErrorMessageGap), + modifier = + Modifier + .padding(top = HeartGuardSpacing.AuthErrorMessageGap) + .then( + if (isLiveRegion) { + Modifier.semantics { + liveRegion = LiveRegionMode.Polite + } + } else { + Modifier + }, + ), color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall, ) @@ -556,6 +587,7 @@ private fun AuthSignUpScreenPreview() { isSubmitting = false, statusMessage = null, isStatusError = false, + isVerificationCodeError = false, step = SIGN_UP_EMAIL_STEP, onEvent = {}, ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 02f865cf..43861bbf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -36,7 +36,8 @@ 인증번호 다시 받기 재요청 %1$d초 - 남은 시간 %1$02d:%2$02d + 남은 시간 + %1$02d:%2$02d 인증번호 입력 %1$d자리 입력됨 인증 완료 @@ -53,7 +54,8 @@ 먼저 이메일 인증코드를 받아주세요. 입력한 정보를 확인해주세요. 가입에 실패했습니다. 코드 만료·이메일 중복 여부를 확인해주세요. - 인증코드가 올바르지 않거나 만료되었습니다. 코드를 다시 확인해주세요. + 인증번호가 올바르지 않아요. 다시 확인해주세요. + 인증번호가 만료되었어요. 새 인증번호를 받아주세요. 회사 계정 설정이 완료되지 않았습니다. 관리자에게 문의해주세요. 로그인 8자 이상 입력해주세요 diff --git a/docs/PRODUCT_BRIEF.md b/docs/PRODUCT_BRIEF.md index bbf79e31..932897e3 100644 --- a/docs/PRODUCT_BRIEF.md +++ b/docs/PRODUCT_BRIEF.md @@ -20,6 +20,15 @@ - 포기할 수 있는 것: Coil 메모리 사용량은 최대한 낮게 유지하고, 서명 URL을 `expiresAt`까지 세션 메모리에서 재사용하며, 다른 기기의 사진 변경은 URL 만료 전까지 늦게 반영될 수 있음 (출처: 사용자 확인 답변, 2026-11-05) - 이렇게 한 이유: 이미지 지연을 줄이려는 목표에 맞추면서 메모리 사용을 아끼기 위해 세션 URL 캐시를 선택함 (출처: 사용자 요청 및 확인 답변, 2026-11-05) +## 회원가입 인증번호 오류 표시 + +- 마지막 확인: 2026-11-07 +- 핵심 사용자: 폭염 현장 작업자 (출처: `README.md`의 개요, 2026-11-06 확인) +- 해결할 문제: 인증번호가 틀렸을 때 입력 오류를 화면에서 즉시 알아보기 어려움 (출처: 사용자 요청 및 제공 스크린샷, 2026-11-06) +- 가장 중요한 점: 입력칸 오류 테두리와 오류 문구로 불일치를 바로 인지하게 함 (출처: 사용자 답변, 2026-11-06) +- 허용할 절충: 현재 앱/API 범위에서 처리하고, 서버 계약이 필요한 남은 시도 횟수는 표시하지 않음 (출처: 사용자 확인 답변, 2026-11-06) +- 이렇게 한 이유: 서버 변경 없이 인증 실패 피드백을 즉시 알아볼 수 있게 하려는 사용자 선택 (출처: 사용자 답변, 2026-11-06) + ## 프로필 사진 변경 시트 제목 간격 - 마지막 확인: 2026-11-06 @@ -49,6 +58,7 @@ ## 변경 이력 - 2026-11-06: 홈 위치 기반 날씨 — coarse 위치 권한 동의 후 현재 좌표를 사용하고, 실패 시 기존 팀 날씨 또는 수동 입력으로 대체. +- 2026-11-06: 회원가입 인증번호 오류 표시 — 불일치·재입력·만료 상태를 구분하고 입력 오류 의미를 보조기술에 전달. - 2026-11-05: 프로필 이미지 로딩 최적화 — 사용자 목표와 승인한 세션 캐시 절충을 반영. - 2026-11-06: 프로필 사진 변경 시트 제목 간격 — 제목 위쪽 여백을 약 5.dp 늘림. - 2026-11-06: 로그인·회원가입 인증 버튼 너비 — 입력 영역과 정렬하고 기존 반응형 여백을 유지.