From 55057dc3c4b8b93e987d7ae5e967297565f6cf8c Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Sat, 15 Nov 2025 23:53:20 +0900 Subject: [PATCH 01/44] =?UTF-8?q?add:=20=EC=95=84=EC=9D=B4=EC=BD=98=20?= =?UTF-8?q?=EC=97=90=EC=85=8B=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/res/drawable/ic_btn_close_sm.xml | 19 +++++++ .../src/main/res/drawable/ic_camera.xml | 13 +++++ .../src/main/res/drawable/ic_camera_gray.xml | 23 ++++++++ .../src/main/res/drawable/ic_car.xml | 12 ++++ .../src/main/res/drawable/ic_complaint.xml | 38 +++++++++++++ .../src/main/res/drawable/ic_hammer.xml | 18 ++++++ .../src/main/res/drawable/ic_light.xml | 29 ++++++++++ .../src/main/res/drawable/ic_location.xml | 38 +++++++++++++ .../src/main/res/drawable/ic_outside_gray.xml | 33 +++++++++++ .../src/main/res/drawable/ic_report.xml | 56 ------------------- .../src/main/res/drawable/ic_water.xml | 15 +++++ .../res/drawable/icon_down_arrow_black.xml | 16 ++++++ 12 files changed, 254 insertions(+), 56 deletions(-) create mode 100644 core/designsystem/src/main/res/drawable/ic_btn_close_sm.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_camera.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_camera_gray.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_car.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_complaint.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_hammer.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_light.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_location.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_outside_gray.xml delete mode 100644 core/designsystem/src/main/res/drawable/ic_report.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_water.xml create mode 100644 core/designsystem/src/main/res/drawable/icon_down_arrow_black.xml diff --git a/core/designsystem/src/main/res/drawable/ic_btn_close_sm.xml b/core/designsystem/src/main/res/drawable/ic_btn_close_sm.xml new file mode 100644 index 00000000..658dc4ee --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_btn_close_sm.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_camera.xml b/core/designsystem/src/main/res/drawable/ic_camera.xml new file mode 100644 index 00000000..a23169f7 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_camera.xml @@ -0,0 +1,13 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_camera_gray.xml b/core/designsystem/src/main/res/drawable/ic_camera_gray.xml new file mode 100644 index 00000000..1d968a9e --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_camera_gray.xml @@ -0,0 +1,23 @@ + + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_car.xml b/core/designsystem/src/main/res/drawable/ic_car.xml new file mode 100644 index 00000000..2c11c1a7 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_car.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_complaint.xml b/core/designsystem/src/main/res/drawable/ic_complaint.xml new file mode 100644 index 00000000..0a3c7adb --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_complaint.xml @@ -0,0 +1,38 @@ + + + + + + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_hammer.xml b/core/designsystem/src/main/res/drawable/ic_hammer.xml new file mode 100644 index 00000000..1120d1b2 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_hammer.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_light.xml b/core/designsystem/src/main/res/drawable/ic_light.xml new file mode 100644 index 00000000..17127aeb --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_light.xml @@ -0,0 +1,29 @@ + + + + + + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_location.xml b/core/designsystem/src/main/res/drawable/ic_location.xml new file mode 100644 index 00000000..71f8bf6a --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_location.xml @@ -0,0 +1,38 @@ + + + + + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_outside_gray.xml b/core/designsystem/src/main/res/drawable/ic_outside_gray.xml new file mode 100644 index 00000000..2fd18396 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_outside_gray.xml @@ -0,0 +1,33 @@ + + + + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_report.xml b/core/designsystem/src/main/res/drawable/ic_report.xml deleted file mode 100644 index aa86b3dc..00000000 --- a/core/designsystem/src/main/res/drawable/ic_report.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - diff --git a/core/designsystem/src/main/res/drawable/ic_water.xml b/core/designsystem/src/main/res/drawable/ic_water.xml new file mode 100644 index 00000000..5e6f2439 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_water.xml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/core/designsystem/src/main/res/drawable/icon_down_arrow_black.xml b/core/designsystem/src/main/res/drawable/icon_down_arrow_black.xml new file mode 100644 index 00000000..7d076be8 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/icon_down_arrow_black.xml @@ -0,0 +1,16 @@ + + + + + + From 6508a7ae2988154e1716c5f3669472f7e0cabafb Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Sun, 16 Nov 2025 23:33:24 +0900 Subject: [PATCH 02/44] =?UTF-8?q?Feat:=20ReportField=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../report/component/ReportField.kt | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/ReportField.kt diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/ReportField.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/ReportField.kt new file mode 100644 index 00000000..d7321e5b --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/ReportField.kt @@ -0,0 +1,47 @@ +package com.threegap.bitnagil.presentation.report.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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 + +@Composable +fun ReportField( + title: String, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = title, + color = BitnagilTheme.colors.coolGray10, + style = BitnagilTheme.typography.body2SemiBold, + ) + Text( + text = "*", + color = BitnagilTheme.colors.error10, + style = BitnagilTheme.typography.body1SemiBold, + ) + } + content() + } +} + +@Preview +@Composable +private fun Preview() { + ReportField(title = "제보 카테고리") {} +} From 904114b1902597297d4f48209c325c171ab53bfa Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Sun, 16 Nov 2025 23:33:30 +0900 Subject: [PATCH 03/44] =?UTF-8?q?Feat:=20BitnagilTextField=20=EB=94=94?= =?UTF-8?q?=EC=9E=90=EC=9D=B8=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=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 --- .../component/atom/BitnagilTextField.kt | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilTextField.kt diff --git a/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilTextField.kt b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilTextField.kt new file mode 100644 index 00000000..4417c1ac --- /dev/null +++ b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilTextField.kt @@ -0,0 +1,89 @@ +package com.threegap.bitnagil.designsystem.component.atom + +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.threegap.bitnagil.designsystem.BitnagilTheme + +@Composable +fun BitnagilTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + textStyle: TextStyle = BitnagilTheme.typography.body2Medium, + textColor: Color = BitnagilTheme.colors.coolGray10, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + singleLine: Boolean = false, + maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, + minLines: Int = 1, + visualTransformation: VisualTransformation = VisualTransformation.None, + onTextLayout: (TextLayoutResult) -> Unit = {}, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + placeholder: @Composable (() -> Unit)? = null, +) { + BasicTextField( + value = value, + onValueChange = onValueChange, + modifier = modifier + .fillMaxWidth() + .background( + color = BitnagilTheme.colors.coolGray99, + shape = RoundedCornerShape(12.dp), + ) + .padding(vertical = 16.dp, horizontal = 20.dp), + textStyle = textStyle.copy(color = textColor), + enabled = enabled, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + singleLine = singleLine, + maxLines = maxLines, + minLines = minLines, + visualTransformation = visualTransformation, + onTextLayout = onTextLayout, + interactionSource = interactionSource, + ) { innerTextField -> + Box( + contentAlignment = Alignment.TopStart, + ) { + if (value.isEmpty() && placeholder != null) { + placeholder() + } + innerTextField() + } + } +} + +@Preview +@Composable +private fun Preview() { + BitnagilTextField( + value = "", + onValueChange = {}, + placeholder = { + Text( + text = "기타사항(직접 입력)", + color = BitnagilTheme.colors.coolGray80, + style = BitnagilTheme.typography.subtitle1Medium, + ) + }, + ) +} From d7b354db979a0ddfca810be3048b9c3760257f4a Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Sun, 16 Nov 2025 23:34:23 +0900 Subject: [PATCH 04/44] =?UTF-8?q?Chore:=20TextButton=20=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B8=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 펙토리 메서드에 파라미터를 추가하여 커스텀 가능하도록 변경 --- .../component/atom/BitnagilTextButton.kt | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilTextButton.kt b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilTextButton.kt index b6898558..9afed8d6 100644 --- a/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilTextButton.kt +++ b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilTextButton.kt @@ -95,13 +95,20 @@ data class BitnagilTextButtonColor( ) { companion object { @Composable - fun default(): BitnagilTextButtonColor = BitnagilTextButtonColor( - defaultBackgroundColor = BitnagilTheme.colors.coolGray10, - pressedBackgroundColor = BitnagilTheme.colors.coolGray5, - disabledBackgroundColor = BitnagilTheme.colors.coolGray96, - defaultTextColor = BitnagilTheme.colors.white, - pressedTextColor = BitnagilTheme.colors.coolGray20, - disabledTextColor = BitnagilTheme.colors.white, + fun default( + defaultBackgroundColor: Color = BitnagilTheme.colors.coolGray10, + pressedBackgroundColor: Color = BitnagilTheme.colors.coolGray5, + disabledBackgroundColor: Color = BitnagilTheme.colors.coolGray96, + defaultTextColor: Color = BitnagilTheme.colors.white, + pressedTextColor: Color = BitnagilTheme.colors.coolGray20, + disabledTextColor: Color = BitnagilTheme.colors.white, + ): BitnagilTextButtonColor = BitnagilTextButtonColor( + defaultBackgroundColor = defaultBackgroundColor, + pressedBackgroundColor = pressedBackgroundColor, + disabledBackgroundColor = disabledBackgroundColor, + defaultTextColor = defaultTextColor, + pressedTextColor = pressedTextColor, + disabledTextColor = disabledTextColor, ) @Composable From 7f4fb3a45667f5b608758b8c180b85436bdf4da9 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Sun, 16 Nov 2025 23:35:06 +0900 Subject: [PATCH 05/44] =?UTF-8?q?Feat:=20ReportCategory=20=EC=9D=B4?= =?UTF-8?q?=EB=84=98=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../report/model/ReportCategory.kt | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportCategory.kt diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportCategory.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportCategory.kt new file mode 100644 index 00000000..1e282945 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportCategory.kt @@ -0,0 +1,32 @@ +package com.threegap.bitnagil.presentation.report.model + +import androidx.annotation.DrawableRes +import com.threegap.bitnagil.designsystem.R + +enum class ReportCategory( + @DrawableRes val icon: Int, + val title: String, + val description: String, +) { + TRANSPORTATION( + icon = R.drawable.ic_car, + title = "교통 시설", + description = "신호등 고장, 표지판 파손, 횡단보도 등", + ), + LIGHTING( + icon = R.drawable.ic_light, + title = "조명 시설", + description = "가로등, 보안등 파손 등", + ), + WATERFACILITY( + icon = R.drawable.ic_water, + title = "상하수도 시설", + description = "맨홀 뚜껑 손상 등", + ), + AMENITY( + icon = R.drawable.ic_hammer, + title = "편의 시설", + description = "벤치 파손, 휴지통 넘침 등", + ), + ; +} From f314346536c8dcf08dc80a12581ab8e0a21d1b21 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Sun, 16 Nov 2025 23:37:05 +0900 Subject: [PATCH 06/44] =?UTF-8?q?Feat:=20ReportCategorySelector=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/ReportCategorySelector.kt | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/ReportCategorySelector.kt diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/ReportCategorySelector.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/ReportCategorySelector.kt new file mode 100644 index 00000000..daeaa1cb --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/ReportCategorySelector.kt @@ -0,0 +1,60 @@ +package com.threegap.bitnagil.presentation.report.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +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.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 + +@Composable +fun ReportCategorySelector( + title: String?, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .background( + color = BitnagilTheme.colors.coolGray99, + shape = RoundedCornerShape(12.dp), + ) + .fillMaxWidth() + .height(56.dp) + .clickableWithoutRipple { onClick() } + .padding(vertical = 14.dp, horizontal = 20.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = title ?: "카테고리 선택", + color = if (title == null) BitnagilTheme.colors.coolGray80 else BitnagilTheme.colors.coolGray10, + style = BitnagilTheme.typography.body2Medium, + ) + + BitnagilIcon( + id = R.drawable.icon_down_arrow_black, + tint = BitnagilTheme.colors.coolGray10, + ) + } +} + +@Preview +@Composable +private fun Preview() { + ReportCategorySelector( + title = "카테고리 선택", + onClick = {}, + ) +} From 8740439aac248fa617acf8354a6c646f5e537381 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Sun, 16 Nov 2025 23:44:32 +0900 Subject: [PATCH 07/44] =?UTF-8?q?Feat:=20CurrentLocationInput=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../report/component/CurrentLocationInput.kt | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/CurrentLocationInput.kt diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/CurrentLocationInput.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/CurrentLocationInput.kt new file mode 100644 index 00000000..4018576f --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/CurrentLocationInput.kt @@ -0,0 +1,77 @@ +package com.threegap.bitnagil.presentation.report.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +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.R +import com.threegap.bitnagil.designsystem.component.atom.BitnagilIcon +import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple + +@Composable +fun CurrentLocationInput( + currentLocation: String?, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier.height(52.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text( + text = currentLocation ?: "현재 위치 검색", + color = if (currentLocation == null) BitnagilTheme.colors.coolGray80 else BitnagilTheme.colors.coolGray10, + style = BitnagilTheme.typography.body2Medium, + modifier = Modifier + .background( + color = BitnagilTheme.colors.coolGray99, + shape = RoundedCornerShape(14.dp), + ) + .fillMaxHeight() + .padding(vertical = 16.dp, horizontal = 20.dp) + .weight(1f), + ) + + Box( + modifier = modifier + .background( + color = BitnagilTheme.colors.orange500, + shape = RoundedCornerShape(12.dp), + ) + .fillMaxHeight() + .padding(14.dp) + .clickableWithoutRipple( + onClick = { onClick() }, + ), + contentAlignment = Alignment.Center, + ) { + BitnagilIcon( + id = R.drawable.ic_location, + tint = null, + modifier = Modifier.size(24.dp), + ) + } + } +} + +@Preview +@Composable +private fun Preview() { + CurrentLocationInput( + currentLocation = null, + onClick = {}, + ) +} From 821116368b7f5c65c74651f58ddabee28925eb79 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Sun, 16 Nov 2025 23:46:07 +0900 Subject: [PATCH 08/44] =?UTF-8?q?Feat:=20ReportCategoryBottomSheet=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/ReportCategoryBottomSheet.kt | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/ReportCategoryBottomSheet.kt diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/ReportCategoryBottomSheet.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/ReportCategoryBottomSheet.kt new file mode 100644 index 00000000..55ca576a --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/ReportCategoryBottomSheet.kt @@ -0,0 +1,143 @@ +package com.threegap.bitnagil.presentation.report.component + +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.report.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.icon, + 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.WATERFACILITY, + onDismiss = {}, + onSelected = {}, + ) +} + +@Preview(showBackground = true) +@Composable +private fun ReportCategoryItemPreview() { + ReportCategoryItem( + title = "교통 시설", + description = "어쩌구", + icon = R.drawable.ic_car, + isSelected = true, + onClick = {}, + ) +} From b5cbf113ff823a92b584b2f009133004af3a6737 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Mon, 17 Nov 2025 22:50:28 +0900 Subject: [PATCH 09/44] =?UTF-8?q?Feat:=20ImageSourceBottomSheet=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/ImageSourceBottomSheet.kt | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/ImageSourceBottomSheet.kt diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/ImageSourceBottomSheet.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/ImageSourceBottomSheet.kt new file mode 100644 index 00000000..52229094 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/ImageSourceBottomSheet.kt @@ -0,0 +1,120 @@ +package com.threegap.bitnagil.presentation.report.component + +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +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.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 kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ImageSourceBottomSheet( + onCameraClick: () -> Unit, + onAlbumClick: () -> Unit, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, +) { + val sheetState = rememberModalBottomSheetState() + val coroutineScope = rememberCoroutineScope() + + fun hideSheetAndExecute(action: () -> Unit) { + coroutineScope + .launch { sheetState.hide() } + .invokeOnCompletion { + if (!sheetState.isVisible) { + onDismiss() + action() + } + } + } + + 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 = 16.dp), + ) { + ImageSourceOptionItem( + iconId = R.drawable.ic_camera_gray, + description = "카메라로 촬영하기", + modifier = Modifier.clickableWithoutRipple( + onClick = { hideSheetAndExecute(onCameraClick) }, + ), + ) + + HorizontalDivider( + color = BitnagilTheme.colors.coolGray97, + modifier = Modifier.padding(vertical = 8.dp), + ) + + ImageSourceOptionItem( + iconId = R.drawable.ic_outside_gray, + description = "앨범에서 선택하기", + modifier = Modifier.clickableWithoutRipple( + onClick = { hideSheetAndExecute(onAlbumClick) }, + ), + ) + } + } +} + +@Composable +private fun ImageSourceOptionItem( + @DrawableRes iconId: Int, + description: String, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(vertical = 14.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + BitnagilIcon(id = iconId, tint = null) + + Text( + text = description, + color = BitnagilTheme.colors.coolGray10, + style = BitnagilTheme.typography.body1Medium, + ) + } +} + +@Preview +@Composable +private fun ImageSourceBottomSheetPreview() { + ImageSourceBottomSheet( + onCameraClick = {}, + onAlbumClick = {}, + onDismiss = {}, + ) +} + +@Preview(showBackground = true) +@Composable +private fun Preview() { + ImageSourceOptionItem( + iconId = R.drawable.ic_camera_gray, + description = "카메라로 촬영하기", + ) +} From 79033e36d3e407778d7f7001e916d0f2e78cf815 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Mon, 17 Nov 2025 23:15:15 +0900 Subject: [PATCH 10/44] =?UTF-8?q?Feat:=20AddPhotoButton=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../report/component/AddPhotoButton.kt | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/AddPhotoButton.kt diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/AddPhotoButton.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/AddPhotoButton.kt new file mode 100644 index 00000000..7636f45e --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/AddPhotoButton.kt @@ -0,0 +1,72 @@ +package com.threegap.bitnagil.presentation.report.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.size +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.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +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 + +@Composable +fun AddPhotoButton( + onClick: () -> Unit, + imageCount: Int, + maxImageCount: Int, + modifier: Modifier = Modifier, +) { + val enabled = imageCount < maxImageCount + val imageCountColor = + if (imageCount == 0) BitnagilTheme.colors.coolGray70 else BitnagilTheme.colors.black + + Column( + modifier = modifier + .background( + color = BitnagilTheme.colors.coolGray99, + shape = RoundedCornerShape(8.dp), + ) + .size(64.dp) + .clickableWithoutRipple( + onClick = onClick, + enabled = enabled, + ), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + BitnagilIcon( + id = R.drawable.ic_camera, + tint = null, + ) + Text( + text = buildAnnotatedString { + withStyle(style = SpanStyle(color = imageCountColor)) { + append("$imageCount") + } + append("/$maxImageCount") + }, + color = BitnagilTheme.colors.coolGray70, + style = BitnagilTheme.typography.caption1Medium, + ) + } +} + +@Preview +@Composable +private fun CameraButtonPreview() { + AddPhotoButton( + onClick = {}, + imageCount = 0, + maxImageCount = 3, + ) +} From fe41ed236ecd3cc47afc092e52b7a8c537389598 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Mon, 17 Nov 2025 23:25:22 +0900 Subject: [PATCH 11/44] =?UTF-8?q?Feat:=20PhotoItem=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../report/component/PhotoItem.kt | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/PhotoItem.kt diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/PhotoItem.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/PhotoItem.kt new file mode 100644 index 00000000..44b0f581 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/PhotoItem.kt @@ -0,0 +1,61 @@ +package com.threegap.bitnagil.presentation.report.component + +import android.net.Uri +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.net.toUri +import coil3.compose.AsyncImage +import com.threegap.bitnagil.designsystem.BitnagilTheme +import com.threegap.bitnagil.designsystem.R +import com.threegap.bitnagil.designsystem.component.atom.BitnagilIconButton + +@Composable +fun PhotoItem( + uri: Uri, + onRemove: (Uri) -> Unit, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.size(64.dp), + ) { + AsyncImage( + model = uri, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxSize() + .clip(RoundedCornerShape(8.dp)), + ) + + BitnagilIconButton( + id = R.drawable.ic_btn_close_sm, + tint = null, + onClick = { onRemove(uri) }, + modifier = Modifier + .size(24.dp) + .align(Alignment.TopEnd) + .offset(x = 10.dp, y = (-10).dp), + ) + } +} + +@Preview +@Composable +private fun Preview() { + PhotoItem( + uri = "".toUri(), + modifier = Modifier.background(color = BitnagilTheme.colors.orange50), + onRemove = {}, + ) +} From be45cbcf8250abe1db1ba1a5a153337593c5a222 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Mon, 17 Nov 2025 23:40:01 +0900 Subject: [PATCH 12/44] =?UTF-8?q?Refactor:=20LogoutConfirmDialog=20=20->?= =?UTF-8?q?=20BitnagilAlertDialog=20=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/block/BitnagilAlertDialog.kt | 26 ++++++++++++------- .../presentation/setting/SettingScreen.kt | 8 ++++-- 2 files changed, 23 insertions(+), 11 deletions(-) rename presentation/src/main/java/com/threegap/bitnagil/presentation/setting/component/block/LogoutConfirmDialog.kt => core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/block/BitnagilAlertDialog.kt (80%) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/component/block/LogoutConfirmDialog.kt b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/block/BitnagilAlertDialog.kt similarity index 80% rename from presentation/src/main/java/com/threegap/bitnagil/presentation/setting/component/block/LogoutConfirmDialog.kt rename to core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/block/BitnagilAlertDialog.kt index efe64301..3bc50c56 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/component/block/LogoutConfirmDialog.kt +++ b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/block/BitnagilAlertDialog.kt @@ -1,4 +1,4 @@ -package com.threegap.bitnagil.presentation.setting.component.block +package com.threegap.bitnagil.designsystem.component.block import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -22,10 +22,14 @@ import com.threegap.bitnagil.designsystem.component.atom.BitnagilTextButtonColor @OptIn(ExperimentalMaterial3Api::class) @Composable -fun LogoutConfirmDialog( +fun BitnagilAlertDialog( + title: String, + description: String, + dismissButtonText: String, + confirmButtonText: String, onDismiss: () -> Unit, onConfirm: () -> Unit, - modifier: Modifier = Modifier, + modifier: Modifier = Modifier ) { BasicAlertDialog( modifier = modifier, @@ -45,7 +49,7 @@ fun LogoutConfirmDialog( verticalArrangement = Arrangement.Center, ) { Text( - text = "로그아웃할까요?", + text = title, color = BitnagilTheme.colors.coolGray10, style = BitnagilTheme.typography.subtitle1SemiBold, ) @@ -53,7 +57,7 @@ fun LogoutConfirmDialog( Spacer(modifier = Modifier.height(6.dp)) Text( - text = "이 기기에서 계정이 로그아웃되고, 다시\n로그인해야 서비스를 계속 이용할 수 있어요.", + text = description, color = BitnagilTheme.colors.coolGray40, style = BitnagilTheme.typography.body2Medium, ) @@ -65,7 +69,7 @@ fun LogoutConfirmDialog( horizontalArrangement = Arrangement.spacedBy(12.dp), ) { BitnagilTextButton( - text = "취소", + text = dismissButtonText, onClick = onDismiss, colors = BitnagilTextButtonColor.cancel(), textStyle = BitnagilTheme.typography.body2Medium, @@ -73,7 +77,7 @@ fun LogoutConfirmDialog( ) BitnagilTextButton( - text = "로그아웃", + text = confirmButtonText, onClick = onConfirm, textStyle = BitnagilTheme.typography.body2Medium, modifier = Modifier.weight(1f), @@ -85,8 +89,12 @@ fun LogoutConfirmDialog( @Preview @Composable -private fun LogoutConfirmDialogPreview() { - LogoutConfirmDialog( +private fun Preview() { + BitnagilAlertDialog( + title = "로그아웃 할까요?", + description = "이 기기에서 계정이 로그아웃되고, 다시\n로그인해야 서비스를 계속 이용할 수 있어요.", + dismissButtonText = "취소", + confirmButtonText = "로그아웃", onDismiss = {}, onConfirm = {}, ) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/SettingScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/SettingScreen.kt index 6794af98..342b36f1 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/SettingScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/SettingScreen.kt @@ -24,12 +24,12 @@ 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.component.block.BitnagilAlertDialog import com.threegap.bitnagil.designsystem.component.block.BitnagilOptionButton import com.threegap.bitnagil.designsystem.component.block.BitnagilTopBar import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple import com.threegap.bitnagil.presentation.common.flow.collectAsEffect import com.threegap.bitnagil.presentation.setting.component.atom.settingtitle.SettingTitle -import com.threegap.bitnagil.presentation.setting.component.block.LogoutConfirmDialog import com.threegap.bitnagil.presentation.setting.model.mvi.SettingIntent import com.threegap.bitnagil.presentation.setting.model.mvi.SettingSideEffect import com.threegap.bitnagil.presentation.setting.model.mvi.SettingState @@ -53,7 +53,11 @@ fun SettingScreenContainer( } if (state.logoutConfirmDialogVisible) { - LogoutConfirmDialog( + BitnagilAlertDialog( + title = "로그아웃 할까요?", + description = "이 기기에서 계정이 로그아웃되고, 다시\n로그인해야 서비스를 계속 이용할 수 있어요.", + dismissButtonText = "취소", + confirmButtonText = "로그아웃", onDismiss = viewModel::hideConfirmDialog, onConfirm = viewModel::logout, ) From 3852d66d7770049a941410c0df03f581fdb52ba7 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Tue, 18 Nov 2025 00:42:53 +0900 Subject: [PATCH 13/44] =?UTF-8?q?Feat:=20=EA=B6=8C=ED=95=9C=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20=EB=A1=9C=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 --- .../component/block/BitnagilAlertDialog.kt | 2 +- gradle/libs.versions.toml | 3 + presentation/build.gradle.kts | 1 + .../common/premission/PermissionHandler.kt | 14 +++ .../premission/RememberPermissionHandler.kt | 92 +++++++++++++++++++ 5 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/common/premission/PermissionHandler.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/common/premission/RememberPermissionHandler.kt diff --git a/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/block/BitnagilAlertDialog.kt b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/block/BitnagilAlertDialog.kt index 3bc50c56..af64bafe 100644 --- a/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/block/BitnagilAlertDialog.kt +++ b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/block/BitnagilAlertDialog.kt @@ -29,7 +29,7 @@ fun BitnagilAlertDialog( confirmButtonText: String, onDismiss: () -> Unit, onConfirm: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { BasicAlertDialog( modifier = modifier, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c094ec84..79edb354 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -55,6 +55,7 @@ javax = "1" kakaoLogin = "2.21.4" lottie-compose = "6.6.0" coil = "3.3.0" +accompanist = "0.37.3" [libraries] ## Android Gradle Plugin @@ -127,6 +128,8 @@ material = { group = "com.google.android.material", name = "material", version.r kakao-v2-user = { group = "com.kakao.sdk", name = "v2-user", version.ref = "kakaoLogin" } lottie-compose = { group = "com.airbnb.android", name = "lottie-compose", version.ref = "lottie-compose" } +accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" } + [bundles] androidx-core = [ "androidx-core-ktx", diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts index 6f8d0b1b..2e5a2663 100644 --- a/presentation/build.gradle.kts +++ b/presentation/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { implementation(libs.kotlinx.serialization.json) implementation(libs.lottie.compose) implementation(libs.bundles.coil) + implementation(libs.accompanist.permissions) testImplementation(libs.junit) testImplementation(libs.kotlin.coroutines.test) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/common/premission/PermissionHandler.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/common/premission/PermissionHandler.kt new file mode 100644 index 00000000..55d12d56 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/common/premission/PermissionHandler.kt @@ -0,0 +1,14 @@ +package com.threegap.bitnagil.presentation.common.premission + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable + +@Stable +interface PermissionHandler { + val isGranted: Boolean + + fun requestPermission() + + @Composable + fun PermissionDialogs() +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/common/premission/RememberPermissionHandler.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/common/premission/RememberPermissionHandler.kt new file mode 100644 index 00000000..23047bd6 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/common/premission/RememberPermissionHandler.kt @@ -0,0 +1,92 @@ +package com.threegap.bitnagil.presentation.common.premission + +import android.content.Intent +import android.net.Uri +import android.provider.Settings +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import com.google.accompanist.permissions.shouldShowRationale +import com.threegap.bitnagil.designsystem.component.block.BitnagilAlertDialog + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun rememberPermissionHandler( + permission: String, + dialogDescription: String, + onGranted: () -> Unit, +): PermissionHandler { + val context = LocalContext.current + var showSettingsDialog by remember { mutableStateOf(false) } + var permissionRequestCount by rememberSaveable { mutableIntStateOf(0) } + + val permissionState = rememberPermissionState( + permission = permission, + onPermissionResult = { isGranted -> + if (isGranted) { + permissionRequestCount = 0 + onGranted() + } else { + permissionRequestCount++ + } + }, + ) + + val handler = remember(context) { + object : PermissionHandler { + override val isGranted: Boolean + get() = permissionState.status.isGranted + + override fun requestPermission() { + when { + permissionState.status.isGranted -> { + onGranted() + } + + permissionState.status.shouldShowRationale -> { + permissionState.launchPermissionRequest() + } + + else -> { + if (permissionRequestCount == 0) { + permissionState.launchPermissionRequest() + } else { + showSettingsDialog = true + } + } + } + } + + @Composable + override fun PermissionDialogs() { + if (showSettingsDialog) { + BitnagilAlertDialog( + title = "권한 설정이 필요합니다.", + description = dialogDescription, + dismissButtonText = "닫기", + confirmButtonText = "설정으로 이동", + onConfirm = { + showSettingsDialog = false + val intent = Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", context.packageName, null), + ) + context.startActivity(intent) + }, + onDismiss = { showSettingsDialog = false }, + ) + } + } + } + } + + return handler +} From 355908b54f7be66acbef05fa6691024f684de4de Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Tue, 18 Nov 2025 01:44:24 +0900 Subject: [PATCH 14/44] =?UTF-8?q?Feat:=20=EC=A0=9C=EB=B3=B4=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=ED=99=94=EB=A9=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 제보하기 화면의 UI 및 기본 로직을 구현합니다. --- app/src/main/AndroidManifest.xml | 15 + .../presentation/report/ReportScreen.kt | 293 ++++++++++++++++++ .../presentation/report/ReportViewModel.kt | 123 ++++++++ presentation/src/main/res/xml/file_paths.xml | 5 + 4 files changed, 436 insertions(+) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportScreen.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt create mode 100644 presentation/src/main/res/xml/file_paths.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f643fa4b..28c5ae01 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,11 @@ xmlns:tools="http://schemas.android.com/tools"> + + + + + + + + + diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportScreen.kt new file mode 100644 index 00000000..625bcba9 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportScreen.kt @@ -0,0 +1,293 @@ +package com.threegap.bitnagil.presentation.report + +import android.Manifest +import android.content.Context +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.ime +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.content.FileProvider +import androidx.hilt.navigation.compose.hiltViewModel +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.threegap.bitnagil.designsystem.BitnagilTheme +import com.threegap.bitnagil.designsystem.component.atom.BitnagilTextButton +import com.threegap.bitnagil.designsystem.component.atom.BitnagilTextButtonColor +import com.threegap.bitnagil.designsystem.component.atom.BitnagilTextField +import com.threegap.bitnagil.designsystem.component.block.BitnagilTopBar +import com.threegap.bitnagil.presentation.common.premission.rememberPermissionHandler +import com.threegap.bitnagil.presentation.report.component.AddPhotoButton +import com.threegap.bitnagil.presentation.report.component.CurrentLocationInput +import com.threegap.bitnagil.presentation.report.component.ImageSourceBottomSheet +import com.threegap.bitnagil.presentation.report.component.PhotoItem +import com.threegap.bitnagil.presentation.report.component.ReportCategoryBottomSheet +import com.threegap.bitnagil.presentation.report.component.ReportCategorySelector +import com.threegap.bitnagil.presentation.report.component.ReportField +import org.orbitmvi.orbit.compose.collectAsState +import org.orbitmvi.orbit.compose.collectSideEffect +import java.io.File + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class) +@Composable +fun ReportScreenContainer( + navigateToBack: () -> Unit, + viewModel: ReportViewModel = hiltViewModel(), +) { + val context = LocalContext.current + val uiState by viewModel.collectAsState() + + viewModel.collectSideEffect { sideEffect -> + when (sideEffect) { + is ReportSideEffect.NavigateToBack -> navigateToBack() + } + } + + var pendingCameraPhotoUri by remember { mutableStateOf(null) } + + val pickMultipleMediaLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.PickMultipleVisualMedia(ReportViewModel.MAX_IMAGE_COUNT), + onResult = viewModel::addImages, + ) + + val takePictureLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.TakePicture(), + onResult = { success -> + if (success) { + pendingCameraPhotoUri?.let { uri -> + viewModel.addImages(listOf(uri)) + } + } + pendingCameraPhotoUri = null + }, + ) + + fun createImageUri(context: Context): Uri { + val cameraDir = File(context.cacheDir, "camera").apply { if (!exists()) mkdirs() } + val imageFile = File(cameraDir, "camera_${System.currentTimeMillis()}.jpg") + return FileProvider.getUriForFile(context, "${context.packageName}.provider", imageFile) + } + + val cameraPermissionHandler = rememberPermissionHandler( + permission = Manifest.permission.CAMERA, + dialogDescription = "카메라 권한이 비활성화됐어요.\n설정에서 허용해 주세요.", + onGranted = { + val imageUri = createImageUri(context) + pendingCameraPhotoUri = imageUri + takePictureLauncher.launch(imageUri) + }, + ) + + val locationPermissionHandler = rememberPermissionHandler( + permission = Manifest.permission.ACCESS_FINE_LOCATION, + dialogDescription = "위치 권한이 비활성화됐어요.\n설정에서 허용해 주세요.", + onGranted = viewModel::fetchCurrentAddress, + ) + + cameraPermissionHandler.PermissionDialogs() + locationPermissionHandler.PermissionDialogs() + + if (uiState.imageSourceBottomSheetVisible) { + ImageSourceBottomSheet( + onCameraClick = { + if (uiState.canAddMoreImages) cameraPermissionHandler.requestPermission() + }, + onAlbumClick = { + if (uiState.canAddMoreImages) { + pickMultipleMediaLauncher.launch( + PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly), + ) + } + }, + onDismiss = viewModel::hideImageSourceBottomSheet, + ) + } + + if (uiState.reportCategoryBottomSheetVisible) { + ReportCategoryBottomSheet( + selectedCategory = uiState.selectedCategory, + onSelected = viewModel::selectReportCategory, + onDismiss = viewModel::hideReportCategoryBottomSheet, + ) + } + + ReportScreen( + uiState = uiState, + onReportTitle = viewModel::updateReportTitle, + onReportDescriptionChange = viewModel::updateReportDescription, + onShowImageSourceBottomSheet = viewModel::showImageSourceBottomSheet, + onShowReportCategoryBottomSheet = viewModel::showReportCategoryBottomSheet, + onRemoveImage = viewModel::removeImage, + onGetCurrentLocationClick = locationPermissionHandler::requestPermission, + onBackClick = viewModel::navigateToBack, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ReportScreen( + uiState: ReportState, + onReportTitle: (String) -> Unit, + onReportDescriptionChange: (String) -> Unit, + onShowImageSourceBottomSheet: () -> Unit, + onShowReportCategoryBottomSheet: () -> Unit, + onRemoveImage: (Uri) -> Unit, + onGetCurrentLocationClick: () -> Unit, + onBackClick: () -> Unit, +) { + val scrollState = rememberScrollState() + + Column( + modifier = Modifier + .fillMaxSize() + .statusBarsPadding() + .windowInsetsPadding(WindowInsets.ime), + ) { + BitnagilTopBar( + title = "제보하기", + showBackButton = true, + onBackClick = onBackClick, + ) + + Column( + modifier = Modifier + .weight(1f) + .verticalScroll(state = scrollState) + .padding(top = 32.dp) + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(28.dp), + ) { + ReportField(title = "사진 첨부") { + Row( + verticalAlignment = Alignment.Bottom, + ) { + AddPhotoButton( + onClick = onShowImageSourceBottomSheet, + imageCount = uiState.reportImages.size, + maxImageCount = ReportViewModel.MAX_IMAGE_COUNT, + ) + + LazyRow( + modifier = Modifier.weight(1f), + horizontalArrangement = Arrangement.spacedBy(14.dp), + contentPadding = PaddingValues(horizontal = 14.dp), + ) { + items(uiState.reportImages) { uri -> + PhotoItem( + uri = uri, + onRemove = onRemoveImage, + ) + } + } + } + } + + ReportField(title = "제목") { + BitnagilTextField( + value = uiState.reportTitle, + onValueChange = onReportTitle, + singleLine = true, + placeholder = { + Text( + text = "제보 제목을 작성해주세요.", + style = BitnagilTheme.typography.body2Medium, + color = BitnagilTheme.colors.coolGray80, + ) + }, + ) + } + + ReportField(title = "카테고리") { + ReportCategorySelector( + title = uiState.selectedCategory?.title, + onClick = onShowReportCategoryBottomSheet, + ) + } + + ReportField(title = "상세 제보 내용") { + BitnagilTextField( + value = uiState.reportDescription, + onValueChange = onReportDescriptionChange, + modifier = Modifier.height(88.dp), + placeholder = { + Text( + text = "어떤 위험인지 간단히 설명해주세요.(100자 내외)", + style = BitnagilTheme.typography.body2Medium, + color = BitnagilTheme.colors.coolGray80, + ) + }, + ) + + Text( + text = "${uiState.reportDescription.length} / 150", + style = BitnagilTheme.typography.caption1Medium, + color = BitnagilTheme.colors.coolGray80, + textAlign = TextAlign.End, + modifier = Modifier.fillMaxWidth(), + ) + } + + ReportField(title = " 신고 위치") { + CurrentLocationInput( + currentLocation = uiState.currentAddress, + onClick = onGetCurrentLocationClick, + ) + } + } + + BitnagilTextButton( + text = "제보하기", + onClick = {}, + colors = BitnagilTextButtonColor.default( + disabledBackgroundColor = BitnagilTheme.colors.coolGray98, + disabledTextColor = BitnagilTheme.colors.coolGray90, + ), + enabled = false, + modifier = Modifier + .fillMaxWidth() + .padding(14.dp), + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun Preview() { + ReportScreen( + uiState = ReportState.Init, + onReportTitle = {}, + onReportDescriptionChange = {}, + onRemoveImage = {}, + onShowImageSourceBottomSheet = {}, + onShowReportCategoryBottomSheet = {}, + onGetCurrentLocationClick = {}, + onBackClick = {}, + ) +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt new file mode 100644 index 00000000..ac89d80c --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt @@ -0,0 +1,123 @@ +package com.threegap.bitnagil.presentation.report + +import android.net.Uri +import androidx.lifecycle.ViewModel +import com.threegap.bitnagil.presentation.report.model.ReportCategory +import dagger.hilt.android.lifecycle.HiltViewModel +import jakarta.inject.Inject +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.viewmodel.container + +@HiltViewModel +class ReportViewModel @Inject constructor() : ViewModel(), ContainerHost { + + override val container: Container = container(initialState = ReportState.Init) + + fun updateReportTitle(title: String) { + intent { + reduce { state.copy(reportTitle = title) } + } + } + + fun updateReportDescription(description: String) { + intent { + reduce { state.copy(reportDescription = description) } + } + } + + fun showImageSourceBottomSheet() { + intent { + reduce { state.copy(imageSourceBottomSheetVisible = true) } + } + } + + fun hideImageSourceBottomSheet() { + intent { + reduce { state.copy(imageSourceBottomSheetVisible = false) } + } + } + + fun showReportCategoryBottomSheet() { + intent { + reduce { state.copy(reportCategoryBottomSheetVisible = true) } + } + } + + fun hideReportCategoryBottomSheet() { + intent { + reduce { state.copy(reportCategoryBottomSheetVisible = false) } + } + } + + fun selectReportCategory(category: ReportCategory) { + intent { + reduce { state.copy(selectedCategory = category) } + } + } + + fun fetchCurrentAddress() { + // TODO + } + + fun addImages(uris: List) { + intent { + val currentImages = state.reportImages + val availableSlots = MAX_IMAGE_COUNT - currentImages.size + + if (availableSlots > 0) { + val imagesToAdd = uris.take(availableSlots) + reduce { state.copy(reportImages = currentImages + imagesToAdd) } + } + } + } + + fun removeImage(uri: Uri) { + intent { + reduce { state.copy(reportImages = state.reportImages - uri) } + } + } + + fun navigateToBack() { + intent { + postSideEffect(ReportSideEffect.NavigateToBack) + } + } + + companion object { + const val MAX_IMAGE_COUNT = 3 + } +} + +data class ReportState( + val reportImages: List, + val reportTitle: String, + val reportDescription: String, + val selectedCategory: ReportCategory?, + val imageSourceBottomSheetVisible: Boolean, + val reportCategoryBottomSheetVisible: Boolean, + val currentAddress: String?, + val currentLatitude: Double?, + val currentLongitude: Double?, +) { + val canAddMoreImages: Boolean + get() = reportImages.size < ReportViewModel.MAX_IMAGE_COUNT + + companion object { + val Init = ReportState( + reportImages = emptyList(), + reportTitle = "", + reportDescription = "", + selectedCategory = null, + imageSourceBottomSheetVisible = false, + reportCategoryBottomSheetVisible = false, + currentAddress = null, + currentLatitude = null, + currentLongitude = null, + ) + } +} + +sealed interface ReportSideEffect { + data object NavigateToBack : ReportSideEffect +} diff --git a/presentation/src/main/res/xml/file_paths.xml b/presentation/src/main/res/xml/file_paths.xml new file mode 100644 index 00000000..d686a1ea --- /dev/null +++ b/presentation/src/main/res/xml/file_paths.xml @@ -0,0 +1,5 @@ + + + + + From 6c98851aa5d6530019c25915e9a74795588d8334 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Tue, 18 Nov 2025 01:52:12 +0900 Subject: [PATCH 15/44] =?UTF-8?q?Feat:=20=EC=A0=9C=EB=B3=B4=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=ED=99=94=EB=A9=B4=20=EB=9D=BC=EC=9A=B0=ED=8C=85=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/threegap/bitnagil/MainNavHost.kt | 16 ++++++++++++++++ app/src/main/java/com/threegap/bitnagil/Route.kt | 3 +++ .../bitnagil/navigation/home/HomeNavHost.kt | 6 ++++++ .../component/atom/BitnagilFloatingButton.kt | 5 ++++- 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt b/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt index d80369e0..4810d5b1 100644 --- a/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt +++ b/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt @@ -13,6 +13,7 @@ import com.threegap.bitnagil.presentation.login.LoginScreenContainer 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.routinelist.RoutineListScreenContainer import com.threegap.bitnagil.presentation.setting.SettingScreenContainer import com.threegap.bitnagil.presentation.splash.SplashScreenContainer @@ -134,6 +135,11 @@ fun MainNavHost( launchSingleTop = true } }, + navigateToReport = { + navigator.navController.navigate(Route.Report) { + launchSingleTop = true + } + }, ) } @@ -289,5 +295,15 @@ fun MainNavHost( }, ) } + + composable { + ReportScreenContainer( + 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 504f2993..93e1cd0e 100644 --- a/app/src/main/java/com/threegap/bitnagil/Route.kt +++ b/app/src/main/java/com/threegap/bitnagil/Route.kt @@ -48,4 +48,7 @@ sealed interface Route { @Serializable data object Guide : Route + + @Serializable + data object Report : 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 7025bc7d..249a9aef 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 @@ -43,6 +43,7 @@ fun HomeNavHost( navigateToRegisterRoutine: (String?) -> Unit, navigateToEmotion: () -> Unit, navigateToRoutineList: (String) -> Unit, + navigateToReport: () -> Unit, ) { val navigator = rememberHomeNavigator() var showFloatingOverlay by remember { mutableStateOf(false) } @@ -106,6 +107,11 @@ fun HomeNavHost( BitnagilFloatingActionMenu( actions = listOf( + FloatingActionItem( + icon = R.drawable.ic_complaint, + text = "제보하기", + onClick = { navigateToReport() }, + ), FloatingActionItem( icon = R.drawable.ic_routine_add, text = "루틴 등록", diff --git a/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilFloatingButton.kt b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilFloatingButton.kt index ed883b23..391b246e 100644 --- a/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilFloatingButton.kt +++ b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilFloatingButton.kt @@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text @@ -101,7 +102,7 @@ fun BitnagilFloatingActionMenu( ), ) { Column( - modifier = Modifier.padding(16.dp), + modifier = Modifier.padding(vertical = 16.dp), verticalArrangement = Arrangement.spacedBy(24.dp), ) { actions.forEach { action -> @@ -173,6 +174,8 @@ private fun FloatingActionMenuItem( horizontalArrangement = Arrangement.spacedBy(14.dp), modifier = modifier .scale(scale) + .padding(horizontal = 16.dp) + .width(112.dp) .clickableWithoutRipple { onClick() }, ) { BitnagilIcon( From e192f812a201636e8ddfcdad6d485c0050fc5998 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Tue, 18 Nov 2025 03:02:54 +0900 Subject: [PATCH 16/44] =?UTF-8?q?Feat:=20=ED=98=84=EC=9E=AC=20=EC=A3=BC?= =?UTF-8?q?=EC=86=8C=20=EB=B0=8F=20=EC=9C=84=EC=B9=98=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EB=AA=A8=EB=8D=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bitnagil/domain/address/model/CurrentAddress.kt | 7 +++++++ .../bitnagil/domain/address/model/CurrentLocation.kt | 6 ++++++ 2 files changed, 13 insertions(+) create mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/address/model/CurrentAddress.kt create mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/address/model/CurrentLocation.kt diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/address/model/CurrentAddress.kt b/domain/src/main/java/com/threegap/bitnagil/domain/address/model/CurrentAddress.kt new file mode 100644 index 00000000..fc710bc7 --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/address/model/CurrentAddress.kt @@ -0,0 +1,7 @@ +package com.threegap.bitnagil.domain.address.model + +data class CurrentAddress( + val latitude: Double, + val longitude: Double, + val roadAddress: String, +) diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/address/model/CurrentLocation.kt b/domain/src/main/java/com/threegap/bitnagil/domain/address/model/CurrentLocation.kt new file mode 100644 index 00000000..e7857b31 --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/address/model/CurrentLocation.kt @@ -0,0 +1,6 @@ +package com.threegap.bitnagil.domain.address.model + +data class CurrentLocation( + val latitude: Double, + val longitude: Double, +) From 3c10321e1949dbe11db06018eb3c1cd5b0a37b4d Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Tue, 18 Nov 2025 14:05:13 +0900 Subject: [PATCH 17/44] =?UTF-8?q?Feat:=20=ED=98=84=EC=9E=AC=20=EC=A3=BC?= =?UTF-8?q?=EC=86=8C=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 현재 위치를 기반으로 도로명 주소를 조회하는 UseCase를 추가합니다. - 주소 관련 기능을 위한 AddressRepository 인터페이스를 정의합니다. --- .../address/repository/AddressRepository.kt | 8 ++++++ .../usecase/FetchCurrentAddressUseCase.kt | 25 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/address/repository/AddressRepository.kt create mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/address/usecase/FetchCurrentAddressUseCase.kt diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/address/repository/AddressRepository.kt b/domain/src/main/java/com/threegap/bitnagil/domain/address/repository/AddressRepository.kt new file mode 100644 index 00000000..69458e93 --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/address/repository/AddressRepository.kt @@ -0,0 +1,8 @@ +package com.threegap.bitnagil.domain.address.repository + +import com.threegap.bitnagil.domain.address.model.CurrentLocation + +interface AddressRepository { + suspend fun fetchCurrentLocation(): Result + suspend fun fetchCurrentAddress(latitude: Double, longitude: Double): Result +} diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/address/usecase/FetchCurrentAddressUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/address/usecase/FetchCurrentAddressUseCase.kt new file mode 100644 index 00000000..21f7fbcd --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/address/usecase/FetchCurrentAddressUseCase.kt @@ -0,0 +1,25 @@ +package com.threegap.bitnagil.domain.address.usecase + +import com.threegap.bitnagil.domain.address.model.CurrentAddress +import com.threegap.bitnagil.domain.address.repository.AddressRepository +import javax.inject.Inject + +class FetchCurrentAddressUseCase @Inject constructor( + private val addressRepository: AddressRepository, +) { + suspend operator fun invoke(): Result { + return addressRepository.fetchCurrentLocation() + .mapCatching { location -> + val roadAddress = addressRepository.fetchCurrentAddress( + latitude = location.latitude, + longitude = location.longitude, + ).getOrThrow() + + CurrentAddress( + latitude = location.latitude, + longitude = location.longitude, + roadAddress = roadAddress, + ) + } + } +} From 345cb1e7bed100392769758a4f0f59329c40fc13 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Tue, 18 Nov 2025 14:11:50 +0900 Subject: [PATCH 18/44] =?UTF-8?q?feat:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EC=A3=BC=EC=86=8C=20=EC=9D=91=EB=8B=B5=20DTO=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/response/Coord2AddressResponse.kt | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 data/src/main/java/com/threegap/bitnagil/data/address/model/response/Coord2AddressResponse.kt 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 new file mode 100644 index 00000000..5eb0bbe4 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/address/model/response/Coord2AddressResponse.kt @@ -0,0 +1,75 @@ +package com.threegap.bitnagil.data.address.model.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Coord2AddressResponse( + @SerialName("meta") + val meta: Meta, + @SerialName("documents") + val documents: List, +) + +@Serializable +data class Meta( + @SerialName("total_count") + val totalCount: Int, +) + +@Serializable +data class Document( + @SerialName("road_address") + val roadAddress: RoadAddress, + @SerialName("address") + val address: Address, +) + +@Serializable +data class Address( + @SerialName("address_name") + val addressName: String, + @SerialName("region_1depth_name") + val region1depthName: String, + @SerialName("region_2depth_name") + val region2depthName: String, + @SerialName("region_3depth_name") + val region3depthName: String, + @SerialName("mountain_yn") + val mountainYn: String, + @SerialName("main_address_no") + val mainAddressNo: String, + @SerialName("sub_address_no") + val subAddressNo: String, +) + +@Serializable +data class RoadAddress( + @SerialName("address_name") + val addressName: String, + @SerialName("region_1depth_name") + val region1depthName: String, + @SerialName("region_2depth_name") + val region2depthName: String, + @SerialName("region_3depth_name") + val region3depthName: String, + @SerialName("road_name") + val roadName: String, + @SerialName("underground_yn") + val undergroundYn: String, + @SerialName("main_building_no") + val mainBuildingNo: String, + @SerialName("sub_building_no") + val subBuildingNo: String, + @SerialName("building_name") + val buildingName: String, + @SerialName("zone_no") + val zoneNo: String, +) + +fun Coord2AddressResponse.toAddress(): String? { + return this.documents + .firstOrNull() + ?.roadAddress + ?.addressName +} From 0ae10117effcf067ff4c5198d388e393b7a481ca Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Tue, 18 Nov 2025 14:13:58 +0900 Subject: [PATCH 19/44] =?UTF-8?q?Feat:=20=ED=98=84=EC=9E=AC=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=EB=A5=BC=20=EA=B0=80=EC=A0=B8=EC=98=A4=EB=8A=94=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/build.gradle.kts | 2 ++ .../address/datasource/LocationDataSource.kt | 7 ++++++ .../datasourceImpl/LocationDataSourceImpl.kt | 23 +++++++++++++++++++ .../data/address/di/LocationModule.kt | 21 +++++++++++++++++ gradle/libs.versions.toml | 4 ++++ presentation/build.gradle.kts | 1 + 6 files changed, 58 insertions(+) create mode 100644 data/src/main/java/com/threegap/bitnagil/data/address/datasource/LocationDataSource.kt create mode 100644 data/src/main/java/com/threegap/bitnagil/data/address/datasourceImpl/LocationDataSourceImpl.kt create mode 100644 data/src/main/java/com/threegap/bitnagil/data/address/di/LocationModule.kt diff --git a/data/build.gradle.kts b/data/build.gradle.kts index 7f7e00f7..fcee75af 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -18,4 +18,6 @@ dependencies { implementation(libs.bundles.okhttp) implementation(platform(libs.retrofit.bom)) implementation(libs.bundles.retrofit) + implementation(libs.play.services.location) + implementation(libs.kotlinx.coroutines.play) } diff --git a/data/src/main/java/com/threegap/bitnagil/data/address/datasource/LocationDataSource.kt b/data/src/main/java/com/threegap/bitnagil/data/address/datasource/LocationDataSource.kt new file mode 100644 index 00000000..e013ed57 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/address/datasource/LocationDataSource.kt @@ -0,0 +1,7 @@ +package com.threegap.bitnagil.data.address.datasource + +import com.threegap.bitnagil.domain.address.model.CurrentLocation + +interface LocationDataSource { + suspend fun fetchCurrentLocation(): Result +} diff --git a/data/src/main/java/com/threegap/bitnagil/data/address/datasourceImpl/LocationDataSourceImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/address/datasourceImpl/LocationDataSourceImpl.kt new file mode 100644 index 00000000..da74ec0a --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/address/datasourceImpl/LocationDataSourceImpl.kt @@ -0,0 +1,23 @@ +package com.threegap.bitnagil.data.address.datasourceImpl + +import android.annotation.SuppressLint +import com.google.android.gms.location.FusedLocationProviderClient +import com.threegap.bitnagil.data.address.datasource.LocationDataSource +import com.threegap.bitnagil.domain.address.model.CurrentLocation +import kotlinx.coroutines.tasks.await +import javax.inject.Inject + +class LocationDataSourceImpl @Inject constructor( + private val fusedLocationClient: FusedLocationProviderClient, +) : LocationDataSource { + + @SuppressLint("MissingPermission") + override suspend fun fetchCurrentLocation(): Result { + return runCatching { + val location = fusedLocationClient.lastLocation.await() + ?: throw Exception("Location not available") + + CurrentLocation(latitude = location.latitude, longitude = location.longitude) + } + } +} diff --git a/data/src/main/java/com/threegap/bitnagil/data/address/di/LocationModule.kt b/data/src/main/java/com/threegap/bitnagil/data/address/di/LocationModule.kt new file mode 100644 index 00000000..c77e1975 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/address/di/LocationModule.kt @@ -0,0 +1,21 @@ +package com.threegap.bitnagil.data.address.di + +import android.content.Context +import com.google.android.gms.location.FusedLocationProviderClient +import com.google.android.gms.location.LocationServices +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object LocationModule { + + @Provides + @Singleton + fun provideFusedLocationProviderClient(@ApplicationContext context: Context): FusedLocationProviderClient = + LocationServices.getFusedLocationProviderClient(context) +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 79edb354..4384ec3b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -56,6 +56,7 @@ kakaoLogin = "2.21.4" lottie-compose = "6.6.0" coil = "3.3.0" accompanist = "0.37.3" +playServicesLocation = "21.3.0" [libraries] ## Android Gradle Plugin @@ -90,6 +91,7 @@ compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4 ## Kotlin kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } +kotlinx-coroutines-play = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-play-services", version.ref = "kotlinxCoroutines" } kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } ## Hilt @@ -129,6 +131,8 @@ kakao-v2-user = { group = "com.kakao.sdk", name = "v2-user", version.ref = "kaka lottie-compose = { group = "com.airbnb.android", name = "lottie-compose", version.ref = "lottie-compose" } accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" } +play-services-location = { group = "com.google.android.gms", name = "play-services-location", version.ref = "playServicesLocation" } + [bundles] androidx-core = [ diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts index 2e5a2663..dd4172ee 100644 --- a/presentation/build.gradle.kts +++ b/presentation/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { implementation(libs.lottie.compose) implementation(libs.bundles.coil) implementation(libs.accompanist.permissions) + implementation(libs.play.services.location) testImplementation(libs.junit) testImplementation(libs.kotlin.coroutines.test) From cccd2ece28f501022f7a0418932b4ee394ed8ab6 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Tue, 18 Nov 2025 14:17:11 +0900 Subject: [PATCH 20/44] =?UTF-8?q?Feat:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EB=A1=9C=EC=BB=AC=20API=20=EC=97=B0=EB=8F=99=20=EB=B0=8F=20?= =?UTF-8?q?=ED=98=84=EC=9E=AC=20=EC=A3=BC=EC=86=8C=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 11 ++++++ .../bitnagil/di/core/NetworkModule.kt | 39 +++++++++++++++++++ .../threegap/bitnagil/di/core/Qualifier.kt | 4 ++ .../bitnagil/di/data/DataSourceModule.kt | 12 ++++++ .../bitnagil/di/data/RepositoryModule.kt | 6 +++ .../bitnagil/di/data/ServiceModule.kt | 7 ++++ .../address/datasource/AddressDataSource.kt | 7 ++++ .../datasourceImpl/AddressDataSourceImpl.kt | 20 ++++++++++ .../repositoryImpl/AddressRepositoryImpl.kt | 25 ++++++++++++ .../data/address/service/AddressService.kt | 13 +++++++ .../presentation/report/ReportViewModel.kt | 20 +++++++++- 11 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 data/src/main/java/com/threegap/bitnagil/data/address/datasource/AddressDataSource.kt create mode 100644 data/src/main/java/com/threegap/bitnagil/data/address/datasourceImpl/AddressDataSourceImpl.kt create mode 100644 data/src/main/java/com/threegap/bitnagil/data/address/repositoryImpl/AddressRepositoryImpl.kt create mode 100644 data/src/main/java/com/threegap/bitnagil/data/address/service/AddressService.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1bf1f6a0..2519d233 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -46,6 +46,17 @@ android { name = "KAKAO_NATIVE_APP_KEY", value = "\"$kakaoNativeAppKey\"", ) + + val kakaoRestApiKey = + (properties["kakao.rest.api.key"] as? String) + ?: System.getenv("KAKAO_REST_API_KEY") + ?: throw GradleException("KAKAO_REST_API_KEY 값이 없습니다.") + + buildConfigField( + type = "String", + name = "KAKAO_REST_API_KEY", + value = "\"$kakaoRestApiKey\"", + ) } buildTypes { diff --git a/app/src/main/java/com/threegap/bitnagil/di/core/NetworkModule.kt b/app/src/main/java/com/threegap/bitnagil/di/core/NetworkModule.kt index 9d253400..c761a9fa 100644 --- a/app/src/main/java/com/threegap/bitnagil/di/core/NetworkModule.kt +++ b/app/src/main/java/com/threegap/bitnagil/di/core/NetworkModule.kt @@ -30,6 +30,8 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) object NetworkModule { private const val APPLICATION_JSON = "application/json" + private const val REST_API_KEY = BuildConfig.KAKAO_REST_API_KEY + private const val KAKAO_URL = "https://dapi.kakao.com" @Provides @Singleton @@ -74,6 +76,43 @@ object NetworkModule { override suspend fun clearTokens() = dataStore.clearAuthToken() } + @Provides + @Singleton + @Kakao + fun provideKakaoAuthInterceptor(): Interceptor = + Interceptor { chain -> + val request = chain.request().newBuilder() + .addHeader("Authorization", "KakaoAK $REST_API_KEY") + .build() + chain.proceed(request) + } + + @Provides + @Singleton + @Kakao + fun provideKakaoOkHttpClient( + httpLoggingInterceptor: HttpLoggingInterceptor, + @Kakao kakaoAuthInterceptor: Interceptor, + ): OkHttpClient = OkHttpClient.Builder() + .addInterceptor(kakaoAuthInterceptor) + .addInterceptor(httpLoggingInterceptor) + .connectTimeout(10L, TimeUnit.SECONDS) + .writeTimeout(30L, TimeUnit.SECONDS) + .readTimeout(30L, TimeUnit.SECONDS) + .build() + + @Provides + @Singleton + @Kakao + fun provideKakaoRetrofit( + converterFactory: Converter.Factory, + @Kakao okHttpClient: OkHttpClient, + ): Retrofit = Retrofit.Builder() + .baseUrl(KAKAO_URL) + .addConverterFactory(converterFactory) + .client(okHttpClient) + .build() + @Provides @Singleton fun provideTokenAuthenticator( diff --git a/app/src/main/java/com/threegap/bitnagil/di/core/Qualifier.kt b/app/src/main/java/com/threegap/bitnagil/di/core/Qualifier.kt index d01cd30a..b2db3e56 100644 --- a/app/src/main/java/com/threegap/bitnagil/di/core/Qualifier.kt +++ b/app/src/main/java/com/threegap/bitnagil/di/core/Qualifier.kt @@ -9,3 +9,7 @@ annotation class NoneAuth @Qualifier @Retention(AnnotationRetention.BINARY) annotation class Auth + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class Kakao diff --git a/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt b/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt index 21a92847..39045bf5 100644 --- a/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt +++ b/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt @@ -1,5 +1,9 @@ package com.threegap.bitnagil.di.data +import com.threegap.bitnagil.data.address.datasource.AddressDataSource +import com.threegap.bitnagil.data.address.datasource.LocationDataSource +import com.threegap.bitnagil.data.address.datasourceImpl.AddressDataSourceImpl +import com.threegap.bitnagil.data.address.datasourceImpl.LocationDataSourceImpl import com.threegap.bitnagil.data.auth.datasource.AuthLocalDataSource import com.threegap.bitnagil.data.auth.datasource.AuthRemoteDataSource import com.threegap.bitnagil.data.auth.datasourceimpl.AuthLocalDataSourceImpl @@ -63,4 +67,12 @@ abstract class DataSourceModule { @Binds @Singleton abstract fun bindVersionDataSource(versionDataSourceImpl: VersionDataSourceImpl): VersionDataSource + + @Binds + @Singleton + abstract fun bindLocationDataSource(locationDataSourceImpl: LocationDataSourceImpl): LocationDataSource + + @Binds + @Singleton + abstract fun bindAddressDataSource(addressDataSourceImpl: AddressDataSourceImpl): AddressDataSource } diff --git a/app/src/main/java/com/threegap/bitnagil/di/data/RepositoryModule.kt b/app/src/main/java/com/threegap/bitnagil/di/data/RepositoryModule.kt index 93fb9e37..88fef29e 100644 --- a/app/src/main/java/com/threegap/bitnagil/di/data/RepositoryModule.kt +++ b/app/src/main/java/com/threegap/bitnagil/di/data/RepositoryModule.kt @@ -1,5 +1,6 @@ package com.threegap.bitnagil.di.data +import com.threegap.bitnagil.data.address.repositoryImpl.AddressRepositoryImpl import com.threegap.bitnagil.data.auth.repositoryimpl.AuthRepositoryImpl import com.threegap.bitnagil.data.emotion.repositoryImpl.EmotionRepositoryImpl import com.threegap.bitnagil.data.onboarding.repositoryImpl.OnBoardingRepositoryImpl @@ -8,6 +9,7 @@ import com.threegap.bitnagil.data.routine.repositoryImpl.RoutineRepositoryImpl import com.threegap.bitnagil.data.user.repositoryImpl.UserRepositoryImpl import com.threegap.bitnagil.data.version.repositoryImpl.VersionRepositoryImpl import com.threegap.bitnagil.data.writeroutine.repositoryImpl.WriteRoutineRepositoryImpl +import com.threegap.bitnagil.domain.address.repository.AddressRepository import com.threegap.bitnagil.domain.auth.repository.AuthRepository import com.threegap.bitnagil.domain.emotion.repository.EmotionRepository import com.threegap.bitnagil.domain.onboarding.repository.OnBoardingRepository @@ -57,4 +59,8 @@ abstract class RepositoryModule { @Binds @Singleton abstract fun bindVersionRepository(versionRepositoryImpl: VersionRepositoryImpl): VersionRepository + + @Binds + @Singleton + abstract fun bindAddressRepository(addressRepositoryImpl: AddressRepositoryImpl): AddressRepository } diff --git a/app/src/main/java/com/threegap/bitnagil/di/data/ServiceModule.kt b/app/src/main/java/com/threegap/bitnagil/di/data/ServiceModule.kt index 66376c96..de2ab9a2 100644 --- a/app/src/main/java/com/threegap/bitnagil/di/data/ServiceModule.kt +++ b/app/src/main/java/com/threegap/bitnagil/di/data/ServiceModule.kt @@ -1,5 +1,6 @@ package com.threegap.bitnagil.di.data +import com.threegap.bitnagil.data.address.service.AddressService import com.threegap.bitnagil.data.auth.service.AuthService import com.threegap.bitnagil.data.emotion.service.EmotionService import com.threegap.bitnagil.data.onboarding.service.OnBoardingService @@ -9,6 +10,7 @@ import com.threegap.bitnagil.data.user.service.UserService import com.threegap.bitnagil.data.version.service.VersionService import com.threegap.bitnagil.data.writeroutine.service.WriteRoutineService import com.threegap.bitnagil.di.core.Auth +import com.threegap.bitnagil.di.core.Kakao import com.threegap.bitnagil.di.core.NoneAuth import com.threegap.bitnagil.network.token.ReissueService import dagger.Module @@ -66,4 +68,9 @@ object ServiceModule { @Singleton fun provideVersionService(@NoneAuth retrofit: Retrofit): VersionService = retrofit.create(VersionService::class.java) + + @Provides + @Singleton + fun provideAddressService(@Kakao retrofit: Retrofit): AddressService = + retrofit.create(AddressService::class.java) } diff --git a/data/src/main/java/com/threegap/bitnagil/data/address/datasource/AddressDataSource.kt b/data/src/main/java/com/threegap/bitnagil/data/address/datasource/AddressDataSource.kt new file mode 100644 index 00000000..6eab8826 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/address/datasource/AddressDataSource.kt @@ -0,0 +1,7 @@ +package com.threegap.bitnagil.data.address.datasource + +import com.threegap.bitnagil.data.address.model.response.Coord2AddressResponse + +interface AddressDataSource { + suspend fun fetchCurrentAddress(longitude: Double, latitude: Double): Result +} diff --git a/data/src/main/java/com/threegap/bitnagil/data/address/datasourceImpl/AddressDataSourceImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/address/datasourceImpl/AddressDataSourceImpl.kt new file mode 100644 index 00000000..d80e6ae0 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/address/datasourceImpl/AddressDataSourceImpl.kt @@ -0,0 +1,20 @@ +package com.threegap.bitnagil.data.address.datasourceImpl + +import com.threegap.bitnagil.data.address.datasource.AddressDataSource +import com.threegap.bitnagil.data.address.model.response.Coord2AddressResponse +import com.threegap.bitnagil.data.address.service.AddressService +import javax.inject.Inject + +class AddressDataSourceImpl @Inject constructor( + private val addressService: AddressService, +) : AddressDataSource { + + override suspend fun fetchCurrentAddress(longitude: Double, latitude: Double): Result { + return runCatching { + addressService.fetchCurrentAddress( + longitude = longitude.toString(), + latitude = latitude.toString(), + ) + } + } +} diff --git a/data/src/main/java/com/threegap/bitnagil/data/address/repositoryImpl/AddressRepositoryImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/address/repositoryImpl/AddressRepositoryImpl.kt new file mode 100644 index 00000000..a751c8db --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/address/repositoryImpl/AddressRepositoryImpl.kt @@ -0,0 +1,25 @@ +package com.threegap.bitnagil.data.address.repositoryImpl + +import com.threegap.bitnagil.data.address.datasource.AddressDataSource +import com.threegap.bitnagil.data.address.datasource.LocationDataSource +import com.threegap.bitnagil.data.address.model.response.toAddress +import com.threegap.bitnagil.domain.address.model.CurrentLocation +import com.threegap.bitnagil.domain.address.repository.AddressRepository +import javax.inject.Inject + +class AddressRepositoryImpl @Inject constructor( + private val locationDataSource: LocationDataSource, + private val addressDataSource: AddressDataSource, +) : AddressRepository { + + override suspend fun fetchCurrentLocation(): Result { + return locationDataSource.fetchCurrentLocation() + } + + override suspend fun fetchCurrentAddress(latitude: Double, longitude: Double): Result { + return addressDataSource.fetchCurrentAddress(latitude, longitude) + .mapCatching { + it.toAddress() ?: throw Exception("Address not found") + } + } +} diff --git a/data/src/main/java/com/threegap/bitnagil/data/address/service/AddressService.kt b/data/src/main/java/com/threegap/bitnagil/data/address/service/AddressService.kt new file mode 100644 index 00000000..52dcc6e2 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/address/service/AddressService.kt @@ -0,0 +1,13 @@ +package com.threegap.bitnagil.data.address.service + +import com.threegap.bitnagil.data.address.model.response.Coord2AddressResponse +import retrofit2.http.GET +import retrofit2.http.Query + +interface AddressService { + @GET("/v2/local/geo/coord2address.json") + suspend fun fetchCurrentAddress( + @Query("x") longitude: String, + @Query("y") latitude: String, + ): Coord2AddressResponse +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt index ac89d80c..ce888683 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt @@ -2,6 +2,7 @@ package com.threegap.bitnagil.presentation.report import android.net.Uri import androidx.lifecycle.ViewModel +import com.threegap.bitnagil.domain.address.usecase.FetchCurrentAddressUseCase import com.threegap.bitnagil.presentation.report.model.ReportCategory import dagger.hilt.android.lifecycle.HiltViewModel import jakarta.inject.Inject @@ -10,7 +11,9 @@ import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.viewmodel.container @HiltViewModel -class ReportViewModel @Inject constructor() : ViewModel(), ContainerHost { +class ReportViewModel @Inject constructor( + private val fetchCurrentAddressUseCase: FetchCurrentAddressUseCase, +) : ViewModel(), ContainerHost { override val container: Container = container(initialState = ReportState.Init) @@ -57,7 +60,20 @@ class ReportViewModel @Inject constructor() : ViewModel(), ContainerHost) { From b2bcb7c27c6becd1bff5dc91a4d291434250f7bb Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Tue, 18 Nov 2025 15:59:58 +0900 Subject: [PATCH 21/44] =?UTF-8?q?Chore:=20=EC=9C=84=EB=8F=84=20=EA=B2=BD?= =?UTF-8?q?=EB=8F=84=20=EC=88=9C=EC=84=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/address/repositoryImpl/AddressRepositoryImpl.kt | 4 ++-- .../bitnagil/domain/address/repository/AddressRepository.kt | 2 +- .../domain/address/usecase/FetchCurrentAddressUseCase.kt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/data/src/main/java/com/threegap/bitnagil/data/address/repositoryImpl/AddressRepositoryImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/address/repositoryImpl/AddressRepositoryImpl.kt index a751c8db..7be79d21 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/address/repositoryImpl/AddressRepositoryImpl.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/address/repositoryImpl/AddressRepositoryImpl.kt @@ -16,8 +16,8 @@ class AddressRepositoryImpl @Inject constructor( return locationDataSource.fetchCurrentLocation() } - override suspend fun fetchCurrentAddress(latitude: Double, longitude: Double): Result { - return addressDataSource.fetchCurrentAddress(latitude, longitude) + override suspend fun fetchCurrentAddress(longitude: Double, latitude: Double): Result { + return addressDataSource.fetchCurrentAddress(longitude = longitude, latitude = latitude) .mapCatching { it.toAddress() ?: throw Exception("Address not found") } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/address/repository/AddressRepository.kt b/domain/src/main/java/com/threegap/bitnagil/domain/address/repository/AddressRepository.kt index 69458e93..06de39da 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/address/repository/AddressRepository.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/address/repository/AddressRepository.kt @@ -4,5 +4,5 @@ import com.threegap.bitnagil.domain.address.model.CurrentLocation interface AddressRepository { suspend fun fetchCurrentLocation(): Result - suspend fun fetchCurrentAddress(latitude: Double, longitude: Double): Result + suspend fun fetchCurrentAddress(longitude: Double, latitude: Double): Result } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/address/usecase/FetchCurrentAddressUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/address/usecase/FetchCurrentAddressUseCase.kt index 21f7fbcd..f23d2b81 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/address/usecase/FetchCurrentAddressUseCase.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/address/usecase/FetchCurrentAddressUseCase.kt @@ -11,8 +11,8 @@ class FetchCurrentAddressUseCase @Inject constructor( return addressRepository.fetchCurrentLocation() .mapCatching { location -> val roadAddress = addressRepository.fetchCurrentAddress( - latitude = location.latitude, longitude = location.longitude, + latitude = location.latitude, ).getOrThrow() CurrentAddress( From 381eff0dd369df3eb42f09265e889ec44eb4038b Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Tue, 18 Nov 2025 23:23:24 +0900 Subject: [PATCH 22/44] =?UTF-8?q?Chore:=20=EB=AF=B8=EC=82=AC=EC=9A=A9=20?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- presentation/build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts index dd4172ee..2e5a2663 100644 --- a/presentation/build.gradle.kts +++ b/presentation/build.gradle.kts @@ -21,7 +21,6 @@ dependencies { implementation(libs.lottie.compose) implementation(libs.bundles.coil) implementation(libs.accompanist.permissions) - implementation(libs.play.services.location) testImplementation(libs.junit) testImplementation(libs.kotlin.coroutines.test) From eae927cb79d04570b684c9a6a50e5d1f8d5bbfb3 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Wed, 19 Nov 2025 13:21:35 +0900 Subject: [PATCH 23/44] =?UTF-8?q?Feat:=20ImageFile=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EB=AA=A8=EB=8D=B8=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bitnagil/domain/file/model/ImageFile.kt | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/file/model/ImageFile.kt diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/file/model/ImageFile.kt b/domain/src/main/java/com/threegap/bitnagil/domain/file/model/ImageFile.kt new file mode 100644 index 00000000..a66ee74f --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/file/model/ImageFile.kt @@ -0,0 +1,38 @@ +package com.threegap.bitnagil.domain.file.model + +/** + * 업로드할 이미지 파일을 나타내는 도메인 모델 + * + * @property prefix S3 경로의 접두사 (예: "report", "profile") + * @property name 파일명 (예: "image_1234567890.jpg") + * @property mimeType MIME 타입 (예: "image/jpeg", "image/png") + * @property bytes 이미지 파일의 바이트 데이터 + */ +data class ImageFile( + val prefix: String, + val name: String, + val mimeType: String, + val bytes: ByteArray, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ImageFile + + if (prefix != other.prefix) return false + if (name != other.name) return false + if (mimeType != other.mimeType) return false + if (!bytes.contentEquals(other.bytes)) return false + + return true + } + + override fun hashCode(): Int { + var result = prefix.hashCode() + result = 31 * result + name.hashCode() + result = 31 * result + mimeType.hashCode() + result = 31 * result + bytes.contentHashCode() + return result + } +} From be4d1af843d2a742668e598a18504d6e5036b750 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Wed, 19 Nov 2025 13:58:47 +0900 Subject: [PATCH 24/44] =?UTF-8?q?Feat:=20=ED=8C=8C=EC=9D=BC=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20Repository=20=EC=9D=B8=ED=84=B0=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bitnagil/domain/file/repository/FileRepository.kt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/file/repository/FileRepository.kt diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/file/repository/FileRepository.kt b/domain/src/main/java/com/threegap/bitnagil/domain/file/repository/FileRepository.kt new file mode 100644 index 00000000..c2094f3e --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/file/repository/FileRepository.kt @@ -0,0 +1,7 @@ +package com.threegap.bitnagil.domain.file.repository + +import com.threegap.bitnagil.domain.file.model.ImageFile + +interface FileRepository { + suspend fun uploadImages(imageFiles: List): Result> +} From 17cd7fba3d75f62c67f1bc8ad7d33287d777dca3 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Wed, 19 Nov 2025 14:00:36 +0900 Subject: [PATCH 25/44] =?UTF-8?q?Feat:=20FileInfoRequestDto=20=EC=A0=95?= =?UTF-8?q?=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/file/model/request/FileInfoRequestDto.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 data/src/main/java/com/threegap/bitnagil/data/file/model/request/FileInfoRequestDto.kt diff --git a/data/src/main/java/com/threegap/bitnagil/data/file/model/request/FileInfoRequestDto.kt b/data/src/main/java/com/threegap/bitnagil/data/file/model/request/FileInfoRequestDto.kt new file mode 100644 index 00000000..abbc4049 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/file/model/request/FileInfoRequestDto.kt @@ -0,0 +1,12 @@ +package com.threegap.bitnagil.data.file.model.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class FileInfoRequestDto( + @SerialName("prefix") + val prefix: String, + @SerialName("fileName") + val fileName: String, +) From 8465c9ae49b846afad6933897bd7e8341a7239ad Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Wed, 19 Nov 2025 14:02:04 +0900 Subject: [PATCH 26/44] =?UTF-8?q?Feat:=20FileService=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/threegap/bitnagil/di/data/ServiceModule.kt | 6 ++++++ .../bitnagil/data/file/service/FileService.kt | 13 +++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 data/src/main/java/com/threegap/bitnagil/data/file/service/FileService.kt diff --git a/app/src/main/java/com/threegap/bitnagil/di/data/ServiceModule.kt b/app/src/main/java/com/threegap/bitnagil/di/data/ServiceModule.kt index de2ab9a2..b36c1e8e 100644 --- a/app/src/main/java/com/threegap/bitnagil/di/data/ServiceModule.kt +++ b/app/src/main/java/com/threegap/bitnagil/di/data/ServiceModule.kt @@ -3,6 +3,7 @@ package com.threegap.bitnagil.di.data import com.threegap.bitnagil.data.address.service.AddressService import com.threegap.bitnagil.data.auth.service.AuthService import com.threegap.bitnagil.data.emotion.service.EmotionService +import com.threegap.bitnagil.data.file.service.FileService import com.threegap.bitnagil.data.onboarding.service.OnBoardingService import com.threegap.bitnagil.data.recommendroutine.service.RecommendRoutineService import com.threegap.bitnagil.data.routine.service.RoutineService @@ -73,4 +74,9 @@ object ServiceModule { @Singleton fun provideAddressService(@Kakao retrofit: Retrofit): AddressService = retrofit.create(AddressService::class.java) + + @Provides + @Singleton + fun fileService(@Auth retrofit: Retrofit): FileService = + retrofit.create(FileService::class.java) } diff --git a/data/src/main/java/com/threegap/bitnagil/data/file/service/FileService.kt b/data/src/main/java/com/threegap/bitnagil/data/file/service/FileService.kt new file mode 100644 index 00000000..240fe324 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/file/service/FileService.kt @@ -0,0 +1,13 @@ +package com.threegap.bitnagil.data.file.service + +import com.threegap.bitnagil.data.file.model.request.FileInfoRequestDto +import com.threegap.bitnagil.network.model.BaseResponse +import retrofit2.http.Body +import retrofit2.http.POST + +interface FileService { + @POST("/api/v2/files/presigned-urls") + suspend fun fetchPresignedUrls( + @Body fileInfos: List, + ): BaseResponse> +} From 2d2371f20f94814435387c43a8f70d6dfbd9b494 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Wed, 19 Nov 2025 14:02:40 +0900 Subject: [PATCH 27/44] =?UTF-8?q?Feat:=20FileDataSource=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../threegap/bitnagil/di/data/DataSourceModule.kt | 6 ++++++ .../data/file/datasource/FileDataSource.kt | 12 ++++++++++++ .../file/datasourceImpl/FileDataSourceImpl.kt | 15 +++++++++++++++ 3 files changed, 33 insertions(+) create mode 100644 data/src/main/java/com/threegap/bitnagil/data/file/datasource/FileDataSource.kt create mode 100644 data/src/main/java/com/threegap/bitnagil/data/file/datasourceImpl/FileDataSourceImpl.kt diff --git a/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt b/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt index 39045bf5..fc8a7209 100644 --- a/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt +++ b/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt @@ -10,6 +10,7 @@ import com.threegap.bitnagil.data.auth.datasourceimpl.AuthLocalDataSourceImpl import com.threegap.bitnagil.data.auth.datasourceimpl.AuthRemoteDataSourceImpl import com.threegap.bitnagil.data.emotion.datasource.EmotionDataSource import com.threegap.bitnagil.data.emotion.datasourceImpl.EmotionDataSourceImpl +import com.threegap.bitnagil.data.file.repositoryImpl.FileRepositoryImpl import com.threegap.bitnagil.data.onboarding.datasource.OnBoardingDataSource import com.threegap.bitnagil.data.onboarding.datasourceImpl.OnBoardingDataSourceImpl import com.threegap.bitnagil.data.recommendroutine.datasource.RecommendRoutineDataSource @@ -22,6 +23,7 @@ import com.threegap.bitnagil.data.version.datasource.VersionDataSource import com.threegap.bitnagil.data.version.datasourceImpl.VersionDataSourceImpl import com.threegap.bitnagil.data.writeroutine.datasource.WriteRoutineDataSource import com.threegap.bitnagil.data.writeroutine.datasourceImpl.WriteRoutineDataSourceImpl +import com.threegap.bitnagil.domain.file.repository.FileRepository import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -75,4 +77,8 @@ abstract class DataSourceModule { @Binds @Singleton abstract fun bindAddressDataSource(addressDataSourceImpl: AddressDataSourceImpl): AddressDataSource + + @Binds + @Singleton + abstract fun bindFileRepository(impl: FileRepositoryImpl): FileRepository } diff --git a/data/src/main/java/com/threegap/bitnagil/data/file/datasource/FileDataSource.kt b/data/src/main/java/com/threegap/bitnagil/data/file/datasource/FileDataSource.kt new file mode 100644 index 00000000..62bf670b --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/file/datasource/FileDataSource.kt @@ -0,0 +1,12 @@ +package com.threegap.bitnagil.data.file.datasource + +import com.threegap.bitnagil.data.file.model.request.FileInfoRequestDto + +interface FileDataSource { + /** + * presigned URL 요청 + * @param fileInfos 파일 정보 리스트 (prefix + fileName) + * @return key: S3 경로, value: presigned URL + */ + suspend fun fetchPresignedUrls(fileInfos: List): Result> +} diff --git a/data/src/main/java/com/threegap/bitnagil/data/file/datasourceImpl/FileDataSourceImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/file/datasourceImpl/FileDataSourceImpl.kt new file mode 100644 index 00000000..b584d017 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/file/datasourceImpl/FileDataSourceImpl.kt @@ -0,0 +1,15 @@ +package com.threegap.bitnagil.data.file.datasourceImpl + +import com.threegap.bitnagil.data.common.safeApiCall +import com.threegap.bitnagil.data.file.datasource.FileDataSource +import com.threegap.bitnagil.data.file.model.request.FileInfoRequestDto +import com.threegap.bitnagil.data.file.service.FileService +import javax.inject.Inject + +class FileDataSourceImpl @Inject constructor( + private val fileService: FileService, +) : FileDataSource { + override suspend fun fetchPresignedUrls(fileInfos: List): Result> { + return safeApiCall { fileService.fetchPresignedUrls(fileInfos) } + } +} From abe93297167d9e2a6fd8c55e00dc903b0461c6de Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Wed, 19 Nov 2025 14:07:36 +0900 Subject: [PATCH 28/44] =?UTF-8?q?Feat:=20ImageUploader=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PresignedUrl 업로드를 위한 ImageUploader 구현 - Qualifier 선언 모듈 변경 (app -> network) --- .../bitnagil/di/core/NetworkModule.kt | 3 ++ .../bitnagil/di/data/ServiceModule.kt | 6 +-- core/network/build.gradle.kts | 1 + .../threegap/bitnagil/network}/Qualifier.kt | 2 +- .../data/file/uploader/ImageUploader.kt | 40 +++++++++++++++++++ 5 files changed, 48 insertions(+), 4 deletions(-) rename {app/src/main/java/com/threegap/bitnagil/di/core => core/network/src/main/java/com/threegap/bitnagil/network}/Qualifier.kt (87%) create mode 100644 data/src/main/java/com/threegap/bitnagil/data/file/uploader/ImageUploader.kt diff --git a/app/src/main/java/com/threegap/bitnagil/di/core/NetworkModule.kt b/app/src/main/java/com/threegap/bitnagil/di/core/NetworkModule.kt index c761a9fa..1792fa7b 100644 --- a/app/src/main/java/com/threegap/bitnagil/di/core/NetworkModule.kt +++ b/app/src/main/java/com/threegap/bitnagil/di/core/NetworkModule.kt @@ -5,6 +5,9 @@ import android.content.Intent import com.threegap.bitnagil.BuildConfig import com.threegap.bitnagil.MainActivity import com.threegap.bitnagil.datastore.auth.storage.AuthTokenDataStore +import com.threegap.bitnagil.network.Auth +import com.threegap.bitnagil.network.Kakao +import com.threegap.bitnagil.network.NoneAuth import com.threegap.bitnagil.network.auth.AuthInterceptor import com.threegap.bitnagil.network.auth.TokenAuthenticator import com.threegap.bitnagil.network.token.ReissueService diff --git a/app/src/main/java/com/threegap/bitnagil/di/data/ServiceModule.kt b/app/src/main/java/com/threegap/bitnagil/di/data/ServiceModule.kt index b36c1e8e..443adb85 100644 --- a/app/src/main/java/com/threegap/bitnagil/di/data/ServiceModule.kt +++ b/app/src/main/java/com/threegap/bitnagil/di/data/ServiceModule.kt @@ -10,9 +10,9 @@ import com.threegap.bitnagil.data.routine.service.RoutineService import com.threegap.bitnagil.data.user.service.UserService import com.threegap.bitnagil.data.version.service.VersionService import com.threegap.bitnagil.data.writeroutine.service.WriteRoutineService -import com.threegap.bitnagil.di.core.Auth -import com.threegap.bitnagil.di.core.Kakao -import com.threegap.bitnagil.di.core.NoneAuth +import com.threegap.bitnagil.network.Auth +import com.threegap.bitnagil.network.Kakao +import com.threegap.bitnagil.network.NoneAuth import com.threegap.bitnagil.network.token.ReissueService import dagger.Module import dagger.Provides diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index 36852524..f3aa8160 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -13,4 +13,5 @@ dependencies { implementation(libs.bundles.retrofit) implementation(platform(libs.okhttp.bom)) implementation(libs.bundles.okhttp) + implementation(libs.javax.inject) } diff --git a/app/src/main/java/com/threegap/bitnagil/di/core/Qualifier.kt b/core/network/src/main/java/com/threegap/bitnagil/network/Qualifier.kt similarity index 87% rename from app/src/main/java/com/threegap/bitnagil/di/core/Qualifier.kt rename to core/network/src/main/java/com/threegap/bitnagil/network/Qualifier.kt index b2db3e56..5fd9294e 100644 --- a/app/src/main/java/com/threegap/bitnagil/di/core/Qualifier.kt +++ b/core/network/src/main/java/com/threegap/bitnagil/network/Qualifier.kt @@ -1,4 +1,4 @@ -package com.threegap.bitnagil.di.core +package com.threegap.bitnagil.network import javax.inject.Qualifier diff --git a/data/src/main/java/com/threegap/bitnagil/data/file/uploader/ImageUploader.kt b/data/src/main/java/com/threegap/bitnagil/data/file/uploader/ImageUploader.kt new file mode 100644 index 00000000..0a3a5486 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/file/uploader/ImageUploader.kt @@ -0,0 +1,40 @@ +package com.threegap.bitnagil.data.file.uploader + +import android.util.Log +import com.threegap.bitnagil.domain.file.model.ImageFile +import com.threegap.bitnagil.network.NoneAuth +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import java.io.IOException +import javax.inject.Inject + +class ImageUploader @Inject constructor( + @NoneAuth private val okHttpClient: OkHttpClient, +) { + suspend fun uploadToPresignedUrl(imageFile: ImageFile, presignedUrl: String): Result { + return withContext(Dispatchers.IO) { + runCatching { + val requestBody = imageFile.bytes.toRequestBody( + contentType = imageFile.mimeType.toMediaTypeOrNull(), + ) + + val request = Request.Builder() + .url(presignedUrl) + .put(requestBody) + .build() + + okHttpClient.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + val errorBody = response.body?.string() ?: "No error details" + Log.e("ImageUploader", "Upload failed: ${imageFile.name}, code=${response.code}, error=$errorBody") + throw IOException("Upload failed: ${response.code}") + } + } + } + } + } +} From ee3bffaa8575fb9eb2c97c271d760d52658e5635 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Thu, 20 Nov 2025 01:14:12 +0900 Subject: [PATCH 29/44] =?UTF-8?q?Feat:=20S3=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이미지 파일 목록을 받아 Presigned URL을 요청합니다. - 발급받은 Presigned URL을 사용하여 S3에 이미지를 업로드합니다. - 업로드 성공 시, 이미지의 key 목록을 반환합니다. --- .../bitnagil/di/data/DataSourceModule.kt | 6 +-- .../bitnagil/di/data/RepositoryModule.kt | 6 +++ .../file/repositoryImpl/FileRepositoryImpl.kt | 42 +++++++++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 data/src/main/java/com/threegap/bitnagil/data/file/repositoryImpl/FileRepositoryImpl.kt diff --git a/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt b/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt index fc8a7209..2e596b02 100644 --- a/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt +++ b/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt @@ -10,7 +10,8 @@ import com.threegap.bitnagil.data.auth.datasourceimpl.AuthLocalDataSourceImpl import com.threegap.bitnagil.data.auth.datasourceimpl.AuthRemoteDataSourceImpl import com.threegap.bitnagil.data.emotion.datasource.EmotionDataSource import com.threegap.bitnagil.data.emotion.datasourceImpl.EmotionDataSourceImpl -import com.threegap.bitnagil.data.file.repositoryImpl.FileRepositoryImpl +import com.threegap.bitnagil.data.file.datasource.FileDataSource +import com.threegap.bitnagil.data.file.datasourceImpl.FileDataSourceImpl import com.threegap.bitnagil.data.onboarding.datasource.OnBoardingDataSource import com.threegap.bitnagil.data.onboarding.datasourceImpl.OnBoardingDataSourceImpl import com.threegap.bitnagil.data.recommendroutine.datasource.RecommendRoutineDataSource @@ -23,7 +24,6 @@ import com.threegap.bitnagil.data.version.datasource.VersionDataSource import com.threegap.bitnagil.data.version.datasourceImpl.VersionDataSourceImpl import com.threegap.bitnagil.data.writeroutine.datasource.WriteRoutineDataSource import com.threegap.bitnagil.data.writeroutine.datasourceImpl.WriteRoutineDataSourceImpl -import com.threegap.bitnagil.domain.file.repository.FileRepository import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -80,5 +80,5 @@ abstract class DataSourceModule { @Binds @Singleton - abstract fun bindFileRepository(impl: FileRepositoryImpl): FileRepository + abstract fun bindFileDataSource(impl: FileDataSourceImpl): FileDataSource } diff --git a/app/src/main/java/com/threegap/bitnagil/di/data/RepositoryModule.kt b/app/src/main/java/com/threegap/bitnagil/di/data/RepositoryModule.kt index 88fef29e..94ce0e43 100644 --- a/app/src/main/java/com/threegap/bitnagil/di/data/RepositoryModule.kt +++ b/app/src/main/java/com/threegap/bitnagil/di/data/RepositoryModule.kt @@ -3,6 +3,7 @@ package com.threegap.bitnagil.di.data import com.threegap.bitnagil.data.address.repositoryImpl.AddressRepositoryImpl import com.threegap.bitnagil.data.auth.repositoryimpl.AuthRepositoryImpl import com.threegap.bitnagil.data.emotion.repositoryImpl.EmotionRepositoryImpl +import com.threegap.bitnagil.data.file.repositoryImpl.FileRepositoryImpl import com.threegap.bitnagil.data.onboarding.repositoryImpl.OnBoardingRepositoryImpl import com.threegap.bitnagil.data.recommendroutine.repositoryImpl.RecommendRoutineRepositoryImpl import com.threegap.bitnagil.data.routine.repositoryImpl.RoutineRepositoryImpl @@ -12,6 +13,7 @@ import com.threegap.bitnagil.data.writeroutine.repositoryImpl.WriteRoutineReposi import com.threegap.bitnagil.domain.address.repository.AddressRepository import com.threegap.bitnagil.domain.auth.repository.AuthRepository import com.threegap.bitnagil.domain.emotion.repository.EmotionRepository +import com.threegap.bitnagil.domain.file.repository.FileRepository import com.threegap.bitnagil.domain.onboarding.repository.OnBoardingRepository import com.threegap.bitnagil.domain.recommendroutine.repository.RecommendRoutineRepository import com.threegap.bitnagil.domain.routine.repository.RoutineRepository @@ -63,4 +65,8 @@ abstract class RepositoryModule { @Binds @Singleton abstract fun bindAddressRepository(addressRepositoryImpl: AddressRepositoryImpl): AddressRepository + + @Binds + @Singleton + abstract fun bindFileRepository(impl: FileRepositoryImpl): FileRepository } diff --git a/data/src/main/java/com/threegap/bitnagil/data/file/repositoryImpl/FileRepositoryImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/file/repositoryImpl/FileRepositoryImpl.kt new file mode 100644 index 00000000..dcfb0658 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/file/repositoryImpl/FileRepositoryImpl.kt @@ -0,0 +1,42 @@ +package com.threegap.bitnagil.data.file.repositoryImpl + +import com.threegap.bitnagil.data.file.datasource.FileDataSource +import com.threegap.bitnagil.data.file.model.request.FileInfoRequestDto +import com.threegap.bitnagil.data.file.uploader.ImageUploader +import com.threegap.bitnagil.domain.file.model.ImageFile +import com.threegap.bitnagil.domain.file.repository.FileRepository +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import javax.inject.Inject + +class FileRepositoryImpl @Inject constructor( + private val fileDataSource: FileDataSource, + private val imageUploader: ImageUploader, +) : FileRepository { + override suspend fun uploadImages(imageFiles: List): Result> { + return runCatching { + val fileInfos = imageFiles.map { imageFile -> + FileInfoRequestDto(prefix = imageFile.prefix, fileName = imageFile.name) + } + + val presignedUrlMap: Map = fileDataSource + .fetchPresignedUrls(fileInfos) + .getOrThrow() + + val presignedUrls = presignedUrlMap.values.toList() + + coroutineScope { + imageFiles.zip(presignedUrls).map { (imageFile, presignedUrl) -> + async { + imageUploader + .uploadToPresignedUrl(imageFile, presignedUrl) + .getOrThrow() + } + }.awaitAll() + } + + presignedUrls.map { url -> url.substringBefore("?") } + } + } +} From 23943672f167c9210548cc4d9070ea82d0352e38 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Thu, 20 Nov 2025 01:16:45 +0900 Subject: [PATCH 30/44] =?UTF-8?q?Feat:=20=EC=A0=9C=EB=B3=B4=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=97=85=EB=A1=9C=EB=93=9C=20UseCase=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../file/usecase/UploadReportImagesUseCase.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/file/usecase/UploadReportImagesUseCase.kt diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/file/usecase/UploadReportImagesUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/file/usecase/UploadReportImagesUseCase.kt new file mode 100644 index 00000000..b792aa80 --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/file/usecase/UploadReportImagesUseCase.kt @@ -0,0 +1,13 @@ +package com.threegap.bitnagil.domain.file.usecase + +import com.threegap.bitnagil.domain.file.model.ImageFile +import com.threegap.bitnagil.domain.file.repository.FileRepository +import javax.inject.Inject + +class UploadReportImagesUseCase @Inject constructor( + private val fileRepository: FileRepository, +) { + suspend operator fun invoke(imageFiles: List): Result> { + return fileRepository.uploadImages(imageFiles) + } +} From 29592f5ce450e5e77f7a4cc3ba7a6da060d042fb Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Thu, 20 Nov 2025 01:24:10 +0900 Subject: [PATCH 31/44] =?UTF-8?q?Feat:=20Report=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EB=AA=A8=EB=8D=B8=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ReportCategory enum class 정의 --- .../bitnagil/domain/report/model/Report.kt | 11 +++++++ .../domain/report/model/ReportCategory.kt | 30 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/report/model/Report.kt create mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/report/model/ReportCategory.kt diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/report/model/Report.kt b/domain/src/main/java/com/threegap/bitnagil/domain/report/model/Report.kt new file mode 100644 index 00000000..881025cf --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/report/model/Report.kt @@ -0,0 +1,11 @@ +package com.threegap.bitnagil.domain.report.model + +data class Report( + val title: String, + val content: String, + val category: ReportCategory, + val imageUrls: List, + val address: String, + val latitude: Double, + val longitude: Double, +) diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/report/model/ReportCategory.kt b/domain/src/main/java/com/threegap/bitnagil/domain/report/model/ReportCategory.kt new file mode 100644 index 00000000..51315b3c --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/report/model/ReportCategory.kt @@ -0,0 +1,30 @@ +package com.threegap.bitnagil.domain.report.model + +enum class ReportCategory { + TRANSPORTATION, + LIGHTING, + WATERFACILITY, + AMENITY, + ; + + companion object { + fun fromString(value: String): ReportCategory { + return when (value) { + "TRANSPORTATION" -> TRANSPORTATION + "LIGHTING" -> LIGHTING + "WATERFACILITY" -> WATERFACILITY + "AMENITY" -> AMENITY + else -> throw IllegalArgumentException("Invalid ReportCategory value: $value") + } + } + + fun toString(value: ReportCategory): String { + return when (value) { + TRANSPORTATION -> "TRANSPORTATION" + LIGHTING -> "LIGHTING" + WATERFACILITY -> "WATERFACILITY" + AMENITY -> "AMENITY" + } + } + } +} From 9bcf5e240c776009a3d747c94746adc92d8c6a34 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Thu, 20 Nov 2025 01:26:21 +0900 Subject: [PATCH 32/44] =?UTF-8?q?Feat:=20=EC=A0=9C=EB=B3=B4=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20API=20=EB=B0=8F=20data=20=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=96=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 제보하기 API service를 추가합니다. - 제보하기 관련 data, domain 레이어 모듈(DataSource, Repository, UseCase)을 추가합니다. - 제보하기 관련 DI Module을 추가합니다. --- .../bitnagil/di/data/DataSourceModule.kt | 6 ++++ .../bitnagil/di/data/RepositoryModule.kt | 6 ++++ .../bitnagil/di/data/ServiceModule.kt | 8 ++++- .../report/datasource/ReportDataSource.kt | 7 ++++ .../datasourceImpl/ReportDataSourceImpl.kt | 15 ++++++++ .../report/model/request/ReportRequestDto.kt | 36 +++++++++++++++++++ .../repositoryImpl/ReportRepositoryImpl.kt | 15 ++++++++ .../data/report/service/ReportService.kt | 13 +++++++ .../report/repository/ReportRepository.kt | 7 ++++ .../report/usecase/SubmitReportUseCase.kt | 13 +++++++ 10 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 data/src/main/java/com/threegap/bitnagil/data/report/datasource/ReportDataSource.kt create mode 100644 data/src/main/java/com/threegap/bitnagil/data/report/datasourceImpl/ReportDataSourceImpl.kt create mode 100644 data/src/main/java/com/threegap/bitnagil/data/report/model/request/ReportRequestDto.kt create mode 100644 data/src/main/java/com/threegap/bitnagil/data/report/repositoryImpl/ReportRepositoryImpl.kt create mode 100644 data/src/main/java/com/threegap/bitnagil/data/report/service/ReportService.kt create mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/report/repository/ReportRepository.kt create mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/report/usecase/SubmitReportUseCase.kt diff --git a/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt b/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt index 2e596b02..0d399e5a 100644 --- a/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt +++ b/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt @@ -16,6 +16,8 @@ import com.threegap.bitnagil.data.onboarding.datasource.OnBoardingDataSource import com.threegap.bitnagil.data.onboarding.datasourceImpl.OnBoardingDataSourceImpl import com.threegap.bitnagil.data.recommendroutine.datasource.RecommendRoutineDataSource import com.threegap.bitnagil.data.recommendroutine.datasourceImpl.RecommendRoutineDataSourceImpl +import com.threegap.bitnagil.data.report.datasource.ReportDataSource +import com.threegap.bitnagil.data.report.datasourceImpl.ReportDataSourceImpl import com.threegap.bitnagil.data.routine.datasource.RoutineRemoteDataSource import com.threegap.bitnagil.data.routine.datasourceImpl.RoutineRemoteDataSourceImpl import com.threegap.bitnagil.data.user.datasource.UserDataSource @@ -81,4 +83,8 @@ abstract class DataSourceModule { @Binds @Singleton abstract fun bindFileDataSource(impl: FileDataSourceImpl): FileDataSource + + @Binds + @Singleton + abstract fun bindReportDataSource(impl: ReportDataSourceImpl): ReportDataSource } diff --git a/app/src/main/java/com/threegap/bitnagil/di/data/RepositoryModule.kt b/app/src/main/java/com/threegap/bitnagil/di/data/RepositoryModule.kt index 94ce0e43..cc8ddb90 100644 --- a/app/src/main/java/com/threegap/bitnagil/di/data/RepositoryModule.kt +++ b/app/src/main/java/com/threegap/bitnagil/di/data/RepositoryModule.kt @@ -6,6 +6,7 @@ import com.threegap.bitnagil.data.emotion.repositoryImpl.EmotionRepositoryImpl import com.threegap.bitnagil.data.file.repositoryImpl.FileRepositoryImpl import com.threegap.bitnagil.data.onboarding.repositoryImpl.OnBoardingRepositoryImpl import com.threegap.bitnagil.data.recommendroutine.repositoryImpl.RecommendRoutineRepositoryImpl +import com.threegap.bitnagil.data.report.repositoryImpl.ReportRepositoryImpl import com.threegap.bitnagil.data.routine.repositoryImpl.RoutineRepositoryImpl import com.threegap.bitnagil.data.user.repositoryImpl.UserRepositoryImpl import com.threegap.bitnagil.data.version.repositoryImpl.VersionRepositoryImpl @@ -16,6 +17,7 @@ import com.threegap.bitnagil.domain.emotion.repository.EmotionRepository import com.threegap.bitnagil.domain.file.repository.FileRepository import com.threegap.bitnagil.domain.onboarding.repository.OnBoardingRepository import com.threegap.bitnagil.domain.recommendroutine.repository.RecommendRoutineRepository +import com.threegap.bitnagil.domain.report.repository.ReportRepository import com.threegap.bitnagil.domain.routine.repository.RoutineRepository import com.threegap.bitnagil.domain.user.repository.UserRepository import com.threegap.bitnagil.domain.version.repository.VersionRepository @@ -69,4 +71,8 @@ abstract class RepositoryModule { @Binds @Singleton abstract fun bindFileRepository(impl: FileRepositoryImpl): FileRepository + + @Binds + @Singleton + abstract fun bindReportRepository(impl: ReportRepositoryImpl): ReportRepository } diff --git a/app/src/main/java/com/threegap/bitnagil/di/data/ServiceModule.kt b/app/src/main/java/com/threegap/bitnagil/di/data/ServiceModule.kt index 443adb85..602d4a26 100644 --- a/app/src/main/java/com/threegap/bitnagil/di/data/ServiceModule.kt +++ b/app/src/main/java/com/threegap/bitnagil/di/data/ServiceModule.kt @@ -6,6 +6,7 @@ import com.threegap.bitnagil.data.emotion.service.EmotionService import com.threegap.bitnagil.data.file.service.FileService import com.threegap.bitnagil.data.onboarding.service.OnBoardingService import com.threegap.bitnagil.data.recommendroutine.service.RecommendRoutineService +import com.threegap.bitnagil.data.report.service.ReportService import com.threegap.bitnagil.data.routine.service.RoutineService import com.threegap.bitnagil.data.user.service.UserService import com.threegap.bitnagil.data.version.service.VersionService @@ -77,6 +78,11 @@ object ServiceModule { @Provides @Singleton - fun fileService(@Auth retrofit: Retrofit): FileService = + fun provideFileService(@Auth retrofit: Retrofit): FileService = retrofit.create(FileService::class.java) + + @Provides + @Singleton + fun provideReportService(@Auth retrofit: Retrofit): ReportService = + retrofit.create(ReportService::class.java) } 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 new file mode 100644 index 00000000..2c2170f7 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/report/datasource/ReportDataSource.kt @@ -0,0 +1,7 @@ +package com.threegap.bitnagil.data.report.datasource + +import com.threegap.bitnagil.data.report.model.request.ReportRequestDto + +interface ReportDataSource { + suspend fun submitReport(reportRequestDto: ReportRequestDto): 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 new file mode 100644 index 00000000..14305d47 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/report/datasourceImpl/ReportDataSourceImpl.kt @@ -0,0 +1,15 @@ +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.service.ReportService +import javax.inject.Inject + +class ReportDataSourceImpl @Inject constructor( + private val reportService: ReportService, +) : ReportDataSource { + override suspend fun submitReport(reportRequestDto: ReportRequestDto): Result { + return safeApiCall { reportService.submitReport(reportRequestDto) } + } +} diff --git a/data/src/main/java/com/threegap/bitnagil/data/report/model/request/ReportRequestDto.kt b/data/src/main/java/com/threegap/bitnagil/data/report/model/request/ReportRequestDto.kt new file mode 100644 index 00000000..3c82651f --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/report/model/request/ReportRequestDto.kt @@ -0,0 +1,36 @@ +package com.threegap.bitnagil.data.report.model.request + +import com.threegap.bitnagil.domain.report.model.Report +import com.threegap.bitnagil.domain.report.model.ReportCategory +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ReportRequestDto( + @SerialName("reportTitle") + val reportTitle: String, + @SerialName("reportContent") + val reportContent: String, + @SerialName("reportCategory") + val reportCategory: String, + @SerialName("reportImageUrls") + val reportImageUrls: List, + @SerialName("reportLocation") + val reportLocation: String, + @SerialName("latitude") + val latitude: Double, + @SerialName("longitude") + val longitude: Double, +) + +fun Report.toDto(): ReportRequestDto { + return ReportRequestDto( + reportTitle = this.title, + reportContent = this.content, + reportCategory = ReportCategory.toString(this.category), + reportImageUrls = this.imageUrls, + reportLocation = this.address, + latitude = this.latitude, + longitude = this.longitude, + ) +} 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 new file mode 100644 index 00000000..f0261faf --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/report/repositoryImpl/ReportRepositoryImpl.kt @@ -0,0 +1,15 @@ +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.domain.report.model.Report +import com.threegap.bitnagil.domain.report.repository.ReportRepository +import javax.inject.Inject + +class ReportRepositoryImpl @Inject constructor( + private val reportDataSource: ReportDataSource, +) : ReportRepository { + override suspend fun submitReport(report: Report): Result { + return reportDataSource.submitReport(report.toDto()) + } +} 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 new file mode 100644 index 00000000..27a697f0 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/report/service/ReportService.kt @@ -0,0 +1,13 @@ +package com.threegap.bitnagil.data.report.service + +import com.threegap.bitnagil.data.report.model.request.ReportRequestDto +import com.threegap.bitnagil.network.model.BaseResponse +import retrofit2.http.Body +import retrofit2.http.POST + +interface ReportService { + @POST("/api/v2/reports") + suspend fun submitReport( + @Body reportRequestDto: ReportRequestDto, + ): BaseResponse +} 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 new file mode 100644 index 00000000..b6cd018a --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/report/repository/ReportRepository.kt @@ -0,0 +1,7 @@ +package com.threegap.bitnagil.domain.report.repository + +import com.threegap.bitnagil.domain.report.model.Report + +interface ReportRepository { + suspend fun submitReport(report: Report): Result +} diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/report/usecase/SubmitReportUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/report/usecase/SubmitReportUseCase.kt new file mode 100644 index 00000000..1d0de378 --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/report/usecase/SubmitReportUseCase.kt @@ -0,0 +1,13 @@ +package com.threegap.bitnagil.domain.report.usecase + +import com.threegap.bitnagil.domain.report.model.Report +import com.threegap.bitnagil.domain.report.repository.ReportRepository +import javax.inject.Inject + +class SubmitReportUseCase @Inject constructor( + private val reportRepository: ReportRepository, +) { + suspend operator fun invoke(report: Report): Result { + return reportRepository.submitReport(report) + } +} From c8beeb1f7e16a9a056a10f663006d03589da7034 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Thu, 20 Nov 2025 01:32:38 +0900 Subject: [PATCH 33/44] =?UTF-8?q?Chore:=20ui=EC=97=90=EC=84=9C=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=A4=91=EC=9D=B8=20ReportCategory=20=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=EB=B0=8D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ReportCategory -> ReportCategoryUi --- .../bitnagil/presentation/report/ReportViewModel.kt | 4 ++-- .../report/component/ReportCategoryBottomSheet.kt | 10 +++++----- .../model/{ReportCategory.kt => ReportCategoryUi.kt} | 12 +++++++++++- 3 files changed, 18 insertions(+), 8 deletions(-) rename presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/{ReportCategory.kt => ReportCategoryUi.kt} (68%) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt index ce888683..13c935dc 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt @@ -53,7 +53,7 @@ class ReportViewModel @Inject constructor( } } - fun selectReportCategory(category: ReportCategory) { + fun selectReportCategory(category: ReportCategoryUi) { intent { reduce { state.copy(selectedCategory = category) } } @@ -109,7 +109,7 @@ data class ReportState( val reportImages: List, val reportTitle: String, val reportDescription: String, - val selectedCategory: ReportCategory?, + val selectedCategory: ReportCategoryUi?, val imageSourceBottomSheetVisible: Boolean, val reportCategoryBottomSheetVisible: Boolean, val currentAddress: String?, diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/ReportCategoryBottomSheet.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/ReportCategoryBottomSheet.kt index 55ca576a..acbdd7cf 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/ReportCategoryBottomSheet.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/ReportCategoryBottomSheet.kt @@ -24,15 +24,15 @@ 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.report.model.ReportCategory +import com.threegap.bitnagil.presentation.report.model.ReportCategoryUi import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable fun ReportCategoryBottomSheet( - selectedCategory: ReportCategory?, + selectedCategory: ReportCategoryUi?, onDismiss: () -> Unit, - onSelected: (ReportCategory) -> Unit, + onSelected: (ReportCategoryUi) -> Unit, modifier: Modifier = Modifier, ) { val sheetState = rememberModalBottomSheetState() @@ -48,7 +48,7 @@ fun ReportCategoryBottomSheet( Column( modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 28.dp), ) { - ReportCategory.entries.forEachIndexed { index, category -> + ReportCategoryUi.entries.forEachIndexed { index, category -> ReportCategoryItem( icon = category.icon, title = category.title, @@ -124,7 +124,7 @@ private fun ReportCategoryItem( @Composable private fun ReportCategoryBottomSheetPreview() { ReportCategoryBottomSheet( - selectedCategory = ReportCategory.WATERFACILITY, + selectedCategory = ReportCategoryUi.WATERFACILITY, onDismiss = {}, onSelected = {}, ) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportCategory.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportCategoryUi.kt similarity index 68% rename from presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportCategory.kt rename to presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportCategoryUi.kt index 1e282945..74be04de 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportCategory.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportCategoryUi.kt @@ -2,8 +2,9 @@ package com.threegap.bitnagil.presentation.report.model import androidx.annotation.DrawableRes import com.threegap.bitnagil.designsystem.R +import com.threegap.bitnagil.domain.report.model.ReportCategory -enum class ReportCategory( +enum class ReportCategoryUi( @DrawableRes val icon: Int, val title: String, val description: String, @@ -29,4 +30,13 @@ enum class ReportCategory( description = "벤치 파손, 휴지통 넘침 등", ), ; + + fun toDomain(): ReportCategory { + return when (this) { + TRANSPORTATION -> ReportCategory.TRANSPORTATION + LIGHTING -> ReportCategory.LIGHTING + WATERFACILITY -> ReportCategory.WATERFACILITY + AMENITY -> ReportCategory.AMENITY + } + } } From e9cbb52cb0f434e0a669880868224c70bc2f154b Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Thu, 20 Nov 2025 17:00:06 +0900 Subject: [PATCH 34/44] =?UTF-8?q?Refactor:=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=20=EA=B4=80=EB=A0=A8=20=EC=9C=A0=ED=8B=B8?= =?UTF-8?q?=EB=A6=AC=ED=8B=B0=20=ED=95=A8=EC=88=98=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/file/ImageFileConverter.kt | 64 +++++++++++++++++++ .../presentation/report/ReportScreen.kt | 12 +--- 2 files changed, 66 insertions(+), 10 deletions(-) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/common/file/ImageFileConverter.kt diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/common/file/ImageFileConverter.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/common/file/ImageFileConverter.kt new file mode 100644 index 00000000..f2748f81 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/common/file/ImageFileConverter.kt @@ -0,0 +1,64 @@ +package com.threegap.bitnagil.presentation.common.file + +import android.content.ContentResolver +import android.content.Context +import android.net.Uri +import android.provider.OpenableColumns +import android.webkit.MimeTypeMap +import androidx.core.content.FileProvider +import com.threegap.bitnagil.domain.file.model.ImageFile +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File +import java.util.UUID + +fun extractFileName(uri: Uri, contentResolver: ContentResolver): String { + contentResolver.query( + uri, + arrayOf(OpenableColumns.DISPLAY_NAME), + null, + null, + null, + )?.use { cursor -> + if (cursor.moveToFirst()) { + val name = cursor.getString(0) + if (!name.isNullOrBlank()) return name + } + } + + val extension = MimeTypeMap.getSingleton() + .getExtensionFromMimeType(contentResolver.getType(uri)) ?: "jpg" + + val uniqueId = UUID.randomUUID().toString().take(8) + return "image_${System.currentTimeMillis()}_$uniqueId.$extension" +} + +suspend fun convertUriToImageFile( + uri: Uri, + prefix: String, + context: Context, +): ImageFile? = withContext(Dispatchers.IO) { + try { + val contentResolver = context.contentResolver + val fileName = extractFileName(uri, contentResolver) + val mimeType = contentResolver.getType(uri) ?: "image/jpeg" + + val bytes = contentResolver.openInputStream(uri)?.use { it.readBytes() } + ?: return@withContext null + + ImageFile( + prefix = prefix, + name = fileName, + mimeType = mimeType, + bytes = bytes, + ) + } catch (e: Exception) { + null + } +} + +fun Context.createCameraImageUri(): Uri { + val cameraDir = File(cacheDir, "camera").apply { if (!exists()) mkdirs() } + val imageFile = File(cameraDir, "camera_${System.currentTimeMillis()}.jpg") + return FileProvider.getUriForFile(this, "$packageName.provider", imageFile) +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportScreen.kt index 625bcba9..eef553b6 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportScreen.kt @@ -1,7 +1,6 @@ package com.threegap.bitnagil.presentation.report import android.Manifest -import android.content.Context import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest @@ -35,7 +34,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.core.content.FileProvider import androidx.hilt.navigation.compose.hiltViewModel import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.threegap.bitnagil.designsystem.BitnagilTheme @@ -43,6 +41,7 @@ import com.threegap.bitnagil.designsystem.component.atom.BitnagilTextButton import com.threegap.bitnagil.designsystem.component.atom.BitnagilTextButtonColor import com.threegap.bitnagil.designsystem.component.atom.BitnagilTextField import com.threegap.bitnagil.designsystem.component.block.BitnagilTopBar +import com.threegap.bitnagil.presentation.common.file.createCameraImageUri import com.threegap.bitnagil.presentation.common.premission.rememberPermissionHandler import com.threegap.bitnagil.presentation.report.component.AddPhotoButton import com.threegap.bitnagil.presentation.report.component.CurrentLocationInput @@ -53,7 +52,6 @@ import com.threegap.bitnagil.presentation.report.component.ReportCategorySelecto import com.threegap.bitnagil.presentation.report.component.ReportField import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect -import java.io.File @OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class) @Composable @@ -89,17 +87,11 @@ fun ReportScreenContainer( }, ) - fun createImageUri(context: Context): Uri { - val cameraDir = File(context.cacheDir, "camera").apply { if (!exists()) mkdirs() } - val imageFile = File(cameraDir, "camera_${System.currentTimeMillis()}.jpg") - return FileProvider.getUriForFile(context, "${context.packageName}.provider", imageFile) - } - val cameraPermissionHandler = rememberPermissionHandler( permission = Manifest.permission.CAMERA, dialogDescription = "카메라 권한이 비활성화됐어요.\n설정에서 허용해 주세요.", onGranted = { - val imageUri = createImageUri(context) + val imageUri = context.createCameraImageUri() pendingCameraPhotoUri = imageUri takePictureLauncher.launch(imageUri) }, From 9fa568b965a287b6310e715fde015ddaa627f2f3 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Thu, 20 Nov 2025 17:00:19 +0900 Subject: [PATCH 35/44] =?UTF-8?q?Feat:=20=EC=A0=9C=EB=B3=B4=20=EC=A0=9C?= =?UTF-8?q?=EC=B6=9C=ED=95=98=EA=B8=B0=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/report/ReportScreen.kt | 27 +++--- .../presentation/report/ReportViewModel.kt | 95 ++++++++++++++++++- 2 files changed, 105 insertions(+), 17 deletions(-) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportScreen.kt index eef553b6..4d43fdcb 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportScreen.kt @@ -132,12 +132,13 @@ fun ReportScreenContainer( ReportScreen( uiState = uiState, - onReportTitle = viewModel::updateReportTitle, - onReportDescriptionChange = viewModel::updateReportDescription, + onReportTitleChange = viewModel::updateReportTitle, + onReportContentChange = viewModel::updateReportContent, onShowImageSourceBottomSheet = viewModel::showImageSourceBottomSheet, onShowReportCategoryBottomSheet = viewModel::showReportCategoryBottomSheet, onRemoveImage = viewModel::removeImage, onGetCurrentLocationClick = locationPermissionHandler::requestPermission, + onSubmitClick = viewModel::submitReportWithImages, onBackClick = viewModel::navigateToBack, ) } @@ -146,12 +147,13 @@ fun ReportScreenContainer( @Composable private fun ReportScreen( uiState: ReportState, - onReportTitle: (String) -> Unit, - onReportDescriptionChange: (String) -> Unit, + onReportTitleChange: (String) -> Unit, + onReportContentChange: (String) -> Unit, onShowImageSourceBottomSheet: () -> Unit, onShowReportCategoryBottomSheet: () -> Unit, onRemoveImage: (Uri) -> Unit, onGetCurrentLocationClick: () -> Unit, + onSubmitClick: () -> Unit, onBackClick: () -> Unit, ) { val scrollState = rememberScrollState() @@ -204,7 +206,7 @@ private fun ReportScreen( ReportField(title = "제목") { BitnagilTextField( value = uiState.reportTitle, - onValueChange = onReportTitle, + onValueChange = onReportTitleChange, singleLine = true, placeholder = { Text( @@ -225,8 +227,8 @@ private fun ReportScreen( ReportField(title = "상세 제보 내용") { BitnagilTextField( - value = uiState.reportDescription, - onValueChange = onReportDescriptionChange, + value = uiState.reportContent, + onValueChange = onReportContentChange, modifier = Modifier.height(88.dp), placeholder = { Text( @@ -238,7 +240,7 @@ private fun ReportScreen( ) Text( - text = "${uiState.reportDescription.length} / 150", + text = "${uiState.reportContent.length} / 150", style = BitnagilTheme.typography.caption1Medium, color = BitnagilTheme.colors.coolGray80, textAlign = TextAlign.End, @@ -256,12 +258,12 @@ private fun ReportScreen( BitnagilTextButton( text = "제보하기", - onClick = {}, + onClick = onSubmitClick, colors = BitnagilTextButtonColor.default( disabledBackgroundColor = BitnagilTheme.colors.coolGray98, disabledTextColor = BitnagilTheme.colors.coolGray90, ), - enabled = false, + enabled = uiState.isSubmittable, modifier = Modifier .fillMaxWidth() .padding(14.dp), @@ -274,12 +276,13 @@ private fun ReportScreen( private fun Preview() { ReportScreen( uiState = ReportState.Init, - onReportTitle = {}, - onReportDescriptionChange = {}, + onReportTitleChange = {}, + onReportContentChange = {}, onRemoveImage = {}, onShowImageSourceBottomSheet = {}, onShowReportCategoryBottomSheet = {}, onGetCurrentLocationClick = {}, + onSubmitClick = {}, onBackClick = {}, ) } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt index 13c935dc..0e842bb4 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt @@ -1,18 +1,30 @@ package com.threegap.bitnagil.presentation.report +import android.content.Context import android.net.Uri import androidx.lifecycle.ViewModel import com.threegap.bitnagil.domain.address.usecase.FetchCurrentAddressUseCase -import com.threegap.bitnagil.presentation.report.model.ReportCategory +import com.threegap.bitnagil.domain.file.usecase.UploadReportImagesUseCase +import com.threegap.bitnagil.domain.report.model.Report +import com.threegap.bitnagil.domain.report.usecase.SubmitReportUseCase +import com.threegap.bitnagil.presentation.common.file.convertUriToImageFile +import com.threegap.bitnagil.presentation.report.model.ReportCategoryUi import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import jakarta.inject.Inject +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.viewmodel.container @HiltViewModel class ReportViewModel @Inject constructor( + @ApplicationContext private val context: Context, private val fetchCurrentAddressUseCase: FetchCurrentAddressUseCase, + private val uploadReportImagesUseCase: UploadReportImagesUseCase, + private val submitReportUseCase: SubmitReportUseCase, ) : ViewModel(), ContainerHost { override val container: Container = container(initialState = ReportState.Init) @@ -23,9 +35,9 @@ class ReportViewModel @Inject constructor( } } - fun updateReportDescription(description: String) { + fun updateReportContent(content: String) { intent { - reduce { state.copy(reportDescription = description) } + reduce { state.copy(reportContent = content) } } } @@ -100,6 +112,59 @@ class ReportViewModel @Inject constructor( } } + fun submitReportWithImages() { + intent { + val category = state.selectedCategory ?: return@intent + val address = state.currentAddress ?: return@intent + val latitude = state.currentLatitude ?: return@intent + val longitude = state.currentLongitude ?: return@intent + + reduce { state.copy(submitState = SubmitState.SUBMITTING) } + + val imageFiles = coroutineScope { + state.reportImages + .map { uri -> + async { + convertUriToImageFile(uri = uri, prefix = "report", context = context) + } + } + .awaitAll() + .filterNotNull() + } + + if (imageFiles.size < state.reportImages.size) { + reduce { state.copy(submitState = SubmitState.ERROR) } + return@intent + } + + val uploadedPaths = uploadReportImagesUseCase(imageFiles).getOrElse { error -> + reduce { state.copy(submitState = SubmitState.ERROR) } + return@intent + } + + reduce { state.copy(uploadedImagePaths = uploadedPaths) } + + submitReportUseCase( + Report( + title = state.reportTitle, + content = state.reportContent, + category = category.toDomain(), + address = address, + latitude = latitude, + longitude = longitude, + imageUrls = uploadedPaths, + ), + ).fold( + onSuccess = { + reduce { state.copy(submitState = SubmitState.SUCCESS) } + }, + onFailure = { it -> + reduce { state.copy(submitState = SubmitState.ERROR) } + }, + ) + } + } + companion object { const val MAX_IMAGE_COUNT = 3 } @@ -108,32 +173,52 @@ class ReportViewModel @Inject constructor( data class ReportState( val reportImages: List, val reportTitle: String, - val reportDescription: String, + val reportContent: String, val selectedCategory: ReportCategoryUi?, val imageSourceBottomSheetVisible: Boolean, val reportCategoryBottomSheetVisible: Boolean, val currentAddress: String?, val currentLatitude: Double?, val currentLongitude: Double?, + val uploadedImagePaths: List, + val submitState: SubmitState, ) { val canAddMoreImages: Boolean get() = reportImages.size < ReportViewModel.MAX_IMAGE_COUNT + val isSubmittable: Boolean + get() = reportImages.isNotEmpty() && + reportTitle.isNotEmpty() && + reportContent.isNotEmpty() && + selectedCategory != null && + currentAddress != null && + currentLatitude != null && + currentLongitude != null + companion object { val Init = ReportState( reportImages = emptyList(), reportTitle = "", - reportDescription = "", + reportContent = "", selectedCategory = null, imageSourceBottomSheetVisible = false, reportCategoryBottomSheetVisible = false, currentAddress = null, currentLatitude = null, currentLongitude = null, + uploadedImagePaths = emptyList(), + submitState = SubmitState.IDLE, ) } } +enum class SubmitState { + IDLE, + SUBMITTING, + SUCCESS, + ERROR, +} + sealed interface ReportSideEffect { data object NavigateToBack : ReportSideEffect } From 77d93c50711f2af3b7ce6a2c882400649326ecdf Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Fri, 21 Nov 2025 08:38:55 +0900 Subject: [PATCH 36/44] =?UTF-8?q?Chore:=20import=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../threegap/bitnagil/presentation/report/ReportViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt index 0e842bb4..b33983aa 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt @@ -11,7 +11,7 @@ import com.threegap.bitnagil.presentation.common.file.convertUriToImageFile import com.threegap.bitnagil.presentation.report.model.ReportCategoryUi import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext -import jakarta.inject.Inject +import javax.inject.Inject import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope From 7590df52ad1e13a0124d0ad3e8065fbaa7d02dcf Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Fri, 21 Nov 2025 10:30:43 +0900 Subject: [PATCH 37/44] =?UTF-8?q?Add:=20=EC=9D=B4=EB=AF=B8=EC=A7=80,=20?= =?UTF-8?q?=EC=95=84=EC=9D=B4=EC=BD=98=20=EC=97=90=EC=85=8B=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../res/drawable-hdpi/img_pomo_loading.webp | Bin 0 -> 5440 bytes .../res/drawable-mdpi/img_pomo_loading.webp | Bin 0 -> 3446 bytes .../res/drawable-xhdpi/img_pomo_loading.webp | Bin 0 -> 7302 bytes .../res/drawable-xxhdpi/img_pomo_loading.webp | Bin 0 -> 11386 bytes .../drawable-xxxhdpi/img_pomo_loading.webp | Bin 0 -> 16090 bytes .../src/main/res/drawable/ic_loading.xml | 40 ++++++++++++++++++ 6 files changed, 40 insertions(+) create mode 100644 core/designsystem/src/main/res/drawable-hdpi/img_pomo_loading.webp create mode 100644 core/designsystem/src/main/res/drawable-mdpi/img_pomo_loading.webp create mode 100644 core/designsystem/src/main/res/drawable-xhdpi/img_pomo_loading.webp create mode 100644 core/designsystem/src/main/res/drawable-xxhdpi/img_pomo_loading.webp create mode 100644 core/designsystem/src/main/res/drawable-xxxhdpi/img_pomo_loading.webp create mode 100644 core/designsystem/src/main/res/drawable/ic_loading.xml diff --git a/core/designsystem/src/main/res/drawable-hdpi/img_pomo_loading.webp b/core/designsystem/src/main/res/drawable-hdpi/img_pomo_loading.webp new file mode 100644 index 0000000000000000000000000000000000000000..7f81da5f38a037ec1c8882cbca61167a89c9ed85 GIT binary patch literal 5440 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 literal 0 HcmV?d00001 diff --git a/core/designsystem/src/main/res/drawable-mdpi/img_pomo_loading.webp b/core/designsystem/src/main/res/drawable-mdpi/img_pomo_loading.webp new file mode 100644 index 0000000000000000000000000000000000000000..3ebfb84e1f1b23b6b76dc0faa59884ba64ad4455 GIT binary patch literal 3446 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 YLOwM=8xQLqF6TNx7?d(HR>J@Q01D5p<^TWy literal 0 HcmV?d00001 diff --git a/core/designsystem/src/main/res/drawable-xhdpi/img_pomo_loading.webp b/core/designsystem/src/main/res/drawable-xhdpi/img_pomo_loading.webp new file mode 100644 index 0000000000000000000000000000000000000000..21d836bf6136ff0d5bac586098e5dc008aeae096 GIT binary patch literal 7302 zcmV;19C_nXNk&F~8~^}UMM6+kP&il$0000G0001g0RXuH06|PpNIeVy00A7eZQC&P z|7WLdnImEXkgClCp*Yh50I-Z?B^~aL4r|-CF}G*iwr#C#<80fuZQGjb9tNE@KWpY5 zBVq#VR>0)Qx>D~!0BvDc2opk`Ljse*-x=J_U^|1Ia|mG8#hQ!OG};|R0A*vfg%*XD zT~!}g5itSafB*k=y8IOj6TM9{o98YfT&UFEcPX$cwGtI+=QO=eHM2SGUpw!!3ZQm1 zn`-~LdhSw)s}uy4$8}HpubgIP_sQUqYubOip1Uw+L|BHca*ijXr3_o{$vBZ(o7##; z#fS`BxK?zOcB(CYoPC2uX~UfSfC+g{b93&9c$l|0LqXawf3@I1)O^;xV1Q3^{Nnua zX}&8bPp)a+Yt5Xfh47KK^0X+PoHJI7qe{j$wNO6u#nXbRQgS6~0o9f!9@AnvOOjYE ztXGa$EwIW7l4-Ghr031Tt0$-C%_6KdF>e;+XI5lbn4dX$SfpD*%4MZ zE!^6+RH%jgk%($RKhwY(HGAejh6VnS0k+sb5}?7GC48qvU!wQ0@ZTJ}50*fE1a2Fa z!)MsovN%iBG!52+_OLw860+Sg*(0{iRLl~uw`sY&Iap;_I`0yz%M$tw)x%OMf!Zyn zBThw=VjQN^vf85bu(VE&(ZdpZmk52Z)II~mGdCvjQA@6dr&@Yza9Hy*Vza3wSR%vI zGCTsqYB`p;s3m!XMJ&xFDm_mfo*OL7>HX6gWatGgXY!&gA;!0mcqc; zscJa9oa#U@@u?x9`&5z8d}>IjKHW$dE^P+I6MiNRkz%2H)Uj}SbYkIlh!qRdp>AMg ze-1Q=KMf7TpB)WW-Pz#q?oNTj=?>}t8x8oGxHI5zx-;Q0yr~Aq!<&JJ3cv2njfdfl z;$eC-@L=MM078o|3<&GKY>0UGr6S_?r6Xec(hyPf>y^JHM+fZw|(FH8(i8YXT}DkjvE6BFji4GMNdL9x40&k2g= z#b&TPy(kKcxwyj2B73c^p92&oYvOjPPQ&QAA|8g+6@8e7y@!EyJz{9p z_r=_R8EauftSn`R%(;{bnE^jt$_^PTD`Cv=N@zf4TS|jWT?#2;+FArL$69Cr=3ER< zyc~<64!@#s8LOckE>>1U11@zr8(fa%(1Dh@o-)B=yc}w1+1En@ENwvomSaKm!b)9H znMhGua!1O&CQ_gnuSt+H)S9&lQP5^OUdffbVlHL&apDibTbAnH(=wBj<6 zVoRb96<$(Nq-;wfK}uZ{9YA?sR8gGFMUmjdRz)3t^nyuQ7Il=!x`+ox#=?l?r2sKr z84+!Jt#wmql*Rqs0gORDGB8djsN-UIQ6?Mh=?G8s zQO890q+sIogfKA+`b<1gOjJ)gCd3nlA7Ntmq+p_Z!kBpXq+nurLK6~)D+3bUl?I8! z6?!3J_`--VUj!2D$_WW_^@3lA1iP|9VsnKQ5xXxP5!IJ60U^H70L1Rh0f_EQ1H>%8 z?0_)wg%MGGDTrvk&;Z2YOasJlrc69cX9y6+zOVtZ`XY!pd?^Zu(--22hwcn@JXCLX zJTz|@55pUpaIoUdjR*1O!b9`sz{BZ{;NkX$1{_ShdEudZbK{{oQw)#d3{5=j-ZVT^ zZ)m`;!>_=PZZtH1UTBd2y)`tjKLQ3NhX)jtJ!()8^0*`6B$r=E5T6bpbn;nXFoc{g z7?`7x-va@okmmw`gPeaQZ?r<*x5Uk3FBHHlYdUrc;z-#L3gnTkd0(ecC?!#INTG;o zhE%5>QYffLdPZl8ZB5L)&8$&)IVJPH&bATpE)?L5C{w7DF~@ndLb0Ann7welTA_fy zqIcBRYuyXQT|zgU1Tqm%n_+cDQ%I`oKRKt5zjDLR%pPl zhDp;xuPB{)DffTh^r7d4%4`02=ryKk-?u`vZM*@8h-CZ+&FiSln=t*;b9F~*C$ zh_rq!?10AB&hxydtzB`!k7+J~LY~9igai+BwIJb>9}!V`n6niF>`#6AGvg;e`{9>9 z`<-8yx97)y3pScKB8{fz(yo&R_3zeh*yJfQ#|#)hKJvuRe{XL8>buw1zyG71`*t3? zb^6GP*&`wX_4thos3DMu*kx&VD7pFS%_EtDK@C~v=bNK(gWGnLwvFiBFT_Qwxk$nl zi--mp)M_8U_R!szUp`$d92^`lboE;`YCtm#LI^P`l6?K~-Pc!#XLYnSBpOzNUaLq{ zQHEkrL;v^pYsX6FjT$zfN2ngG96|624kYR~$kkw%1-VAUSAJ$t<5m6j;w)bSO#on7 zEaR!`@7#QH?e@Xtqy3u@M7_kbnzrlNn~R&L`?*AnSb9Z)8o@uGp4hi_$NJ2)q@qhC zsv!0G`SJDXLt1F@>pDWy$anbs?4*SGGw06E+W4Rpl}#fCVL_U_y<&(-oy22tAy2D) z+^MI(D1<%b*E0IsY19=gyRli@b{;zO;OYH)Pf3JPsptDMCbV~MQk&Si?(N1GUy~!O zt9Z3@Qjn#DUGrF`!&lvtsSqYse7>C5-I%CJ?4XS#!p@~9=Q*=#2Go@9R3bdBzB*I8 znxMqBchr`L48qu-Q*B_?=?>WbfkF72ToEFu5{`8DCkA11=6<>}Q7QAi6vFEA)w-21 ze1x$31%Aa!5QZOJA;D7|x4c4}y*MtvLcsVue*5iLF`#<{w~@nOC4kN)tVSL1g`m== zm+;xQ)nQyvm4=yLu^9C@+6;m!CM;%Mrf}DXVyOUBE6a7-;m-`VqOXjRKv4aJ*l%d$ z(u*?eq<&u9f(L4{QO|WB7zP`uuPyQwfEo|QPCa7RU42hdxQG6}B8&%WFBEGz29KH% zpPsSf!Z#&NQ%H?h=+`TH=z_YEEpc)4^>m47+h9y;(?%v*)IiX|6HsQYJLITvEJnOML zufLMXs74t@$)6pV-^0#I!h;;hBS5_k#m$=fOxpBO-KbHjXmy2@?>9|o)LJX)TH$nZ|sTkk${FN!8h-);^i*| zEhl9kUO!kD+0n~P+t@6q*OX~VMSCvGD4(~~Ekt%s#tq3X2Kt#gL-@M$K{Y-;K^A|a{xz`pH!H>}^jW=+bx@qzsXiaePfX0&dZm=A zGF9ojTStl)%!w)5wr|<;E&C4aC``>rUwZI%X$7daB^IF45L6PfAi#o<$LA8DOqP*r zu++;;0Ls?)2<<##eoE1%gGY9)%uSAqOG?Yg&Ptlu)5}QAhoA}}*QouBwMTasNA_y( zr%Ql;BnALhP&gnO5C8!1WB{E3Du4mH0X~sNpiCvBqaq>ESop9K32AQpe3*r+PsY5; zW~o0Fe_rhW{NMHdoZl_`W$Ry&dr|&9_W$*sQ~$Z}2jf|0VgCz-_U4fqMt^ zfBC=RJaIKI$OrQ?jBn8WfIm$C)cb(?Wd6thm->%8KTvxHe>eW^|8LhT*dOl4vfu95 zZ-5q8z8h|jeQBB@)T?{34yA_gaX%IZb|KWOd$A6sTi)!;Pj(^Ht9mUx^N#X#d9RmI z)T?{33niptA;H5YW1|dPd@Z&71 zJdA_PX|IObS!P=TWs0`Yu8N_V?~V9#0zqj9d-f9pOHXzo_Yua*$@fb|2gmM!OZtPW z#c=5JV$v-=*^E=6tK_EF=AZ$`M|x9wwp&8ew^f-c-t0xlI}O&Fr$teg#qy7hSWz&H zQ2?QU0W}{;C^n$;ZPDt7qs_8B-6EDBl;~HoIrL5kN1LRO;+PNhzy<1hZ{mpmDk*wK z!jQPQ`V&ldeCRTYX!Tq!IVW|BhDAFkJ(~C{n-2aJ4N)T}1Db!BfdbY00|~`3IQ|%5 zL?Xmw8fUz#d$D`?b70$EbzMsCKl1jk2ONVX&q^^c`w))R=c+l>Q%q1i+js~Mey5nd9SP@PVB~aL^_pkb|KWOd&_();kM}W zUlIUy_n;VuN1FI;x;)pzb){jC<=}KU)di|X8*Y*2$f0c2o?Y`}sL_f8evv)chf;)H zmgPH=%@*>9`FjR(4wpBj*>WY~p0M9ldo?1MJQkdJ;D^*hqs`Q9R$}|=!v$MtArs;n z-W>tUmOIZm3E?!?=8S2dkwOHXq43=)5J6?08H0i~z)%CLD_tMY^$D3(PrLVf@M z{@+YXA!oa%yGGq0LII2LxVM8j>pI^lui+#>n|`Lm!ReD>mu{x@w&obXrJdtA|Q{aHsuI?07$e1hv>>}QDxSo#qHevh7>u^##9d0 z@UAbL3GPIK-^_Ngc`f|G1uwI+ZwlJ6of>SO`|&kcEet)d@9x8^db|t_d&=W@-r-1Y z={4I2VyoG0TENB!)2YD`I*9neXamTs)noW?J+*0vw9!X zO3iy=%ew@%Buu`0*}<~2T`TO(PCxRV#9NL?PkKEO6|RdqZ;qBGRtKPe#%t%!dCbb3 zjg>*Rx_6Pt5=M9W_F{`@u{4QUM{I2-CEk}jKM3GkL%j3KD?2y9@alb;CJWf&NUT02 zC5hfHMoz9?cn=FeD10ehEvNR?8?o=4Bw4E7gH=wHCkz^8FQPD}cAL#WwI=sk=6UWS z@Un4RDu^@{dzY6y`Rxk>moZJOdEr(4u&Z+{eqU#2>S!5Frmh11reqC%Vji5TMi>V1 z*K-Z_TeVv1ht&k`&#m1Cdy5_!2oXCYI7dms>e-mk!wHdK7j9hfP#k;<0E6j z5t2ec)rHt?v%t`*m#Na9kn=cyHiR|uJ(Vb_S1xw)tNa|VU}Ep+@Ua*jxea-puv7PU zw?V6ig@K~uW(Zi2Q9L#CEH!QhDnrYbK8|s7RWcZAKnA)LcGzmjr}ZuNdN-5P>mh*S z#^a*6Hx2b@yv%In57 z&<7wH@|5-P8EHhWJV$}ON)2>a-3CvN?j)Jf%Rw3#W zsoNzC_uz=Ho-Pw$`g{MF=R`ZW3X53!PI_rvm508)5=ZOmeAXf1W|sg{1J~39wM@I` z+UVh6&5p8i=6d`$347Z-J=IKJva%nHk6>>pfu75ytjwKCwHo8U%9~z84O#yz-t_-u z{raDSS_6D(@6eF|bF#QZu@4yj^aQxK652i!m#DW=BSo;?7=gl(Z9J{9Zt?$0d`Xc| z3R?g2v5G3%8CGOtKjrpt{zMm)WRJ1C1S5hpDtmwj-ePY;gLG8S4H!r8-!+Dt z*g$Owvjol>hxwanL!6Qe-B)8*_oshG#&*WV@nNNlq`yL7>MNI>TsH!8d=ADn47~b; zRsXC%G105e9T6ll3lD_lVK+QK_8HO~I`0Yp7!MEvK?dhyvLj}Ic|z;cSYQ`@UAYQP3_&SiA$Fjr zAty%K3Sb&bk3AuHg_!9mL&`mAJ;h27e{9YhZ;w=7@&ADVEFgCY-@MYA zHu{}cK5k<@npX$8`kgiwz+>7+yxXVMejUFTa|)~JH1|l6kH5&)&0! zlpPmI-4f+pN2j@7Q)TF_2qq7Q9VL+DTT!yI7t>p3Yyw);fgu6i5+8Ou)o*6&xQjl< zv9ycbHfI`wm-TYH_;RMe#enX}I-~^~2fyVuy`0AF2<}mT!+4`r>~(T02EttpCn?x^ z=*F6(b_EXARs;f4cWN?@)Q5-}B+l-dTAuCsEaEsV34Fiue=~fjVDQke6Jh=Wh#fcv z#4cpQO%FcD#x2<2rs~IbPnNWW5Za(|y|@0lv3idhr`BT#?2Z6C3Jgab=9Voq0#!U5 zW|m71DZm!KcY*!?&?P!?T$9KA_P8fDxGl~576EBBzxRh#0_y*9G||UBqNL*&?vYB; zj1brHMQd6c*bu*l)<31l<0iSb5yzXAuPx0to%2Q;fSN`v{;H|5k6ONNY+IqEZmC$j zNfsmhBtNqA-Y^^m9@S#mFI9yO`0iijx8DOo!ds^am;39osl~KT{KZ3)+A@T)D9V;S zll!D8Xyh}wDt{9sK^9=4A}X48OU16_C@mt2X`<(Unc5iJD9>S>}*(>{nMr!EonGu_*#IF?a7Fy(T7MKgj>VcSW0!AiEUtt{8F00u(;UaVWb{UO3@%*wO-Eh8%vY<4t-*>%P5*7Z;`^e zoR(g?96M_MDPlRJgb>h((r(~4iZTA70E{=W$E_r33nd4|f;ZXdf!riE2L==6*>h3$ zfHxg4hW($^Re+U20%ox@N9X6*LB#3hSvVF>x!`quL(PX%tFmR-gk@j zmz3x!vW+2{oFi9%_?1*_JnF+-PKcQ4rirQzan-g|;Sv3zJacH2=ah@=lN+(I92XR5 z(19Q*LiR5O?O?=rI6jI@`G*gib-sAsdlmFY_xg`IpP~zu;gtv(IaW$m#j_r)|mtf{T4aWOqZ+ z`5W^-RD{yYElU$6Db#xh@WtJT+;{QSyX`l)CI$@n!LBP_Tpf$_w7_%b)-OYo%h40xZzPg~{qCNtXp8jb4*!bZ$K(P8^t^i9LhSK*ZgjN@vP5 zyPE23@^qt?K@&^(TYmxjrphSq)lJmba?5quf|c!vCKf&fp+`=SNP%6>n>ShCnud+R z@4LC&NsMJG@hB6w%j+8Q{QHnWyLYRcZR)e&ka2XP0zPjg2omd8%I9_~IkTKQ>H(k9 z-~|YMD-T2UI4aG7Oc&lkpD#(sCvK^2TNfFPacojWG!dzJ-^|lAh9s79>UZ9>cz$Z) zAt)DGO&n-eaj78M8+8R!1_&~1<{k?DM0Mc|Q7G3A@|6p10`okCvp6`b=h5pQ8>%{*W(T9?9(zj%erXaH= zyZ8>as*m5#=M}c^9AVde{a{8C-dipT!U&*(HIXb_O7ET!2TXm2lQAhwvJo|GKQ`Wu zzy_W)uaB#sJ`z4=j>sx%Sjdys$Qu==a|bI8Adi$(PstrO`ZLIBc%D4rDRerQCgd#U gm3N%e!pg+N4!iDLy$lWpfims7ngMdX*ahmq0A`LgTmS$7 literal 0 HcmV?d00001 diff --git a/core/designsystem/src/main/res/drawable-xxhdpi/img_pomo_loading.webp b/core/designsystem/src/main/res/drawable-xxhdpi/img_pomo_loading.webp new file mode 100644 index 0000000000000000000000000000000000000000..62063a17dd04106cce8f5fed6c0aa7a2f25a8116 GIT binary patch literal 11386 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%0AbB>47{((!C;eydw8|`M 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 NEt<|q@aqrb{|B;b2QZD2|xx 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⋙sPiiI?5rg2 z{G*?>Qm6{c40FzRR_&#(CO^vU5IscPWGTe3d?)bj^lC*d>3vBDl3>yLjJkQG(fk+~ zfv6-(v~AIBf{l;-`yY(_l;%Tj2tHjB%%5tK@mPis h$|xBQusP^%(+NMI_rAmsgKaFVo3*C{#o#Y8{eK+#cGds@ literal 0 HcmV?d00001 diff --git a/core/designsystem/src/main/res/drawable/ic_loading.xml b/core/designsystem/src/main/res/drawable/ic_loading.xml new file mode 100644 index 00000000..b42a852d --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_loading.xml @@ -0,0 +1,40 @@ + + + + + + + + + + From 03c0bef58772731cc2ef0b4005725b298d9b7981 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Fri, 21 Nov 2025 12:31:29 +0900 Subject: [PATCH 38/44] =?UTF-8?q?Feat:=20SubmittingReportContent=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../template/SubmittingReportContent.kt | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/template/SubmittingReportContent.kt diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/template/SubmittingReportContent.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/template/SubmittingReportContent.kt new file mode 100644 index 00000000..82800a6e --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/template/SubmittingReportContent.kt @@ -0,0 +1,64 @@ +package com.threegap.bitnagil.presentation.report.component.template + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +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 + +@Composable +fun SubmittingReportContent( + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .fillMaxSize() + .padding(top = 80.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + BitnagilIcon( + id = R.drawable.ic_loading, + tint = null, + modifier = Modifier.padding(bottom = 12.dp) + ) + + Text( + text = "제보중...", + color = BitnagilTheme.colors.coolGray10, + style = BitnagilTheme.typography.title2Bold, + modifier = Modifier.padding(bottom = 16.dp) + ) + + Text( + text = "포모가 열심\n제보하고 있어요", + color = BitnagilTheme.colors.coolGray40, + style = BitnagilTheme.typography.body1Medium, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(72.dp)) + + Image( + painter = painterResource(R.drawable.img_pomo_loading), + contentDescription = null + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun Preview() { + SubmittingReportContent() +} From cee367761e852a78f13c6b0980a5d31f11d9f2d2 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Fri, 21 Nov 2025 12:31:48 +0900 Subject: [PATCH 39/44] =?UTF-8?q?Feat:=20CompleteReportContent=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../report/component/CompleteReportCard.kt | 148 ++++++++++++++++++ .../template/CompleteReportContent.kt | 114 ++++++++++++++ 2 files changed, 262 insertions(+) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/CompleteReportCard.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/template/CompleteReportContent.kt diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/CompleteReportCard.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/CompleteReportCard.kt new file mode 100644 index 00000000..fac7b8ee --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/CompleteReportCard.kt @@ -0,0 +1,148 @@ +package com.threegap.bitnagil.presentation.report.component + +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +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.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import com.threegap.bitnagil.designsystem.BitnagilTheme +import com.threegap.bitnagil.presentation.report.model.ReportCategoryUi + +@Composable +fun CompleteReportCard( + title: String, + category: ReportCategoryUi?, + address: String?, + content: String, + images: List, + modifier: Modifier = Modifier +) { + val context = LocalContext.current + + Column( + modifier = modifier + .background( + color = BitnagilTheme.colors.coolGray98, + shape = RoundedCornerShape(12.dp) + ) + .padding(20.dp) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + Text( + text = "제보 요약정보", + color = BitnagilTheme.colors.coolGray10, + style = BitnagilTheme.typography.body2SemiBold, + ) + + CompleteReportCardItem( + title = "제목" + ) { + Text( + text = title, + color = BitnagilTheme.colors.coolGray10, + style = BitnagilTheme.typography.body1Medium, + ) + } + + CompleteReportCardItem( + title = "키테고리" + ) { + Text( + text = category?.title ?: "카테고리 없음", + color = BitnagilTheme.colors.coolGray10, + style = BitnagilTheme.typography.body1Medium, + ) + } + + CompleteReportCardItem( + title = "신고 위치" + ) { + Text( + text = address ?: "주소 없음", + color = BitnagilTheme.colors.coolGray10, + style = BitnagilTheme.typography.body1Medium, + ) + } + + CompleteReportCardItem( + title = "제보 내용" + ) { + Text( + text = content, + color = BitnagilTheme.colors.coolGray10, + style = BitnagilTheme.typography.body1Medium, + textAlign = TextAlign.End + ) + } + + CompleteReportCardItem( + title = "제보 사진" + ) { + LazyRow( + horizontalArrangement = Arrangement.End, + ) { + items(images) { image -> + AsyncImage( + model = image, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .padding(start = 12.dp) + .size(48.dp) + .clip(RoundedCornerShape(8.dp)), + ) + } + } + } + } +} + +@Composable +private fun CompleteReportCardItem( + title: String, + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + color = BitnagilTheme.colors.coolGray30, + style = BitnagilTheme.typography.body1Medium, + modifier = Modifier.padding(end = 12.dp) + ) + content() + } +} + +@Preview +@Composable +private fun Preview() { + CompleteReportCard( + title = "어쩌구", + category = ReportCategoryUi.WATERFACILITY, + address = "서울특별시 강남구 역삼동", + content = "어쩌구 저쩌구", + images = listOf(), + ) +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/template/CompleteReportContent.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/template/CompleteReportContent.kt new file mode 100644 index 00000000..bf6a5789 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/template/CompleteReportContent.kt @@ -0,0 +1,114 @@ +package com.threegap.bitnagil.presentation.report.component.template + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +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.component.atom.BitnagilTextButton +import com.threegap.bitnagil.designsystem.component.atom.BitnagilTextButtonColor +import com.threegap.bitnagil.presentation.report.ReportState +import com.threegap.bitnagil.presentation.report.component.CompleteReportCard + +@Composable +fun CompleteReportContent( + uiState: ReportState, + onConfirmClick: () -> Unit, + modifier: Modifier = Modifier +) { + val scrollState = rememberScrollState() + + Column( + modifier = modifier + .fillMaxSize() + .verticalScroll(state = scrollState) + .padding(top = 80.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + BitnagilIcon( + id = R.drawable.ic_check_circle_orange, + tint = null, + modifier = Modifier + .padding(bottom = 12.dp) + .size(40.dp) + ) + + Text( + text = "제보가 완료되었습니다.", + color = BitnagilTheme.colors.coolGray10, + style = BitnagilTheme.typography.title2Bold, + modifier = Modifier.padding(bottom = 16.dp) + ) + + Text( + text = "빛나길에서 접수 후 완료되면\n신고를 진행합니다.", + color = BitnagilTheme.colors.coolGray40, + style = BitnagilTheme.typography.body1Medium, + textAlign = TextAlign.Center + ) + + Box( + contentAlignment = Alignment.TopCenter + ) { + Image( + painter = painterResource(R.drawable.onboarding_character), + contentDescription = null, + modifier = Modifier + .padding(top = 34.dp) + .size(134.dp, 148.dp) + ) + + CompleteReportCard( + modifier = Modifier + .offset(y = 115.dp) + .padding(horizontal = 18.dp), + title = uiState.reportTitle, + category = uiState.selectedCategory, + address = uiState.currentAddress, + content = uiState.reportContent, + images = uiState.uploadedImagePaths, + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + BitnagilTextButton( + text = "확인", + onClick = onConfirmClick, + colors = BitnagilTextButtonColor.default( + disabledBackgroundColor = BitnagilTheme.colors.coolGray98, + disabledTextColor = BitnagilTheme.colors.coolGray90, + ), + enabled = true, + modifier = Modifier + .fillMaxWidth() + .padding(14.dp), + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun Preview() { + CompleteReportContent( + uiState = ReportState.Init, + onConfirmClick = {} + ) +} From 10ee8fe2dbb29657b89790a3cc5f55c7f71a8db4 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Fri, 21 Nov 2025 12:54:22 +0900 Subject: [PATCH 40/44] =?UTF-8?q?Refactor:=20ReportCategory=20=ED=99=95?= =?UTF-8?q?=EC=9E=A5=ED=95=A8=EC=88=98=20=EC=A0=95=EC=9D=98=20=EB=B0=8F=20?= =?UTF-8?q?=EA=B8=B0=EC=A1=B4=20ui=20=EB=AA=A8=EB=8D=B8=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/report/ReportScreen.kt | 2 +- .../presentation/report/ReportViewModel.kt | 4 +- .../report/component/CompleteReportCard.kt | 33 ++++++++------- .../component/ReportCategoryBottomSheet.kt | 19 +++++---- .../report/model/ReportCategoryExtension.kt | 28 +++++++++++++ .../report/model/ReportCategoryUi.kt | 42 ------------------- 6 files changed, 59 insertions(+), 69 deletions(-) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportCategoryExtension.kt delete mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportCategoryUi.kt diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportScreen.kt index 4d43fdcb..d6762241 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportScreen.kt @@ -220,7 +220,7 @@ private fun ReportScreen( ReportField(title = "카테고리") { ReportCategorySelector( - title = uiState.selectedCategory?.title, + title = uiState.selectedCategory?.uiTitle, onClick = onShowReportCategoryBottomSheet, ) } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt index b33983aa..41047775 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt @@ -6,9 +6,9 @@ import androidx.lifecycle.ViewModel import com.threegap.bitnagil.domain.address.usecase.FetchCurrentAddressUseCase import com.threegap.bitnagil.domain.file.usecase.UploadReportImagesUseCase import com.threegap.bitnagil.domain.report.model.Report +import com.threegap.bitnagil.domain.report.model.ReportCategory import com.threegap.bitnagil.domain.report.usecase.SubmitReportUseCase import com.threegap.bitnagil.presentation.common.file.convertUriToImageFile -import com.threegap.bitnagil.presentation.report.model.ReportCategoryUi import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject @@ -65,7 +65,7 @@ class ReportViewModel @Inject constructor( } } - fun selectReportCategory(category: ReportCategoryUi) { + fun selectReportCategory(category: ReportCategory) { intent { reduce { state.copy(selectedCategory = category) } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/CompleteReportCard.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/CompleteReportCard.kt index fac7b8ee..af299133 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/CompleteReportCard.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/CompleteReportCard.kt @@ -22,16 +22,17 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import com.threegap.bitnagil.designsystem.BitnagilTheme -import com.threegap.bitnagil.presentation.report.model.ReportCategoryUi +import com.threegap.bitnagil.domain.report.model.ReportCategory +import com.threegap.bitnagil.presentation.report.model.uiTitle @Composable fun CompleteReportCard( title: String, - category: ReportCategoryUi?, + category: ReportCategory?, address: String?, content: String, images: List, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val context = LocalContext.current @@ -39,11 +40,11 @@ fun CompleteReportCard( modifier = modifier .background( color = BitnagilTheme.colors.coolGray98, - shape = RoundedCornerShape(12.dp) + shape = RoundedCornerShape(12.dp), ) .padding(20.dp) .fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(20.dp) + verticalArrangement = Arrangement.spacedBy(20.dp), ) { Text( text = "제보 요약정보", @@ -52,7 +53,7 @@ fun CompleteReportCard( ) CompleteReportCardItem( - title = "제목" + title = "제목", ) { Text( text = title, @@ -62,17 +63,17 @@ fun CompleteReportCard( } CompleteReportCardItem( - title = "키테고리" + title = "키테고리", ) { Text( - text = category?.title ?: "카테고리 없음", + text = category?.uiTitle ?: "카테고리 없음", color = BitnagilTheme.colors.coolGray10, style = BitnagilTheme.typography.body1Medium, ) } CompleteReportCardItem( - title = "신고 위치" + title = "신고 위치", ) { Text( text = address ?: "주소 없음", @@ -82,18 +83,18 @@ fun CompleteReportCard( } CompleteReportCardItem( - title = "제보 내용" + title = "제보 내용", ) { Text( text = content, color = BitnagilTheme.colors.coolGray10, style = BitnagilTheme.typography.body1Medium, - textAlign = TextAlign.End + textAlign = TextAlign.End, ) } CompleteReportCardItem( - title = "제보 사진" + title = "제보 사진", ) { LazyRow( horizontalArrangement = Arrangement.End, @@ -118,18 +119,18 @@ fun CompleteReportCard( private fun CompleteReportCardItem( title: String, modifier: Modifier = Modifier, - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { Row( modifier = modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { Text( text = title, color = BitnagilTheme.colors.coolGray30, style = BitnagilTheme.typography.body1Medium, - modifier = Modifier.padding(end = 12.dp) + modifier = Modifier.padding(end = 12.dp), ) content() } @@ -140,7 +141,7 @@ private fun CompleteReportCardItem( private fun Preview() { CompleteReportCard( title = "어쩌구", - category = ReportCategoryUi.WATERFACILITY, + category = ReportCategory.WATERFACILITY, address = "서울특별시 강남구 역삼동", content = "어쩌구 저쩌구", images = listOf(), diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/ReportCategoryBottomSheet.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/ReportCategoryBottomSheet.kt index acbdd7cf..d414b9b4 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/ReportCategoryBottomSheet.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/ReportCategoryBottomSheet.kt @@ -24,15 +24,18 @@ 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.report.model.ReportCategoryUi +import com.threegap.bitnagil.domain.report.model.ReportCategory +import com.threegap.bitnagil.presentation.report.model.iconRes +import com.threegap.bitnagil.presentation.report.model.uiDescription +import com.threegap.bitnagil.presentation.report.model.uiTitle import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable fun ReportCategoryBottomSheet( - selectedCategory: ReportCategoryUi?, + selectedCategory: ReportCategory?, onDismiss: () -> Unit, - onSelected: (ReportCategoryUi) -> Unit, + onSelected: (ReportCategory) -> Unit, modifier: Modifier = Modifier, ) { val sheetState = rememberModalBottomSheetState() @@ -48,11 +51,11 @@ fun ReportCategoryBottomSheet( Column( modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 28.dp), ) { - ReportCategoryUi.entries.forEachIndexed { index, category -> + ReportCategory.entries.forEachIndexed { index, category -> ReportCategoryItem( - icon = category.icon, - title = category.title, - description = category.description, + icon = category.iconRes, + title = category.uiTitle, + description = category.uiDescription, isSelected = selectedCategory == category, onClick = { onSelected(category) @@ -124,7 +127,7 @@ private fun ReportCategoryItem( @Composable private fun ReportCategoryBottomSheetPreview() { ReportCategoryBottomSheet( - selectedCategory = ReportCategoryUi.WATERFACILITY, + selectedCategory = ReportCategory.WATERFACILITY, onDismiss = {}, onSelected = {}, ) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportCategoryExtension.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportCategoryExtension.kt new file mode 100644 index 00000000..de494241 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportCategoryExtension.kt @@ -0,0 +1,28 @@ +package com.threegap.bitnagil.presentation.report.model + +import com.threegap.bitnagil.designsystem.R +import com.threegap.bitnagil.domain.report.model.ReportCategory + +val ReportCategory.uiTitle: String + get() = when (this) { + ReportCategory.TRANSPORTATION -> "교통 시설" + ReportCategory.LIGHTING -> "조명 시설" + ReportCategory.WATERFACILITY -> "상하수도 시설" + ReportCategory.AMENITY -> "편의 시설" + } + +val ReportCategory.uiDescription: String + get() = when (this) { + ReportCategory.TRANSPORTATION -> "신호등 고장, 표지판 파손, 횡단보도 등" + ReportCategory.LIGHTING -> "가로등, 보안등 파손 등" + ReportCategory.WATERFACILITY -> "맨홀 뚜껑 손상 등" + ReportCategory.AMENITY -> "벤치 파손, 휴지통 넘침 등" + } + +val ReportCategory.iconRes: Int + get() = when (this) { + ReportCategory.TRANSPORTATION -> R.drawable.ic_car + ReportCategory.LIGHTING -> R.drawable.ic_light + ReportCategory.WATERFACILITY -> R.drawable.ic_water + ReportCategory.AMENITY -> R.drawable.ic_hammer + } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportCategoryUi.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportCategoryUi.kt deleted file mode 100644 index 74be04de..00000000 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportCategoryUi.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.threegap.bitnagil.presentation.report.model - -import androidx.annotation.DrawableRes -import com.threegap.bitnagil.designsystem.R -import com.threegap.bitnagil.domain.report.model.ReportCategory - -enum class ReportCategoryUi( - @DrawableRes val icon: Int, - val title: String, - val description: String, -) { - TRANSPORTATION( - icon = R.drawable.ic_car, - title = "교통 시설", - description = "신호등 고장, 표지판 파손, 횡단보도 등", - ), - LIGHTING( - icon = R.drawable.ic_light, - title = "조명 시설", - description = "가로등, 보안등 파손 등", - ), - WATERFACILITY( - icon = R.drawable.ic_water, - title = "상하수도 시설", - description = "맨홀 뚜껑 손상 등", - ), - AMENITY( - icon = R.drawable.ic_hammer, - title = "편의 시설", - description = "벤치 파손, 휴지통 넘침 등", - ), - ; - - fun toDomain(): ReportCategory { - return when (this) { - TRANSPORTATION -> ReportCategory.TRANSPORTATION - LIGHTING -> ReportCategory.LIGHTING - WATERFACILITY -> ReportCategory.WATERFACILITY - AMENITY -> ReportCategory.AMENITY - } - } -} From 4859c24e59210e7a6c5a398bb88992160dcc0f89 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Fri, 21 Nov 2025 12:57:16 +0900 Subject: [PATCH 41/44] =?UTF-8?q?Feat:=20=EC=A0=9C=EB=B3=B4=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EC=83=81=ED=83=9C=EC=97=90=20=EB=94=B0=EB=A5=B8=20?= =?UTF-8?q?UI=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20=EC=A0=9C=EC=B6=9C=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ReportState, ReportSideEffect, SubmitState를 model 패키지로 분리 - 제보 제출(submitReportWithImages) 시, 최소 1초의 딜레이 보장 --- .../presentation/report/ReportScreen.kt | 66 ++++++-- .../presentation/report/ReportViewModel.kt | 144 +++++++----------- .../template/CompleteReportContent.kt | 16 +- .../template/SubmittingReportContent.kt | 10 +- .../report/model/ReportSideEffect.kt | 5 + .../presentation/report/model/ReportState.kt | 47 ++++++ .../presentation/report/model/SubmitState.kt | 3 + 7 files changed, 174 insertions(+), 117 deletions(-) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportSideEffect.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportState.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/SubmitState.kt diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportScreen.kt index d6762241..897775c7 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportScreen.kt @@ -5,6 +5,12 @@ import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.SizeTransform +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -50,6 +56,12 @@ import com.threegap.bitnagil.presentation.report.component.PhotoItem import com.threegap.bitnagil.presentation.report.component.ReportCategoryBottomSheet import com.threegap.bitnagil.presentation.report.component.ReportCategorySelector import com.threegap.bitnagil.presentation.report.component.ReportField +import com.threegap.bitnagil.presentation.report.component.template.CompleteReportContent +import com.threegap.bitnagil.presentation.report.component.template.SubmittingReportContent +import com.threegap.bitnagil.presentation.report.model.ReportSideEffect +import com.threegap.bitnagil.presentation.report.model.ReportState +import com.threegap.bitnagil.presentation.report.model.SubmitState +import com.threegap.bitnagil.presentation.report.model.uiTitle import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect @@ -130,17 +142,48 @@ fun ReportScreenContainer( ) } - ReportScreen( - uiState = uiState, - onReportTitleChange = viewModel::updateReportTitle, - onReportContentChange = viewModel::updateReportContent, - onShowImageSourceBottomSheet = viewModel::showImageSourceBottomSheet, - onShowReportCategoryBottomSheet = viewModel::showReportCategoryBottomSheet, - onRemoveImage = viewModel::removeImage, - onGetCurrentLocationClick = locationPermissionHandler::requestPermission, - onSubmitClick = viewModel::submitReportWithImages, - onBackClick = viewModel::navigateToBack, - ) + AnimatedContent( + modifier = Modifier + .fillMaxSize() + .statusBarsPadding(), + targetState = uiState.submitState, + label = "ReportSlideAnimation", + transitionSpec = { + ( + slideIntoContainer( + animationSpec = spring(stiffness = Spring.StiffnessMediumLow), + towards = AnimatedContentTransitionScope.SlideDirection.Start, + ) togetherWith slideOutOfContainer( + animationSpec = spring(stiffness = Spring.StiffnessMediumLow), + towards = AnimatedContentTransitionScope.SlideDirection.Start, + ) + ) + .using(SizeTransform(clip = true)) + }, + ) { submitState -> + when (submitState) { + SubmitState.IDLE -> { + ReportScreen( + uiState = uiState, + onReportTitleChange = viewModel::updateReportTitle, + onReportContentChange = viewModel::updateReportContent, + onShowImageSourceBottomSheet = viewModel::showImageSourceBottomSheet, + onShowReportCategoryBottomSheet = viewModel::showReportCategoryBottomSheet, + onRemoveImage = viewModel::removeImage, + onGetCurrentLocationClick = locationPermissionHandler::requestPermission, + onSubmitClick = viewModel::submitReportWithImages, + onBackClick = viewModel::navigateToBack, + ) + } + SubmitState.SUBMITTING -> SubmittingReportContent() + SubmitState.COMPLETE -> { + CompleteReportContent( + uiState = uiState, + onConfirmClick = viewModel::navigateToBack, + ) + } + } + } } @OptIn(ExperimentalMaterial3Api::class) @@ -161,7 +204,6 @@ private fun ReportScreen( Column( modifier = Modifier .fillMaxSize() - .statusBarsPadding() .windowInsetsPadding(WindowInsets.ime), ) { BitnagilTopBar( diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt index 41047775..ae13b7e7 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt @@ -9,15 +9,19 @@ import com.threegap.bitnagil.domain.report.model.Report import com.threegap.bitnagil.domain.report.model.ReportCategory import com.threegap.bitnagil.domain.report.usecase.SubmitReportUseCase import com.threegap.bitnagil.presentation.common.file.convertUriToImageFile +import com.threegap.bitnagil.presentation.report.model.ReportSideEffect +import com.threegap.bitnagil.presentation.report.model.ReportState +import com.threegap.bitnagil.presentation.report.model.SubmitState import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.viewmodel.container +import javax.inject.Inject @HiltViewModel class ReportViewModel @Inject constructor( @@ -114,54 +118,63 @@ class ReportViewModel @Inject constructor( fun submitReportWithImages() { intent { + reduce { state.copy(submitState = SubmitState.SUBMITTING) } + val category = state.selectedCategory ?: return@intent val address = state.currentAddress ?: return@intent val latitude = state.currentLatitude ?: return@intent val longitude = state.currentLongitude ?: return@intent - reduce { state.copy(submitState = SubmitState.SUBMITTING) } + coroutineScope { + val minDelayJob = async { + delay(1000L) + } - val imageFiles = coroutineScope { - state.reportImages - .map { uri -> - async { - convertUriToImageFile(uri = uri, prefix = "report", context = context) + val processingJob = async { + val imageFiles = state.reportImages + .map { uri -> + async { convertUriToImageFile(uri, "report", context) } } - } - .awaitAll() - .filterNotNull() - } + .awaitAll() + .filterNotNull() - if (imageFiles.size < state.reportImages.size) { - reduce { state.copy(submitState = SubmitState.ERROR) } - return@intent - } + if (imageFiles.size < state.reportImages.size) { + return@async Result.failure(Exception("Image conversion failed")) + } - val uploadedPaths = uploadReportImagesUseCase(imageFiles).getOrElse { error -> - reduce { state.copy(submitState = SubmitState.ERROR) } - return@intent + val uploadedPaths = uploadReportImagesUseCase(imageFiles) + .getOrElse { return@async Result.failure(it) } + + val submitResult = submitReportUseCase( + Report( + title = state.reportTitle, + content = state.reportContent, + category = category, + address = address, + latitude = latitude, + longitude = longitude, + imageUrls = uploadedPaths, + ), + ) + + submitResult.map { uploadedPaths } + } + + minDelayJob.await() + val result = processingJob.await() + + result.fold( + onSuccess = { paths -> + reduce { + state.copy( + submitState = SubmitState.COMPLETE, + uploadedImagePaths = paths, + ) + } + }, + onFailure = { error -> }, + ) } - - reduce { state.copy(uploadedImagePaths = uploadedPaths) } - - submitReportUseCase( - Report( - title = state.reportTitle, - content = state.reportContent, - category = category.toDomain(), - address = address, - latitude = latitude, - longitude = longitude, - imageUrls = uploadedPaths, - ), - ).fold( - onSuccess = { - reduce { state.copy(submitState = SubmitState.SUCCESS) } - }, - onFailure = { it -> - reduce { state.copy(submitState = SubmitState.ERROR) } - }, - ) } } @@ -169,56 +182,3 @@ class ReportViewModel @Inject constructor( const val MAX_IMAGE_COUNT = 3 } } - -data class ReportState( - val reportImages: List, - val reportTitle: String, - val reportContent: String, - val selectedCategory: ReportCategoryUi?, - val imageSourceBottomSheetVisible: Boolean, - val reportCategoryBottomSheetVisible: Boolean, - val currentAddress: String?, - val currentLatitude: Double?, - val currentLongitude: Double?, - val uploadedImagePaths: List, - val submitState: SubmitState, -) { - val canAddMoreImages: Boolean - get() = reportImages.size < ReportViewModel.MAX_IMAGE_COUNT - - val isSubmittable: Boolean - get() = reportImages.isNotEmpty() && - reportTitle.isNotEmpty() && - reportContent.isNotEmpty() && - selectedCategory != null && - currentAddress != null && - currentLatitude != null && - currentLongitude != null - - companion object { - val Init = ReportState( - reportImages = emptyList(), - reportTitle = "", - reportContent = "", - selectedCategory = null, - imageSourceBottomSheetVisible = false, - reportCategoryBottomSheetVisible = false, - currentAddress = null, - currentLatitude = null, - currentLongitude = null, - uploadedImagePaths = emptyList(), - submitState = SubmitState.IDLE, - ) - } -} - -enum class SubmitState { - IDLE, - SUBMITTING, - SUCCESS, - ERROR, -} - -sealed interface ReportSideEffect { - data object NavigateToBack : ReportSideEffect -} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/template/CompleteReportContent.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/template/CompleteReportContent.kt index bf6a5789..8a819ebb 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/template/CompleteReportContent.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/template/CompleteReportContent.kt @@ -24,14 +24,14 @@ import com.threegap.bitnagil.designsystem.R import com.threegap.bitnagil.designsystem.component.atom.BitnagilIcon import com.threegap.bitnagil.designsystem.component.atom.BitnagilTextButton import com.threegap.bitnagil.designsystem.component.atom.BitnagilTextButtonColor -import com.threegap.bitnagil.presentation.report.ReportState import com.threegap.bitnagil.presentation.report.component.CompleteReportCard +import com.threegap.bitnagil.presentation.report.model.ReportState @Composable fun CompleteReportContent( uiState: ReportState, onConfirmClick: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val scrollState = rememberScrollState() @@ -47,32 +47,32 @@ fun CompleteReportContent( tint = null, modifier = Modifier .padding(bottom = 12.dp) - .size(40.dp) + .size(40.dp), ) Text( text = "제보가 완료되었습니다.", color = BitnagilTheme.colors.coolGray10, style = BitnagilTheme.typography.title2Bold, - modifier = Modifier.padding(bottom = 16.dp) + modifier = Modifier.padding(bottom = 16.dp), ) Text( text = "빛나길에서 접수 후 완료되면\n신고를 진행합니다.", color = BitnagilTheme.colors.coolGray40, style = BitnagilTheme.typography.body1Medium, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) Box( - contentAlignment = Alignment.TopCenter + contentAlignment = Alignment.TopCenter, ) { Image( painter = painterResource(R.drawable.onboarding_character), contentDescription = null, modifier = Modifier .padding(top = 34.dp) - .size(134.dp, 148.dp) + .size(134.dp, 148.dp), ) CompleteReportCard( @@ -109,6 +109,6 @@ fun CompleteReportContent( private fun Preview() { CompleteReportContent( uiState = ReportState.Init, - onConfirmClick = {} + onConfirmClick = {}, ) } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/template/SubmittingReportContent.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/template/SubmittingReportContent.kt index 82800a6e..84b75644 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/template/SubmittingReportContent.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/template/SubmittingReportContent.kt @@ -20,7 +20,7 @@ import com.threegap.bitnagil.designsystem.component.atom.BitnagilIcon @Composable fun SubmittingReportContent( - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Column( modifier = modifier @@ -31,28 +31,28 @@ fun SubmittingReportContent( BitnagilIcon( id = R.drawable.ic_loading, tint = null, - modifier = Modifier.padding(bottom = 12.dp) + modifier = Modifier.padding(bottom = 12.dp), ) Text( text = "제보중...", color = BitnagilTheme.colors.coolGray10, style = BitnagilTheme.typography.title2Bold, - modifier = Modifier.padding(bottom = 16.dp) + modifier = Modifier.padding(bottom = 16.dp), ) Text( text = "포모가 열심\n제보하고 있어요", color = BitnagilTheme.colors.coolGray40, style = BitnagilTheme.typography.body1Medium, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) Spacer(modifier = Modifier.height(72.dp)) Image( painter = painterResource(R.drawable.img_pomo_loading), - contentDescription = null + contentDescription = null, ) } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportSideEffect.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportSideEffect.kt new file mode 100644 index 00000000..20ab53a4 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportSideEffect.kt @@ -0,0 +1,5 @@ +package com.threegap.bitnagil.presentation.report.model + +sealed interface ReportSideEffect { + data object NavigateToBack : ReportSideEffect +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportState.kt new file mode 100644 index 00000000..f025ba16 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportState.kt @@ -0,0 +1,47 @@ +package com.threegap.bitnagil.presentation.report.model + +import android.net.Uri +import com.threegap.bitnagil.domain.report.model.ReportCategory +import com.threegap.bitnagil.presentation.report.ReportViewModel + +data class ReportState( + val reportImages: List, + val reportTitle: String, + val reportContent: String, + val selectedCategory: ReportCategory?, + val imageSourceBottomSheetVisible: Boolean, + val reportCategoryBottomSheetVisible: Boolean, + val currentAddress: String?, + val currentLatitude: Double?, + val currentLongitude: Double?, + val uploadedImagePaths: List, + val submitState: SubmitState, +) { + val canAddMoreImages: Boolean + get() = reportImages.size < ReportViewModel.Companion.MAX_IMAGE_COUNT + + val isSubmittable: Boolean + get() = reportImages.isNotEmpty() && + reportTitle.isNotEmpty() && + reportContent.isNotEmpty() && + selectedCategory != null && + currentAddress != null && + currentLatitude != null && + currentLongitude != null + + companion object { + val Init = ReportState( + reportImages = emptyList(), + reportTitle = "", + reportContent = "", + selectedCategory = null, + imageSourceBottomSheetVisible = false, + reportCategoryBottomSheetVisible = false, + currentAddress = null, + currentLatitude = null, + currentLongitude = null, + uploadedImagePaths = emptyList(), + submitState = SubmitState.IDLE, + ) + } +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/SubmitState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/SubmitState.kt new file mode 100644 index 00000000..fe0212bd --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/SubmitState.kt @@ -0,0 +1,3 @@ +package com.threegap.bitnagil.presentation.report.model + +enum class SubmitState { IDLE, SUBMITTING, COMPLETE } From d7b0c907673a345d3b5c8ea654ab3915bc550f29 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Fri, 21 Nov 2025 13:23:55 +0900 Subject: [PATCH 42/44] =?UTF-8?q?Feat:=20=EC=A0=9C=EB=B3=B4=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=ED=82=A4=EB=B3=B4=EB=93=9C=20ux=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 제보 제목 입력 후 '완료'를 누르면 카테고리 선택 창이 나타나도록 수정 - 카테고리 선택 후 제보 내용 입력 필드에 자동으로 포커스가 가도록 수정 - 제보 내용 입력 후 '완료'를 누르면 키보드가 내려가도록 수정 --- .../presentation/report/ReportScreen.kt | 39 ++++++++++++++++++- .../presentation/report/ReportViewModel.kt | 1 + .../report/model/ReportSideEffect.kt | 1 + 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportScreen.kt index 897775c7..78cc1f2a 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportScreen.kt @@ -26,6 +26,8 @@ import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text @@ -36,7 +38,11 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -62,6 +68,7 @@ import com.threegap.bitnagil.presentation.report.model.ReportSideEffect import com.threegap.bitnagil.presentation.report.model.ReportState import com.threegap.bitnagil.presentation.report.model.SubmitState import com.threegap.bitnagil.presentation.report.model.uiTitle +import kotlinx.coroutines.delay import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect @@ -73,10 +80,15 @@ fun ReportScreenContainer( ) { val context = LocalContext.current val uiState by viewModel.collectAsState() + val contentFocusRequester = remember { FocusRequester() } viewModel.collectSideEffect { sideEffect -> when (sideEffect) { is ReportSideEffect.NavigateToBack -> navigateToBack() + is ReportSideEffect.FocusOnContent -> { + delay(100) + contentFocusRequester.requestFocus() + } } } @@ -165,6 +177,7 @@ fun ReportScreenContainer( SubmitState.IDLE -> { ReportScreen( uiState = uiState, + contentFocusRequester = contentFocusRequester, onReportTitleChange = viewModel::updateReportTitle, onReportContentChange = viewModel::updateReportContent, onShowImageSourceBottomSheet = viewModel::showImageSourceBottomSheet, @@ -190,6 +203,7 @@ fun ReportScreenContainer( @Composable private fun ReportScreen( uiState: ReportState, + contentFocusRequester: FocusRequester, onReportTitleChange: (String) -> Unit, onReportContentChange: (String) -> Unit, onShowImageSourceBottomSheet: () -> Unit, @@ -200,6 +214,7 @@ private fun ReportScreen( onBackClick: () -> Unit, ) { val scrollState = rememberScrollState() + val focusManager = LocalFocusManager.current Column( modifier = Modifier @@ -250,6 +265,12 @@ private fun ReportScreen( value = uiState.reportTitle, onValueChange = onReportTitleChange, singleLine = true, + keyboardActions = KeyboardActions( + onDone = { + focusManager.clearFocus() + onShowReportCategoryBottomSheet() + }, + ), placeholder = { Text( text = "제보 제목을 작성해주세요.", @@ -263,7 +284,10 @@ private fun ReportScreen( ReportField(title = "카테고리") { ReportCategorySelector( title = uiState.selectedCategory?.uiTitle, - onClick = onShowReportCategoryBottomSheet, + onClick = { + focusManager.clearFocus() + onShowReportCategoryBottomSheet() + }, ) } @@ -271,7 +295,17 @@ private fun ReportScreen( BitnagilTextField( value = uiState.reportContent, onValueChange = onReportContentChange, - modifier = Modifier.height(88.dp), + modifier = Modifier + .height(88.dp) + .focusRequester(contentFocusRequester), + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done, + ), + keyboardActions = KeyboardActions( + onDone = { + focusManager.clearFocus() + }, + ), placeholder = { Text( text = "어떤 위험인지 간단히 설명해주세요.(100자 내외)", @@ -318,6 +352,7 @@ private fun ReportScreen( private fun Preview() { ReportScreen( uiState = ReportState.Init, + contentFocusRequester = remember { FocusRequester() }, onReportTitleChange = {}, onReportContentChange = {}, onRemoveImage = {}, diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt index ae13b7e7..38811784 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt @@ -66,6 +66,7 @@ class ReportViewModel @Inject constructor( fun hideReportCategoryBottomSheet() { intent { reduce { state.copy(reportCategoryBottomSheetVisible = false) } + postSideEffect(ReportSideEffect.FocusOnContent) } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportSideEffect.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportSideEffect.kt index 20ab53a4..c3374c47 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportSideEffect.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportSideEffect.kt @@ -2,4 +2,5 @@ package com.threegap.bitnagil.presentation.report.model sealed interface ReportSideEffect { data object NavigateToBack : ReportSideEffect + data object FocusOnContent : ReportSideEffect } From cf810cae9dadb132307b4c5c7fb0605b7a48d965 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Fri, 21 Nov 2025 16:48:37 +0900 Subject: [PATCH 43/44] =?UTF-8?q?Fix:=20=ED=86=A0=EB=81=BC=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bitnagil/presentation/report/ReportScreen.kt | 7 ++++--- .../bitnagil/presentation/report/ReportViewModel.kt | 13 ++++++------- .../report/component/CompleteReportCard.kt | 2 +- .../report/component/ReportCategoryBottomSheet.kt | 6 +++--- .../presentation/report/model/ReportState.kt | 5 +++-- 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportScreen.kt index 78cc1f2a..cec60de3 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportScreen.kt @@ -66,6 +66,7 @@ import com.threegap.bitnagil.presentation.report.component.template.CompleteRepo import com.threegap.bitnagil.presentation.report.component.template.SubmittingReportContent import com.threegap.bitnagil.presentation.report.model.ReportSideEffect import com.threegap.bitnagil.presentation.report.model.ReportState +import com.threegap.bitnagil.presentation.report.model.ReportState.Companion.MAX_IMAGE_COUNT import com.threegap.bitnagil.presentation.report.model.SubmitState import com.threegap.bitnagil.presentation.report.model.uiTitle import kotlinx.coroutines.delay @@ -95,7 +96,7 @@ fun ReportScreenContainer( var pendingCameraPhotoUri by remember { mutableStateOf(null) } val pickMultipleMediaLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.PickMultipleVisualMedia(ReportViewModel.MAX_IMAGE_COUNT), + contract = ActivityResultContracts.PickMultipleVisualMedia(MAX_IMAGE_COUNT), onResult = viewModel::addImages, ) @@ -242,7 +243,7 @@ private fun ReportScreen( AddPhotoButton( onClick = onShowImageSourceBottomSheet, imageCount = uiState.reportImages.size, - maxImageCount = ReportViewModel.MAX_IMAGE_COUNT, + maxImageCount = MAX_IMAGE_COUNT, ) LazyRow( @@ -324,7 +325,7 @@ private fun ReportScreen( ) } - ReportField(title = " 신고 위치") { + ReportField(title = "신고 위치") { CurrentLocationInput( currentLocation = uiState.currentAddress, onClick = onGetCurrentLocationClick, diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt index 38811784..761dff11 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/ReportViewModel.kt @@ -11,6 +11,7 @@ import com.threegap.bitnagil.domain.report.usecase.SubmitReportUseCase import com.threegap.bitnagil.presentation.common.file.convertUriToImageFile import com.threegap.bitnagil.presentation.report.model.ReportSideEffect import com.threegap.bitnagil.presentation.report.model.ReportState +import com.threegap.bitnagil.presentation.report.model.ReportState.Companion.MAX_IMAGE_COUNT import com.threegap.bitnagil.presentation.report.model.SubmitState import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext @@ -119,13 +120,13 @@ class ReportViewModel @Inject constructor( fun submitReportWithImages() { intent { - reduce { state.copy(submitState = SubmitState.SUBMITTING) } - val category = state.selectedCategory ?: return@intent val address = state.currentAddress ?: return@intent val latitude = state.currentLatitude ?: return@intent val longitude = state.currentLongitude ?: return@intent + reduce { state.copy(submitState = SubmitState.SUBMITTING) } + coroutineScope { val minDelayJob = async { delay(1000L) @@ -173,13 +174,11 @@ class ReportViewModel @Inject constructor( ) } }, - onFailure = { error -> }, + onFailure = { error -> + reduce { state.copy(submitState = SubmitState.IDLE) } + }, ) } } } - - companion object { - const val MAX_IMAGE_COUNT = 3 - } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/CompleteReportCard.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/CompleteReportCard.kt index af299133..8b29abfc 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/CompleteReportCard.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/CompleteReportCard.kt @@ -63,7 +63,7 @@ fun CompleteReportCard( } CompleteReportCardItem( - title = "키테고리", + title = "카테고리", ) { Text( text = category?.uiTitle ?: "카테고리 없음", diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/ReportCategoryBottomSheet.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/ReportCategoryBottomSheet.kt index d414b9b4..8a4ea0d1 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/ReportCategoryBottomSheet.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/component/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.domain.report.model.ReportCategory import com.threegap.bitnagil.presentation.report.model.iconRes import com.threegap.bitnagil.presentation.report.model.uiDescription @@ -51,7 +50,8 @@ fun ReportCategoryBottomSheet( Column( modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 28.dp), ) { - ReportCategory.entries.forEachIndexed { index, category -> + val categories = ReportCategory.entries + categories.forEachIndexed { index, category -> ReportCategoryItem( icon = category.iconRes, title = category.uiTitle, @@ -67,7 +67,7 @@ fun ReportCategoryBottomSheet( }, ) - if (index < RecommendLevel.entries.size) { + if (index < categories.lastIndex) { HorizontalDivider( color = BitnagilTheme.colors.coolGray97, ) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportState.kt index f025ba16..4676d8b0 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportState.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/report/model/ReportState.kt @@ -2,7 +2,6 @@ package com.threegap.bitnagil.presentation.report.model import android.net.Uri import com.threegap.bitnagil.domain.report.model.ReportCategory -import com.threegap.bitnagil.presentation.report.ReportViewModel data class ReportState( val reportImages: List, @@ -18,7 +17,7 @@ data class ReportState( val submitState: SubmitState, ) { val canAddMoreImages: Boolean - get() = reportImages.size < ReportViewModel.Companion.MAX_IMAGE_COUNT + get() = reportImages.size < MAX_IMAGE_COUNT val isSubmittable: Boolean get() = reportImages.isNotEmpty() && @@ -30,6 +29,8 @@ data class ReportState( currentLongitude != null companion object { + const val MAX_IMAGE_COUNT = 3 + val Init = ReportState( reportImages = emptyList(), reportTitle = "", From da1655a59d401f67c669a27cfbdd23ce6a101fff Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Fri, 21 Nov 2025 20:25:49 +0900 Subject: [PATCH 44/44] Add: KAKAO_REST_API_KEY to develop workflow --- .github/workflows/develop_branch.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/develop_branch.yml b/.github/workflows/develop_branch.yml index 1af3e99b..28ba3704 100644 --- a/.github/workflows/develop_branch.yml +++ b/.github/workflows/develop_branch.yml @@ -24,6 +24,7 @@ jobs: - name: Generate local.properties env: KAKAO_NATIVE_APP_KEY: ${{ secrets.KAKAO_NATIVE_APP_KEY }} + KAKAO_REST_API_KEY: ${{ secrets.KAKAO_REST_API_KEY }} BITNAGIL_DEV_URL: ${{ secrets.BITNAGIL_DEV_URL }} BITNAGIL_PROD_URL: ${{ secrets.BITNAGIL_PROD_URL }} RELEASE_KEY_ALIAS: ${{ secrets.RELEASE_KEY_ALIAS }} @@ -31,6 +32,7 @@ jobs: RELEASE_STORE_PASSWORD: ${{ secrets.RELEASE_STORE_PASSWORD }} run: | echo "kakao.native.app.key=$KAKAO_NATIVE_APP_KEY" >> local.properties + echo "kakao.rest.api.key=$KAKAO_REST_API_KEY" >> local.properties echo "bitnagil.dev.url=$BITNAGIL_DEV_URL" >> local.properties echo "bitnagil.prod.url=$BITNAGIL_PROD_URL" >> local.properties echo "release.key.alias=$RELEASE_KEY_ALIAS" >> local.properties