Skip to content

Commit ee15c53

Browse files
authored
Merge pull request #67 from YAPP-Github/feat/#66-unsupported-brand-qr
[Feat] #66 미지원 브랜드 QR 스캔 시 대응 기능 구현
2 parents 6ce6dcd + 7d9ef96 commit ee15c53

12 files changed

Lines changed: 259 additions & 29 deletions

File tree

core/designsystem/src/main/java/com/neki/android/core/designsystem/dialog/DoubleButtonAlertDialog.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ fun DoubleButtonAlertDialog(
3737
onClickPrimaryButton: () -> Unit,
3838
onClickGrayButton: () -> Unit,
3939
modifier: Modifier = Modifier,
40-
properties: DialogProperties = DialogProperties(),
40+
properties: DialogProperties = DialogProperties(
41+
usePlatformDefaultWidth = false,
42+
),
4143
) {
4244
Dialog(
4345
onDismissRequest = onDismissRequest,

core/designsystem/src/main/java/com/neki/android/core/designsystem/dialog/SingleButtonAlertDialog.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ fun SingleButtonAlertDialog(
3434
onClick: () -> Unit,
3535
enabled: Boolean = true,
3636
properties: DialogProperties = DialogProperties(
37+
usePlatformDefaultWidth = false,
3738
dismissOnBackPress = false,
3839
dismissOnClickOutside = false,
3940
),
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package com.neki.android.core.designsystem.dialog
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.layout.Arrangement
5+
import androidx.compose.foundation.layout.Column
6+
import androidx.compose.foundation.layout.fillMaxWidth
7+
import androidx.compose.foundation.layout.padding
8+
import androidx.compose.foundation.layout.widthIn
9+
import androidx.compose.foundation.shape.RoundedCornerShape
10+
import androidx.compose.material3.Icon
11+
import androidx.compose.material3.Text
12+
import androidx.compose.runtime.Composable
13+
import androidx.compose.ui.Alignment
14+
import androidx.compose.ui.Modifier
15+
import androidx.compose.ui.draw.clip
16+
import androidx.compose.ui.graphics.Color
17+
import androidx.compose.ui.graphics.vector.ImageVector
18+
import androidx.compose.ui.res.vectorResource
19+
import androidx.compose.ui.text.style.TextAlign
20+
import androidx.compose.ui.text.style.TextDecoration
21+
import androidx.compose.ui.unit.dp
22+
import androidx.compose.ui.window.Dialog
23+
import androidx.compose.ui.window.DialogProperties
24+
import com.neki.android.core.designsystem.ComponentPreview
25+
import com.neki.android.core.designsystem.R
26+
import com.neki.android.core.designsystem.button.CTAButtonPrimary
27+
import com.neki.android.core.designsystem.modifier.clickableSingle
28+
import com.neki.android.core.designsystem.ui.theme.NekiTheme
29+
30+
@Composable
31+
fun SingleButtonWithTextButtonAlertDialog(
32+
title: String,
33+
content: String,
34+
buttonText: String,
35+
textButtonText: String,
36+
onDismissRequest: () -> Unit,
37+
onButtonClick: () -> Unit,
38+
onTextButtonClick: () -> Unit,
39+
enabled: Boolean = true,
40+
properties: DialogProperties = DialogProperties(
41+
usePlatformDefaultWidth = false,
42+
dismissOnBackPress = false,
43+
dismissOnClickOutside = false,
44+
),
45+
) {
46+
Dialog(
47+
onDismissRequest = onDismissRequest,
48+
properties = properties,
49+
) {
50+
Column(
51+
modifier = Modifier
52+
.fillMaxWidth()
53+
.padding(horizontal = 20.dp)
54+
.widthIn(max = 400.dp)
55+
.clip(RoundedCornerShape(20.dp))
56+
.background(NekiTheme.colorScheme.white)
57+
.padding(top = 20.dp),
58+
horizontalAlignment = Alignment.CenterHorizontally,
59+
verticalArrangement = Arrangement.spacedBy(12.dp),
60+
) {
61+
Icon(
62+
imageVector = ImageVector.vectorResource(R.drawable.icon_dialog_alert),
63+
tint = Color.Unspecified,
64+
contentDescription = null,
65+
)
66+
Column(
67+
modifier = Modifier.padding(horizontal = 24.dp),
68+
verticalArrangement = Arrangement.spacedBy(2.dp),
69+
horizontalAlignment = Alignment.CenterHorizontally,
70+
) {
71+
Text(
72+
text = title,
73+
style = NekiTheme.typography.title18Bold,
74+
color = NekiTheme.colorScheme.gray900,
75+
textAlign = TextAlign.Center,
76+
)
77+
Text(
78+
text = content,
79+
style = NekiTheme.typography.body14Regular,
80+
color = NekiTheme.colorScheme.gray500,
81+
textAlign = TextAlign.Center,
82+
)
83+
}
84+
Column(
85+
modifier = Modifier.padding(vertical = 12.dp),
86+
horizontalAlignment = Alignment.CenterHorizontally,
87+
verticalArrangement = Arrangement.spacedBy(8.dp),
88+
) {
89+
CTAButtonPrimary(
90+
text = buttonText,
91+
onClick = onButtonClick,
92+
modifier = Modifier
93+
.fillMaxWidth()
94+
.padding(horizontal = 12.dp),
95+
enabled = enabled,
96+
)
97+
Text(
98+
modifier = Modifier
99+
.padding(
100+
vertical = 4.dp,
101+
horizontal = 56.dp,
102+
)
103+
.clickableSingle(onClick = onTextButtonClick),
104+
text = textButtonText,
105+
style = NekiTheme.typography.body14Regular,
106+
color = NekiTheme.colorScheme.primary600,
107+
textDecoration = TextDecoration.Underline,
108+
)
109+
}
110+
}
111+
}
112+
}
113+
114+
@ComponentPreview
115+
@Composable
116+
private fun SingleButtonWithTextButtonAlertDialogPreview() {
117+
NekiTheme {
118+
SingleButtonWithTextButtonAlertDialog(
119+
title = "메인 텍스트가 들어가는 곳",
120+
content = "보조 설명 텍스트가 들어가는 공간입니다",
121+
buttonText = "텍스트",
122+
textButtonText = "텍스트 공간",
123+
onDismissRequest = {},
124+
onButtonClick = {},
125+
onTextButtonClick = {},
126+
)
127+
}
128+
}

detekt-config.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ exceptions:
5454
active: false
5555

5656
style:
57+
FunctionOnlyReturningConstant:
58+
active: false
5759
UnusedParameter:
5860
active: false
5961
MagicNumber:

feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/navigation/ArchiveEntryProvider.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import com.neki.android.feature.archive.impl.main.ArchiveMainViewModel
2121
import com.neki.android.feature.archive.impl.photo.AllPhotoRoute
2222
import com.neki.android.feature.archive.impl.photo_detail.PhotoDetailRoute
2323
import com.neki.android.feature.archive.impl.photo_detail.PhotoDetailViewModel
24+
import com.neki.android.feature.photo_upload.api.QRScanResult
2425
import com.neki.android.feature.photo_upload.api.navigateToQRScan
2526
import com.neki.android.feature.photo_upload.api.navigateToUploadAlbum
2627
import dagger.Module
@@ -44,8 +45,16 @@ private fun EntryProviderScope<NavKey>.archiveEntry(navigator: Navigator) {
4445
entry<ArchiveNavKey.Archive> {
4546
val resultBus = LocalResultEventBus.current
4647
val viewModel = hiltViewModel<ArchiveMainViewModel>()
47-
ResultEffect<String>(resultBus) { imageUrl ->
48-
viewModel.store.onIntent(ArchiveMainIntent.QRCodeScanned(imageUrl))
48+
ResultEffect<QRScanResult>(resultBus) { result ->
49+
when (result) {
50+
is QRScanResult.QRCodeScanned -> {
51+
viewModel.store.onIntent(ArchiveMainIntent.QRCodeScanned(result.imageUrl))
52+
}
53+
54+
QRScanResult.OpenGallery -> {
55+
viewModel.store.onIntent(ArchiveMainIntent.ClickGalleryUploadRow)
56+
}
57+
}
4958
}
5059
ResultEffect<Boolean>(resultBus) { hasUpdated ->
5160
if (hasUpdated) viewModel.store.onIntent(ArchiveMainIntent.RefreshArchiveMainScreen)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.neki.android.feature.photo_upload.api
2+
3+
sealed interface QRScanResult {
4+
data class QRCodeScanned(val imageUrl: String) : QRScanResult
5+
data object OpenGallery : QRScanResult
6+
}

feature/photo-upload/impl/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ android {
2020
}
2121

2222
defaultConfig {
23+
buildConfigField("String", "BRAND_PROPOSAL_URL", properties["BRAND_PROPOSAL_URL"].toString())
2324
buildConfigField("String", "PHOTOISM_URL", properties["PHOTOISM_URL"].toString())
2425
buildConfigField("String", "PHOTOISM_IMAGE_URL", properties["PHOTOISM_IMAGE_URL"].toString())
2526
buildConfigField("String", "PHOTOISM_IMG_URL_MIME_TYPE", properties["PHOTOISM_IMG_URL_MIME_TYPE"].toString())

feature/photo-upload/impl/src/main/java/com/neki/android/feature/photo_upload/impl/di/PhotoUploadEntryProvider.kt

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import com.neki.android.core.navigation.EntryProviderInstaller
77
import com.neki.android.core.navigation.Navigator
88
import com.neki.android.core.navigation.result.LocalResultEventBus
99
import com.neki.android.feature.archive.api.navigateToAlbumDetail
10-
import com.neki.android.feature.archive.api.navigateToArchive
1110
import com.neki.android.feature.photo_upload.api.PhotoUploadNavKey
11+
import com.neki.android.feature.photo_upload.api.QRScanResult
1212
import com.neki.android.feature.photo_upload.impl.album.UploadAlbumRoute
1313
import com.neki.android.feature.photo_upload.impl.album.UploadAlbumViewModel
1414
import com.neki.android.feature.photo_upload.impl.qrscan.QRScanRoute
@@ -35,10 +35,7 @@ private fun EntryProviderScope<NavKey>.photoUploadEntry(navigator: Navigator) {
3535

3636
QRScanRoute(
3737
navigateBack = navigator::goBack,
38-
navigateToHome = {
39-
resultBus.sendResult<String>(result = it)
40-
navigator.navigateToArchive()
41-
},
38+
setQRResult = { resultBus.sendResult<QRScanResult>(result = it) },
4239
)
4340
}
4441
entry<PhotoUploadNavKey.UploadAlbum> { key ->

feature/photo-upload/impl/src/main/java/com/neki/android/feature/photo_upload/impl/qrscan/QRScanContract.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ data class QRScanState(
55
val viewType: QRScanViewType = QRScanViewType.QR_SCAN,
66
val scannedUrl: String? = null,
77
val detectedImageUrl: String? = null,
8-
val isShowInfoDialog: Boolean = false,
8+
val isShowShouldDownloadDialog: Boolean = false,
9+
val isShowUnSupportedBrandDialog: Boolean = false,
910
val isTorchEnabled: Boolean = false,
1011
)
1112

@@ -15,12 +16,19 @@ sealed interface QRScanIntent {
1516
data class ScanQRCode(val scannedUrl: String) : QRScanIntent
1617
data class SetViewType(val viewType: QRScanViewType) : QRScanIntent
1718
data class DetectImageUrl(val imageUrl: String) : QRScanIntent
19+
data object DismissShouldDownloadDialog : QRScanIntent
20+
data object ClickGoDownload : QRScanIntent
21+
data object DismissUnSupportedBrandDialog : QRScanIntent
22+
data object ClickUploadGallery : QRScanIntent
23+
data object ClickProposeBrand : QRScanIntent
1824
}
1925

2026
sealed interface QRScanSideEffect {
2127
data object NavigateBack : QRScanSideEffect
22-
data class NavigateToHome(val imageUrl: String) : QRScanSideEffect
28+
data class SetQRScannedResult(val imageUrl: String) : QRScanSideEffect
2329
data class ShowToast(val message: String) : QRScanSideEffect
30+
data object OpenBrandProposalUrl : QRScanSideEffect
31+
data object SetOpenGalleryResult : QRScanSideEffect
2432
}
2533

2634
enum class QRScanViewType {

feature/photo-upload/impl/src/main/java/com/neki/android/feature/photo_upload/impl/qrscan/QRScanScreen.kt

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,54 @@
11
package com.neki.android.feature.photo_upload.impl.qrscan
22

3+
import android.content.Intent
34
import androidx.compose.foundation.layout.fillMaxSize
45
import androidx.compose.runtime.Composable
56
import androidx.compose.runtime.LaunchedEffect
67
import androidx.compose.runtime.getValue
8+
import androidx.compose.runtime.remember
79
import androidx.compose.ui.Modifier
10+
import androidx.compose.ui.platform.LocalContext
811
import androidx.compose.ui.tooling.preview.Preview
12+
import androidx.core.net.toUri
913
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
1014
import androidx.lifecycle.compose.collectAsStateWithLifecycle
15+
import com.neki.android.core.designsystem.dialog.SingleButtonAlertDialog
16+
import com.neki.android.core.designsystem.dialog.SingleButtonWithTextButtonAlertDialog
1117
import com.neki.android.core.designsystem.ui.theme.NekiTheme
1218
import com.neki.android.core.ui.compose.collectWithLifecycle
19+
import com.neki.android.core.ui.toast.NekiToast
20+
import com.neki.android.feature.photo_upload.api.QRScanResult
21+
import com.neki.android.feature.photo_upload.impl.BuildConfig
1322
import com.neki.android.feature.photo_upload.impl.qrscan.component.PhotoWebViewContent
1423
import com.neki.android.feature.photo_upload.impl.qrscan.component.QRScannerContent
1524

1625
@Composable
1726
internal fun QRScanRoute(
1827
viewModel: QRScanViewModel = hiltViewModel(),
1928
navigateBack: () -> Unit,
20-
navigateToHome: (String) -> Unit,
29+
setQRResult: (QRScanResult) -> Unit = {},
2130
) {
2231
val uiState by viewModel.store.uiState.collectAsStateWithLifecycle()
32+
val context = LocalContext.current
33+
val nekiToast = remember { NekiToast(context) }
2334

2435
viewModel.store.sideEffects.collectWithLifecycle { sideEffect ->
2536
when (sideEffect) {
2637
QRScanSideEffect.NavigateBack -> navigateBack()
27-
is QRScanSideEffect.NavigateToHome -> navigateToHome(sideEffect.imageUrl)
28-
is QRScanSideEffect.ShowToast -> {}
38+
is QRScanSideEffect.SetQRScannedResult -> {
39+
setQRResult(QRScanResult.QRCodeScanned(sideEffect.imageUrl))
40+
navigateBack()
41+
}
42+
is QRScanSideEffect.ShowToast -> nekiToast.showToast(sideEffect.message)
43+
QRScanSideEffect.OpenBrandProposalUrl -> {
44+
val intent = Intent(Intent.ACTION_VIEW, BuildConfig.BRAND_PROPOSAL_URL.toUri())
45+
context.startActivity(intent)
46+
}
47+
48+
QRScanSideEffect.SetOpenGalleryResult -> {
49+
setQRResult(QRScanResult.OpenGallery)
50+
navigateBack()
51+
}
2952
}
3053
}
3154
QRScanScreen(
@@ -63,6 +86,28 @@ internal fun QRScanScreen(
6386
}
6487
}
6588
}
89+
90+
if (uiState.isShowShouldDownloadDialog) {
91+
SingleButtonAlertDialog(
92+
title = "갤러리에 사진을 먼저 다운받아주세요",
93+
content = "해당 브랜드는 웹사이트에서 사진을 저장해야\n네키에 자동으로 저장돼요",
94+
buttonText = "사진 다운로드하러가기",
95+
onDismissRequest = { onIntent(QRScanIntent.DismissShouldDownloadDialog) },
96+
onClick = { onIntent(QRScanIntent.ClickGoDownload) },
97+
)
98+
}
99+
100+
if (uiState.isShowUnSupportedBrandDialog) {
101+
SingleButtonWithTextButtonAlertDialog(
102+
title = "지원하지 않는 브랜드예요",
103+
content = "갤러리에서 사진을 추가해 바로 저장할 수 있어요\n원하는 브랜드가 있다면 제안해주세요!",
104+
buttonText = "갤러리에서 추가하기",
105+
textButtonText = "브랜드 제안하기",
106+
onDismissRequest = { onIntent(QRScanIntent.DismissUnSupportedBrandDialog) },
107+
onButtonClick = { onIntent(QRScanIntent.ClickUploadGallery) },
108+
onTextButtonClick = { onIntent(QRScanIntent.ClickProposeBrand) },
109+
)
110+
}
66111
}
67112

68113
@Preview(showBackground = true)

0 commit comments

Comments
 (0)