From 8e0e7e95c3fc8d9e2d75b7c083e67a010dac8584 Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Wed, 19 Nov 2025 22:21:53 +0900 Subject: [PATCH 1/8] =?UTF-8?q?Feat:=20=EC=A0=9C=EB=B3=B4=20=ED=9E=88?= =?UTF-8?q?=EC=8A=A4=ED=86=A0=EB=A6=AC=20UI=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../designsystem/color/BitnagilColors.kt | 2 + .../bitnagil/designsystem/color/Color.kt | 2 + .../component/atom/BitnagilChip.kt | 70 +++++++ .../reporthistory/ReportHistoryScreen.kt | 179 ++++++++++++++++++ .../component/atom/ReportProcessBadge.kt | 60 ++++++ .../component/block/ReportHistoryItem.kt | 135 +++++++++++++ .../reporthistory/model/ReportCategory.kt | 28 +++ .../model/ReportHistoriesPerDayUiModel.kt | 8 + .../reporthistory/model/ReportHistoryState.kt | 35 ++++ .../model/ReportHistoryUiModel.kt | 10 + .../reporthistory/model/ReportProcess.kt | 10 + .../model/ReportProcessWithCount.kt | 13 ++ 12 files changed, 552 insertions(+) create mode 100644 core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilChip.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryScreen.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/component/atom/ReportProcessBadge.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/component/block/ReportHistoryItem.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportCategory.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportHistoriesPerDayUiModel.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportHistoryState.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportHistoryUiModel.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportProcess.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportProcessWithCount.kt diff --git a/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/color/BitnagilColors.kt b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/color/BitnagilColors.kt index c295efca..3176a19a 100644 --- a/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/color/BitnagilColors.kt +++ b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/color/BitnagilColors.kt @@ -12,8 +12,10 @@ data class BitnagilColors( val error: Color = Error, val error10: Color = Error10, val skyBlue10: Color = SkyBlue10, + val blue300: Color = Blue300, val purple10: Color = Purple10, val green10: Color = Green10, + val green300: Color = Green300, val pink10: Color = Pink10, val yellow10: Color = Yellow10, val progressBarGradientStartColor: Color = ProgressBarGradientStartColor, diff --git a/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/color/Color.kt b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/color/Color.kt index 851d91b3..23112455 100644 --- a/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/color/Color.kt +++ b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/color/Color.kt @@ -8,8 +8,10 @@ val Kakao = Color(0xFFFEE500) val Error = Color(0xFFFF6868) val Error10 = Color(0xFFFF5858) val SkyBlue10 = Color(0xFFDBF1FF) +val Blue300 = Color(0xFF0093F5) val Purple10 = Color(0xFFE6E2FF) val Green10 = Color(0xFFE6F5C6) +val Green300 = Color(0xFF3FAF00) val Pink10 = Color(0xFFFEE3E9) val Yellow10 = Color(0xFFFFF5C7) val ProgressBarGradientStartColor = Color(0xFFA9CFFF) diff --git a/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilChip.kt b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilChip.kt new file mode 100644 index 00000000..57c3574f --- /dev/null +++ b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilChip.kt @@ -0,0 +1,70 @@ +package com.threegap.bitnagil.designsystem.component.atom + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.threegap.bitnagil.designsystem.BitnagilTheme +import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple + +@Composable +fun BitnagilChip( + title: String, + isSelected: Boolean, + onCategorySelected: () -> Unit, + modifier: Modifier = Modifier, + count: Int? = null, +) { + val chipTitle = if (count == null) title else "$title $count" + + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .background( + color = if (!isSelected) BitnagilTheme.colors.white else BitnagilTheme.colors.coolGray10, + shape = RoundedCornerShape(20.dp), + ) + .height(36.dp) + .clickableWithoutRipple(onClick = onCategorySelected) + .padding( + vertical = 6.dp, + horizontal = 14.dp, + ), + ) { + Text( + text = chipTitle, + color = if (!isSelected) BitnagilTheme.colors.coolGray60 else BitnagilTheme.colors.white, + style = if (!isSelected) BitnagilTheme.typography.caption1Medium else BitnagilTheme.typography.caption1SemiBold, + ) + } +} + +@Preview +@Composable +private fun RecommendCategoryChipStatesPreview() { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + BitnagilChip( + title = "맞춤 추천", + isSelected = true, + onCategorySelected = {}, + ) + + BitnagilChip( + title = "나가봐요", + isSelected = false, + onCategorySelected = {}, + count = 1, + ) + } +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryScreen.kt new file mode 100644 index 00000000..c6a57b37 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryScreen.kt @@ -0,0 +1,179 @@ +package com.threegap.bitnagil.presentation.reporthistory + +import androidx.compose.foundation.background +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.threegap.bitnagil.designsystem.BitnagilTheme +import com.threegap.bitnagil.designsystem.R +import com.threegap.bitnagil.designsystem.component.atom.BitnagilChip +import com.threegap.bitnagil.designsystem.component.atom.BitnagilIcon +import com.threegap.bitnagil.designsystem.component.block.BitnagilTopBar +import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple +import com.threegap.bitnagil.presentation.reporthistory.component.block.ReportHistoryItem +import com.threegap.bitnagil.presentation.reporthistory.model.ReportCategory +import com.threegap.bitnagil.presentation.reporthistory.model.ReportHistoriesPerDayUiModel +import com.threegap.bitnagil.presentation.reporthistory.model.ReportHistoryState +import com.threegap.bitnagil.presentation.reporthistory.model.ReportHistoryUiModel +import com.threegap.bitnagil.presentation.reporthistory.model.ReportProcess + +@Composable +fun ReportHistoryScreenContainer() { +} + +@Composable +private fun ReportHistoryScreen( + modifier: Modifier = Modifier, + state: ReportHistoryState, +) { + val horizontalScreenState = rememberScrollState() + + Column( + modifier = modifier + .fillMaxSize() + .background(color = BitnagilTheme.colors.coolGray99) + .statusBarsPadding(), + ) { + BitnagilTopBar( + title = "내 제보 기록", + showBackButton = true, + onBackClick = {}, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier.horizontalScroll(horizontalScreenState), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Spacer(modifier = Modifier.width(8.dp)) + + state.reportProcessWithCounts.forEach { reportProcessWithCount -> + BitnagilChip( + title = reportProcessWithCount.process.title, + isSelected = state.selectedReportProcess == reportProcessWithCount.process, + onCategorySelected = {}, + count = if (reportProcessWithCount.count == 0) null else reportProcessWithCount.count, + ) + } + + // chip list + + Spacer(modifier = Modifier.width(8.dp)) + } + + Spacer(modifier = Modifier.height(24.dp)) + + Box( + modifier = Modifier.weight(1f), + ) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(horizontal = 16.dp), + ) { + state.reportHistoriesPerDays.forEach { reportHistoriesPerDay -> + stickyHeader { + Box( + modifier = Modifier + .fillMaxWidth() + .height(40.dp) + .background(color = BitnagilTheme.colors.coolGray99), + ) { + Text( + text = reportHistoriesPerDay.date.toString(), + modifier = Modifier.align(Alignment.CenterStart), + style = BitnagilTheme.typography.body2SemiBold, + ) + } + } + + itemsIndexed(reportHistoriesPerDay.reports) { index, report -> + ReportHistoryItem( + modifier = Modifier.padding(bottom = if (index == reportHistoriesPerDay.reports.lastIndex) 24.dp else 10.dp), + report = report, + onClick = {}, + ) + } + } + } + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(5.dp), + modifier = Modifier + .height(40.dp) + .align(Alignment.TopEnd) + .clickableWithoutRipple { }, + ) { + Text( + text = "카테고리", + color = BitnagilTheme.colors.coolGray40, + style = BitnagilTheme.typography.body2Medium, + modifier = Modifier.padding(start = 10.dp), + ) + + BitnagilIcon( + id = R.drawable.ic_down_arrow, + tint = BitnagilTheme.colors.coolGray40, + modifier = Modifier + .padding(end = 13.dp) + .size(16.dp), + ) + } + } + } +} + +@Composable +@Preview +private fun ReportHistoryScreenPreview() { + BitnagilTheme { + ReportHistoryScreen( + state = ReportHistoryState.Init.copy( + reportHistoriesPerDays = List(10) { + ReportHistoriesPerDayUiModel( + date = java.time.LocalDate.now(), + reports = listOf( + ReportHistoryUiModel( + id = "1", + title = "제보 1", + imageUrl = "-", + location = "서울특별시 성북구 안암로 106", + process = ReportProcess.Reported, + category = ReportCategory.Amenities, + ), + ReportHistoryUiModel( + id = "1", + title = "제보 1", + imageUrl = "-", + location = "서울특별시 성북구 안암로 106", + process = ReportProcess.Progress, + category = ReportCategory.Amenities, + ), + ), + ) + }, + ), + ) + } +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/component/atom/ReportProcessBadge.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/component/atom/ReportProcessBadge.kt new file mode 100644 index 00000000..b93bf260 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/component/atom/ReportProcessBadge.kt @@ -0,0 +1,60 @@ +package com.threegap.bitnagil.presentation.reporthistory.component.atom + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.threegap.bitnagil.designsystem.BitnagilTheme +import com.threegap.bitnagil.presentation.reporthistory.model.ReportProcess + +@Composable +fun ReportProcessBadge( + modifier: Modifier = Modifier, + reportProcess: ReportProcess, +) { + Text( + text = reportProcess.title, + style = BitnagilTheme.typography.caption1SemiBold, + color = reportProcess.getProcessBadgeTextColor(), + modifier = modifier + .background(color = reportProcess.getProcessBadgeBackgroundColor(), shape = RoundedCornerShape(6.dp)) + .padding(horizontal = 10.dp, vertical = 4.dp), + ) +} + +@Composable +private fun ReportProcess.getProcessBadgeBackgroundColor(): Color = + when (this) { + ReportProcess.Reported -> BitnagilTheme.colors.green10 + ReportProcess.Progress -> BitnagilTheme.colors.skyBlue10 + else -> BitnagilTheme.colors.coolGray95 + } + +@Composable +private fun ReportProcess.getProcessBadgeTextColor(): Color = + when (this) { + ReportProcess.Reported -> BitnagilTheme.colors.green300 + ReportProcess.Progress -> BitnagilTheme.colors.blue300 + else -> BitnagilTheme.colors.coolGray40 + } + +@Composable +@Preview +private fun ReportProcessBadgePreview() { + BitnagilTheme { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + ReportProcessBadge(reportProcess = ReportProcess.Progress) + ReportProcessBadge(reportProcess = ReportProcess.Reported) + ReportProcessBadge(reportProcess = ReportProcess.Complete) + } + } +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/component/block/ReportHistoryItem.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/component/block/ReportHistoryItem.kt new file mode 100644 index 00000000..88841c41 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/component/block/ReportHistoryItem.kt @@ -0,0 +1,135 @@ +package com.threegap.bitnagil.presentation.reporthistory.component.block + +import androidx.compose.foundation.background +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.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import coil3.request.ImageRequest +import com.threegap.bitnagil.designsystem.BitnagilTheme +import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple +import com.threegap.bitnagil.presentation.reporthistory.component.atom.ReportProcessBadge +import com.threegap.bitnagil.presentation.reporthistory.model.ReportCategory +import com.threegap.bitnagil.presentation.reporthistory.model.ReportHistoryUiModel +import com.threegap.bitnagil.presentation.reporthistory.model.ReportProcess + +@Composable +fun ReportHistoryItem( + modifier: Modifier = Modifier, + report: ReportHistoryUiModel, + onClick: (String) -> Unit, +) { + Row( + modifier = modifier + .background(color = BitnagilTheme.colors.white, shape = RoundedCornerShape(12.dp)) + .clickableWithoutRipple(onClick = { onClick(report.id) }) + .padding(vertical = 14.dp, horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Column( + modifier = Modifier.weight(1f), + ) { + ReportProcessBadge(reportProcess = report.process) + + Spacer(modifier = Modifier.height(8.dp)) + + Text(text = report.title, style = BitnagilTheme.typography.body2Medium, maxLines = 2) + + Spacer(modifier = Modifier.height(12.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text(text = report.category.title, style = BitnagilTheme.typography.caption1Medium.copy(color = BitnagilTheme.colors.coolGray50)) + + Box( + modifier = Modifier + .size(4.dp) + .background(color = BitnagilTheme.colors.coolGray90, shape = CircleShape), + ) + + Text( + text = report.location, + modifier = Modifier.weight(1f), + style = BitnagilTheme.typography.caption1Medium.copy(color = BitnagilTheme.colors.coolGray50), + maxLines = 1, + ) + } + } + + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(report.imageUrl) + .build(), + modifier = Modifier + .size(74.dp) + .clip(shape = RoundedCornerShape(9.dp)) + .background(color = BitnagilTheme.colors.black), + contentDescription = null, + ) + } +} + +@Composable +@Preview +private fun ReportHistoryItemPreview() { + BitnagilTheme { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + ReportHistoryItem( + report = ReportHistoryUiModel( + id = "1", + title = "다이소 덕소점 앞 가로등 심해요", + imageUrl = "-", + location = "서울특별시 성북구 안암로 106", + process = ReportProcess.Reported, + category = ReportCategory.TrafficFacilities, + ), + onClick = {}, + ) + + ReportHistoryItem( + report = ReportHistoryUiModel( + id = "1", + title = "다이소 덕소점 앞 가로등 깜빡이는데 어쩌죠 이거 진짜로 심각한 문제인데 이 뒷부분은 figma에서 짤려 보여서 임의로 채웁니다", + imageUrl = "-", + location = "서울특별시 성북구 안암로 106", + process = ReportProcess.Progress, + category = ReportCategory.TrafficFacilities, + ), + onClick = {}, + ) + + ReportHistoryItem( + report = ReportHistoryUiModel( + id = "1", + title = "퇴근하고 싶어요", + imageUrl = "-", + location = "서울특별시 성북구 안암로 106 서울특별시 성북구 안암로 106 서울특별시 성북구 안암로 106", + process = ReportProcess.Complete, + category = ReportCategory.Amenities, + ), + onClick = {}, + ) + } + } +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportCategory.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportCategory.kt new file mode 100644 index 00000000..6784f2f1 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportCategory.kt @@ -0,0 +1,28 @@ +package com.threegap.bitnagil.presentation.reporthistory.model + +enum class ReportCategory( + val title: String, + val description: String, + val iconResourceId: Int, +) { + TrafficFacilities( + title = "교통 시설", + description = "신호등 고장, 표지판 파손, 횡단보도 등", + iconResourceId = -1, + ), + LightingFacilities( + title = "조명 시설", + description = "가로등, 보안등 파손 등", + iconResourceId = -1, + ), + WaterFacilities( + title = "상하수도 시설", + description = "맨홀 뚜껑 손상 등", + iconResourceId = -1, + ), + Amenities( + title = "편의 시설", + description = "벤치 파손, 휴지통 넘침 등", + iconResourceId = -1, + ), +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportHistoriesPerDayUiModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportHistoriesPerDayUiModel.kt new file mode 100644 index 00000000..4f233ba4 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportHistoriesPerDayUiModel.kt @@ -0,0 +1,8 @@ +package com.threegap.bitnagil.presentation.reporthistory.model + +import java.time.LocalDate + +data class ReportHistoriesPerDayUiModel( + val date: LocalDate, + val reports: List, +) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportHistoryState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportHistoryState.kt new file mode 100644 index 00000000..9665742f --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportHistoryState.kt @@ -0,0 +1,35 @@ +package com.threegap.bitnagil.presentation.reporthistory.model + +data class ReportHistoryState( + val reportProcesses: List, + val reportCategories: List, + val selectedReportCategory: ReportCategory?, + val selectedReportProcess: ReportProcess, + val reportHistoriesPerDays: List, +) { + val reportProcessWithCounts: List = listOf( + ReportProcessWithCount(ReportProcess.Total, reportHistoriesPerDays.sumOf { it.reports.size }), + ReportProcessWithCount( + ReportProcess.Reported, + reportHistoriesPerDays.sumOf { it.reports.filter { report -> report.process == ReportProcess.Reported }.size }, + ), + ReportProcessWithCount( + ReportProcess.Progress, + reportHistoriesPerDays.sumOf { it.reports.filter { report -> report.process == ReportProcess.Progress }.size }, + ), + ReportProcessWithCount( + ReportProcess.Complete, + reportHistoriesPerDays.sumOf { it.reports.filter { report -> report.process == ReportProcess.Complete }.size }, + ), + ) + + companion object { + val Init = ReportHistoryState( + reportProcesses = listOf(), + reportCategories = listOf(), + selectedReportCategory = null, + selectedReportProcess = ReportProcess.Total, + reportHistoriesPerDays = listOf(), + ) + } +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportHistoryUiModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportHistoryUiModel.kt new file mode 100644 index 00000000..9db6d46b --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportHistoryUiModel.kt @@ -0,0 +1,10 @@ +package com.threegap.bitnagil.presentation.reporthistory.model + +data class ReportHistoryUiModel( + val id: String, + val title: String, + val imageUrl: String, + val location: String, + val process: ReportProcess, + val category: ReportCategory, +) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportProcess.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportProcess.kt new file mode 100644 index 00000000..29d5102a --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportProcess.kt @@ -0,0 +1,10 @@ +package com.threegap.bitnagil.presentation.reporthistory.model + +enum class ReportProcess( + val title: String, +) { + Total(title = "전체"), + Reported(title = "제보 완료"), + Progress(title = "처리 중"), + Complete(title = "처리 완료"), +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportProcessWithCount.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportProcessWithCount.kt new file mode 100644 index 00000000..18ba2606 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportProcessWithCount.kt @@ -0,0 +1,13 @@ +package com.threegap.bitnagil.presentation.reporthistory.model + +data class ReportProcessWithCount( + val process: ReportProcess, + val count: Int, +) { + companion object { + val Init = ReportProcessWithCount( + process = ReportProcess.Total, + count = 0, + ) + } +} From 1533248db649005fa7a646197ee8747a9811dbc0 Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Thu, 20 Nov 2025 21:13:54 +0900 Subject: [PATCH 2/8] =?UTF-8?q?FIX:=20=EC=A0=9C=EB=B3=B4=20=ED=9E=88?= =?UTF-8?q?=EC=8A=A4=ED=86=A0=EB=A6=AC=20UI=20State=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20-=20=EC=84=A0=ED=83=9D=EB=90=9C=20=EC=B9=B4=ED=85=8C?= =?UTF-8?q?=EA=B3=A0=EB=A6=AC/=EC=A7=84=ED=96=89=20=EC=83=81=ED=99=A9?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=9D=BC=20=ED=95=B4=EB=8B=B9=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EC=A0=9C=EB=B3=B4=20=EB=AA=A9=EB=A1=9D=EB=A7=8C=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reporthistory/ReportHistoryScreen.kt | 109 ++++++++++-------- .../reporthistory/model/ReportHistoryState.kt | 29 +++++ 2 files changed, 88 insertions(+), 50 deletions(-) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryScreen.kt index c6a57b37..7c88200f 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryScreen.kt @@ -77,69 +77,78 @@ private fun ReportHistoryScreen( ) } - // chip list - Spacer(modifier = Modifier.width(8.dp)) } Spacer(modifier = Modifier.height(24.dp)) Box( - modifier = Modifier.weight(1f), + modifier = Modifier.fillMaxWidth().weight(1f), ) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(horizontal = 16.dp), - ) { - state.reportHistoriesPerDays.forEach { reportHistoriesPerDay -> - stickyHeader { - Box( - modifier = Modifier - .fillMaxWidth() - .height(40.dp) - .background(color = BitnagilTheme.colors.coolGray99), - ) { - Text( - text = reportHistoriesPerDay.date.toString(), - modifier = Modifier.align(Alignment.CenterStart), - style = BitnagilTheme.typography.body2SemiBold, - ) + if (state.filteredReportHistoriesPerDays.isNotEmpty()) + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(horizontal = 16.dp), + ) { + state.reportHistoriesPerDays.forEach { reportHistoriesPerDay -> + stickyHeader { + Box( + modifier = Modifier + .fillMaxWidth() + .height(40.dp) + .background(color = BitnagilTheme.colors.coolGray99), + ) { + Text( + text = reportHistoriesPerDay.date.toString(), + modifier = Modifier.align(Alignment.CenterStart), + style = BitnagilTheme.typography.body2SemiBold, + ) + } } - } - itemsIndexed(reportHistoriesPerDay.reports) { index, report -> - ReportHistoryItem( - modifier = Modifier.padding(bottom = if (index == reportHistoriesPerDay.reports.lastIndex) 24.dp else 10.dp), - report = report, - onClick = {}, - ) + itemsIndexed(reportHistoriesPerDay.reports) { index, report -> + ReportHistoryItem( + modifier = Modifier.padding(bottom = if (index == reportHistoriesPerDay.reports.lastIndex) 24.dp else 10.dp), + report = report, + onClick = {}, + ) + } } } - } - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(5.dp), - modifier = Modifier - .height(40.dp) - .align(Alignment.TopEnd) - .clickableWithoutRipple { }, - ) { - Text( - text = "카테고리", - color = BitnagilTheme.colors.coolGray40, - style = BitnagilTheme.typography.body2Medium, - modifier = Modifier.padding(start = 10.dp), - ) + else + Column( + modifier = Modifier.align(Alignment.Center), + verticalArrangement = Arrangement.spacedBy(2.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(text = "제보한 내역이 없어요.", style = BitnagilTheme.typography.subtitle1SemiBold) + Text(text = "원하는 카테고리로 제보를 시작해 보세요.", style = BitnagilTheme.typography.body2Regular, color = BitnagilTheme.colors.coolGray70) + } - BitnagilIcon( - id = R.drawable.ic_down_arrow, - tint = BitnagilTheme.colors.coolGray40, + if (state.showCategorySelectButton) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(5.dp), modifier = Modifier - .padding(end = 13.dp) - .size(16.dp), - ) - } + .height(40.dp) + .align(Alignment.TopEnd) + .clickableWithoutRipple { }, + ) { + Text( + text = state.selectedReportCategory?.title ?: "카테고리", + color = BitnagilTheme.colors.coolGray40, + style = BitnagilTheme.typography.body2Medium, + modifier = Modifier.padding(start = 10.dp), + ) + + BitnagilIcon( + id = R.drawable.ic_down_arrow, + tint = BitnagilTheme.colors.coolGray40, + modifier = Modifier + .padding(end = 13.dp) + .size(16.dp), + ) + } } } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportHistoryState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportHistoryState.kt index 9665742f..5de0116d 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportHistoryState.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportHistoryState.kt @@ -7,6 +7,33 @@ data class ReportHistoryState( val selectedReportProcess: ReportProcess, val reportHistoriesPerDays: List, ) { + val filteredReportHistoriesPerDays: List = reportHistoriesPerDays + .map { reportHistoriesPerDay -> + reportHistoriesPerDay.copy( + reports = reportHistoriesPerDay.reports.filter { + val processMatched = when (selectedReportProcess) { + ReportProcess.Total -> true + ReportProcess.Reported -> it.process == ReportProcess.Reported + ReportProcess.Progress -> it.process == ReportProcess.Progress + ReportProcess.Complete -> it.process == ReportProcess.Complete + } + + val categoryMatched = when(selectedReportCategory) { + ReportCategory.TrafficFacilities -> it.category == ReportCategory.TrafficFacilities + ReportCategory.LightingFacilities -> it.category == ReportCategory.LightingFacilities + ReportCategory.WaterFacilities -> it.category == ReportCategory.WaterFacilities + ReportCategory.Amenities -> it.category == ReportCategory.Amenities + null -> true + } + + processMatched && categoryMatched + }, + ) + } + .filter { reportHistoriesPerDay -> + reportHistoriesPerDay.reports.isNotEmpty() + } + val reportProcessWithCounts: List = listOf( ReportProcessWithCount(ReportProcess.Total, reportHistoriesPerDays.sumOf { it.reports.size }), ReportProcessWithCount( @@ -23,6 +50,8 @@ data class ReportHistoryState( ), ) + val showCategorySelectButton: Boolean = reportHistoriesPerDays.isNotEmpty() + companion object { val Init = ReportHistoryState( reportProcesses = listOf(), From c472f16bee74388bf43d307e23c3b5d1a8869066 Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Thu, 20 Nov 2025 21:47:48 +0900 Subject: [PATCH 3/8] =?UTF-8?q?Feat:=20=EC=A0=9C=EB=B3=B4=20=EC=B9=B4?= =?UTF-8?q?=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=84=A0=ED=83=9D=20bottomSheet?= =?UTF-8?q?=EA=B5=AC=ED=98=84,=20=EC=A0=9C=EB=B3=B4=20=ED=9E=88=EC=8A=A4?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20ViewModel=20=EB=BC=88=EB=8C=80=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=B0=8F=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reporthistory/ReportHistoryScreen.kt | 52 ++++++- .../reporthistory/ReportHistoryViewModel.kt | 87 +++++++++++ .../template/ReportCategoryBottomSheet.kt | 143 ++++++++++++++++++ .../reporthistory/model/ReportCategory.kt | 10 +- .../model/ReportHistorySideEffect.kt | 3 + .../reporthistory/model/ReportHistoryState.kt | 6 +- 6 files changed, 286 insertions(+), 15 deletions(-) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryViewModel.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/component/template/ReportCategoryBottomSheet.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportHistorySideEffect.kt diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryScreen.kt index 7c88200f..8eb757c7 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryScreen.kt @@ -20,10 +20,12 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.rememberScrollState import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import com.threegap.bitnagil.designsystem.BitnagilTheme import com.threegap.bitnagil.designsystem.R import com.threegap.bitnagil.designsystem.component.atom.BitnagilChip @@ -31,20 +33,46 @@ import com.threegap.bitnagil.designsystem.component.atom.BitnagilIcon import com.threegap.bitnagil.designsystem.component.block.BitnagilTopBar import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple import com.threegap.bitnagil.presentation.reporthistory.component.block.ReportHistoryItem +import com.threegap.bitnagil.presentation.reporthistory.component.template.ReportCategoryBottomSheet import com.threegap.bitnagil.presentation.reporthistory.model.ReportCategory import com.threegap.bitnagil.presentation.reporthistory.model.ReportHistoriesPerDayUiModel import com.threegap.bitnagil.presentation.reporthistory.model.ReportHistoryState import com.threegap.bitnagil.presentation.reporthistory.model.ReportHistoryUiModel import com.threegap.bitnagil.presentation.reporthistory.model.ReportProcess +import org.orbitmvi.orbit.compose.collectAsState @Composable -fun ReportHistoryScreenContainer() { +fun ReportHistoryScreenContainer( + viewModel: ReportHistoryViewModel = hiltViewModel(), + navigateToBack: () -> Unit, + navigateToReportDetail: (String) -> Unit, +) { + val state by viewModel.collectAsState() + + if (state.showSelectReportCategoryBottomSheet) + ReportCategoryBottomSheet( + selectedCategory = state.selectedReportCategory, + onDismiss = viewModel::hideReportCategoryBottomSheet, + onSelected = viewModel::selectReportCategory + ) + + ReportHistoryScreen( + state = state, + onClickPreviousButton = navigateToBack, + onClickReportProcessChip = viewModel::selectReportProcess, + onClickReportCategoryButton = viewModel::showReportCategoryBottomSheet, + onClickReportItem = navigateToReportDetail + ) } @Composable private fun ReportHistoryScreen( modifier: Modifier = Modifier, state: ReportHistoryState, + onClickPreviousButton: () -> Unit, + onClickReportProcessChip: (ReportProcess) -> Unit, + onClickReportCategoryButton: () -> Unit, + onClickReportItem: (String) -> Unit, ) { val horizontalScreenState = rememberScrollState() @@ -57,7 +85,7 @@ private fun ReportHistoryScreen( BitnagilTopBar( title = "내 제보 기록", showBackButton = true, - onBackClick = {}, + onBackClick = onClickPreviousButton, ) Spacer(modifier = Modifier.height(16.dp)) @@ -72,7 +100,9 @@ private fun ReportHistoryScreen( BitnagilChip( title = reportProcessWithCount.process.title, isSelected = state.selectedReportProcess == reportProcessWithCount.process, - onCategorySelected = {}, + onCategorySelected = { + onClickReportProcessChip(reportProcessWithCount.process) + }, count = if (reportProcessWithCount.count == 0) null else reportProcessWithCount.count, ) } @@ -83,14 +113,16 @@ private fun ReportHistoryScreen( Spacer(modifier = Modifier.height(24.dp)) Box( - modifier = Modifier.fillMaxWidth().weight(1f), + modifier = Modifier + .fillMaxWidth() + .weight(1f), ) { if (state.filteredReportHistoriesPerDays.isNotEmpty()) LazyColumn( modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(horizontal = 16.dp), ) { - state.reportHistoriesPerDays.forEach { reportHistoriesPerDay -> + state.filteredReportHistoriesPerDays.forEach { reportHistoriesPerDay -> stickyHeader { Box( modifier = Modifier @@ -110,7 +142,9 @@ private fun ReportHistoryScreen( ReportHistoryItem( modifier = Modifier.padding(bottom = if (index == reportHistoriesPerDay.reports.lastIndex) 24.dp else 10.dp), report = report, - onClick = {}, + onClick = { + onClickReportItem(report.id) + }, ) } } @@ -132,7 +166,7 @@ private fun ReportHistoryScreen( modifier = Modifier .height(40.dp) .align(Alignment.TopEnd) - .clickableWithoutRipple { }, + .clickableWithoutRipple(onClick = onClickReportCategoryButton), ) { Text( text = state.selectedReportCategory?.title ?: "카테고리", @@ -183,6 +217,10 @@ private fun ReportHistoryScreenPreview() { ) }, ), + onClickPreviousButton = {}, + onClickReportProcessChip = {}, + onClickReportCategoryButton = {}, + onClickReportItem = {}, ) } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryViewModel.kt new file mode 100644 index 00000000..d97e99a2 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryViewModel.kt @@ -0,0 +1,87 @@ +package com.threegap.bitnagil.presentation.reporthistory + +import androidx.lifecycle.ViewModel +import com.threegap.bitnagil.presentation.reporthistory.model.ReportCategory +import com.threegap.bitnagil.presentation.reporthistory.model.ReportHistoriesPerDayUiModel +import com.threegap.bitnagil.presentation.reporthistory.model.ReportHistorySideEffect +import com.threegap.bitnagil.presentation.reporthistory.model.ReportHistoryState +import com.threegap.bitnagil.presentation.reporthistory.model.ReportHistoryUiModel +import com.threegap.bitnagil.presentation.reporthistory.model.ReportProcess +import dagger.hilt.android.lifecycle.HiltViewModel +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.viewmodel.container +import javax.inject.Inject + +@HiltViewModel +class ReportHistoryViewModel @Inject constructor() : ContainerHost, ViewModel() { + override val container: Container = container(initialState = ReportHistoryState.Init) + + init { + loadReportHistories() + } + + private fun loadReportHistories() = intent { + reduce { + state.copy( + reportHistoriesPerDays = List(10) { + ReportHistoriesPerDayUiModel( + date = java.time.LocalDate.now(), + reports = listOf( + ReportHistoryUiModel( + id = "1", + title = "제보 1", + imageUrl = "-", + location = "서울특별시 성북구 안암로 106", + process = ReportProcess.Reported, + category = ReportCategory.Amenities, + ), + ReportHistoryUiModel( + id = "1", + title = "제보 1", + imageUrl = "-", + location = "서울특별시 성북구 안암로 106", + process = ReportProcess.Progress, + category = ReportCategory.Amenities, + ), + ), + ) + }, + ) + } + } + + fun selectReportCategory(reportCategory: ReportCategory) = intent { + val currentSelectedReportCategory = state.selectedReportCategory + + reduce { + state.copy( + selectedReportCategory = if (currentSelectedReportCategory == reportCategory) null else reportCategory + ) + } + } + + fun selectReportProcess(reportProcess: ReportProcess) = intent { + reduce { + state.copy( + selectedReportProcess = reportProcess + ) + } + } + + fun showReportCategoryBottomSheet() = intent { + reduce { + state.copy( + showSelectReportCategoryBottomSheet = true + ) + } + } + + fun hideReportCategoryBottomSheet() = intent { + reduce { + state.copy( + showSelectReportCategoryBottomSheet = false + ) + } + } +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/component/template/ReportCategoryBottomSheet.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/component/template/ReportCategoryBottomSheet.kt new file mode 100644 index 00000000..b1db2dff --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/component/template/ReportCategoryBottomSheet.kt @@ -0,0 +1,143 @@ +package com.threegap.bitnagil.presentation.reporthistory.component.template + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.threegap.bitnagil.designsystem.BitnagilTheme +import com.threegap.bitnagil.designsystem.R +import com.threegap.bitnagil.designsystem.component.atom.BitnagilIcon +import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple +import com.threegap.bitnagil.domain.recommendroutine.model.RecommendLevel +import com.threegap.bitnagil.presentation.reporthistory.model.ReportCategory +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ReportCategoryBottomSheet( + selectedCategory: ReportCategory?, + onDismiss: () -> Unit, + onSelected: (ReportCategory) -> Unit, + modifier: Modifier = Modifier, +) { + val sheetState = rememberModalBottomSheetState() + val coroutineScope = rememberCoroutineScope() + + ModalBottomSheet( + sheetState = sheetState, + onDismissRequest = onDismiss, + containerColor = BitnagilTheme.colors.white, + contentColor = BitnagilTheme.colors.white, + modifier = modifier, + ) { + Column( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 28.dp), + ) { + ReportCategory.entries.forEachIndexed { index, category -> + ReportCategoryItem( + icon = category.iconResourceId, + title = category.title, + description = category.description, + isSelected = selectedCategory == category, + onClick = { + onSelected(category) + coroutineScope + .launch { sheetState.hide() } + .invokeOnCompletion { + if (!sheetState.isVisible) onDismiss() + } + }, + ) + + if (index < RecommendLevel.entries.size) { + HorizontalDivider( + color = BitnagilTheme.colors.coolGray97, + ) + } + } + } + } +} + +@Composable +private fun ReportCategoryItem( + title: String, + description: String, + @DrawableRes icon: Int, + isSelected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .clickableWithoutRipple { onClick() } + .padding(vertical = 14.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + BitnagilIcon(id = icon, tint = null) + + Spacer(modifier = Modifier.width(14.dp)) + + Column { + Text( + text = title, + color = BitnagilTheme.colors.coolGray10, + style = BitnagilTheme.typography.body1Medium, + ) + + Text( + text = description, + color = BitnagilTheme.colors.coolGray70, + style = BitnagilTheme.typography.body2Medium, + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + if (isSelected) { + BitnagilIcon( + id = R.drawable.ic_check_md, + tint = BitnagilTheme.colors.orange500, + ) + } + } +} + +@Preview +@Composable +private fun ReportCategoryBottomSheetPreview() { + ReportCategoryBottomSheet( + selectedCategory = ReportCategory.WaterFacilities, + onDismiss = {}, + onSelected = {}, + ) +} + +@Preview(showBackground = true) +@Composable +private fun ReportCategoryItemPreview() { + ReportCategoryItem( + title = "교통 시설", + description = "어쩌구", + icon = R.drawable.ic_check_md, + isSelected = true, + onClick = {}, + ) +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportCategory.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportCategory.kt index 6784f2f1..bad49ae2 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportCategory.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportCategory.kt @@ -1,5 +1,7 @@ package com.threegap.bitnagil.presentation.reporthistory.model +import com.threegap.bitnagil.designsystem.R + enum class ReportCategory( val title: String, val description: String, @@ -8,21 +10,21 @@ enum class ReportCategory( TrafficFacilities( title = "교통 시설", description = "신호등 고장, 표지판 파손, 횡단보도 등", - iconResourceId = -1, + iconResourceId = R.drawable.ic_check_md, ), LightingFacilities( title = "조명 시설", description = "가로등, 보안등 파손 등", - iconResourceId = -1, + iconResourceId = R.drawable.ic_check_md, ), WaterFacilities( title = "상하수도 시설", description = "맨홀 뚜껑 손상 등", - iconResourceId = -1, + iconResourceId = R.drawable.ic_check_md, ), Amenities( title = "편의 시설", description = "벤치 파손, 휴지통 넘침 등", - iconResourceId = -1, + iconResourceId = R.drawable.ic_check_md, ), } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportHistorySideEffect.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportHistorySideEffect.kt new file mode 100644 index 00000000..9bf7b78a --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportHistorySideEffect.kt @@ -0,0 +1,3 @@ +package com.threegap.bitnagil.presentation.reporthistory.model + +sealed interface ReportHistorySideEffect diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportHistoryState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportHistoryState.kt index 5de0116d..891bb82b 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportHistoryState.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportHistoryState.kt @@ -1,11 +1,10 @@ package com.threegap.bitnagil.presentation.reporthistory.model data class ReportHistoryState( - val reportProcesses: List, - val reportCategories: List, val selectedReportCategory: ReportCategory?, val selectedReportProcess: ReportProcess, val reportHistoriesPerDays: List, + val showSelectReportCategoryBottomSheet: Boolean, ) { val filteredReportHistoriesPerDays: List = reportHistoriesPerDays .map { reportHistoriesPerDay -> @@ -54,11 +53,10 @@ data class ReportHistoryState( companion object { val Init = ReportHistoryState( - reportProcesses = listOf(), - reportCategories = listOf(), selectedReportCategory = null, selectedReportProcess = ReportProcess.Total, reportHistoriesPerDays = listOf(), + showSelectReportCategoryBottomSheet = false ) } } From 71aac61167ecb026e4d86323ca9807c6a155cc51 Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Sat, 22 Nov 2025 15:26:58 +0900 Subject: [PATCH 4/8] =?UTF-8?q?Feat:=20=EC=A0=9C=EB=B3=B4=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20UseCase=20=EB=B0=8F=20Reposito?= =?UTF-8?q?ry=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../report/datasource/ReportDataSource.kt | 2 ++ .../datasourceImpl/ReportDataSourceImpl.kt | 5 +++ .../response/ReportHistoriesPerDateDto.kt | 19 +++++++++++ .../report/model/response/ReportItemDto.kt | 33 +++++++++++++++++++ .../repositoryImpl/ReportRepositoryImpl.kt | 7 ++++ .../data/report/service/ReportService.kt | 5 +++ .../domain/report/model/ReportItem.kt | 10 ++++++ .../domain/report/model/ReportStatus.kt | 27 +++++++++++++++ .../report/repository/ReportRepository.kt | 3 ++ .../usecase/GetReportHistoriesUseCase.kt | 14 ++++++++ 10 files changed, 125 insertions(+) create mode 100644 data/src/main/java/com/threegap/bitnagil/data/report/model/response/ReportHistoriesPerDateDto.kt create mode 100644 data/src/main/java/com/threegap/bitnagil/data/report/model/response/ReportItemDto.kt create mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/report/model/ReportItem.kt create mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/report/model/ReportStatus.kt create mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/report/usecase/GetReportHistoriesUseCase.kt diff --git a/data/src/main/java/com/threegap/bitnagil/data/report/datasource/ReportDataSource.kt b/data/src/main/java/com/threegap/bitnagil/data/report/datasource/ReportDataSource.kt index 2c2170f7..e672d191 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/report/datasource/ReportDataSource.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/report/datasource/ReportDataSource.kt @@ -1,7 +1,9 @@ package com.threegap.bitnagil.data.report.datasource import com.threegap.bitnagil.data.report.model.request.ReportRequestDto +import com.threegap.bitnagil.data.report.model.response.ReportHistoriesPerDateDto interface ReportDataSource { suspend fun submitReport(reportRequestDto: ReportRequestDto): Result + suspend fun getReports(): Result } diff --git a/data/src/main/java/com/threegap/bitnagil/data/report/datasourceImpl/ReportDataSourceImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/report/datasourceImpl/ReportDataSourceImpl.kt index 14305d47..c980beef 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/report/datasourceImpl/ReportDataSourceImpl.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/report/datasourceImpl/ReportDataSourceImpl.kt @@ -3,6 +3,7 @@ package com.threegap.bitnagil.data.report.datasourceImpl import com.threegap.bitnagil.data.common.safeApiCall import com.threegap.bitnagil.data.report.datasource.ReportDataSource import com.threegap.bitnagil.data.report.model.request.ReportRequestDto +import com.threegap.bitnagil.data.report.model.response.ReportHistoriesPerDateDto import com.threegap.bitnagil.data.report.service.ReportService import javax.inject.Inject @@ -12,4 +13,8 @@ class ReportDataSourceImpl @Inject constructor( override suspend fun submitReport(reportRequestDto: ReportRequestDto): Result { return safeApiCall { reportService.submitReport(reportRequestDto) } } + + override suspend fun getReports(): Result { + return safeApiCall { reportService.getReports() } + } } diff --git a/data/src/main/java/com/threegap/bitnagil/data/report/model/response/ReportHistoriesPerDateDto.kt b/data/src/main/java/com/threegap/bitnagil/data/report/model/response/ReportHistoriesPerDateDto.kt new file mode 100644 index 00000000..27577b76 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/report/model/response/ReportHistoriesPerDateDto.kt @@ -0,0 +1,19 @@ +package com.threegap.bitnagil.data.report.model.response + +import com.threegap.bitnagil.domain.report.model.ReportItem +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import java.time.LocalDate + +@Serializable +data class ReportHistoriesPerDateDto( + @SerialName("reportInfos") + val reportInfos: Map>, +) + +fun ReportHistoriesPerDateDto.toDomainMap(): Map> = + this.reportInfos + .mapKeys { LocalDate.parse(it.key) } + .mapValues { entry -> + entry.value.map { it.toDomain() } + } diff --git a/data/src/main/java/com/threegap/bitnagil/data/report/model/response/ReportItemDto.kt b/data/src/main/java/com/threegap/bitnagil/data/report/model/response/ReportItemDto.kt new file mode 100644 index 00000000..1b26ff2a --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/report/model/response/ReportItemDto.kt @@ -0,0 +1,33 @@ +package com.threegap.bitnagil.data.report.model.response + +import com.threegap.bitnagil.domain.report.model.ReportCategory +import com.threegap.bitnagil.domain.report.model.ReportItem +import com.threegap.bitnagil.domain.report.model.ReportStatus +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ReportItemDto( + @SerialName("reportId") + val reportId: Int, + @SerialName("reportStatus") + val reportStatus: String, + @SerialName("reportTitle") + val reportTitle: String, + @SerialName("reportCategory") + val reportCategory: String, + @SerialName("reportLocation") + val reportLocation: String, + @SerialName("reportImageUrl") + val reportImageUrl: String, +) + +fun ReportItemDto.toDomain(): ReportItem = + ReportItem( + id = this.reportId, + status = ReportStatus.fromString(this.reportStatus), + title = this.reportTitle, + category = ReportCategory.fromString(this.reportCategory), + address = this.reportLocation, + imageUrl = this.reportImageUrl, + ) diff --git a/data/src/main/java/com/threegap/bitnagil/data/report/repositoryImpl/ReportRepositoryImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/report/repositoryImpl/ReportRepositoryImpl.kt index f0261faf..dcfe9e36 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/report/repositoryImpl/ReportRepositoryImpl.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/report/repositoryImpl/ReportRepositoryImpl.kt @@ -2,8 +2,11 @@ package com.threegap.bitnagil.data.report.repositoryImpl import com.threegap.bitnagil.data.report.datasource.ReportDataSource import com.threegap.bitnagil.data.report.model.request.toDto +import com.threegap.bitnagil.data.report.model.response.toDomainMap import com.threegap.bitnagil.domain.report.model.Report +import com.threegap.bitnagil.domain.report.model.ReportItem import com.threegap.bitnagil.domain.report.repository.ReportRepository +import java.time.LocalDate import javax.inject.Inject class ReportRepositoryImpl @Inject constructor( @@ -12,4 +15,8 @@ class ReportRepositoryImpl @Inject constructor( override suspend fun submitReport(report: Report): Result { return reportDataSource.submitReport(report.toDto()) } + + override suspend fun getReportHistories(): Result>> { + return reportDataSource.getReports().map { it.toDomainMap() } + } } diff --git a/data/src/main/java/com/threegap/bitnagil/data/report/service/ReportService.kt b/data/src/main/java/com/threegap/bitnagil/data/report/service/ReportService.kt index 27a697f0..f6d25128 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/report/service/ReportService.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/report/service/ReportService.kt @@ -1,8 +1,10 @@ package com.threegap.bitnagil.data.report.service import com.threegap.bitnagil.data.report.model.request.ReportRequestDto +import com.threegap.bitnagil.data.report.model.response.ReportHistoriesPerDateDto import com.threegap.bitnagil.network.model.BaseResponse import retrofit2.http.Body +import retrofit2.http.GET import retrofit2.http.POST interface ReportService { @@ -10,4 +12,7 @@ interface ReportService { suspend fun submitReport( @Body reportRequestDto: ReportRequestDto, ): BaseResponse + + @GET("/api/v2/reports") + suspend fun getReports(): BaseResponse } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/report/model/ReportItem.kt b/domain/src/main/java/com/threegap/bitnagil/domain/report/model/ReportItem.kt new file mode 100644 index 00000000..92fa471a --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/report/model/ReportItem.kt @@ -0,0 +1,10 @@ +package com.threegap.bitnagil.domain.report.model + +data class ReportItem( + val id: Int, + val title: String, + val category: ReportCategory, + val status: ReportStatus, + val imageUrl: String, + val address: String, +) diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/report/model/ReportStatus.kt b/domain/src/main/java/com/threegap/bitnagil/domain/report/model/ReportStatus.kt new file mode 100644 index 00000000..a618d0db --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/report/model/ReportStatus.kt @@ -0,0 +1,27 @@ +package com.threegap.bitnagil.domain.report.model + +enum class ReportStatus { + Pending, + InProgress, + Completed, + ; + + companion object { + fun fromString(value: String): ReportStatus { + return when (value) { + "PENDING" -> Pending + "IN_PROGRESS" -> InProgress + "COMPLETED" -> Completed + else -> throw IllegalArgumentException("Invalid ReportStatus value: $value") + } + } + + fun toString(value: ReportStatus): String { + return when (value) { + Pending -> "PENDING" + InProgress -> "IN_PROGRESS" + Completed -> "COMPLETED" + } + } + } +} diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/report/repository/ReportRepository.kt b/domain/src/main/java/com/threegap/bitnagil/domain/report/repository/ReportRepository.kt index b6cd018a..39a68033 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/report/repository/ReportRepository.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/report/repository/ReportRepository.kt @@ -1,7 +1,10 @@ package com.threegap.bitnagil.domain.report.repository import com.threegap.bitnagil.domain.report.model.Report +import com.threegap.bitnagil.domain.report.model.ReportItem +import java.time.LocalDate interface ReportRepository { suspend fun submitReport(report: Report): Result + suspend fun getReportHistories(): Result>> } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/report/usecase/GetReportHistoriesUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/report/usecase/GetReportHistoriesUseCase.kt new file mode 100644 index 00000000..97af0197 --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/report/usecase/GetReportHistoriesUseCase.kt @@ -0,0 +1,14 @@ +package com.threegap.bitnagil.domain.report.usecase + +import com.threegap.bitnagil.domain.report.model.ReportItem +import com.threegap.bitnagil.domain.report.repository.ReportRepository +import java.time.LocalDate +import javax.inject.Inject + +class GetReportHistoriesUseCase @Inject constructor( + private val reportRepository: ReportRepository, +) { + suspend operator fun invoke(): Result>> { + return reportRepository.getReportHistories() + } +} From 91f55d4cbe5bfabf825ada187fe1a4da5f489779 Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Sat, 22 Nov 2025 15:32:26 +0900 Subject: [PATCH 5/8] =?UTF-8?q?Feat:=20=EC=A0=9C=EB=B3=B4=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=ED=99=94=EB=A9=B4=EC=97=90=20=EC=A0=9C=EB=B3=B4=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20UseCase=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20=EB=B0=8F=20=EB=A7=88=EC=9D=B4=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EC=97=90=EC=84=9C=20=EC=A0=9C=EB=B3=B4=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=ED=99=94=EB=A9=B4=20=EC=9D=B4=EB=8F=99=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/threegap/bitnagil/MainNavHost.kt | 18 ++++++ .../main/java/com/threegap/bitnagil/Route.kt | 3 + .../bitnagil/navigation/home/HomeNavHost.kt | 2 + .../presentation/mypage/MyPageScreen.kt | 9 +++ .../reporthistory/ReportHistoryScreen.kt | 17 ++--- .../reporthistory/ReportHistoryViewModel.kt | 62 +++++++++---------- .../reporthistory/model/ReportCategory.kt | 21 +++++-- .../reporthistory/model/ReportHistoryState.kt | 4 +- .../model/ReportHistoryUiModel.kt | 17 ++++- .../reporthistory/model/ReportProcess.kt | 13 ++++ 10 files changed, 121 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt b/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt index 4810d5b1..39fd583d 100644 --- a/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt +++ b/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt @@ -14,6 +14,7 @@ import com.threegap.bitnagil.presentation.onboarding.OnBoardingScreenContainer import com.threegap.bitnagil.presentation.onboarding.OnBoardingViewModel import com.threegap.bitnagil.presentation.onboarding.model.navarg.OnBoardingScreenArg import com.threegap.bitnagil.presentation.report.ReportScreenContainer +import com.threegap.bitnagil.presentation.reporthistory.ReportHistoryScreenContainer import com.threegap.bitnagil.presentation.routinelist.RoutineListScreenContainer import com.threegap.bitnagil.presentation.setting.SettingScreenContainer import com.threegap.bitnagil.presentation.splash.SplashScreenContainer @@ -140,6 +141,11 @@ fun MainNavHost( launchSingleTop = true } }, + navigateToReportHistory = { + navigator.navController.navigate(Route.ReportHistory) { + launchSingleTop = true + } + }, ) } @@ -305,5 +311,17 @@ fun MainNavHost( }, ) } + + composable { + ReportHistoryScreenContainer( + navigateToBack = { + if (navigator.navController.previousBackStackEntry != null) { + navigator.navController.popBackStack() + } + }, + navigateToReportDetail = { + }, + ) + } } } diff --git a/app/src/main/java/com/threegap/bitnagil/Route.kt b/app/src/main/java/com/threegap/bitnagil/Route.kt index 93e1cd0e..5a9ad70e 100644 --- a/app/src/main/java/com/threegap/bitnagil/Route.kt +++ b/app/src/main/java/com/threegap/bitnagil/Route.kt @@ -51,4 +51,7 @@ sealed interface Route { @Serializable data object Report : Route + + @Serializable + data object ReportHistory : Route } diff --git a/app/src/main/java/com/threegap/bitnagil/navigation/home/HomeNavHost.kt b/app/src/main/java/com/threegap/bitnagil/navigation/home/HomeNavHost.kt index 249a9aef..07a01787 100644 --- a/app/src/main/java/com/threegap/bitnagil/navigation/home/HomeNavHost.kt +++ b/app/src/main/java/com/threegap/bitnagil/navigation/home/HomeNavHost.kt @@ -44,6 +44,7 @@ fun HomeNavHost( navigateToEmotion: () -> Unit, navigateToRoutineList: (String) -> Unit, navigateToReport: () -> Unit, + navigateToReportHistory: () -> Unit, ) { val navigator = rememberHomeNavigator() var showFloatingOverlay by remember { mutableStateOf(false) } @@ -89,6 +90,7 @@ fun HomeNavHost( navigateToOnBoarding = navigateToOnBoarding, navigateToNotice = navigateToNotice, navigateToQnA = navigateToQnA, + navigateToReportHistory = navigateToReportHistory, ) } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/MyPageScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/MyPageScreen.kt index 3feabcac..6bb355ae 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/MyPageScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/MyPageScreen.kt @@ -38,6 +38,7 @@ fun MyPageScreenContainer( navigateToNotice: () -> Unit, navigateToQnA: () -> Unit, navigateToOnBoarding: () -> Unit, + navigateToReportHistory: () -> Unit, ) { val state by myPageViewModel.stateFlow.collectAsState() @@ -47,6 +48,7 @@ fun MyPageScreenContainer( onClickNotice = navigateToNotice, onClickResetOnBoarding = navigateToOnBoarding, onClickQnA = navigateToQnA, + onClickReportHistory = navigateToReportHistory, ) } @@ -57,6 +59,7 @@ private fun MyPageScreen( onClickNotice: () -> Unit, onClickResetOnBoarding: () -> Unit, onClickQnA: () -> Unit, + onClickReportHistory: () -> Unit, ) { Column( modifier = Modifier @@ -100,6 +103,11 @@ private fun MyPageScreen( Spacer(modifier = Modifier.height(16.dp)) + BitnagilOptionButton( + title = "내 제보 기록", + onClick = onClickReportHistory, + ) + BitnagilOptionButton( title = "내 목표 재설정", onClick = onClickResetOnBoarding, @@ -127,6 +135,7 @@ fun MyPageScreenPreview() { onClickNotice = { }, onClickResetOnBoarding = { }, onClickQnA = { }, + onClickReportHistory = { }, ) } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryScreen.kt index 8eb757c7..b637c213 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryScreen.kt @@ -49,19 +49,20 @@ fun ReportHistoryScreenContainer( ) { val state by viewModel.collectAsState() - if (state.showSelectReportCategoryBottomSheet) + if (state.showSelectReportCategoryBottomSheet) { ReportCategoryBottomSheet( selectedCategory = state.selectedReportCategory, onDismiss = viewModel::hideReportCategoryBottomSheet, - onSelected = viewModel::selectReportCategory + onSelected = viewModel::selectReportCategory, ) + } ReportHistoryScreen( state = state, onClickPreviousButton = navigateToBack, onClickReportProcessChip = viewModel::selectReportProcess, onClickReportCategoryButton = viewModel::showReportCategoryBottomSheet, - onClickReportItem = navigateToReportDetail + onClickReportItem = navigateToReportDetail, ) } @@ -117,7 +118,7 @@ private fun ReportHistoryScreen( .fillMaxWidth() .weight(1f), ) { - if (state.filteredReportHistoriesPerDays.isNotEmpty()) + if (state.filteredReportHistoriesPerDays.isNotEmpty()) { LazyColumn( modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(horizontal = 16.dp), @@ -149,17 +150,18 @@ private fun ReportHistoryScreen( } } } - else + } else { Column( modifier = Modifier.align(Alignment.Center), verticalArrangement = Arrangement.spacedBy(2.dp), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { Text(text = "제보한 내역이 없어요.", style = BitnagilTheme.typography.subtitle1SemiBold) Text(text = "원하는 카테고리로 제보를 시작해 보세요.", style = BitnagilTheme.typography.body2Regular, color = BitnagilTheme.colors.coolGray70) } + } - if (state.showCategorySelectButton) + if (state.showCategorySelectButton) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(5.dp), @@ -183,6 +185,7 @@ private fun ReportHistoryScreen( .size(16.dp), ) } + } } } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryViewModel.kt index d97e99a2..bc441846 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryViewModel.kt @@ -1,6 +1,7 @@ package com.threegap.bitnagil.presentation.reporthistory import androidx.lifecycle.ViewModel +import com.threegap.bitnagil.domain.report.usecase.GetReportHistoriesUseCase import com.threegap.bitnagil.presentation.reporthistory.model.ReportCategory import com.threegap.bitnagil.presentation.reporthistory.model.ReportHistoriesPerDayUiModel import com.threegap.bitnagil.presentation.reporthistory.model.ReportHistorySideEffect @@ -14,7 +15,9 @@ import org.orbitmvi.orbit.viewmodel.container import javax.inject.Inject @HiltViewModel -class ReportHistoryViewModel @Inject constructor() : ContainerHost, ViewModel() { +class ReportHistoryViewModel @Inject constructor( + private val getReportHistoriesUseCase: GetReportHistoriesUseCase, +) : ContainerHost, ViewModel() { override val container: Container = container(initialState = ReportHistoryState.Init) init { @@ -22,33 +25,30 @@ class ReportHistoryViewModel @Inject constructor() : ContainerHost + val reportHistoriesPerDays = reportHistoriesPerDate + .map { reportHistoryPerDateMap -> + ReportHistoriesPerDayUiModel( + date = reportHistoryPerDateMap.key, + reports = reportHistoryPerDateMap.value.map { + ReportHistoryUiModel.fromDomain(it) + }, + ) + } + .sortedByDescending { reportHistoryPerDate -> + reportHistoryPerDate.date + } + + reduce { + state.copy( + reportHistoriesPerDays = reportHistoriesPerDays, ) - }, - ) - } + } + }, + onFailure = { + }, + ) } fun selectReportCategory(reportCategory: ReportCategory) = intent { @@ -56,7 +56,7 @@ class ReportHistoryViewModel @Inject constructor() : ContainerHost TrafficFacilities + DomainReportCategory.LIGHTING -> LightingFacilities + DomainReportCategory.WATERFACILITY -> WaterFacilities + DomainReportCategory.AMENITY -> Amenities + } + } + } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportHistoryState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportHistoryState.kt index 891bb82b..80de9be4 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportHistoryState.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportHistoryState.kt @@ -17,7 +17,7 @@ data class ReportHistoryState( ReportProcess.Complete -> it.process == ReportProcess.Complete } - val categoryMatched = when(selectedReportCategory) { + val categoryMatched = when (selectedReportCategory) { ReportCategory.TrafficFacilities -> it.category == ReportCategory.TrafficFacilities ReportCategory.LightingFacilities -> it.category == ReportCategory.LightingFacilities ReportCategory.WaterFacilities -> it.category == ReportCategory.WaterFacilities @@ -56,7 +56,7 @@ data class ReportHistoryState( selectedReportCategory = null, selectedReportProcess = ReportProcess.Total, reportHistoriesPerDays = listOf(), - showSelectReportCategoryBottomSheet = false + showSelectReportCategoryBottomSheet = false, ) } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportHistoryUiModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportHistoryUiModel.kt index 9db6d46b..ee6f32af 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportHistoryUiModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportHistoryUiModel.kt @@ -1,5 +1,7 @@ package com.threegap.bitnagil.presentation.reporthistory.model +import com.threegap.bitnagil.domain.report.model.ReportItem + data class ReportHistoryUiModel( val id: String, val title: String, @@ -7,4 +9,17 @@ data class ReportHistoryUiModel( val location: String, val process: ReportProcess, val category: ReportCategory, -) +) { + companion object { + fun fromDomain(reportItem: ReportItem): ReportHistoryUiModel { + return ReportHistoryUiModel( + id = "${reportItem.id}", + title = reportItem.title, + imageUrl = reportItem.imageUrl, + location = reportItem.address, + process = ReportProcess.fromDomain(reportItem.status), + category = ReportCategory.fromDomain(reportItem.category), + ) + } + } +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportProcess.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportProcess.kt index 29d5102a..f4456f59 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportProcess.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportProcess.kt @@ -1,5 +1,7 @@ package com.threegap.bitnagil.presentation.reporthistory.model +import com.threegap.bitnagil.domain.report.model.ReportStatus + enum class ReportProcess( val title: String, ) { @@ -7,4 +9,15 @@ enum class ReportProcess( Reported(title = "제보 완료"), Progress(title = "처리 중"), Complete(title = "처리 완료"), + ; + + companion object { + fun fromDomain(status: ReportStatus): ReportProcess { + return when (status) { + ReportStatus.Pending -> Reported + ReportStatus.InProgress -> Progress + ReportStatus.Completed -> Complete + } + } + } } From 59d500dcb07055ca6bfe3edee1e812912558f3c9 Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Sat, 22 Nov 2025 17:03:57 +0900 Subject: [PATCH 6/8] =?UTF-8?q?FIX:=20=EC=A0=9C=EB=B3=B4=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EB=82=A0=EC=A7=9C=20=ED=91=9C=EC=8B=9C=20=ED=98=95?= =?UTF-8?q?=EC=8B=9D=EC=9D=84=20"25.11.03=20=EC=9B=94"=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=EB=A1=9C=20=ED=91=9C=EC=8B=9C=EB=90=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/reporthistory/ReportHistoryScreen.kt | 3 ++- .../presentation/reporthistory/util/LocalDateUtils.kt | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/util/LocalDateUtils.kt diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryScreen.kt index b637c213..139c422d 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryScreen.kt @@ -39,6 +39,7 @@ import com.threegap.bitnagil.presentation.reporthistory.model.ReportHistoriesPer import com.threegap.bitnagil.presentation.reporthistory.model.ReportHistoryState import com.threegap.bitnagil.presentation.reporthistory.model.ReportHistoryUiModel import com.threegap.bitnagil.presentation.reporthistory.model.ReportProcess +import com.threegap.bitnagil.presentation.reporthistory.util.toPresentationFormat import org.orbitmvi.orbit.compose.collectAsState @Composable @@ -132,7 +133,7 @@ private fun ReportHistoryScreen( .background(color = BitnagilTheme.colors.coolGray99), ) { Text( - text = reportHistoriesPerDay.date.toString(), + text = reportHistoriesPerDay.date.toPresentationFormat(), modifier = Modifier.align(Alignment.CenterStart), style = BitnagilTheme.typography.body2SemiBold, ) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/util/LocalDateUtils.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/util/LocalDateUtils.kt new file mode 100644 index 00000000..9d9b54fd --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/util/LocalDateUtils.kt @@ -0,0 +1,10 @@ +package com.threegap.bitnagil.presentation.reporthistory.util + +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import java.util.Locale + +fun LocalDate.toPresentationFormat(): String { + val formatter = DateTimeFormatter.ofPattern("yy.MM.dd E", Locale.KOREAN) + return this.format(formatter) +} From 40b9d87c0b0e48b233da358e87695679fcf961b7 Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Sat, 22 Nov 2025 17:27:33 +0900 Subject: [PATCH 7/8] =?UTF-8?q?FIX:=20ReportCategoryBottomSheet=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EA=B5=AC=EB=B6=84=EC=84=A0=20=ED=91=9C=EC=8B=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=ED=95=98=EC=97=AC=20=EC=9E=98=EB=AA=BB?= =?UTF-8?q?=EB=90=9C=20enum=20=EC=B0=B8=EC=A1=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/template/ReportCategoryBottomSheet.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/component/template/ReportCategoryBottomSheet.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/component/template/ReportCategoryBottomSheet.kt index b1db2dff..1e1ce5d5 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/component/template/ReportCategoryBottomSheet.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/component/template/ReportCategoryBottomSheet.kt @@ -23,7 +23,6 @@ import com.threegap.bitnagil.designsystem.BitnagilTheme import com.threegap.bitnagil.designsystem.R import com.threegap.bitnagil.designsystem.component.atom.BitnagilIcon import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple -import com.threegap.bitnagil.domain.recommendroutine.model.RecommendLevel import com.threegap.bitnagil.presentation.reporthistory.model.ReportCategory import kotlinx.coroutines.launch @@ -64,7 +63,7 @@ fun ReportCategoryBottomSheet( }, ) - if (index < RecommendLevel.entries.size) { + if (index < ReportCategory.entries.lastIndex) { HorizontalDivider( color = BitnagilTheme.colors.coolGray97, ) From 18d6f61704bca156dc1268b19f1e4edf270159fd Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Sat, 22 Nov 2025 23:22:43 +0900 Subject: [PATCH 8/8] =?UTF-8?q?FIX:=20BitnagilChip=EC=9D=98=20count?= =?UTF-8?q?=EC=9D=B8=EC=9E=90=20=EC=A0=9C=EA=B1=B0,=20=EC=84=B8=EB=B6=80?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../designsystem/component/atom/BitnagilChip.kt | 12 +----------- .../reporthistory/ReportHistoryScreen.kt | 3 +-- .../reporthistory/ReportHistoryViewModel.kt | 2 +- .../reporthistory/model/ReportProcessWithCount.kt | 2 ++ 4 files changed, 5 insertions(+), 14 deletions(-) diff --git a/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilChip.kt b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilChip.kt index 57c3574f..4da43069 100644 --- a/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilChip.kt +++ b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilChip.kt @@ -22,10 +22,7 @@ fun BitnagilChip( isSelected: Boolean, onCategorySelected: () -> Unit, modifier: Modifier = Modifier, - count: Int? = null, ) { - val chipTitle = if (count == null) title else "$title $count" - Box( contentAlignment = Alignment.Center, modifier = modifier @@ -41,7 +38,7 @@ fun BitnagilChip( ), ) { Text( - text = chipTitle, + text = title, color = if (!isSelected) BitnagilTheme.colors.coolGray60 else BitnagilTheme.colors.white, style = if (!isSelected) BitnagilTheme.typography.caption1Medium else BitnagilTheme.typography.caption1SemiBold, ) @@ -59,12 +56,5 @@ private fun RecommendCategoryChipStatesPreview() { isSelected = true, onCategorySelected = {}, ) - - BitnagilChip( - title = "나가봐요", - isSelected = false, - onCategorySelected = {}, - count = 1, - ) } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryScreen.kt index 139c422d..a9280669 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryScreen.kt @@ -100,12 +100,11 @@ private fun ReportHistoryScreen( state.reportProcessWithCounts.forEach { reportProcessWithCount -> BitnagilChip( - title = reportProcessWithCount.process.title, + title = reportProcessWithCount.titleWithCount, isSelected = state.selectedReportProcess == reportProcessWithCount.process, onCategorySelected = { onClickReportProcessChip(reportProcessWithCount.process) }, - count = if (reportProcessWithCount.count == 0) null else reportProcessWithCount.count, ) } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryViewModel.kt index bc441846..28028bae 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/ReportHistoryViewModel.kt @@ -25,7 +25,7 @@ class ReportHistoryViewModel @Inject constructor( } private fun loadReportHistories() = intent { - getReportHistoriesUseCase.invoke().fold( + getReportHistoriesUseCase().fold( onSuccess = { reportHistoriesPerDate -> val reportHistoriesPerDays = reportHistoriesPerDate .map { reportHistoryPerDateMap -> diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportProcessWithCount.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportProcessWithCount.kt index 18ba2606..df6409b4 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportProcessWithCount.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reporthistory/model/ReportProcessWithCount.kt @@ -4,6 +4,8 @@ data class ReportProcessWithCount( val process: ReportProcess, val count: Int, ) { + val titleWithCount: String = if (count == 0) process.title else "${process.title} $count" + companion object { val Init = ReportProcessWithCount( process = ReportProcess.Total,