Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0f3bc46
fix(timeletter): 서버 실패 상태와 재시도 처리
koongmai Aug 28, 2026
4c82a50
Merge branch 'develop' into fix/714
OnlyReadOneBook Aug 28, 2026
a12b983
Merge branch 'develop' into fix/714
OnlyReadOneBook Aug 28, 2026
78585c4
Merge branch 'develop' into fix/714
OnlyReadOneBook Aug 28, 2026
13f4627
Merge branch 'develop' into fix/714
OnlyReadOneBook Aug 28, 2026
d20c1a4
Merge branch 'develop' into fix/714
OnlyReadOneBook Aug 28, 2026
321ddeb
ci: CI Test Plan 재검증 트리거
OnlyReadOneBook Aug 28, 2026
d4debbc
Merge branch 'develop' into fix/714
OnlyReadOneBook Aug 28, 2026
f9b4b46
Merge branch 'develop' into fix/714
OnlyReadOneBook Aug 28, 2026
951f3fe
Merge branch 'develop' into fix/714
OnlyReadOneBook Aug 29, 2026
f9a48d3
fix(timeletter): 리뷰 지적 6건과 컴파일 블로커를 해소한다
koongmai Sep 5, 2026
c5ec3e7
Merge remote-tracking branch 'origin/develop' into fix/714
koongmai Sep 5, 2026
963dbf6
fix(timeletter): develop 머지 후 빠졌던 수정사항 2건을 커밋한다
koongmai Sep 5, 2026
1edc58c
fix(timeletter): 발신 목록 조회 실패 시 재시도 UI를 되살린다
koongmai Sep 8, 2026
95941c7
fix(timeletter): 수신인 목록 Loading·Error 상단바 제목을 Success와 맞춘다
koongmai Sep 9, 2026
e190647
fix(timeletter): 수신인 선택 상태를 rememberSaveable로 옮겨 백그라운드 복귀 시 소실을 막는다
koongmai Sep 9, 2026
9b6770b
fix(timeletter): deleteSelected 성공 분기의 중복된 isDeleting 대입을 지운다
koongmai Sep 9, 2026
a89e102
fix(timeletter): 조회 성공 뒤 재조회 실패가 빈 목록으로 가리는 문제를 고친다
koongmai Sep 12, 2026
83ff32d
fix(timeletter): 수신인 선택 소실과 로그아웃 신호 유실을 고친다 (#714)
koongmai Sep 13, 2026
fddd25b
test(timeletter): 수신인 선택 유지를 uiState 순환으로 검증한다 (#714)
koongmai Sep 13, 2026
25a4c9e
Merge remote-tracking branch 'origin/develop' into fix/714
koongmai Sep 13, 2026
2409ef8
fix(timeletter): androidTest를 RecipientListScreen의 상태 없는 시그니처에 맞춘다 (#…
koongmai Sep 13, 2026
e6c2151
Merge remote-tracking branch 'origin/develop' into fix/714
koongmai Oct 13, 2026
9977831
fix(timeletter): 수신인 선택을 receiverListStateFlow 로 옮겨 실패를 빈 목록과 가른다 (#714)
koongmai Oct 13, 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
@@ -1,17 +1,20 @@
package com.afternote.afternote_fe

import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.onRoot
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.afternote.afternote_fe.test.FailureArtifactRule
import com.afternote.afternote_fe.test.appTestUserRepository
import com.afternote.core.model.user.Receiver
import com.afternote.core.ui.theme.AfternoteTheme
import com.afternote.feature.timeletter.presentation.screen.sender.RecipientListScreen
import com.afternote.feature.timeletter.presentation.viewmodel.RecipientListUiState
import com.afternote.feature.timeletter.presentation.viewmodel.RecipientListViewModel
import org.junit.Rule
import org.junit.Test
Expand Down Expand Up @@ -43,22 +46,26 @@ class ReceiverSessionIsolationAndroidTest {

composeRule.setContent {
AfternoteTheme {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
RecipientListScreen(
uiState = uiState,
onBackClick = {},
onConfirmClick = {},
viewModel = viewModel,
onRetry = viewModel::retry,
)
}
}

composeRule.waitUntil(timeoutMillis = TIMEOUT_MILLIS) {
viewModel.recipients.value.any { it.name == PREVIOUS_ACCOUNT_RECEIVER }
(viewModel.uiState.value as? RecipientListUiState.Success)
?.recipients
?.any { it.name == PREVIOUS_ACCOUNT_RECEIVER } == true
}
composeRule.onNodeWithText(PREVIOUS_ACCOUNT_RECEIVER).assertIsDisplayed()

composeRule.runOnIdle { repository.receiverState.value = emptyList() }
composeRule.waitUntil(timeoutMillis = TIMEOUT_MILLIS) {
viewModel.recipients.value.isEmpty()
(viewModel.uiState.value as? RecipientListUiState.Success)?.recipients?.isEmpty() == true
}

composeRule.onNodeWithText("수신인 목록").assertIsDisplayed()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,13 +234,15 @@ class TimeLetterMindRecordCompletionAndroidTest {
composeRule.setContent {
AfternoteTheme {
if (showRecipient) {
val recipientUiState by recipientViewModel.uiState.collectAsStateWithLifecycle()
RecipientListScreen(
uiState = recipientUiState,
onBackClick = { showRecipient = false },
onConfirmClick = { recipients ->
writeViewModel.setRecipients(recipients.map { it.receiverId })
showRecipient = false
},
viewModel = recipientViewModel,
onRetry = recipientViewModel::retry,
)
} else {
val uiState by writeViewModel.uiState.collectAsStateWithLifecycle()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ fun AlignButton(
selected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
) {
Box(
modifier =
Expand All @@ -33,7 +34,7 @@ fun AlignButton(
} else {
Modifier
},
).clickable(onClick = onClick),
).clickable(enabled = enabled, onClick = onClick),
contentAlignment = Alignment.Center,
) {
Icon(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@ fun RecipientCard(
recipientName: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
) {
Row(
modifier =
modifier
.fillMaxWidth()
.height(115.dp)
.background(AfternoteDesign.colors.gray2)
.clickable(onClick = onClick)
.clickable(enabled = enabled, onClick = onClick)
.padding(horizontal = 20.dp, vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ fun SendScheduleRow(
onDateClick: () -> Unit,
onTimeClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
) {
Row(
modifier =
Expand All @@ -39,7 +40,7 @@ fun SendScheduleRow(
)
Spacer(modifier = Modifier.width(12.dp))
Row(
modifier = Modifier.clickable(onClick = onDateClick),
modifier = Modifier.clickable(enabled = enabled, onClick = onDateClick),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
Expand All @@ -65,7 +66,7 @@ fun SendScheduleRow(
)
Spacer(modifier = Modifier.width(12.dp))
Row(
modifier = Modifier.clickable(onClick = onTimeClick),
modifier = Modifier.clickable(enabled = enabled, onClick = onTimeClick),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ fun TimeLetterBottomBar(
onDraftClick: () -> Unit,
onDraftCountClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
) {
Row(
modifier =
Expand All @@ -54,7 +55,7 @@ fun TimeLetterBottomBar(
modifier =
Modifier
.size(24.dp)
.clickable(onClick = onMediaAddClick),
.clickable(enabled = enabled, onClick = onMediaAddClick),
contentAlignment = Alignment.Center,
) {
Icon(
Expand All @@ -71,7 +72,7 @@ fun TimeLetterBottomBar(
modifier =
Modifier
.size(24.dp)
.clickable(onClick = onTextStyleClick),
.clickable(enabled = enabled, onClick = onTextStyleClick),
contentAlignment = Alignment.Center,
) {
Icon(
Expand Down Expand Up @@ -99,22 +100,25 @@ fun TimeLetterBottomBar(
painter = painterResource(CoreUiR.drawable.core_ui_ic_align_center),
selected = textAlign == TextAlign.Center, // 상태 반영
onClick = onAlignCenterClick,
enabled = enabled,
)
AlignButton(
painter = painterResource(CoreUiR.drawable.core_ui_ic_align_left),
selected = textAlign == TextAlign.Start,
onClick = onAlignLeftClick,
enabled = enabled,
)
AlignButton(
painter = painterResource(CoreUiR.drawable.core_ui_ic_align_right),
selected = textAlign == TextAlign.End,
onClick = onAlignRightClick,
enabled = enabled,
)
}

Spacer(modifier = Modifier.weight(1f))

TextButton(onClick = onDraftClick) {
TextButton(onClick = onDraftClick, enabled = enabled) {
Text(
text = "임시저장",
style = AfternoteDesign.typography.captionLargeR,
Expand All @@ -126,6 +130,7 @@ fun TimeLetterBottomBar(
Modifier
.sizeIn(minWidth = 48.dp, minHeight = 48.dp)
.clickable(
enabled = enabled,
onClickLabel = "임시저장 목록 열기",
role = Role.Button,
onClick = onDraftCountClick,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.afternote.feature.timeletter.presentation.component

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.afternote.core.ui.theme.AfternoteDesign
import com.afternote.feature.timeletter.presentation.R

@Composable
fun TimeLetterLoadErrorContent(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[minor] 공용 컴포넌트로 뽑은 것 자체는 좋은데, 같은 모듈의 기존 인라인 에러+재시도 UI 3곳(TimeLetterDetailScreen·RecipientTimeletterScreen·RecipientTimeLetterDetailScreen — filled Button, bodySmallR/gray6)과 스타일이 다릅니다(여긴 TextButton, bodyLargeR/gray7). 이대로면 타임레터 모듈 안에 실패 UI가 두 벌 공존합니다. 이 PR에서 그 3곳을 이 컴포넌트로 정렬하거나, 최소한 스타일을 기존과 맞춰 주세요. (#446 통일 맥락의 후속으로 빼도 됩니다 — 그 경우 한 줄 남겨 주시면 됩니다.)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

스타일 통일은 이 PR 범위 밖으로 미뤘습니다 — PR 본문 'Work Description'에 후속 과제로 명시해뒀습니다. "다시 시도" 하드코딩만 f9a48d3d3에서 리소스(R.string.timeletter_retry)로 뺐습니다.

message: String,
onRetry: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text(
text = message,
style = AfternoteDesign.typography.bodyLargeR,
color = AfternoteDesign.colors.gray7,
)
TextButton(onClick = onRetry) {
Text(
text = stringResource(R.string.timeletter_retry),
style = AfternoteDesign.typography.bodyLargeB,
color = AfternoteDesign.colors.gray9,
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
Expand All @@ -23,6 +25,8 @@ fun TimeLetterTextButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
isActive: Boolean = false,
isLoading: Boolean = false,
enabled: Boolean = true,
) {
val shape = RoundedCornerShape(6.dp)
val backgroundColor =
Expand All @@ -38,14 +42,22 @@ fun TimeLetterTextButton(
.height(36.dp)
.clip(shape)
.background(color = backgroundColor, shape = shape)
.clickable(onClick = onClick)
.clickable(enabled = enabled && !isLoading, onClick = onClick)
.padding(start = 18.dp, top = 8.dp, end = 18.dp, bottom = 8.dp),
) {
Text(
text = text,
style = AfternoteDesign.typography.bodySmallR,
color = textColor,
)
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(18.dp),
color = AfternoteDesign.colors.gray7,
strokeWidth = 2.dp,
)
} else {
Text(
text = text,
style = AfternoteDesign.typography.bodySmallR,
color = textColor,
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import com.afternote.feature.timeletter.presentation.screen.sender.RecipientList
import com.afternote.feature.timeletter.presentation.screen.sender.TimeLetterDetailScreen
import com.afternote.feature.timeletter.presentation.screen.sender.TimeLetterWriteScreen
import com.afternote.feature.timeletter.presentation.screen.sender.TimeletterScreen
import com.afternote.feature.timeletter.presentation.viewmodel.RecipientListViewModel
import com.afternote.feature.timeletter.presentation.viewmodel.TimeLetterWriteViewModel
import com.afternote.feature.timeletter.presentation.viewmodel.TimeletterViewModel

Expand Down Expand Up @@ -63,6 +64,7 @@ fun NavGraphBuilder.timeLetterNavGraph(
onDraftClick = { title, textContents -> viewModel.saveDraft(title, textContents) },
onNavigateToDraft = actions::onNavigateToDraft,
onErrorShown = { viewModel.clearError() },
onRetryEditingLetter = viewModel::retryEditingLetter,
onRecipientClick = { title, textContents ->
viewModel.updateDraftContent(title, textContents)
actions.onNavigateToRecipient()
Expand Down Expand Up @@ -122,12 +124,16 @@ fun NavGraphBuilder.timeLetterNavGraph(
?: navController.getBackStackEntry(TimeLetterRoute.TimeLetterWriteRoute())
}
val writeViewModel: TimeLetterWriteViewModel = hiltViewModel(writeEntry)
val recipientListViewModel: RecipientListViewModel = hiltViewModel()
val recipientListUiState by recipientListViewModel.uiState.collectAsStateWithLifecycle()
RecipientListScreen(
uiState = recipientListUiState,
onBackClick = actions::onRecipientBack,
onConfirmClick = { recipients ->
writeViewModel.setRecipients(recipients.map { it.receiverId })
actions.onRecipientBack()
},
onRetry = recipientListViewModel::retry,
)
}

Expand All @@ -137,12 +143,16 @@ fun NavGraphBuilder.timeLetterNavGraph(
navController.getBackStackEntry(TimeLetterRoute.TimeLetterHomeRoute)
}
val timeletterViewModel: TimeletterViewModel = hiltViewModel(homeEntry)
val recipientListViewModel: RecipientListViewModel = hiltViewModel()
val recipientListUiState by recipientListViewModel.uiState.collectAsStateWithLifecycle()
RecipientListScreen(
uiState = recipientListUiState,
onBackClick = actions::onRecipientFilterBack,
onConfirmClick = { recipients ->
timeletterViewModel.setReceiverFilter(recipients.map { it.receiverId })
actions.onRecipientFilterBack()
},
onRetry = recipientListViewModel::retry,
allowEmptyConfirm = true,
)
}
Expand Down
Loading
Loading