Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -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
Expand All @@ -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) }
Expand All @@ -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),
Expand All @@ -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),
Expand Down Expand Up @@ -130,6 +135,7 @@ private fun AuthVerificationCodeInputPreview() {
code = "123",
onCodeChange = {},
enabled = true,
errorDescription = stringResource(R.string.auth_signup_invalid_code_error),
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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) {
Expand All @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -76,6 +79,7 @@ internal fun AuthSignUpScreen(
isSubmitting: Boolean,
statusMessage: String?,
isStatusError: Boolean,
isVerificationCodeError: Boolean,
step: Int,
onEvent: (AuthSignUpScreenEvent) -> Unit,
modifier: Modifier = Modifier,
Expand Down Expand Up @@ -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) {
Expand All @@ -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 =
Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -556,6 +587,7 @@ private fun AuthSignUpScreenPreview() {
isSubmitting = false,
statusMessage = null,
isStatusError = false,
isVerificationCodeError = false,
step = SIGN_UP_EMAIL_STEP,
onEvent = {},
)
Expand Down
6 changes: 4 additions & 2 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
<string name="auth_signup_resend_code_action">인증번호 다시 받기</string>
<string name="auth_signup_resend_code">재요청</string>
<string name="auth_signup_resend_wait">%1$d초</string>
<string name="auth_signup_code_remaining_time">남은 시간 %1$02d:%2$02d</string>
<string name="auth_signup_code_remaining_time_label">남은 시간</string>
<string name="auth_signup_code_remaining_time_value">%1$02d:%2$02d</string>
<string name="auth_signup_code_accessibility">인증번호 입력</string>
<string name="auth_signup_code_digit_count">%1$d자리 입력됨</string>
<string name="auth_signup_email_verified">인증 완료</string>
Expand All @@ -53,7 +54,8 @@
<string name="auth_signup_code_required">먼저 이메일 인증코드를 받아주세요.</string>
<string name="auth_signup_form_error">입력한 정보를 확인해주세요.</string>
<string name="auth_signup_registration_error">가입에 실패했습니다. 코드 만료·이메일 중복 여부를 확인해주세요.</string>
<string name="auth_signup_invalid_code_error">인증코드가 올바르지 않거나 만료되었습니다. 코드를 다시 확인해주세요.</string>
<string name="auth_signup_invalid_code_error">인증번호가 올바르지 않아요. 다시 확인해주세요.</string>
<string name="auth_signup_expired_code_error">인증번호가 만료되었어요. 새 인증번호를 받아주세요.</string>
<string name="auth_signup_account_not_ready_error">회사 계정 설정이 완료되지 않았습니다. 관리자에게 문의해주세요.</string>
<string name="auth_signup_app_login">로그인</string>
<string name="auth_signup_password_hint">8자 이상 입력해주세요</string>
Expand Down
10 changes: 10 additions & 0 deletions docs/PRODUCT_BRIEF.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -49,6 +58,7 @@

## 변경 이력
- 2026-11-06: 홈 위치 기반 날씨 — coarse 위치 권한 동의 후 현재 좌표를 사용하고, 실패 시 기존 팀 날씨 또는 수동 입력으로 대체.
- 2026-11-06: 회원가입 인증번호 오류 표시 — 불일치·재입력·만료 상태를 구분하고 입력 오류 의미를 보조기술에 전달.
- 2026-11-05: 프로필 이미지 로딩 최적화 — 사용자 목표와 승인한 세션 캐시 절충을 반영.
- 2026-11-06: 프로필 사진 변경 시트 제목 간격 — 제목 위쪽 여백을 약 5.dp 늘림.
- 2026-11-06: 로그인·회원가입 인증 버튼 너비 — 입력 영역과 정렬하고 기존 반응형 여백을 유지.
Expand Down
Loading