From 0cfd13e38222f23b07dcba05699652e3d5b70454 Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Sun, 23 Nov 2025 12:15:47 +0900 Subject: [PATCH 1/5] =?UTF-8?q?Feat:=20=EC=A0=9C=EB=B3=B4=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=EC=A1=B0=ED=9A=8C=20=ED=99=94=EB=A9=B4=20UI=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reportdetail/ReportDetailScreen.kt | 135 ++++++++++++++++++ .../component/atom/ReportProcessBadge.kt | 60 ++++++++ .../block/ReportDetailLabeledContent.kt | 42 ++++++ .../reportdetail/model/ReportCategory.kt | 31 ++++ .../reportdetail/model/ReportProcess.kt | 22 +++ .../model/mvi/ReportDetailSideEffect.kt | 3 + .../model/mvi/ReportDetailState.kt | 27 ++++ .../model/navarg/ReportDetailScreenArg.kt | 8 ++ .../reportdetail/util/LocalDateUtils.kt | 10 ++ 9 files changed, 338 insertions(+) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/ReportDetailScreen.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/component/atom/ReportProcessBadge.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/component/block/ReportDetailLabeledContent.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/model/ReportCategory.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/model/ReportProcess.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/model/mvi/ReportDetailSideEffect.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/model/mvi/ReportDetailState.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/model/navarg/ReportDetailScreenArg.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/util/LocalDateUtils.kt diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/ReportDetailScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/ReportDetailScreen.kt new file mode 100644 index 00000000..64ffeaa7 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/ReportDetailScreen.kt @@ -0,0 +1,135 @@ +package com.threegap.bitnagil.presentation.reportdetail + +import androidx.compose.foundation.background +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.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.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.component.block.BitnagilTopBar +import com.threegap.bitnagil.presentation.reportdetail.component.atom.ReportProcessBadge +import com.threegap.bitnagil.presentation.reportdetail.component.block.ReportDetailLabeledContent +import com.threegap.bitnagil.presentation.reportdetail.model.mvi.ReportDetailState +import com.threegap.bitnagil.presentation.reportdetail.util.toPresentationFormatInReportDetail + +@Composable +fun ReportDetailScreenContainer( + +) { + +} + +@Composable +private fun ReportDetailScreen( + modifier: Modifier = Modifier, + onClickPreviousButton: () -> Unit, + state: ReportDetailState, +) { + val verticalScrollState = rememberScrollState() + + Column( + modifier = modifier + .fillMaxSize() + .verticalScroll(verticalScrollState) + .background(color = BitnagilTheme.colors.white) + .statusBarsPadding() + .padding(horizontal = 20.dp), + ) { + BitnagilTopBar( + title = "제보하기", + showBackButton = true, + onBackClick = onClickPreviousButton, + ) + + Spacer(modifier = Modifier.height(20.dp)) + + ReportProcessBadge(reportProcess = state.reportProcess) + + Spacer(modifier = Modifier.height(6.dp)) + + Text( + text = state.date.toPresentationFormatInReportDetail(), + style = BitnagilTheme.typography.subtitle1SemiBold, + ) + + Spacer(modifier = Modifier.height(14.dp)) + + Row { + state.imageUrls.forEach { imageUrl -> + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(imageUrl) + .build(), + modifier = Modifier + .size(74.dp) + .clip(shape = RoundedCornerShape(9.dp)) + .background(color = BitnagilTheme.colors.black), + contentDescription = null, + ) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(28.dp), + ) { + ReportDetailLabeledContent( + label = "제목", + content = state.reportTitle, + ) + + ReportDetailLabeledContent( + label = "카테고리", + content = state.reportCategory.title, + ) + + ReportDetailLabeledContent( + label = "상세 제목 내용", + content = state.reportContent, + ) + + ReportDetailLabeledContent( + label = "내 위치", + content = state.location, + ) + } + + Spacer(modifier = Modifier.height(36.dp)) + } +} + +@Composable +@Preview +private fun ReportDetailScreenPreview() { + BitnagilTheme { + ReportDetailScreen( + state = ReportDetailState.Init.copy( + reportContent = "Lorem ipsum dolor sit amet, " + + "consectetur adipiscing elit," + + " sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. " + + "Nisl tincidunt eget nullam non.", + ), + onClickPreviousButton = {}, + ) + } +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/component/atom/ReportProcessBadge.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/component/atom/ReportProcessBadge.kt new file mode 100644 index 00000000..9977e533 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/component/atom/ReportProcessBadge.kt @@ -0,0 +1,60 @@ +package com.threegap.bitnagil.presentation.reportdetail.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.reportdetail.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/reportdetail/component/block/ReportDetailLabeledContent.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/component/block/ReportDetailLabeledContent.kt new file mode 100644 index 00000000..e623068c --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/component/block/ReportDetailLabeledContent.kt @@ -0,0 +1,42 @@ +package com.threegap.bitnagil.presentation.reportdetail.component.block + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +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.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.threegap.bitnagil.designsystem.BitnagilTheme + +@Composable +fun ReportDetailLabeledContent( + modifier: Modifier = Modifier, + label: String, + content: String, +) { + Column( + modifier = modifier, + ) { + Text( + text = label, + style = BitnagilTheme.typography.body2SemiBold, + color = BitnagilTheme.colors.coolGray10, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + modifier = Modifier + .fillMaxWidth() + .background(color = BitnagilTheme.colors.coolGray99, shape = RoundedCornerShape(12.dp)) + .padding(vertical = 16.dp, horizontal = 20.dp), + text = content, + style = BitnagilTheme.typography.body2Medium, + ) + } +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/model/ReportCategory.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/model/ReportCategory.kt new file mode 100644 index 00000000..96a91f1d --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/model/ReportCategory.kt @@ -0,0 +1,31 @@ +package com.threegap.bitnagil.presentation.reportdetail.model +import com.threegap.bitnagil.domain.report.model.ReportCategory as DomainReportCategory + +enum class ReportCategory( + val title: String, +) { + TrafficFacilities( + title = "교통 시설", + ), + LightingFacilities( + title = "조명 시설", + ), + WaterFacilities( + title = "상하수도 시설", + ), + Amenities( + title = "편의 시설", + ), + ; + + companion object { + fun fromDomain(domainReportCategory: com.threegap.bitnagil.domain.report.model.ReportCategory): ReportCategory { + return when (domainReportCategory) { + DomainReportCategory.TRANSPORTATION -> TrafficFacilities + DomainReportCategory.LIGHTING -> LightingFacilities + DomainReportCategory.WATERFACILITY -> WaterFacilities + DomainReportCategory.AMENITY -> Amenities + } + } + } +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/model/ReportProcess.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/model/ReportProcess.kt new file mode 100644 index 00000000..1fe8cecc --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/model/ReportProcess.kt @@ -0,0 +1,22 @@ +package com.threegap.bitnagil.presentation.reportdetail.model + +import com.threegap.bitnagil.domain.report.model.ReportStatus + +enum class ReportProcess( + val title: String, +) { + 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 + } + } + } +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/model/mvi/ReportDetailSideEffect.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/model/mvi/ReportDetailSideEffect.kt new file mode 100644 index 00000000..40bdd9b7 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/model/mvi/ReportDetailSideEffect.kt @@ -0,0 +1,3 @@ +package com.threegap.bitnagil.presentation.reportdetail.model.mvi + +interface ReportDetailSideEffect diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/model/mvi/ReportDetailState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/model/mvi/ReportDetailState.kt new file mode 100644 index 00000000..834a5a9f --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/model/mvi/ReportDetailState.kt @@ -0,0 +1,27 @@ +package com.threegap.bitnagil.presentation.reportdetail.model.mvi + +import com.threegap.bitnagil.presentation.reportdetail.model.ReportCategory +import com.threegap.bitnagil.presentation.reportdetail.model.ReportProcess +import java.time.LocalDate + +data class ReportDetailState( + val reportProcess: ReportProcess, + val reportTitle: String, + val reportContent: String, + val reportCategory: ReportCategory, + val imageUrls: List, + val location: String, + val date: LocalDate, +) { + companion object { + val Init = ReportDetailState( + reportProcess = ReportProcess.Reported, + reportTitle = "", + reportContent = "", + reportCategory = ReportCategory.TrafficFacilities, + imageUrls = emptyList(), + location = "", + date = LocalDate.now(), + ) + } +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/model/navarg/ReportDetailScreenArg.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/model/navarg/ReportDetailScreenArg.kt new file mode 100644 index 00000000..fa4839b2 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/model/navarg/ReportDetailScreenArg.kt @@ -0,0 +1,8 @@ +package com.threegap.bitnagil.presentation.reportdetail.model.navarg + +import kotlinx.serialization.Serializable + +@Serializable +data class ReportDetailScreenArg( + val reportId: Int, +) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/util/LocalDateUtils.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/util/LocalDateUtils.kt new file mode 100644 index 00000000..67f3c93f --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/util/LocalDateUtils.kt @@ -0,0 +1,10 @@ +package com.threegap.bitnagil.presentation.reportdetail.util + +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import java.util.Locale + +fun LocalDate.toPresentationFormatInReportDetail(): String { + val formatter = DateTimeFormatter.ofPattern("yy.MM.dd (E)", Locale.KOREAN) + return this.format(formatter) +} From 4c5ffd2e462c147b5726cd33468b1765ec17eb81 Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Sun, 23 Nov 2025 13:00:47 +0900 Subject: [PATCH 2/5] =?UTF-8?q?Feat:=20=EC=A0=9C=EB=B3=B4=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=EC=A1=B0=ED=9A=8C=20=ED=99=94=EB=A9=B4=20ViewModel,?= =?UTF-8?q?=20=EC=A0=9C=EB=B3=B4=20=EC=83=81=EC=84=B8=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?UseCase=20=EB=B0=8F=20Repository=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/threegap/bitnagil/MainNavHost.kt | 25 ++++++++- .../main/java/com/threegap/bitnagil/Route.kt | 3 ++ .../report/datasource/ReportDataSource.kt | 2 + .../datasourceImpl/ReportDataSourceImpl.kt | 5 ++ .../report/model/response/ReportDetailDto.kt | 38 +++++++++++++ .../repositoryImpl/ReportRepositoryImpl.kt | 6 +++ .../data/report/service/ReportService.kt | 7 +++ .../domain/report/model/ReportDetail.kt | 14 +++++ .../report/repository/ReportRepository.kt | 2 + .../domain/report/usecase/GetReportUseCase.kt | 13 +++++ .../reportdetail/ReportDetailScreen.kt | 12 ++++- .../reportdetail/ReportDetailViewModel.kt | 53 +++++++++++++++++++ .../model/navarg/ReportDetailScreenArg.kt | 2 +- 13 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 data/src/main/java/com/threegap/bitnagil/data/report/model/response/ReportDetailDto.kt create mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/report/model/ReportDetail.kt create mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/report/usecase/GetReportUseCase.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/ReportDetailViewModel.kt diff --git a/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt b/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt index 39fd583d..0df6644b 100644 --- a/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt +++ b/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt @@ -14,6 +14,9 @@ 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.reportdetail.ReportDetailScreenContainer +import com.threegap.bitnagil.presentation.reportdetail.ReportDetailViewModel +import com.threegap.bitnagil.presentation.reportdetail.model.navarg.ReportDetailScreenArg import com.threegap.bitnagil.presentation.reporthistory.ReportHistoryScreenContainer import com.threegap.bitnagil.presentation.routinelist.RoutineListScreenContainer import com.threegap.bitnagil.presentation.setting.SettingScreenContainer @@ -319,7 +322,27 @@ fun MainNavHost( navigator.navController.popBackStack() } }, - navigateToReportDetail = { + navigateToReportDetail = { reportId -> + navigator.navController.navigate(Route.ReportDetail(reportId = reportId)) { + launchSingleTop = true + } + }, + ) + } + + composable { navBackStackEntry -> + val arg = navBackStackEntry.toRoute() + val reportDetailScreenArg = ReportDetailScreenArg(reportId = arg.reportId) + val viewModel = hiltViewModel { factory -> + factory.create(reportDetailScreenArg) + } + + ReportDetailScreenContainer( + viewModel = viewModel, + navigateToBack = { + if (navigator.navController.previousBackStackEntry != null) { + navigator.navController.popBackStack() + } }, ) } diff --git a/app/src/main/java/com/threegap/bitnagil/Route.kt b/app/src/main/java/com/threegap/bitnagil/Route.kt index 5a9ad70e..7c056d30 100644 --- a/app/src/main/java/com/threegap/bitnagil/Route.kt +++ b/app/src/main/java/com/threegap/bitnagil/Route.kt @@ -54,4 +54,7 @@ sealed interface Route { @Serializable data object ReportHistory : Route + + @Serializable + data class ReportDetail(val reportId: String) : Route } 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 e672d191..82cdf53a 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,9 +1,11 @@ package com.threegap.bitnagil.data.report.datasource import com.threegap.bitnagil.data.report.model.request.ReportRequestDto +import com.threegap.bitnagil.data.report.model.response.ReportDetailDto import com.threegap.bitnagil.data.report.model.response.ReportHistoriesPerDateDto interface ReportDataSource { suspend fun submitReport(reportRequestDto: ReportRequestDto): Result suspend fun getReports(): Result + suspend fun getReport(reportId: String): 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 c980beef..0351d514 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.ReportDetailDto import com.threegap.bitnagil.data.report.model.response.ReportHistoriesPerDateDto import com.threegap.bitnagil.data.report.service.ReportService import javax.inject.Inject @@ -17,4 +18,8 @@ class ReportDataSourceImpl @Inject constructor( override suspend fun getReports(): Result { return safeApiCall { reportService.getReports() } } + + override suspend fun getReport(reportId: String): Result { + return safeApiCall { reportService.getReport(reportId = reportId) } + } } diff --git a/data/src/main/java/com/threegap/bitnagil/data/report/model/response/ReportDetailDto.kt b/data/src/main/java/com/threegap/bitnagil/data/report/model/response/ReportDetailDto.kt new file mode 100644 index 00000000..3c9cb097 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/report/model/response/ReportDetailDto.kt @@ -0,0 +1,38 @@ +package com.threegap.bitnagil.data.report.model.response + +import com.threegap.bitnagil.domain.report.model.ReportCategory +import com.threegap.bitnagil.domain.report.model.ReportDetail +import com.threegap.bitnagil.domain.report.model.ReportStatus +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import java.time.LocalDate + +@Serializable +class ReportDetailDto( + @SerialName("reportDate") + val reportDate: String, + @SerialName("reportStatus") + val reportStatus: String, + @SerialName("reportTitle") + val reportTitle: String, + @SerialName("reportContent") + val reportContent: String, + @SerialName("reportCategory") + val reportCategory: String, + @SerialName("reportLocation") + val reportLocation: String, + @SerialName("reportImageUrls") + val reportImageUrls: List, +) + +fun ReportDetailDto.toDomain(id: String?): ReportDetail = + ReportDetail( + id = id ?: "", + date = LocalDate.parse(this.reportDate), + status = ReportStatus.fromString(this.reportStatus), + title = this.reportTitle, + content = this.reportContent, + category = ReportCategory.fromString(this.reportCategory), + address = this.reportLocation, + imageUrls = this.reportImageUrls, + ) 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 dcfe9e36..07cc0f32 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,10 @@ 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.toDomain import com.threegap.bitnagil.data.report.model.response.toDomainMap import com.threegap.bitnagil.domain.report.model.Report +import com.threegap.bitnagil.domain.report.model.ReportDetail import com.threegap.bitnagil.domain.report.model.ReportItem import com.threegap.bitnagil.domain.report.repository.ReportRepository import java.time.LocalDate @@ -19,4 +21,8 @@ class ReportRepositoryImpl @Inject constructor( override suspend fun getReportHistories(): Result>> { return reportDataSource.getReports().map { it.toDomainMap() } } + + override suspend fun getReport(reportId: String): Result { + return reportDataSource.getReport(reportId = reportId).map { it.toDomain(id = reportId) } + } } 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 f6d25128..f89d71de 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,11 +1,13 @@ package com.threegap.bitnagil.data.report.service import com.threegap.bitnagil.data.report.model.request.ReportRequestDto +import com.threegap.bitnagil.data.report.model.response.ReportDetailDto 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 +import retrofit2.http.Path interface ReportService { @POST("/api/v2/reports") @@ -15,4 +17,9 @@ interface ReportService { @GET("/api/v2/reports") suspend fun getReports(): BaseResponse + + @GET("/api/v2/reports/{reportId}") + suspend fun getReport( + @Path("reportId") reportId: String, + ): BaseResponse } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/report/model/ReportDetail.kt b/domain/src/main/java/com/threegap/bitnagil/domain/report/model/ReportDetail.kt new file mode 100644 index 00000000..cc764d1d --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/report/model/ReportDetail.kt @@ -0,0 +1,14 @@ +package com.threegap.bitnagil.domain.report.model + +import java.time.LocalDate + +data class ReportDetail( + val id: String, + val title: String, + val content: String, + val category: ReportCategory, + val status: ReportStatus, + val imageUrls: List, + val address: String, + val date: LocalDate, +) 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 39a68033..512b5b8b 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,10 +1,12 @@ package com.threegap.bitnagil.domain.report.repository import com.threegap.bitnagil.domain.report.model.Report +import com.threegap.bitnagil.domain.report.model.ReportDetail import com.threegap.bitnagil.domain.report.model.ReportItem import java.time.LocalDate interface ReportRepository { suspend fun submitReport(report: Report): Result suspend fun getReportHistories(): Result>> + suspend fun getReport(reportId: String): Result } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/report/usecase/GetReportUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/report/usecase/GetReportUseCase.kt new file mode 100644 index 00000000..42b8c5da --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/report/usecase/GetReportUseCase.kt @@ -0,0 +1,13 @@ +package com.threegap.bitnagil.domain.report.usecase + +import com.threegap.bitnagil.domain.report.model.ReportDetail +import com.threegap.bitnagil.domain.report.repository.ReportRepository +import javax.inject.Inject + +class GetReportUseCase @Inject constructor( + private val reportRepository: ReportRepository, +) { + suspend operator fun invoke(id: String): Result { + return reportRepository.getReport(reportId = id) + } +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/ReportDetailScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/ReportDetailScreen.kt index 64ffeaa7..aec672bc 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/ReportDetailScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/ReportDetailScreen.kt @@ -16,8 +16,10 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -29,12 +31,19 @@ import com.threegap.bitnagil.presentation.reportdetail.component.atom.ReportProc import com.threegap.bitnagil.presentation.reportdetail.component.block.ReportDetailLabeledContent import com.threegap.bitnagil.presentation.reportdetail.model.mvi.ReportDetailState import com.threegap.bitnagil.presentation.reportdetail.util.toPresentationFormatInReportDetail +import org.orbitmvi.orbit.compose.collectAsState @Composable fun ReportDetailScreenContainer( - + viewModel: ReportDetailViewModel, + navigateToBack: () -> Unit, ) { + val state by viewModel.collectAsState() + ReportDetailScreen( + state = state, + onClickPreviousButton = navigateToBack, + ) } @Composable @@ -82,6 +91,7 @@ private fun ReportDetailScreen( .size(74.dp) .clip(shape = RoundedCornerShape(9.dp)) .background(color = BitnagilTheme.colors.black), + contentScale = ContentScale.Crop, contentDescription = null, ) } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/ReportDetailViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/ReportDetailViewModel.kt new file mode 100644 index 00000000..d28ddc4d --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/ReportDetailViewModel.kt @@ -0,0 +1,53 @@ +package com.threegap.bitnagil.presentation.reportdetail + +import androidx.lifecycle.ViewModel +import com.threegap.bitnagil.domain.report.usecase.GetReportUseCase +import com.threegap.bitnagil.presentation.reportdetail.model.ReportCategory +import com.threegap.bitnagil.presentation.reportdetail.model.ReportProcess +import com.threegap.bitnagil.presentation.reportdetail.model.mvi.ReportDetailSideEffect +import com.threegap.bitnagil.presentation.reportdetail.model.mvi.ReportDetailState +import com.threegap.bitnagil.presentation.reportdetail.model.navarg.ReportDetailScreenArg +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.lifecycle.HiltViewModel +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.viewmodel.container + +@HiltViewModel(assistedFactory = ReportDetailViewModel.Factory::class) +class ReportDetailViewModel @AssistedInject constructor( + private val getReportDetailUseCase: GetReportUseCase, + @Assisted private val reportDetailArg: ReportDetailScreenArg, +) : ContainerHost, ViewModel() { + override val container: Container = container(initialState = ReportDetailState.Init) + + @AssistedFactory + interface Factory { + fun create(reportDetailArg: ReportDetailScreenArg): ReportDetailViewModel + } + + init { + loadReportDetail(reportId = reportDetailArg.reportId) + } + + private fun loadReportDetail(reportId: String) = intent { + getReportDetailUseCase(id = reportId).fold( + onSuccess = { reportDetail -> + reduce { + state.copy( + reportProcess = ReportProcess.fromDomain(reportDetail.status), + reportTitle = reportDetail.title, + reportContent = reportDetail.content, + reportCategory = ReportCategory.fromDomain(reportDetail.category), + imageUrls = reportDetail.imageUrls, + location = reportDetail.address, + date = reportDetail.date, + ) + } + }, + onFailure = { + }, + ) + } +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/model/navarg/ReportDetailScreenArg.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/model/navarg/ReportDetailScreenArg.kt index fa4839b2..60a68ad1 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/model/navarg/ReportDetailScreenArg.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/model/navarg/ReportDetailScreenArg.kt @@ -4,5 +4,5 @@ import kotlinx.serialization.Serializable @Serializable data class ReportDetailScreenArg( - val reportId: Int, + val reportId: String, ) From a5971a25cdc92f589549966f54c4092e3d22d043 Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Sun, 23 Nov 2025 13:02:21 +0900 Subject: [PATCH 3/5] =?UTF-8?q?FIX:=20=EC=A0=9C=EB=B3=B4=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=ED=91=9C=EC=8B=9C=20=EB=B0=A9=EC=8B=9D?= =?UTF-8?q?=EC=9D=84=20ContentScale.Crop=EC=9C=BC=EB=A1=9C=20=ED=86=B5?= =?UTF-8?q?=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reporthistory/component/block/ReportHistoryItem.kt | 2 ++ 1 file changed, 2 insertions(+) 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 index 88841c41..57b940ac 100644 --- 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 @@ -17,6 +17,7 @@ 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.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -83,6 +84,7 @@ fun ReportHistoryItem( .size(74.dp) .clip(shape = RoundedCornerShape(9.dp)) .background(color = BitnagilTheme.colors.black), + contentScale = ContentScale.Crop, contentDescription = null, ) } From d51fc2af83da914a981a1d58737375d90bdbf45d Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Sun, 23 Nov 2025 13:17:23 +0900 Subject: [PATCH 4/5] =?UTF-8?q?FIX:=20=EC=97=AD=EC=A7=80=EC=98=A4=EC=BD=94?= =?UTF-8?q?=EB=94=A9=EC=97=90=EC=84=9C=20roadAddress=EA=B0=80=20null?= =?UTF-8?q?=EB=A1=9C=20=EB=93=A4=EC=96=B4=EC=98=AC=20=EB=95=8C=20=ED=8C=8C?= =?UTF-8?q?=EC=8B=B1=20=EC=97=90=EB=9F=AC=EA=B0=80=20=EB=B0=9C=EC=83=9D?= =?UTF-8?q?=ED=95=B4=20=EC=A3=BC=EC=86=8C=EA=B0=80=20=EB=B6=88=EB=9F=AC?= =?UTF-8?q?=EC=99=80=EC=A7=80=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/address/model/response/Coord2AddressResponse.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/src/main/java/com/threegap/bitnagil/data/address/model/response/Coord2AddressResponse.kt b/data/src/main/java/com/threegap/bitnagil/data/address/model/response/Coord2AddressResponse.kt index 6feac294..9769ce59 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/address/model/response/Coord2AddressResponse.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/address/model/response/Coord2AddressResponse.kt @@ -20,7 +20,7 @@ data class Meta( @Serializable data class Document( @SerialName("road_address") - val roadAddress: RoadAddress, + val roadAddress: RoadAddress?, @SerialName("address") val address: Address, ) From faaf271439584ee30f73fbbda55d06bc7572a8f6 Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Sun, 23 Nov 2025 13:23:35 +0900 Subject: [PATCH 5/5] =?UTF-8?q?FIX:=20=EC=A0=9C=EB=B3=B4=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=ED=99=94=EB=A9=B4=20=EC=83=81=EB=8B=A8=20toolbar?= =?UTF-8?q?=EC=97=90=20=EC=A0=81=EC=9A=A9=EB=90=9C=20horizontal=20?= =?UTF-8?q?=ED=8C=A8=EB=94=A9=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reportdetail/ReportDetailScreen.kt | 103 +++++++++--------- 1 file changed, 54 insertions(+), 49 deletions(-) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/ReportDetailScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/ReportDetailScreen.kt index aec672bc..6a5d0dbb 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/ReportDetailScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/reportdetail/ReportDetailScreen.kt @@ -57,10 +57,8 @@ private fun ReportDetailScreen( Column( modifier = modifier .fillMaxSize() - .verticalScroll(verticalScrollState) .background(color = BitnagilTheme.colors.white) - .statusBarsPadding() - .padding(horizontal = 20.dp), + .statusBarsPadding(), ) { BitnagilTopBar( title = "제보하기", @@ -68,63 +66,70 @@ private fun ReportDetailScreen( onBackClick = onClickPreviousButton, ) - Spacer(modifier = Modifier.height(20.dp)) + Column( + modifier = Modifier + .weight(1f) + .verticalScroll(verticalScrollState) + .padding(horizontal = 20.dp), + ) { + Spacer(modifier = Modifier.height(20.dp)) - ReportProcessBadge(reportProcess = state.reportProcess) + ReportProcessBadge(reportProcess = state.reportProcess) - Spacer(modifier = Modifier.height(6.dp)) + Spacer(modifier = Modifier.height(6.dp)) - Text( - text = state.date.toPresentationFormatInReportDetail(), - style = BitnagilTheme.typography.subtitle1SemiBold, - ) + Text( + text = state.date.toPresentationFormatInReportDetail(), + style = BitnagilTheme.typography.subtitle1SemiBold, + ) - Spacer(modifier = Modifier.height(14.dp)) - - Row { - state.imageUrls.forEach { imageUrl -> - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(imageUrl) - .build(), - modifier = Modifier - .size(74.dp) - .clip(shape = RoundedCornerShape(9.dp)) - .background(color = BitnagilTheme.colors.black), - contentScale = ContentScale.Crop, - contentDescription = null, - ) + Spacer(modifier = Modifier.height(14.dp)) + + Row { + state.imageUrls.forEach { imageUrl -> + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(imageUrl) + .build(), + modifier = Modifier + .size(74.dp) + .clip(shape = RoundedCornerShape(9.dp)) + .background(color = BitnagilTheme.colors.black), + contentScale = ContentScale.Crop, + contentDescription = null, + ) + } } - } - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(24.dp)) - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(28.dp), - ) { - ReportDetailLabeledContent( - label = "제목", - content = state.reportTitle, - ) + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(28.dp), + ) { + ReportDetailLabeledContent( + label = "제목", + content = state.reportTitle, + ) - ReportDetailLabeledContent( - label = "카테고리", - content = state.reportCategory.title, - ) + ReportDetailLabeledContent( + label = "카테고리", + content = state.reportCategory.title, + ) - ReportDetailLabeledContent( - label = "상세 제목 내용", - content = state.reportContent, - ) + ReportDetailLabeledContent( + label = "상세 제목 내용", + content = state.reportContent, + ) - ReportDetailLabeledContent( - label = "내 위치", - content = state.location, - ) - } + ReportDetailLabeledContent( + label = "내 위치", + content = state.location, + ) + } - Spacer(modifier = Modifier.height(36.dp)) + Spacer(modifier = Modifier.height(36.dp)) + } } }