From f29bad167990ce15d09d35808748c1bdf4e27110 Mon Sep 17 00:00:00 2001 From: kyungmin Date: Wed, 12 Aug 2026 23:38:11 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=201:1=20=EB=AC=B8=EC=9D=98=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EB=B0=8F=20Preview=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../navigation/AppNavigationActions.kt | 20 + .../presentation/model/InquiryUiModel.kt | 39 ++ .../navigation/SettingNavActions.kt | 10 + .../navigation/SettingNavGraph.kt | 28 +- .../presentation/navigation/SettingRoute.kt | 11 + .../presentation/screen/InquiryScreens.kt | 356 ++++++++++++++++++ .../src/main/res/values/strings.xml | 11 + 7 files changed, 474 insertions(+), 1 deletion(-) create mode 100644 feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/model/InquiryUiModel.kt create mode 100644 feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/InquiryScreens.kt diff --git a/app/src/main/java/com/afternote/afternote_fe/navigation/AppNavigationActions.kt b/app/src/main/java/com/afternote/afternote_fe/navigation/AppNavigationActions.kt index 8e27e28a86..54e934229c 100644 --- a/app/src/main/java/com/afternote/afternote_fe/navigation/AppNavigationActions.kt +++ b/app/src/main/java/com/afternote/afternote_fe/navigation/AppNavigationActions.kt @@ -328,6 +328,26 @@ fun rememberSettingNavActions(appState: AppState): SettingNavActions = override fun onNoticeBack() { appState.navController.popBackStack() } + + override fun onNavigateToInquiry() { + appState.navController.navigate(SettingRoute.InquiryListRoute) + } + + override fun onInquiryBack() { + appState.navController.popBackStack() + } + + override fun onNavigateToInquiryDetail(inquiryId: Long) { + appState.navController.navigate(SettingRoute.InquiryDetailRoute(inquiryId)) + } + + override fun onNavigateToInquiryWrite() { + appState.navController.navigate(SettingRoute.InquiryWriteRoute) + } + + override fun onInquirySubmitted() { + appState.navController.popBackStack() + } } } diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/model/InquiryUiModel.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/model/InquiryUiModel.kt new file mode 100644 index 0000000000..e5760d6428 --- /dev/null +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/model/InquiryUiModel.kt @@ -0,0 +1,39 @@ +package com.afternote.feature.setting.presentation.model + +enum class InquiryStatus { RECEIVED, ANSWERED } + +data class InquiryUiModel( + val id: Long, + val status: InquiryStatus, + val date: String, + val title: String, + val content: String, + val answer: String?, +) + +val sampleInquiries = + listOf( + InquiryUiModel( + id = 1L, + status = InquiryStatus.ANSWERED, + date = "2025.08.09.", + title = "타임레터 발송일 변경이 안 돼요", + content = + "타임레터를 작성한 뒤 발송일을 변경하려고 하는데 날짜를 선택해도 기존 날짜로 계속 표시됩니다. " + + "앱을 종료했다가 다시 실행해도 동일하고, 수정 버튼을 눌러 저장해도 변경사항이 반영되지 않아요.\n" + + "현재 발송 예정일은 8월 20일로 설정되어 있고, 9월 15일로 변경하려고 합니다. 확인 부탁드립니다.", + answer = + "안녕하세요 애프터노트입니다.\n\n타임레터 발신인은 마이페이지에서 변경하실 수 있습니다.\n" + + "마이페이지 > 타임레터 관리에서 변경을 원하시는 타임레터를 선택한 후, 발송예정일을 변경해 주세요.\n" + + "변경된 날짜는 저장 후 정상적으로 반영됩니다.\n\n" + + "추가로 이용에 어려움이 있으실 경우 언제든지 문의해 주세요.\n\n감사합니다.", + ), + InquiryUiModel( + id = 2L, + status = InquiryStatus.RECEIVED, + date = "2025.08.09.", + title = "타임레터 발송일 변경이 안 돼요", + content = "발송 예약일은 마이페이지에서 변경이 가능한지 문의드립니다.", + answer = null, + ), + ) diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingNavActions.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingNavActions.kt index 44bef11258..3599c43c54 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingNavActions.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingNavActions.kt @@ -64,4 +64,14 @@ interface SettingNavActions { fun onNavigateToNotice() fun onNoticeBack() + + fun onNavigateToInquiry() + + fun onInquiryBack() + + fun onNavigateToInquiryDetail(inquiryId: Long) + + fun onNavigateToInquiryWrite() + + fun onInquirySubmitted() } diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingNavGraph.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingNavGraph.kt index c47d60b995..15f6c85583 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingNavGraph.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingNavGraph.kt @@ -11,9 +11,13 @@ import androidx.navigation.navigation import androidx.navigation.toRoute import com.afternote.core.ui.Route import com.afternote.feature.setting.presentation.component.PinSetupStep +import com.afternote.feature.setting.presentation.model.sampleInquiries import com.afternote.feature.setting.presentation.screen.AppLockSetupScreen import com.afternote.feature.setting.presentation.screen.ConnectedAccountsScreen import com.afternote.feature.setting.presentation.screen.DeliveryConditionScreen +import com.afternote.feature.setting.presentation.screen.InquiryDetailScreen +import com.afternote.feature.setting.presentation.screen.InquiryListScreen +import com.afternote.feature.setting.presentation.screen.InquiryWriteScreen import com.afternote.feature.setting.presentation.screen.NoticeListScreen import com.afternote.feature.setting.presentation.screen.PassKeyListScreen import com.afternote.feature.setting.presentation.screen.PassKeyMakingScreen @@ -52,7 +56,7 @@ fun NavGraphBuilder.settingNavGraph( onPasskeyClick = actions::onNavigateToPasskey, onAppLockClick = actions::onNavigateToAppLock, onFaqClick = {}, - onInquiryClick = {}, + onInquiryClick = actions::onNavigateToInquiry, onNoticeClick = actions::onNavigateToNotice, onTermsClick = {}, onPrivacyClick = {}, @@ -187,5 +191,27 @@ fun NavGraphBuilder.settingNavGraph( onBackClick = actions::onNoticeBack, ) } + + composable { + InquiryListScreen( + inquiries = sampleInquiries, + onBackClick = actions::onInquiryBack, + onInquiryClick = actions::onNavigateToInquiryDetail, + onNewInquiryClick = actions::onNavigateToInquiryWrite, + ) + } + + composable { backStackEntry -> + val id = backStackEntry.toRoute().inquiryId + val inquiry = sampleInquiries.firstOrNull { it.id == id } ?: return@composable + InquiryDetailScreen(inquiry = inquiry, onBackClick = actions::onInquiryBack) + } + + composable { + InquiryWriteScreen( + onBackClick = actions::onInquiryBack, + onSubmitClick = actions::onInquirySubmitted, + ) + } } } diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingRoute.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingRoute.kt index 898cb15077..26dae13e3e 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingRoute.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingRoute.kt @@ -53,4 +53,15 @@ sealed interface SettingRoute { @Serializable data object NoticeRoute : SettingRoute + + @Serializable + data object InquiryListRoute : SettingRoute + + @Serializable + data class InquiryDetailRoute( + val inquiryId: Long, + ) : SettingRoute + + @Serializable + data object InquiryWriteRoute : SettingRoute } diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/InquiryScreens.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/InquiryScreens.kt new file mode 100644 index 0000000000..84ba38ba74 --- /dev/null +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/InquiryScreens.kt @@ -0,0 +1,356 @@ +package com.afternote.feature.setting.presentation.screen + +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +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.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.afternote.core.ui.button.AfternoteButton +import com.afternote.core.ui.button.AfternoteButtonType +import com.afternote.core.ui.theme.AfternoteDesign +import com.afternote.core.ui.theme.AfternoteTheme +import com.afternote.core.ui.topbar.DetailTopBar +import com.afternote.feature.setting.presentation.R +import com.afternote.feature.setting.presentation.model.InquiryStatus +import com.afternote.feature.setting.presentation.model.InquiryUiModel +import com.afternote.feature.setting.presentation.model.sampleInquiries + +@Composable +fun InquiryListScreen( + inquiries: List, + onBackClick: () -> Unit, + onInquiryClick: (Long) -> Unit, + onNewInquiryClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + topBar = { InquiryTopBar(onBackClick) }, + bottomBar = { + AfternoteButton( + text = stringResource(R.string.inquiry_new), + onClick = onNewInquiryClick, + modifier = Modifier.navigationBarsPadding().padding(horizontal = 20.dp, vertical = 16.dp), + ) + }, + containerColor = AfternoteDesign.colors.gray1, + ) { padding -> + LazyColumn( + modifier = Modifier.fillMaxSize().padding(padding).padding(horizontal = 20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + item { Spacer(Modifier.height(2.dp)) } + items(inquiries, key = { it.id }) { inquiry -> + InquiryListItem(inquiry, onClick = { onInquiryClick(inquiry.id) }) + } + } + } +} + +@Composable +private fun InquiryListItem( + inquiry: InquiryUiModel, + onClick: () -> Unit, +) { + Surface( + modifier = Modifier.fillMaxWidth().clickable(onClick = onClick), + shape = RoundedCornerShape(6.dp), + color = AfternoteDesign.colors.white, + border = BorderStroke(1.dp, AfternoteDesign.colors.gray2), + ) { + Column(Modifier.padding(16.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Surface( + shape = RoundedCornerShape(18.dp), + color = if (inquiry.status == InquiryStatus.ANSWERED) Color(0xFFDCEDE3) else AfternoteDesign.colors.gray2, + ) { + Text( + text = + stringResource( + if (inquiry.status == + InquiryStatus.ANSWERED + ) { + R.string.inquiry_answered + } else { + R.string.inquiry_received + }, + ), + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + style = AfternoteDesign.typography.captionLargeB, + ) + } + Spacer(Modifier.weight(1f)) + Text(inquiry.date, style = AfternoteDesign.typography.footnoteCaption, color = AfternoteDesign.colors.gray7) + } + Spacer(Modifier.height(12.dp)) + Text(inquiry.title, style = AfternoteDesign.typography.bodyBase, color = AfternoteDesign.colors.gray9) + Spacer(Modifier.height(6.dp)) + Text( + text = inquiry.content.replace('\n', ' '), + maxLines = 2, + overflow = TextOverflow.Ellipsis, + style = AfternoteDesign.typography.bodySmallR, + color = AfternoteDesign.colors.gray6, + ) + } + } +} + +@Composable +fun InquiryDetailScreen( + inquiry: InquiryUiModel, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + topBar = { InquiryTopBar(onBackClick) }, + containerColor = AfternoteDesign.colors.gray1, + ) { padding -> + Column(Modifier.fillMaxSize().padding(padding).verticalScroll(rememberScrollState())) { + Column(Modifier.padding(horizontal = 20.dp, vertical = 24.dp)) { + Text(inquiry.date, style = AfternoteDesign.typography.footnoteCaption, color = AfternoteDesign.colors.gray6) + Spacer(Modifier.height(10.dp)) + Text(inquiry.title, style = AfternoteDesign.typography.h2, color = AfternoteDesign.colors.gray9) + } + HorizontalDivider(color = AfternoteDesign.colors.gray2) + Column(Modifier.padding(horizontal = 20.dp, vertical = 24.dp)) { + Text(inquiry.content, style = AfternoteDesign.typography.bodySmallR, color = AfternoteDesign.colors.gray9) + inquiry.answer?.let { answer -> + HorizontalDivider(Modifier.padding(vertical = 28.dp), color = AfternoteDesign.colors.gray3) + Text( + stringResource(R.string.inquiry_answer), + style = AfternoteDesign.typography.captionLargeR, + color = AfternoteDesign.colors.gray6, + ) + Spacer(Modifier.height(10.dp)) + Surface( + shape = RoundedCornerShape(6.dp), + color = AfternoteDesign.colors.white, + border = BorderStroke(1.dp, AfternoteDesign.colors.gray2), + ) { + Text( + answer, + Modifier.padding(24.dp), + style = AfternoteDesign.typography.bodySmallR, + color = AfternoteDesign.colors.gray9, + ) + } + } + } + } + } +} + +@Composable +fun InquiryWriteScreen( + onBackClick: () -> Unit, + onSubmitClick: () -> Unit, + modifier: Modifier = Modifier, +) { + var type by rememberSaveable { mutableStateOf("\ud0c0\uc784\ub808\ud130") } + var title by rememberSaveable { mutableStateOf("") } + var content by rememberSaveable { mutableStateOf("") } + var attachments by remember { mutableStateOf>(emptyList()) } + val onAddScreenshots: () -> Unit = + if (LocalInspectionMode.current) { + ({}) + } else { + val picker = + rememberLauncherForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris -> + attachments = (attachments + uris).distinct().take(MAX_INQUIRY_IMAGES) + } + ({ picker.launch("image/*") }) + } + val canSubmit = title.isNotBlank() && content.isNotBlank() + + Scaffold( + modifier = modifier.imePadding(), + topBar = { InquiryTopBar(onBackClick) }, + bottomBar = { + AfternoteButton( + text = stringResource(R.string.inquiry_submit), + onClick = onSubmitClick, + type = if (canSubmit) AfternoteButtonType.Default else AfternoteButtonType.Un, + modifier = Modifier.navigationBarsPadding().padding(horizontal = 20.dp, vertical = 16.dp), + ) + }, + containerColor = AfternoteDesign.colors.gray1, + ) { padding -> + Column( + Modifier + .fillMaxSize() + .padding(padding) + .verticalScroll(rememberScrollState()) + .padding(horizontal = 20.dp, vertical = 16.dp), + ) { + InquiryTypeDropdown(type, onSelected = { type = it }) + Spacer(Modifier.height(22.dp)) + InquiryFieldLabel(stringResource(R.string.inquiry_title)) + OutlinedTextField( + value = title, + onValueChange = { title = it }, + modifier = Modifier.fillMaxWidth(), + placeholder = { Text(stringResource(R.string.inquiry_text_field)) }, + singleLine = true, + shape = RoundedCornerShape(8.dp), + ) + Spacer(Modifier.height(18.dp)) + InquiryFieldLabel(stringResource(R.string.inquiry_content)) + OutlinedTextField( + value = content, + onValueChange = { content = it }, + modifier = Modifier.fillMaxWidth().height(180.dp), + placeholder = { Text(stringResource(R.string.inquiry_text_field)) }, + shape = RoundedCornerShape(8.dp), + ) + Spacer(Modifier.height(20.dp)) + Surface( + modifier = + Modifier + .fillMaxWidth() + .clickable( + enabled = attachments.size < MAX_INQUIRY_IMAGES, + onClick = onAddScreenshots, + ), + shape = RoundedCornerShape(8.dp), + color = AfternoteDesign.colors.white, + border = BorderStroke(1.dp, AfternoteDesign.colors.gray2), + ) { + Row(Modifier.padding(horizontal = 16.dp, vertical = 18.dp), verticalAlignment = Alignment.CenterVertically) { + Icon( + painter = painterResource(com.afternote.core.ui.R.drawable.core_ui_ic_image), + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = AfternoteDesign.colors.gray7, + ) + Spacer(Modifier.size(12.dp)) + Text( + stringResource(R.string.inquiry_add_screenshot, attachments.size, MAX_INQUIRY_IMAGES), + style = AfternoteDesign.typography.bodySmallR, + ) + } + } + } + } +} + +@Composable +private fun InquiryTypeDropdown( + selected: String, + onSelected: (String) -> Unit, +) { + var expanded by remember { mutableStateOf(false) } + InquiryFieldLabel(stringResource(R.string.inquiry_type)) + Box { + Row( + Modifier.fillMaxWidth().clickable { expanded = true }.padding(vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text(selected, Modifier.weight(1f), style = AfternoteDesign.typography.bodySmallR) + Text("\u2304", style = AfternoteDesign.typography.bodyLargeR) + } + DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { + listOf( + "\ud0c0\uc784\ub808\ud130", + "\ub9c8\uc74c \uae30\ub85d", + "\uc560\ud504\ud130\ub178\ud2b8", + "\uae30\ud0c0", + ).forEach { item -> + DropdownMenuItem(text = { Text(item) }, onClick = { + onSelected(item) + expanded = false + }) + } + } + } + HorizontalDivider(color = AfternoteDesign.colors.gray2) +} + +@Composable +private fun InquiryFieldLabel(text: String) { + Text(text, style = AfternoteDesign.typography.bodySmallR, color = AfternoteDesign.colors.gray7) + Spacer(Modifier.height(8.dp)) +} + +@Composable +private fun InquiryTopBar(onBackClick: () -> Unit) { + DetailTopBar(stringResource(R.string.inquiry_title_bar), onBackClick = onBackClick) +} + +@Preview(name = "Inquiry list", showBackground = true) +@Composable +private fun InquiryListScreenPreview() { + AfternoteTheme { + InquiryListScreen( + inquiries = sampleInquiries, + onBackClick = {}, + onInquiryClick = {}, + onNewInquiryClick = {}, + ) + } +} + +@Preview(name = "Inquiry detail - answered", showBackground = true) +@Composable +private fun InquiryDetailScreenPreview() { + AfternoteTheme { + InquiryDetailScreen( + inquiry = sampleInquiries.first(), + onBackClick = {}, + ) + } +} + +@Preview(name = "Inquiry write", showBackground = true) +@Composable +private fun InquiryWriteScreenPreview() { + AfternoteTheme { + InquiryWriteScreen( + onBackClick = {}, + onSubmitClick = {}, + ) + } +} + +private const val MAX_INQUIRY_IMAGES = 3 diff --git a/feature/setting/presentation/src/main/res/values/strings.xml b/feature/setting/presentation/src/main/res/values/strings.xml index 94824b058f..7250a45964 100644 --- a/feature/setting/presentation/src/main/res/values/strings.xml +++ b/feature/setting/presentation/src/main/res/values/strings.xml @@ -23,6 +23,17 @@ FAQ 고객센터 + 1:1 문의 + 새 문의 접수하기 + 접수 중 + 답변 완료 + 답변 + 문의 유형 + 제목 + 내용 + Text Field + 스크린샷 추가하기 (%1$d/%2$d, 최대 3장) + 문의 접수하기 공지사항 등록된 공지사항이 없어요. 이용약관 From 8802f687e33af9a269b0f7a5ace4a9933c168c87 Mon Sep 17 00:00:00 2001 From: kyungmin Date: Sun, 23 Aug 2026 01:53:21 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat(setting):=20=EB=AC=B8=EC=9D=98=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EC=8A=A4?= =?UTF-8?q?=ED=83=80=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/screen/InquiryScreens.kt | 18 ++++++++++++++---- .../src/main/res/values/strings.xml | 5 +++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/InquiryScreens.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/InquiryScreens.kt index 84ba38ba74..678038325e 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/InquiryScreens.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/InquiryScreens.kt @@ -120,7 +120,7 @@ private fun InquiryListItem( ) } Spacer(Modifier.weight(1f)) - Text(inquiry.date, style = AfternoteDesign.typography.footnoteCaption, color = AfternoteDesign.colors.gray7) + Text(inquiry.date, style = AfternoteDesign.typography.footnoteCaption, color = AfternoteDesign.colors.gray9) } Spacer(Modifier.height(12.dp)) Text(inquiry.title, style = AfternoteDesign.typography.bodyBase, color = AfternoteDesign.colors.gray9) @@ -149,7 +149,7 @@ fun InquiryDetailScreen( ) { padding -> Column(Modifier.fillMaxSize().padding(padding).verticalScroll(rememberScrollState())) { Column(Modifier.padding(horizontal = 20.dp, vertical = 24.dp)) { - Text(inquiry.date, style = AfternoteDesign.typography.footnoteCaption, color = AfternoteDesign.colors.gray6) + Text(inquiry.date, style = AfternoteDesign.typography.footnoteCaption, color = AfternoteDesign.colors.gray8) Spacer(Modifier.height(10.dp)) Text(inquiry.title, style = AfternoteDesign.typography.h2, color = AfternoteDesign.colors.gray9) } @@ -231,7 +231,12 @@ fun InquiryWriteScreen( value = title, onValueChange = { title = it }, modifier = Modifier.fillMaxWidth(), - placeholder = { Text(stringResource(R.string.inquiry_text_field)) }, + placeholder = { + Text( + text = stringResource(R.string.inquiry_title_text_field), + color = AfternoteDesign.colors.gray4, + ) + }, singleLine = true, shape = RoundedCornerShape(8.dp), ) @@ -241,7 +246,12 @@ fun InquiryWriteScreen( value = content, onValueChange = { content = it }, modifier = Modifier.fillMaxWidth().height(180.dp), - placeholder = { Text(stringResource(R.string.inquiry_text_field)) }, + placeholder = { + Text( + text = stringResource(R.string.inquiry_content_text_field), + color = AfternoteDesign.colors.gray4, + ) + }, shape = RoundedCornerShape(8.dp), ) Spacer(Modifier.height(20.dp)) diff --git a/feature/setting/presentation/src/main/res/values/strings.xml b/feature/setting/presentation/src/main/res/values/strings.xml index 7250a45964..77267abf55 100644 --- a/feature/setting/presentation/src/main/res/values/strings.xml +++ b/feature/setting/presentation/src/main/res/values/strings.xml @@ -31,8 +31,9 @@ 문의 유형 제목 내용 - Text Field - 스크린샷 추가하기 (%1$d/%2$d, 최대 3장) + 제목을 입력해 주세요. + 내용을 입력해 주세요. + 스크린샷 추가하기 (최대 3장) 문의 접수하기 공지사항 등록된 공지사항이 없어요. From ea3f9d3e8ee0dbbacc45866a6070acb51da6db57 Mon Sep 17 00:00:00 2001 From: kyungmin Date: Sun, 6 Sep 2026 12:18:12 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix(setting):=201:1=20=EB=AC=B8=EC=9D=98=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EC=9D=B4=20=EC=A7=80=EC=96=B4=EB=82=B8=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=A5=BC=20=EB=B3=B4=EC=97=AC?= =?UTF-8?q?=EC=A3=BC=EA=B1=B0=EB=82=98=20=EC=A0=91=EC=88=98=20=EC=84=B1?= =?UTF-8?q?=EA=B3=B5=EC=9D=84=20=EA=B0=80=EC=9E=A5=ED=95=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EA=B2=8C=20=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - InquiryListRoute/InquiryDetailRoute 가 프로덕션에서 sampleInquiries 를 실제 사용자 이력처럼 보여주던 문제를 고친다. sampleInquiries 는 InquiryScreens.kt 의 @Preview 전용 데이터로만 남기고 프로덕션 라우트에는 빈 상태를 그린다 (NoticeListScreen 이 이미 쓰고 있는 패턴과 동일). - InquiryDetailScreen 은 inquiry 를 nullable 로 받아, 조회 API 가 아직 없어 실제 항목을 찾을 수 없는 경우 "찾을 수 없음" 상태를 보여준다. - InquiryWriteScreen 의 접수 버튼은 (아직 없는) 접수 API 를 호출한 것처럼 뒤로 가지 않는다. 대신 아직 지원하지 않는 기능임을 스낵바로 알리고 화면에 남아 작성 중이던 내용을 보존한다 — #1629 에서 확립한 "미구현 기능은 성공으로 포장하지 않는다" 원칙과 동일하다. - 이제 아무것도 하지 않는 SettingNavActions.onInquirySubmitted() / AppNavigationActions 구현을 제거했다. 접수 API 가 연결되면 다시 추가한다. Afternote-BE#246 (문의 접수·조회 엔드포인트 부재) 가 열려 있는 동안에는 merge-order-guard 가 계속 이 PR 을 막을 것이다 — 이건 백엔드 이슈가 닫혀야 풀리는 별도 사안이라 이번 수정 범위에 포함하지 않았다. Co-Authored-By: Claude Sonnet 5 --- .../navigation/AppNavigationActions.kt | 4 - .../presentation/model/InquiryUiModel.kt | 27 ----- .../navigation/SettingNavActions.kt | 2 - .../navigation/SettingNavGraph.kt | 12 +-- .../presentation/screen/InquiryScreens.kt | 101 +++++++++++++++--- .../src/main/res/values/strings.xml | 3 + 6 files changed, 97 insertions(+), 52 deletions(-) diff --git a/app/src/main/java/com/afternote/afternote_fe/navigation/AppNavigationActions.kt b/app/src/main/java/com/afternote/afternote_fe/navigation/AppNavigationActions.kt index 816c622292..f76a648607 100644 --- a/app/src/main/java/com/afternote/afternote_fe/navigation/AppNavigationActions.kt +++ b/app/src/main/java/com/afternote/afternote_fe/navigation/AppNavigationActions.kt @@ -372,10 +372,6 @@ fun rememberSettingNavActions(appState: AppState): SettingNavActions = override fun onNavigateToInquiryWrite() { appState.navController.navigate(SettingRoute.InquiryWriteRoute) } - - override fun onInquirySubmitted() { - appState.navController.popBackStack() - } } } diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/model/InquiryUiModel.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/model/InquiryUiModel.kt index e5760d6428..98bfed89da 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/model/InquiryUiModel.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/model/InquiryUiModel.kt @@ -10,30 +10,3 @@ data class InquiryUiModel( val content: String, val answer: String?, ) - -val sampleInquiries = - listOf( - InquiryUiModel( - id = 1L, - status = InquiryStatus.ANSWERED, - date = "2025.08.09.", - title = "타임레터 발송일 변경이 안 돼요", - content = - "타임레터를 작성한 뒤 발송일을 변경하려고 하는데 날짜를 선택해도 기존 날짜로 계속 표시됩니다. " + - "앱을 종료했다가 다시 실행해도 동일하고, 수정 버튼을 눌러 저장해도 변경사항이 반영되지 않아요.\n" + - "현재 발송 예정일은 8월 20일로 설정되어 있고, 9월 15일로 변경하려고 합니다. 확인 부탁드립니다.", - answer = - "안녕하세요 애프터노트입니다.\n\n타임레터 발신인은 마이페이지에서 변경하실 수 있습니다.\n" + - "마이페이지 > 타임레터 관리에서 변경을 원하시는 타임레터를 선택한 후, 발송예정일을 변경해 주세요.\n" + - "변경된 날짜는 저장 후 정상적으로 반영됩니다.\n\n" + - "추가로 이용에 어려움이 있으실 경우 언제든지 문의해 주세요.\n\n감사합니다.", - ), - InquiryUiModel( - id = 2L, - status = InquiryStatus.RECEIVED, - date = "2025.08.09.", - title = "타임레터 발송일 변경이 안 돼요", - content = "발송 예약일은 마이페이지에서 변경이 가능한지 문의드립니다.", - answer = null, - ), - ) diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingNavActions.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingNavActions.kt index 3599c43c54..c6bdeb019d 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingNavActions.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingNavActions.kt @@ -72,6 +72,4 @@ interface SettingNavActions { fun onNavigateToInquiryDetail(inquiryId: Long) fun onNavigateToInquiryWrite() - - fun onInquirySubmitted() } diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingNavGraph.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingNavGraph.kt index 15f6c85583..3934782bbb 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingNavGraph.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/navigation/SettingNavGraph.kt @@ -11,7 +11,6 @@ import androidx.navigation.navigation import androidx.navigation.toRoute import com.afternote.core.ui.Route import com.afternote.feature.setting.presentation.component.PinSetupStep -import com.afternote.feature.setting.presentation.model.sampleInquiries import com.afternote.feature.setting.presentation.screen.AppLockSetupScreen import com.afternote.feature.setting.presentation.screen.ConnectedAccountsScreen import com.afternote.feature.setting.presentation.screen.DeliveryConditionScreen @@ -194,23 +193,22 @@ fun NavGraphBuilder.settingNavGraph( composable { InquiryListScreen( - inquiries = sampleInquiries, + inquiries = emptyList(), onBackClick = actions::onInquiryBack, onInquiryClick = actions::onNavigateToInquiryDetail, onNewInquiryClick = actions::onNavigateToInquiryWrite, ) } - composable { backStackEntry -> - val id = backStackEntry.toRoute().inquiryId - val inquiry = sampleInquiries.firstOrNull { it.id == id } ?: return@composable - InquiryDetailScreen(inquiry = inquiry, onBackClick = actions::onInquiryBack) + composable { + // 문의 조회 API 가 아직 없다 (Afternote-BE#246). 실제 데이터 소스가 연결되기 전까지는 + // 지어낸 문의를 보여주는 대신 "찾을 수 없음" 상태를 그린다. + InquiryDetailScreen(inquiry = null, onBackClick = actions::onInquiryBack) } composable { InquiryWriteScreen( onBackClick = actions::onInquiryBack, - onSubmitClick = actions::onInquirySubmitted, ) } } diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/InquiryScreens.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/InquiryScreens.kt index 678038325e..f50c780ea0 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/InquiryScreens.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/screen/InquiryScreens.kt @@ -28,12 +28,15 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -53,7 +56,7 @@ import com.afternote.core.ui.topbar.DetailTopBar import com.afternote.feature.setting.presentation.R import com.afternote.feature.setting.presentation.model.InquiryStatus import com.afternote.feature.setting.presentation.model.InquiryUiModel -import com.afternote.feature.setting.presentation.model.sampleInquiries +import kotlinx.coroutines.launch @Composable fun InquiryListScreen( @@ -75,18 +78,38 @@ fun InquiryListScreen( }, containerColor = AfternoteDesign.colors.gray1, ) { padding -> - LazyColumn( - modifier = Modifier.fillMaxSize().padding(padding).padding(horizontal = 20.dp), - verticalArrangement = Arrangement.spacedBy(10.dp), - ) { - item { Spacer(Modifier.height(2.dp)) } - items(inquiries, key = { it.id }) { inquiry -> - InquiryListItem(inquiry, onClick = { onInquiryClick(inquiry.id) }) + if (inquiries.isEmpty()) { + InquiryEmptyState(modifier = Modifier.fillMaxSize().padding(padding)) + } else { + LazyColumn( + modifier = Modifier.fillMaxSize().padding(padding).padding(horizontal = 20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + item { Spacer(Modifier.height(2.dp)) } + items(inquiries, key = { it.id }) { inquiry -> + InquiryListItem(inquiry, onClick = { onInquiryClick(inquiry.id) }) + } } } } } +@Composable +private fun InquiryEmptyState( + modifier: Modifier = Modifier, + message: String = stringResource(R.string.inquiry_list_empty), +) { + Box( + modifier = modifier.padding(horizontal = 20.dp).padding(top = 24.dp), + ) { + Text( + text = message, + style = AfternoteDesign.typography.bodyLargeR, + color = AfternoteDesign.colors.gray8, + ) + } +} + @Composable private fun InquiryListItem( inquiry: InquiryUiModel, @@ -138,7 +161,7 @@ private fun InquiryListItem( @Composable fun InquiryDetailScreen( - inquiry: InquiryUiModel, + inquiry: InquiryUiModel?, onBackClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -147,6 +170,13 @@ fun InquiryDetailScreen( topBar = { InquiryTopBar(onBackClick) }, containerColor = AfternoteDesign.colors.gray1, ) { padding -> + if (inquiry == null) { + InquiryEmptyState( + modifier = Modifier.fillMaxSize().padding(padding), + message = stringResource(R.string.inquiry_detail_not_found), + ) + return@Scaffold + } Column(Modifier.fillMaxSize().padding(padding).verticalScroll(rememberScrollState())) { Column(Modifier.padding(horizontal = 20.dp, vertical = 24.dp)) { Text(inquiry.date, style = AfternoteDesign.typography.footnoteCaption, color = AfternoteDesign.colors.gray8) @@ -185,7 +215,6 @@ fun InquiryDetailScreen( @Composable fun InquiryWriteScreen( onBackClick: () -> Unit, - onSubmitClick: () -> Unit, modifier: Modifier = Modifier, ) { var type by rememberSaveable { mutableStateOf("\ud0c0\uc784\ub808\ud130") } @@ -203,6 +232,9 @@ fun InquiryWriteScreen( ({ picker.launch("image/*") }) } val canSubmit = title.isNotBlank() && content.isNotBlank() + val snackbarHostState = remember { SnackbarHostState() } + val coroutineScope = rememberCoroutineScope() + val submitNotSupportedMessage = stringResource(R.string.inquiry_submit_not_supported) Scaffold( modifier = modifier.imePadding(), @@ -210,11 +242,16 @@ fun InquiryWriteScreen( bottomBar = { AfternoteButton( text = stringResource(R.string.inquiry_submit), - onClick = onSubmitClick, + onClick = { + // \uc811\uc218 API \uac00 \uc544\uc9c1 \uc5c6\ub2e4 (Afternote-BE#246). \uc811\uc218\ub41c \uac83\ucc98\ub7fc \ub4a4\ub85c \uac00\uc9c0 \uc54a\uace0 + // \uc544\uc9c1 \uc9c0\uc6d0\ud558\uc9c0 \uc54a\ub294 \uae30\ub2a5\uc784\uc744 \uadf8\ub300\ub85c \uc54c\ub9b0\ub2e4 \u2014 \uc131\uacf5\ud55c \ucc99 \ub2eb\uc9c0 \uc54a\ub294\ub2e4 (#1629 \uc640 \ub3d9\uc77c\ud55c \uc6d0\uce59). + coroutineScope.launch { snackbarHostState.showSnackbar(submitNotSupportedMessage) } + }, type = if (canSubmit) AfternoteButtonType.Default else AfternoteButtonType.Un, modifier = Modifier.navigationBarsPadding().padding(horizontal = 20.dp, vertical = 16.dp), ) }, + snackbarHost = { SnackbarHost(snackbarHostState) }, containerColor = AfternoteDesign.colors.gray1, ) { padding -> Column( @@ -341,6 +378,19 @@ private fun InquiryListScreenPreview() { } } +@Preview(name = "Inquiry list - empty", showBackground = true) +@Composable +private fun InquiryListScreenEmptyPreview() { + AfternoteTheme { + InquiryListScreen( + inquiries = emptyList(), + onBackClick = {}, + onInquiryClick = {}, + onNewInquiryClick = {}, + ) + } +} + @Preview(name = "Inquiry detail - answered", showBackground = true) @Composable private fun InquiryDetailScreenPreview() { @@ -352,13 +402,40 @@ private fun InquiryDetailScreenPreview() { } } +// 프리뷰 전용 표본 데이터 — 프로덕션 라우트(SettingNavGraph)에서는 사용하지 않는다. +private val sampleInquiries = + listOf( + InquiryUiModel( + id = 1L, + status = InquiryStatus.ANSWERED, + date = "2025.08.09.", + title = "타임레터 발송일 변경이 안 돼요", + content = + "타임레터를 작성한 뒤 발송일을 변경하려고 하는데 날짜를 선택해도 기존 날짜로 계속 표시됩니다. " + + "앱을 종료했다가 다시 실행해도 동일하고, 수정 버튼을 눌러 저장해도 변경사항이 반영되지 않아요.\n" + + "현재 발송 예정일은 8월 20일로 설정되어 있고, 9월 15일로 변경하려고 합니다. 확인 부탁드립니다.", + answer = + "안녕하세요 애프터노트입니다.\n\n타임레터 발신인은 마이페이지에서 변경하실 수 있습니다.\n" + + "마이페이지 > 타임레터 관리에서 변경을 원하시는 타임레터를 선택한 후, 발송예정일을 변경해 주세요.\n" + + "변경된 날짜는 저장 후 정상적으로 반영됩니다.\n\n" + + "추가로 이용에 어려움이 있으실 경우 언제든지 문의해 주세요.\n\n감사합니다.", + ), + InquiryUiModel( + id = 2L, + status = InquiryStatus.RECEIVED, + date = "2025.08.09.", + title = "타임레터 발송일 변경이 안 돼요", + content = "발송 예약일은 마이페이지에서 변경이 가능한지 문의드립니다.", + answer = null, + ), + ) + @Preview(name = "Inquiry write", showBackground = true) @Composable private fun InquiryWriteScreenPreview() { AfternoteTheme { InquiryWriteScreen( onBackClick = {}, - onSubmitClick = {}, ) } } diff --git a/feature/setting/presentation/src/main/res/values/strings.xml b/feature/setting/presentation/src/main/res/values/strings.xml index 0d596999da..f1c231ea88 100644 --- a/feature/setting/presentation/src/main/res/values/strings.xml +++ b/feature/setting/presentation/src/main/res/values/strings.xml @@ -34,6 +34,9 @@ 내용을 입력해 주세요. 스크린샷 추가하기 (최대 3장) 문의 접수하기 + 등록된 문의가 없어요. + 문의 내역을 찾을 수 없어요. + 문의 접수 기능은 아직 준비 중이에요. 조금만 기다려 주세요. 공지사항 등록된 공지사항이 없어요. 이용약관