diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1145c234..27f33512 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -10,7 +10,7 @@ android { defaultConfig { applicationId = "com.withpeace.withpeace" targetSdk = 34 - versionCode = 2 + versionCode = 4 versionName = "1.0.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -20,7 +20,6 @@ android { } packaging { resources { - excludes += "/META-INF/{AL2.0,LGPL2.1}" merges += "META-INF/LICENSE.md" merges += "META-INF/LICENSE-notice.md" } @@ -41,6 +40,9 @@ dependencies { implementation(libs.androidx.core.splashscreen) implementation(project(":feature:login")) implementation(project(":feature:signup")) + implementation(project(":feature:policyconsent")) + implementation(project(":feature:privacypolicy")) + implementation(project(":feature:termsofservice")) implementation(project(":feature:home")) implementation(project(":feature:postlist")) implementation(project(":feature:mypage")) @@ -58,4 +60,7 @@ dependencies { implementation(project(":core:analytics")) implementation(project(":core:designsystem")) testImplementation(project(":core:testing")) + + implementation("com.google.android.play:app-update:2.1.0") + implementation("com.google.android.play:app-update-ktx:2.1.0") } diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json index 14a8fc60..6e51644d 100644 --- a/app/release/output-metadata.json +++ b/app/release/output-metadata.json @@ -11,7 +11,7 @@ "type": "SINGLE", "filters": [], "attributes": [], - "versionCode": 2, + "versionCode": 3, "versionName": "1.0.0", "outputFile": "app-release.apk" } diff --git a/app/src/main/java/com/withpeace/withpeace/MainActivity.kt b/app/src/main/java/com/withpeace/withpeace/MainActivity.kt index a865a5b6..faa34761 100644 --- a/app/src/main/java/com/withpeace/withpeace/MainActivity.kt +++ b/app/src/main/java/com/withpeace/withpeace/MainActivity.kt @@ -1,8 +1,13 @@ package com.withpeace.withpeace +import android.content.Intent +import android.net.Uri import android.os.Bundle +import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.compose.runtime.CompositionLocalProvider import androidx.core.splashscreen.SplashScreen @@ -11,6 +16,13 @@ import androidx.core.view.WindowCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle +import com.google.android.gms.tasks.Task +import com.google.android.play.core.appupdate.AppUpdateInfo +import com.google.android.play.core.appupdate.AppUpdateManager +import com.google.android.play.core.appupdate.AppUpdateManagerFactory +import com.google.android.play.core.appupdate.AppUpdateOptions +import com.google.android.play.core.install.model.AppUpdateType +import com.google.android.play.core.install.model.UpdateAvailability import com.withpeace.withpeace.core.analytics.AnalyticsHelper import com.withpeace.withpeace.core.analytics.LocalAnalyticsHelper import com.withpeace.withpeace.core.designsystem.theme.WithpeaceTheme @@ -28,9 +40,17 @@ class MainActivity : ComponentActivity() { @Inject lateinit var analyticsHelper: AnalyticsHelper + private val appUpdateManager by lazy { AppUpdateManagerFactory.create(this) } + + private val updateActivityResultLauncher = + registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result: ActivityResult -> + if (result.resultCode != RESULT_OK) { + finish() + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - // System Bar에 가려지는 뷰 영역을 개발자가 제어하겠다. WindowCompat.setDecorFitsSystemWindows(window, false) @@ -39,11 +59,16 @@ class MainActivity : ComponentActivity() { splashScreen.setKeepOnScreenCondition { true } // 처음 앱 켰을때만 2초기다림, 화면회전에는 기다리면 안됨 repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.isLogin.collect { isLogin -> - when (isLogin) { - true -> composeStart(HOME_ROUTE) - false -> composeStart(LOGIN_ROUTE) - else -> {} // StateFlow의 상태를 Null로 설정함으로서, 로그인 상태가 업데이트 된 이후로 화면을 보여주도록 하기위함 + viewModel.uiState.collect { uiState -> + when (uiState) { + MainUiState.Home -> composeStart(HOME_ROUTE) + MainUiState.Login -> composeStart(LOGIN_ROUTE) + MainUiState.Update -> { + compulsionUpdate() + } + + MainUiState.Error -> finish() + MainUiState.Loading -> {} } splashScreen.setKeepOnScreenCondition { false } } @@ -62,4 +87,64 @@ class MainActivity : ComponentActivity() { } } } -} \ No newline at end of file + private fun compulsionUpdate() { + val appUpdateInfoTask: Task = appUpdateManager.appUpdateInfo + + appUpdateInfoTask.addOnSuccessListener { appUpdateInfo -> + if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE + && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE) + ) { + startUpdateFlow(appUpdateManager, appUpdateInfo) + } else { + Toast.makeText(this, "앱을 사용하려면 업데이트가 필요해요!", Toast.LENGTH_LONG).show() + redirectToPlayStore() + } + } + } + + private fun startUpdateFlow(appUpdateManager: AppUpdateManager, appUpdateInfo: AppUpdateInfo) { + try { + appUpdateManager.startUpdateFlowForResult( + appUpdateInfo, + updateActivityResultLauncher, + AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build(), + ) + } catch (e: Exception) { + e.printStackTrace() + } + } + + private fun redirectToPlayStore() { + val packageName = packageName + try { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$packageName"))) + } catch (e: Exception) { + startActivity( + Intent( + Intent.ACTION_VIEW, + Uri.parse("https://play.google.com/store/apps/details?id=$packageName"), + ), + ) + } + finish() + } + + override fun onResume() { + super.onResume() + + appUpdateManager + .appUpdateInfo + .addOnSuccessListener { appUpdateInfo -> + if (appUpdateInfo.updateAvailability() + == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS + ) { + // If an in-app update is already running, resume the update. + appUpdateManager.startUpdateFlowForResult( + appUpdateInfo, + updateActivityResultLauncher, + AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build(), + ) + } + } + } +} diff --git a/app/src/main/java/com/withpeace/withpeace/MainUiState.kt b/app/src/main/java/com/withpeace/withpeace/MainUiState.kt new file mode 100644 index 00000000..8468e43d --- /dev/null +++ b/app/src/main/java/com/withpeace/withpeace/MainUiState.kt @@ -0,0 +1,9 @@ +package com.withpeace.withpeace + +sealed interface MainUiState { + data object Login : MainUiState + data object Update : MainUiState + data object Error : MainUiState + data object Home : MainUiState + data object Loading : MainUiState +} \ No newline at end of file diff --git a/app/src/main/java/com/withpeace/withpeace/MainViewModel.kt b/app/src/main/java/com/withpeace/withpeace/MainViewModel.kt index 237ce678..680d78f1 100644 --- a/app/src/main/java/com/withpeace/withpeace/MainViewModel.kt +++ b/app/src/main/java/com/withpeace/withpeace/MainViewModel.kt @@ -2,21 +2,46 @@ package com.withpeace.withpeace import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.withpeace.withpeace.core.domain.usecase.CheckAppUpdateUseCase import com.withpeace.withpeace.core.domain.usecase.IsLoginUseCase import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class MainViewModel @Inject constructor( private val isLoginUseCase: IsLoginUseCase, + private val checkAppUpdateUseCase: CheckAppUpdateUseCase, ) : ViewModel() { - private val _isLogin: MutableStateFlow = MutableStateFlow(null) - val isLogin = _isLogin.asStateFlow() + private val _uiState: Channel = Channel() + val uiState = _uiState.receiveAsFlow() init { - viewModelScope.launch { _isLogin.value = isLoginUseCase() } + checkUpdate() + } + + private fun checkUpdate() { + viewModelScope.launch { + checkAppUpdateUseCase( + currentVersion = BuildConfig.VERSION_CODE, + onError = { + _uiState.send(MainUiState.Error) + }, + ).collect { shouldUpdate -> + if (shouldUpdate) { + _uiState.send(MainUiState.Update) + return@collect + } + val isLogin = isLoginUseCase() + if (isLogin) { + _uiState.send(MainUiState.Home) + } else { + _uiState.send(MainUiState.Login) + } + } + } } } + diff --git a/app/src/main/java/com/withpeace/withpeace/navigation/NavHost.kt b/app/src/main/java/com/withpeace/withpeace/navigation/NavHost.kt index cdeb7b06..2fce60e6 100644 --- a/app/src/main/java/com/withpeace/withpeace/navigation/NavHost.kt +++ b/app/src/main/java/com/withpeace/withpeace/navigation/NavHost.kt @@ -19,6 +19,8 @@ import com.withpeace.withpeace.feature.mypage.navigation.MY_PAGE_CHANGED_IMAGE_A import com.withpeace.withpeace.feature.mypage.navigation.MY_PAGE_CHANGED_NICKNAME_ARGUMENT import com.withpeace.withpeace.feature.mypage.navigation.MY_PAGE_ROUTE import com.withpeace.withpeace.feature.mypage.navigation.myPageNavGraph +import com.withpeace.withpeace.feature.policyconsent.navigation.navigateToPolicyConsent +import com.withpeace.withpeace.feature.policyconsent.navigation.policyConsentGraph import com.withpeace.withpeace.feature.policydetail.navigation.navigateToPolicyDetail import com.withpeace.withpeace.feature.policydetail.navigation.policyDetailNavGraph import com.withpeace.withpeace.feature.postdetail.navigation.POST_DETAIL_ROUTE_WITH_ARGUMENT @@ -27,6 +29,8 @@ import com.withpeace.withpeace.feature.postdetail.navigation.postDetailGraph import com.withpeace.withpeace.feature.postlist.navigation.POST_LIST_DELETED_POST_ID_ARGUMENT import com.withpeace.withpeace.feature.postlist.navigation.POST_LIST_ROUTE import com.withpeace.withpeace.feature.postlist.navigation.postListGraph +import com.withpeace.withpeace.feature.privacypolicy.navigation.navigateToPrivacyPolicy +import com.withpeace.withpeace.feature.privacypolicy.navigation.privacyPolicyGraph import com.withpeace.withpeace.feature.registerpost.navigation.IMAGE_LIST_ARGUMENT import com.withpeace.withpeace.feature.registerpost.navigation.REGISTER_POST_ARGUMENT import com.withpeace.withpeace.feature.registerpost.navigation.REGISTER_POST_ROUTE @@ -34,6 +38,8 @@ import com.withpeace.withpeace.feature.registerpost.navigation.navigateToRegiste import com.withpeace.withpeace.feature.registerpost.navigation.registerPostNavGraph import com.withpeace.withpeace.feature.signup.navigation.navigateSignUp import com.withpeace.withpeace.feature.signup.navigation.signUpNavGraph +import com.withpeace.withpeace.feature.termsofservice.navigation.navigateToTermsOfService +import com.withpeace.withpeace.feature.termsofservice.navigation.termsOfServiceGraph @Composable fun WithpeaceNavHost( @@ -50,7 +56,7 @@ fun WithpeaceNavHost( loginNavGraph( onShowSnackBar = onShowSnackBar, onSignUpNeeded = { - navController.navigateSignUp() + navController.navigateToPolicyConsent() }, onLoginSuccess = { navController.navigateHome( @@ -63,6 +69,32 @@ fun WithpeaceNavHost( ) }, ) + policyConsentGraph( + onShowSnackBar = onShowSnackBar, + onSuccessToNext = { + navController.navigateSignUp() + }, + onShowTermsOfServiceClick = { + navController.navigateToTermsOfService() + }, + onShowPrivacyPolicyClick = { + navController.navigateToPrivacyPolicy() + }, + ) + termsOfServiceGraph( + onShowSnackBar = onShowSnackBar, + onClickBackButton = { + navController.popBackStack() + }, + ) + + privacyPolicyGraph( + onShowSnackBar = onShowSnackBar, + onClickBackButton = { + navController.popBackStack() + }, + ) + signUpNavGraph( onShowSnackBar = onShowSnackBar, onNavigateToGallery = { diff --git a/app/src/main/res/drawable/splash_inset.xml b/app/src/main/res/drawable/splash_inset.xml index a2701bfc..31f60999 100644 --- a/app/src/main/res/drawable/splash_inset.xml +++ b/app/src/main/res/drawable/splash_inset.xml @@ -1,6 +1,6 @@ Unit, + currentVersion: Int, + ): Flow = flow { + appUpdateService.shouldUpdate(currentVersion).suspendMapSuccess { + emit(this.data) + }.suspendOnFailure { + onError(ResponseError.UNKNOWN_ERROR) + } + } +} diff --git a/core/designsystem/src/main/java/com/withpeace/withpeace/core/designsystem/theme/Type.kt b/core/designsystem/src/main/java/com/withpeace/withpeace/core/designsystem/theme/Type.kt index fa5e553b..72b31893 100644 --- a/core/designsystem/src/main/java/com/withpeace/withpeace/core/designsystem/theme/Type.kt +++ b/core/designsystem/src/main/java/com/withpeace/withpeace/core/designsystem/theme/Type.kt @@ -169,4 +169,9 @@ data class WithPeaceTypography( fontWeight = FontWeight.Bold, fontSize = 16.sp, ), + val semiBold16Sp: TextStyle = TextStyle( + fontFamily = PretendardFont, + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp, + ), ) diff --git a/core/designsystem/src/main/java/com/withpeace/withpeace/core/designsystem/ui/Checkbox.kt b/core/designsystem/src/main/java/com/withpeace/withpeace/core/designsystem/ui/Checkbox.kt new file mode 100644 index 00000000..749c8f17 --- /dev/null +++ b/core/designsystem/src/main/java/com/withpeace/withpeace/core/designsystem/ui/Checkbox.kt @@ -0,0 +1,61 @@ +package com.withpeace.withpeace.core.designsystem.ui + +import androidx.compose.foundation.background +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CheckboxDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import com.withpeace.withpeace.core.designsystem.theme.WithpeaceTheme + +@Composable +fun CheonghaCheckbox( + modifier: Modifier = Modifier, + checked: Boolean, + onCheckedChanged: (Boolean) -> Unit, +) { + Checkbox( + modifier = modifier, + colors = CheckboxDefaults.colors( + checkedColor = WithpeaceTheme.colors.MainPurple, + uncheckedColor = WithpeaceTheme.colors.SystemGray2, + checkmarkColor = WithpeaceTheme.colors.SystemWhite, + ), + checked = checked, + onCheckedChange = onCheckedChanged, + ) +} + +@Composable +fun TransparentCheonghaCheckbox( + modifier: Modifier = Modifier, + checked: Boolean, + onCheckedChanged: (Boolean) -> Unit, +) { + Checkbox( + modifier = modifier, + colors = CheckboxDefaults.colors( + checkedColor = Color.Transparent, + uncheckedColor = Color.Transparent, + ).copy( + uncheckedCheckmarkColor = WithpeaceTheme.colors.SystemGray2, + checkedCheckmarkColor = WithpeaceTheme.colors.MainPurple, + uncheckedBoxColor = Color.Unspecified, + ), + checked = checked, + onCheckedChange = onCheckedChanged, + ) +} + +@Composable +@Preview +fun TransparentCheonghaCheckboxPreview() { + WithpeaceTheme { + TransparentCheonghaCheckbox( + modifier = Modifier.background(color = WithpeaceTheme.colors.SystemWhite), + checked = false, + onCheckedChanged = {}, + ) + } +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/withpeace/withpeace/core/designsystem/ui/TopAppBar.kt b/core/designsystem/src/main/java/com/withpeace/withpeace/core/designsystem/ui/TopAppBar.kt index 23ea2a3b..ebefdfd8 100644 --- a/core/designsystem/src/main/java/com/withpeace/withpeace/core/designsystem/ui/TopAppBar.kt +++ b/core/designsystem/src/main/java/com/withpeace/withpeace/core/designsystem/ui/TopAppBar.kt @@ -28,7 +28,6 @@ fun WithPeaceBackButtonTopAppBar( ) { TopAppBar( title = title, - modifier = modifier.padding(end = 24.dp), navigationIcon = { Icon( modifier = diff --git a/core/domain/src/main/java/com/withpeace/withpeace/core/domain/model/policy/YouthPolicy.kt b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/model/policy/YouthPolicy.kt index 107ac223..74b670d0 100644 --- a/core/domain/src/main/java/com/withpeace/withpeace/core/domain/model/policy/YouthPolicy.kt +++ b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/model/policy/YouthPolicy.kt @@ -18,4 +18,10 @@ data class YouthPolicy( val screeningAndAnnouncement: String, val applicationSite: String, val submissionDocuments: String, + + val additionalUsefulInformation: String, + val supervisingAuthority: String, + val operatingOrganization: String, + val businessRelatedReferenceSite1: String, + val businessRelatedReferenceSite2: String, ) \ No newline at end of file diff --git a/core/domain/src/main/java/com/withpeace/withpeace/core/domain/repository/AppUpdateRepository.kt b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/repository/AppUpdateRepository.kt new file mode 100644 index 00000000..5806006a --- /dev/null +++ b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/repository/AppUpdateRepository.kt @@ -0,0 +1,8 @@ +package com.withpeace.withpeace.core.domain.repository + +import com.withpeace.withpeace.core.domain.model.error.CheonghaError +import kotlinx.coroutines.flow.Flow + +interface AppUpdateRepository { + fun shouldUpdate(onError: suspend (CheonghaError) -> Unit, currentVersion: Int): Flow +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/withpeace/withpeace/core/domain/usecase/CheckAppUpdateUseCase.kt b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/usecase/CheckAppUpdateUseCase.kt new file mode 100644 index 00000000..e4ac04af --- /dev/null +++ b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/usecase/CheckAppUpdateUseCase.kt @@ -0,0 +1,15 @@ +package com.withpeace.withpeace.core.domain.usecase + +import com.withpeace.withpeace.core.domain.model.error.CheonghaError +import com.withpeace.withpeace.core.domain.repository.AppUpdateRepository +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class CheckAppUpdateUseCase @Inject constructor( + private val appUpdateRepository: AppUpdateRepository, +) { + operator fun invoke( + currentVersion: Int, + onError: suspend (CheonghaError) -> Unit, + ): Flow = appUpdateRepository.shouldUpdate(onError, currentVersion) +} \ No newline at end of file diff --git a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/di/ServiceModule.kt b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/di/ServiceModule.kt index c58175ff..9c482745 100644 --- a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/di/ServiceModule.kt +++ b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/di/ServiceModule.kt @@ -1,8 +1,9 @@ package com.withpeace.withpeace.core.network.di.di -import com.withpeace.withpeace.core.network.di.service.UserService +import com.withpeace.withpeace.core.network.di.service.AppUpdateService import com.withpeace.withpeace.core.network.di.service.AuthService import com.withpeace.withpeace.core.network.di.service.PostService +import com.withpeace.withpeace.core.network.di.service.UserService import com.withpeace.withpeace.core.network.di.service.YouthPolicyService import dagger.Module import dagger.Provides @@ -31,6 +32,11 @@ object ServiceModule { fun providesUserService(@Named("general") retrofit: Retrofit): UserService = retrofit.create(UserService::class.java) + @Provides + @Singleton + fun providesAppUpdateService(@Named("general") retrofit: Retrofit): AppUpdateService = + retrofit.create(AppUpdateService::class.java) + @Provides @Singleton fun providesYouthPolicyService(@Named("youth_policy") retrofit: Retrofit): YouthPolicyService = diff --git a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/response/YouthPolicyListResponse.kt b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/response/YouthPolicyListResponse.kt index 898220ac..ef6d7c03 100644 --- a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/response/YouthPolicyListResponse.kt +++ b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/response/YouthPolicyListResponse.kt @@ -49,5 +49,18 @@ data class YouthPolicyEntity( @PropertyElement(name = "rqutUrla", writeAsCData = true) val applicationSite: String?, @PropertyElement(name = "pstnPaprCn", writeAsCData = true) - val submissionDocuments: String? + val submissionDocuments: String?, + + @PropertyElement(name = "etct", writeAsCData = true) + val etc: String?, // 기타 유익 정보 + @PropertyElement(name = "mngtMson", writeAsCData = true) + val managingInstitution: String?, // 주관 기관 + @PropertyElement(name = "cnsgNmor", writeAsCData = true) + val operatingOrganization: String?, // 운영 기관 + @PropertyElement(name = "rfcSiteUrla1", writeAsCData = true) + val businessReferenceSite1: String?, // 사업관련 참고 사이트 1 + @PropertyElement(name = "rfcSiteUrla2", writeAsCData = true) + val businessReferenceSite2: String? // 사업관련 참고 사이트 2 + + ) \ No newline at end of file diff --git a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/service/AppUpdateService.kt b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/service/AppUpdateService.kt new file mode 100644 index 00000000..cf2e412c --- /dev/null +++ b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/service/AppUpdateService.kt @@ -0,0 +1,12 @@ +package com.withpeace.withpeace.core.network.di.service + +import com.skydoves.sandwich.ApiResponse +import com.withpeace.withpeace.core.network.di.response.BaseResponse +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Query + +interface AppUpdateService { + @GET("/api/v1/app/check/android") + suspend fun shouldUpdate(@Query("currentVersion") currentVersion: Int): ApiResponse> +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/withpeace/withpeace/core/ui/common/WebView.kt b/core/ui/src/main/java/com/withpeace/withpeace/core/ui/common/WebView.kt new file mode 100644 index 00000000..28f5ce3f --- /dev/null +++ b/core/ui/src/main/java/com/withpeace/withpeace/core/ui/common/WebView.kt @@ -0,0 +1,91 @@ +package com.withpeace.withpeace.core.ui.common + +import android.annotation.SuppressLint +import android.content.Context +import android.view.ViewGroup +import android.webkit.WebChromeClient +import android.webkit.WebView +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView +import com.withpeace.withpeace.core.designsystem.theme.WithpeaceTheme + +@Composable +fun WithpeaceWebView( + url: String?, + modifier: Modifier = Modifier, +) { + if (url != null) { + WebView( + url = url, + modifier = modifier, + ) + } else { + Column( + modifier = modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = "오류가 발생했습니다 다시 시도해주세요", + ) + } + } +} + +@Composable +private fun WebView( + url: String, + modifier: Modifier = Modifier, +) { + var progress by remember { mutableIntStateOf(0) } + + Column(modifier = modifier) { + if (progress != 100) { + LinearProgressIndicator( + progress = progress / 100f, + color = WithpeaceTheme.colors.MainPurple, + ) + } + AndroidView( + factory = { context -> + createWebView(context) { progress = it } + }, + update = { + it.loadUrl(url) + }, + ) + } +} + +@SuppressLint("SetJavaScriptEnabled") +private fun createWebView(context: Context, onSetProgress: (Int) -> Unit) = WebView(context).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ) + webChromeClient = object : WebChromeClient() { + override fun onProgressChanged(view: WebView, newProgress: Int) { + onSetProgress(newProgress) + super.onProgressChanged(view, newProgress) + } + } + settings.apply { + builtInZoomControls = false + domStorageEnabled = true + javaScriptEnabled = true + loadWithOverviewMode = true + blockNetworkLoads = false + setSupportZoom(false) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/withpeace/withpeace/core/ui/policy/YouthPolicyUiModel.kt b/core/ui/src/main/java/com/withpeace/withpeace/core/ui/policy/YouthPolicyUiModel.kt index a029207e..49fcb60d 100644 --- a/core/ui/src/main/java/com/withpeace/withpeace/core/ui/policy/YouthPolicyUiModel.kt +++ b/core/ui/src/main/java/com/withpeace/withpeace/core/ui/policy/YouthPolicyUiModel.kt @@ -1,5 +1,6 @@ package com.withpeace.withpeace.core.ui.policy +import android.annotation.SuppressLint import androidx.annotation.DrawableRes import com.withpeace.withpeace.core.domain.model.policy.PolicyClassification import com.withpeace.withpeace.core.domain.model.policy.YouthPolicy @@ -7,6 +8,7 @@ import com.withpeace.withpeace.core.ui.serializable.parseNavigationValue import com.withpeace.withpeace.core.ui.serializable.toNavigationValue import java.io.Serializable +@SuppressLint("SupportAnnotationUsage") @kotlinx.serialization.Serializable data class YouthPolicyUiModel( val id: String, @@ -26,6 +28,13 @@ data class YouthPolicyUiModel( val screeningAndAnnouncement: String, val applicationSite: String, val submissionDocuments: String, + + //추가 정보를 확인해 보세요 + val additionalUsefulInformation: String, + val supervisingAuthority: String, + val operatingOrganization: String, + val businessRelatedReferenceSite1: String, + val businessRelatedReferenceSite2: String, ): Serializable { companion object { fun toNavigationValue(value: YouthPolicyUiModel): String = @@ -55,6 +64,11 @@ fun YouthPolicy.toUiModel(): YouthPolicyUiModel { screeningAndAnnouncement = screeningAndAnnouncement, applicationSite = applicationSite, submissionDocuments = submissionDocuments, + additionalUsefulInformation = additionalUsefulInformation, + supervisingAuthority = supervisingAuthority, + operatingOrganization = operatingOrganization, + businessRelatedReferenceSite1 = businessRelatedReferenceSite1, + businessRelatedReferenceSite2 = businessRelatedReferenceSite2, ) } @@ -77,5 +91,10 @@ fun YouthPolicyUiModel.toDomain(): YouthPolicy { screeningAndAnnouncement = screeningAndAnnouncement, applicationSite = applicationSite, submissionDocuments = submissionDocuments, + additionalUsefulInformation = additionalUsefulInformation, + supervisingAuthority = supervisingAuthority, + operatingOrganization = operatingOrganization, + businessRelatedReferenceSite1 = businessRelatedReferenceSite1, + businessRelatedReferenceSite2 = businessRelatedReferenceSite2, ) } \ No newline at end of file diff --git a/feature/login/src/main/res/drawable/app_logo.xml b/core/ui/src/main/res/drawable/ic_app_logo.xml similarity index 100% rename from feature/login/src/main/res/drawable/app_logo.xml rename to core/ui/src/main/res/drawable/ic_app_logo.xml diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index f7cdd37f..9c31af9b 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -1,5 +1,8 @@ + + 청하 로고 + 자유 정보 diff --git a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeScreen.kt b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeScreen.kt index 50e1a0fe..a72d7b0b 100644 --- a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeScreen.kt +++ b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeScreen.kt @@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.BottomSheetDefaults import androidx.compose.material3.Card @@ -51,11 +50,11 @@ import androidx.paging.compose.itemKey import com.withpeace.withpeace.core.designsystem.theme.WithpeaceTheme import com.withpeace.withpeace.core.designsystem.util.dropShadow import com.withpeace.withpeace.core.ui.analytics.TrackScreenViewEvent -import com.withpeace.withpeace.feature.home.filtersetting.FilterBottomSheet import com.withpeace.withpeace.core.ui.policy.ClassificationUiModel import com.withpeace.withpeace.core.ui.policy.RegionUiModel -import com.withpeace.withpeace.feature.home.uistate.PolicyFiltersUiModel import com.withpeace.withpeace.core.ui.policy.YouthPolicyUiModel +import com.withpeace.withpeace.feature.home.filtersetting.FilterBottomSheet +import com.withpeace.withpeace.feature.home.uistate.PolicyFiltersUiModel import kotlinx.coroutines.launch @Composable @@ -194,14 +193,12 @@ private fun HomeHeader( Box( modifier = modifier .fillMaxWidth() - .padding(vertical = 10.dp, horizontal = 24.dp), + .padding(horizontal = 24.dp).padding(bottom = 16.dp), ) { Image( - modifier = modifier - .size(36.dp) - .clip(CircleShape) - .align(Alignment.Center), - painter = painterResource(id = R.drawable.home_logo), + modifier = modifier.align(Alignment.BottomCenter) + .size(47.dp), + painter = painterResource(id = R.drawable.ic_home_logo), contentDescription = stringResource(R.string.cheongha_logo), ) Image( diff --git a/feature/home/src/main/res/drawable/home_logo.xml b/feature/home/src/main/res/drawable/home_logo.xml deleted file mode 100644 index df743866..00000000 --- a/feature/home/src/main/res/drawable/home_logo.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - diff --git a/feature/home/src/main/res/drawable/ic_home_logo.xml b/feature/home/src/main/res/drawable/ic_home_logo.xml new file mode 100644 index 00000000..cc68f430 --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_home_logo.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/feature/login/src/main/java/com/withpeace/withpeace/feature/login/LoginScreen.kt b/feature/login/src/main/java/com/withpeace/withpeace/feature/login/LoginScreen.kt index 535f6a28..1f43bc30 100644 --- a/feature/login/src/main/java/com/withpeace/withpeace/feature/login/LoginScreen.kt +++ b/feature/login/src/main/java/com/withpeace/withpeace/feature/login/LoginScreen.kt @@ -33,7 +33,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel -import com.withpeace.withpeace.core.analytics.LocalAnalyticsHelper import com.withpeace.withpeace.core.designsystem.theme.WithpeaceTheme import com.withpeace.withpeace.googlelogin.GoogleLoginManager import kotlinx.coroutines.launch @@ -99,7 +98,7 @@ fun LoginScreen( Spacer(modifier = Modifier.height(152.dp)) Image( modifier = Modifier.size(150.dp), - painter = painterResource(id = R.drawable.app_logo), + painter = painterResource(id = com.withpeace.withpeace.core.ui.R.drawable.ic_app_logo), contentDescription = stringResource(R.string.app_logo_content_description), ) Spacer(modifier = Modifier.height(40.dp)) diff --git a/feature/policyconsent/.gitignore b/feature/policyconsent/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/policyconsent/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/policyconsent/build.gradle.kts b/feature/policyconsent/build.gradle.kts new file mode 100644 index 00000000..39b548b6 --- /dev/null +++ b/feature/policyconsent/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id("convention.feature") +} + +android { + namespace = "com.withpeace.withpeace.feature.policyconsent" +} + +dependencies { +} \ No newline at end of file diff --git a/feature/policyconsent/consumer-rules.pro b/feature/policyconsent/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/feature/policyconsent/proguard-rules.pro b/feature/policyconsent/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/policyconsent/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/policyconsent/src/androidTest/java/com/withpeace/withpeace/feature/policyconsent/ExampleInstrumentedTest.kt b/feature/policyconsent/src/androidTest/java/com/withpeace/withpeace/feature/policyconsent/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..58ca1971 --- /dev/null +++ b/feature/policyconsent/src/androidTest/java/com/withpeace/withpeace/feature/policyconsent/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.withpeace.withpeace.feature.policyconsent + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.withpeace.withpeace.feature.policyconsent.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/feature/policyconsent/src/main/AndroidManifest.xml b/feature/policyconsent/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/policyconsent/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/policyconsent/src/main/java/com/withpeace/withpeace/feature/policyconsent/PolicyConsentScreen.kt b/feature/policyconsent/src/main/java/com/withpeace/withpeace/feature/policyconsent/PolicyConsentScreen.kt new file mode 100644 index 00000000..2c72b9f5 --- /dev/null +++ b/feature/policyconsent/src/main/java/com/withpeace/withpeace/feature/policyconsent/PolicyConsentScreen.kt @@ -0,0 +1,282 @@ +package com.withpeace.withpeace.feature.policyconsent + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +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.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.selection.toggleable +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalMinimumInteractiveComponentEnforcement +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.withpeace.withpeace.core.designsystem.theme.WithpeaceTheme +import com.withpeace.withpeace.core.designsystem.ui.CheonghaCheckbox +import com.withpeace.withpeace.core.ui.analytics.TrackScreenViewEvent +import com.withpeace.withpeace.feature.policyconsent.uistate.PolicyConsentUiEvent +import com.withpeace.withpeace.feature.policyconsent.uistate.PolicyConsentUiState + +@Composable +fun PolicyConsentRoute( + onShowSnackBar: (String) -> Unit, + onShowPrivacyPolicyClick: () -> Unit, + onShowTermsOfServiceClick: () -> Unit, + onSuccessToNext: () -> Unit, + viewModel: PolicyConsentViewModel = hiltViewModel(), +) { + val checkStatus = viewModel.uiState.collectAsStateWithLifecycle() + PolicyConsentScreen( + checkStatus = checkStatus.value, + onPrivacyPolicyChecked = viewModel::onPrivacyPolicyChecked, + onTermsOfServiceChecked = viewModel::onTermsOfServiceChecked, + onAllChecked = viewModel::onAllChecked, + onClickToNext = viewModel::checkToNext, + onShowPrivacyPolicyClick = onShowPrivacyPolicyClick, + onShowTermsOfServiceClick = onShowTermsOfServiceClick, + ) + + LaunchedEffect(key1 = viewModel.policyConsentEvent) { + viewModel.policyConsentEvent.collect { + when (it) { + PolicyConsentUiEvent.SuccessToNext -> onSuccessToNext() + PolicyConsentUiEvent.FailureToNext -> onShowSnackBar("필수 항목을 선택해주세요") + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PolicyConsentScreen( + modifier: Modifier = Modifier, + checkStatus: PolicyConsentUiState, + onPrivacyPolicyChecked: (Boolean) -> Unit, + onTermsOfServiceChecked: (Boolean) -> Unit, + onAllChecked: (Boolean) -> Unit, + onClickToNext: () -> Unit, + onShowPrivacyPolicyClick: () -> Unit, + onShowTermsOfServiceClick: () -> Unit, +) { + val scrollState = rememberScrollState() + Column( + verticalArrangement = Arrangement.SpaceBetween, + modifier = modifier.fillMaxSize(), + ) { + Column( + modifier + .padding(horizontal = 24.dp) + .verticalScroll(scrollState), + ) { + Spacer(modifier = modifier.height(56.dp)) + Image( + modifier = modifier + .size(120.dp) + .align(Alignment.CenterHorizontally), + painter = painterResource(id = com.withpeace.withpeace.core.ui.R.drawable.ic_app_logo), + contentDescription = stringResource(id = com.withpeace.withpeace.core.ui.R.string.cheongha_logo), + ) + Spacer(modifier = modifier.height(24.dp)) + Text( + modifier = modifier.align(Alignment.CenterHorizontally), + textAlign = TextAlign.Center, + text = "청하(청춘하랑)에 어서오세요!\n" + + "약관에 동의하시면 청하와의 여정을\n" + + "시작할 수 있어요!", + style = WithpeaceTheme.typography.title2.merge( + TextStyle( + lineHeight = 28.sp, + ), + ), + ) + Spacer(modifier = modifier.height(32.dp)) + Row( + modifier = modifier.toggleable( + value = checkStatus.allChecked, + role = Role.Checkbox, + onValueChange = { onAllChecked(it) }, + ), + ) { + CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) { + CheonghaCheckbox( + modifier = modifier.align(Alignment.CenterVertically), + checked = checkStatus.allChecked, + onCheckedChanged = { + onAllChecked(it) + }, + ) + } + Text( + modifier = modifier + .align(Alignment.CenterVertically) + .padding(start = 8.dp), + text = "모두 동의", + style = if (checkStatus.allChecked) WithpeaceTheme.typography.semiBold16Sp else WithpeaceTheme.typography.body, + color = WithpeaceTheme.colors.SystemBlack, + ) + } + Spacer(modifier = modifier.height(24.dp)) + HorizontalDivider(color = WithpeaceTheme.colors.SystemGray3) + Spacer(modifier = modifier.height(24.dp)) + Row { + Row( + modifier = modifier.toggleable( + value = checkStatus.termsOfServiceChecked, + role = Role.Checkbox, + onValueChange = { + onTermsOfServiceChecked(it) + }, + ), + ) { + Icon( + Icons.Default.Check, + contentDescription = "서비스 이용약관 체크", + modifier = modifier + .padding(2.dp) + .size(18.dp), + tint = if (checkStatus.termsOfServiceChecked) WithpeaceTheme.colors.MainPurple else WithpeaceTheme.colors.SystemGray2, + ) + Text( + modifier = modifier + .align(Alignment.CenterVertically) + .padding(start = 8.dp), + text = "[필수] 서비스 이용약관", + style = if (checkStatus.termsOfServiceChecked) WithpeaceTheme.typography.semiBold16Sp else WithpeaceTheme.typography.body, + color = WithpeaceTheme.colors.SystemBlack, + ) + } + Text( + text = "보기", + modifier = modifier + .padding(start = 8.dp) + .align(Alignment.CenterVertically) + .clickable { onShowTermsOfServiceClick() }, + textDecoration = TextDecoration.Underline, + style = WithpeaceTheme.typography.caption, + color = WithpeaceTheme.colors.SystemGray2, + ) + } + + Spacer(modifier = modifier.height(16.dp)) + Row { + Row( + modifier = modifier.toggleable( + value = checkStatus.privacyPolicyChecked, + role = Role.Checkbox, + onValueChange = { + onPrivacyPolicyChecked(it) + }, + ), + ) { + Icon( + Icons.Default.Check, + contentDescription = "개인정보 처리 방침 체크", + modifier = modifier + .padding(2.dp) + .size(18.dp), + tint = if (checkStatus.privacyPolicyChecked) WithpeaceTheme.colors.MainPurple else WithpeaceTheme.colors.SystemGray2, + ) + Text( + modifier = modifier + .align(Alignment.CenterVertically) + .padding(start = 8.dp), + text = "[필수] 개인정보처리방침", + style = if (checkStatus.privacyPolicyChecked) WithpeaceTheme.typography.semiBold16Sp else WithpeaceTheme.typography.body, + color = WithpeaceTheme.colors.SystemBlack, + ) + } + Text( + text = "보기", + modifier = modifier + .padding(start = 8.dp) + .align(Alignment.CenterVertically) + .clickable { onShowPrivacyPolicyClick() }, + textDecoration = TextDecoration.Underline, + style = WithpeaceTheme.typography.caption, + color = WithpeaceTheme.colors.SystemGray2, + ) + } + } + NextButton(onClick = onClickToNext) + } + TrackScreenViewEvent(screenName = "policy_consent") +} + +@Composable +private fun NextButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Button( + onClick = { + onClick() + }, + contentPadding = PaddingValues(0.dp), + modifier = modifier + .padding( + bottom = 40.dp, + end = WithpeaceTheme.padding.BasicHorizontalPadding, + start = WithpeaceTheme.padding.BasicHorizontalPadding, + ) + .fillMaxWidth(), + colors = ButtonDefaults.buttonColors(containerColor = WithpeaceTheme.colors.MainPurple), + shape = RoundedCornerShape(9.dp), + ) { + Text( + style = WithpeaceTheme.typography.body, + text = stringResource(R.string.to_next), + modifier = Modifier.padding(vertical = 18.dp), + color = WithpeaceTheme.colors.SystemWhite, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun PolicyConsentPreview() { + WithpeaceTheme { + PolicyConsentScreen( + checkStatus = PolicyConsentUiState( + allChecked = false, + termsOfServiceChecked = false, + privacyPolicyChecked = false, + ), + onPrivacyPolicyChecked = {}, + onTermsOfServiceChecked = {}, + onAllChecked = {}, + onClickToNext = {}, + onShowPrivacyPolicyClick = {}, + onShowTermsOfServiceClick = {}, + ) + } +} diff --git a/feature/policyconsent/src/main/java/com/withpeace/withpeace/feature/policyconsent/PolicyConsentViewModel.kt b/feature/policyconsent/src/main/java/com/withpeace/withpeace/feature/policyconsent/PolicyConsentViewModel.kt new file mode 100644 index 00000000..5d449b4d --- /dev/null +++ b/feature/policyconsent/src/main/java/com/withpeace/withpeace/feature/policyconsent/PolicyConsentViewModel.kt @@ -0,0 +1,72 @@ +package com.withpeace.withpeace.feature.policyconsent + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.withpeace.withpeace.feature.policyconsent.uistate.PolicyConsentUiEvent +import com.withpeace.withpeace.feature.policyconsent.uistate.PolicyConsentUiState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class PolicyConsentViewModel @Inject constructor() : ViewModel() { + private val _uiState = MutableStateFlow( + PolicyConsentUiState( + allChecked = false, termsOfServiceChecked = false, privacyPolicyChecked = false, + ), + ) + val uiState = _uiState.asStateFlow() + + private val _policyConsentUiEvent = Channel() + val policyConsentEvent = _policyConsentUiEvent.receiveAsFlow() + + fun onAllChecked(checked: Boolean) { + _uiState.update { + it.copy( + allChecked = checked, + termsOfServiceChecked = checked, + privacyPolicyChecked = checked, + ) + } + } + + fun onPrivacyPolicyChecked(checked: Boolean) { + _uiState.update { + if (it.termsOfServiceChecked && checked) // 자식이 모두 체크된 경우 + it.copy( + allChecked = true, + privacyPolicyChecked = true, + ) + else it.copy( + allChecked = false, + privacyPolicyChecked = checked, + ) + } + } + + fun onTermsOfServiceChecked(checked: Boolean) { + _uiState.update { + if (it.privacyPolicyChecked && checked) + it.copy( + allChecked = true, + termsOfServiceChecked = true, + ) + else it.copy( + allChecked = false, + termsOfServiceChecked = checked, + ) + } + } + + fun checkToNext() { + viewModelScope.launch { + if (uiState.value.allChecked) _policyConsentUiEvent.send(PolicyConsentUiEvent.SuccessToNext) + else _policyConsentUiEvent.send(PolicyConsentUiEvent.FailureToNext) + } + } +} \ No newline at end of file diff --git a/feature/policyconsent/src/main/java/com/withpeace/withpeace/feature/policyconsent/navigation/PolicyConsentNavigation.kt b/feature/policyconsent/src/main/java/com/withpeace/withpeace/feature/policyconsent/navigation/PolicyConsentNavigation.kt new file mode 100644 index 00000000..bf970bc0 --- /dev/null +++ b/feature/policyconsent/src/main/java/com/withpeace/withpeace/feature/policyconsent/navigation/PolicyConsentNavigation.kt @@ -0,0 +1,28 @@ +package com.withpeace.withpeace.feature.policyconsent.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.withpeace.withpeace.feature.policyconsent.PolicyConsentRoute + +const val POLICY_CONSENT_ROUTE = "policy_consent_route" + +fun NavController.navigateToPolicyConsent(navOptions: NavOptions? = null) = + navigate(POLICY_CONSENT_ROUTE, navOptions) + +fun NavGraphBuilder.policyConsentGraph( + onShowSnackBar: (String) -> Unit, + onShowPrivacyPolicyClick: () -> Unit, + onShowTermsOfServiceClick: () -> Unit, + onSuccessToNext: () -> Unit, +) { + composable(POLICY_CONSENT_ROUTE) { + PolicyConsentRoute( + onShowSnackBar = onShowSnackBar, + onShowPrivacyPolicyClick = onShowPrivacyPolicyClick, + onShowTermsOfServiceClick = onShowTermsOfServiceClick, + onSuccessToNext = onSuccessToNext, + ) + } +} diff --git a/feature/policyconsent/src/main/java/com/withpeace/withpeace/feature/policyconsent/uistate/PolicyConsentUiEvent.kt b/feature/policyconsent/src/main/java/com/withpeace/withpeace/feature/policyconsent/uistate/PolicyConsentUiEvent.kt new file mode 100644 index 00000000..9a45a437 --- /dev/null +++ b/feature/policyconsent/src/main/java/com/withpeace/withpeace/feature/policyconsent/uistate/PolicyConsentUiEvent.kt @@ -0,0 +1,6 @@ +package com.withpeace.withpeace.feature.policyconsent.uistate + +sealed interface PolicyConsentUiEvent { + data object SuccessToNext : PolicyConsentUiEvent + data object FailureToNext : PolicyConsentUiEvent +} \ No newline at end of file diff --git a/feature/policyconsent/src/main/java/com/withpeace/withpeace/feature/policyconsent/uistate/PolicyConsentUiState.kt b/feature/policyconsent/src/main/java/com/withpeace/withpeace/feature/policyconsent/uistate/PolicyConsentUiState.kt new file mode 100644 index 00000000..cf345e90 --- /dev/null +++ b/feature/policyconsent/src/main/java/com/withpeace/withpeace/feature/policyconsent/uistate/PolicyConsentUiState.kt @@ -0,0 +1,9 @@ +package com.withpeace.withpeace.feature.policyconsent.uistate + +data class PolicyConsentUiState( + val allChecked: Boolean, + val termsOfServiceChecked: Boolean, + val privacyPolicyChecked: Boolean, +) { + +} diff --git a/feature/policyconsent/src/main/res/values/strings.xml b/feature/policyconsent/src/main/res/values/strings.xml new file mode 100644 index 00000000..ea4ebf80 --- /dev/null +++ b/feature/policyconsent/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + 다음으로 + \ No newline at end of file diff --git a/feature/policyconsent/src/test/java/com/withpeace/withpeace/feature/policyconsent/ExampleUnitTest.kt b/feature/policyconsent/src/test/java/com/withpeace/withpeace/feature/policyconsent/ExampleUnitTest.kt new file mode 100644 index 00000000..75a05b2b --- /dev/null +++ b/feature/policyconsent/src/test/java/com/withpeace/withpeace/feature/policyconsent/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.withpeace.withpeace.feature.policyconsent + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/feature/policydetail/build.gradle.kts b/feature/policydetail/build.gradle.kts index ad88bc34..078e05a1 100644 --- a/feature/policydetail/build.gradle.kts +++ b/feature/policydetail/build.gradle.kts @@ -7,4 +7,5 @@ android { } dependencies { + implementation(libs.text.flow) } \ No newline at end of file diff --git a/feature/policydetail/src/main/java/com/withpeace/withpeace/feature/policydetail/PolicyDetailScreen.kt b/feature/policydetail/src/main/java/com/withpeace/withpeace/feature/policydetail/PolicyDetailScreen.kt index 3df224ef..e20c399b 100644 --- a/feature/policydetail/src/main/java/com/withpeace/withpeace/feature/policydetail/PolicyDetailScreen.kt +++ b/feature/policydetail/src/main/java/com/withpeace/withpeace/feature/policydetail/PolicyDetailScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.HorizontalDivider @@ -24,9 +25,12 @@ import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.positionInParent import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.LineHeightStyle import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension import androidx.hilt.navigation.compose.hiltViewModel @@ -38,6 +42,8 @@ import com.withpeace.withpeace.core.ui.policy.YouthPolicyUiModel import com.withpeace.withpeace.core.ui.policy.analytics.TrackPolicyDetailScreenViewEvent import com.withpeace.withpeace.feature.policydetail.component.DescriptionTitleAndContent import com.withpeace.withpeace.feature.policydetail.component.HyperLinkDescriptionTitleAndContent +import eu.wewox.textflow.TextFlow +import eu.wewox.textflow.TextFlowObstacleAlignment @Composable fun PolicyDetailRoute( @@ -64,7 +70,7 @@ fun PolicyDetailScreen( } val visibility = remember { derivedStateOf { - scrollState.value >= position.value + scrollState.value >= position.intValue } } Column(modifier = modifier.fillMaxSize()) { @@ -81,6 +87,7 @@ fun PolicyDetailScreen( style = WithpeaceTheme.typography.title1, maxLines = 1, overflow = TextOverflow.Ellipsis, + modifier = modifier.padding(end = 24.dp), ) } }, @@ -103,7 +110,7 @@ fun PolicyDetailScreen( .height(4.dp) .background(WithpeaceTheme.colors.SystemGray3) .onGloballyPositioned { - position.value = it.positionInParent().y.toInt() + position.intValue = it.positionInParent().y.toInt() }, ) PolicySummarySection(policy = policy) @@ -121,6 +128,13 @@ fun PolicyDetailScreen( .background(WithpeaceTheme.colors.SystemGray3), ) ApplicationGuideSection(policy = policy) + Spacer( + modifier = modifier + .fillMaxWidth() + .height(4.dp) + .background(WithpeaceTheme.colors.SystemGray3), + ) + AdditionalInfoSection(policy = policy) } } TrackPolicyDetailScreenViewEvent( @@ -139,7 +153,6 @@ private fun TitleSection( modifier = modifier.padding(horizontal = 24.dp), ) { Spacer(modifier = modifier.height(24.dp)) - // TODO(Tobbar 스크롤 체크) Text( text = policy.title, style = WithpeaceTheme.typography.title1, @@ -150,39 +163,27 @@ private fun TitleSection( .fillMaxWidth() .height(16.dp), ) - ConstraintLayout(modifier = modifier.fillMaxWidth()) { - val (content, image) = createRefs() - Text( - modifier = modifier.constrainAs( - ref = content, - constrainBlock = { - width = Dimension.fillToConstraints - top.linkTo(parent.top) - start.linkTo(parent.start) - end.linkTo(image.start) - }, - ), - overflow = TextOverflow.Ellipsis, - text = policy.content + policy.content + policy.content, - maxLines = 2, - style = WithpeaceTheme.typography.body, - color = WithpeaceTheme.colors.SystemBlack, - ) - Image( - modifier = modifier - .constrainAs( - ref = image, - constrainBlock = { - top.linkTo(parent.top) - start.linkTo(content.end) - end.linkTo(parent.end) - }, - ) - .padding(start = 16.dp), - painter = painterResource(policy.classification.drawableResId), - contentDescription = "정책 분류 이미지", - ) - } + TextFlow(text = policy.content, + style = WithpeaceTheme.typography.body.merge( + TextStyle( + lineHeight = 21.sp, + letterSpacing = 0.16.sp, + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.None, + ), + ) + ), + modifier = Modifier.fillMaxWidth(), + obstacleAlignment = TextFlowObstacleAlignment.TopEnd, + obstacleContent = { + Image( + modifier = modifier.padding(start = 16.dp), + painter = painterResource(policy.classification.drawableResId), + contentDescription = "정책 분류 이미지", + ) + } + ) Spacer(modifier = modifier.height(16.dp)) } } @@ -291,6 +292,49 @@ fun ApplicationGuideSection( title = "제출 서류", content = policy.submissionDocuments, ) + Spacer(modifier = modifier.height(8.dp)) + } +} + +@Composable +fun AdditionalInfoSection( + modifier: Modifier = Modifier, + policy: YouthPolicyUiModel, +) { + Column(modifier = modifier.padding(horizontal = 24.dp)) { + Spacer(modifier = modifier.height(24.dp)) + Text( + text = "추가 정보를 확인해 보세요", + style = WithpeaceTheme.typography.title2, + color = WithpeaceTheme.colors.SystemBlack, + ) + Spacer(modifier = modifier.height(24.dp)) + DescriptionTitleAndContent( + modifier = modifier, + title = "기타 유익 정보", + content = policy.additionalUsefulInformation, + ) + DescriptionTitleAndContent( + modifier = modifier, + title = "주관 기관", + content = policy.supervisingAuthority, + ) + DescriptionTitleAndContent( + modifier = modifier, + title = "운영 기관", + content = policy.operatingOrganization, + ) + HyperLinkDescriptionTitleAndContent( + modifier = modifier, + title = "사업관련 참고 사이트 1", + content = policy.businessRelatedReferenceSite1, + ) + HyperLinkDescriptionTitleAndContent( + modifier = modifier, + title = "사업관련 참고 사이트 2", + content = policy.businessRelatedReferenceSite2, + ) + Spacer(modifier = modifier.height(24.dp)) } } @@ -302,7 +346,7 @@ fun PolicyDetailPreview() { policy = YouthPolicyUiModel( id = "sociosqu", title = "facilis", - content = "civibuscivibuscivibuscivibuscivibuscivibuscivibuscivibuscivibuscivibuscivibuscivibus", + content = "가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바", region = RegionUiModel.대구, ageInfo = "cum", applicationDetails = "지원내용들.....", @@ -316,6 +360,11 @@ fun PolicyDetailPreview() { applicationSite = "consul", submissionDocuments = "an", classification = ClassificationUiModel.JOB, + additionalUsefulInformation = "lorem", + supervisingAuthority = "congue", + operatingOrganization = "brute", + businessRelatedReferenceSite1 = "noluisse", + businessRelatedReferenceSite2 = "quo", ), ) { } diff --git a/feature/policydetail/src/main/java/com/withpeace/withpeace/feature/policydetail/navigation/PolicyDetailNavigation.kt b/feature/policydetail/src/main/java/com/withpeace/withpeace/feature/policydetail/navigation/PolicyDetailNavigation.kt index 6744da73..653eff07 100644 --- a/feature/policydetail/src/main/java/com/withpeace/withpeace/feature/policydetail/navigation/PolicyDetailNavigation.kt +++ b/feature/policydetail/src/main/java/com/withpeace/withpeace/feature/policydetail/navigation/PolicyDetailNavigation.kt @@ -40,18 +40,6 @@ fun NavGraphBuilder.policyDetailNavGraph( val policy: YouthPolicyUiModel = YouthPolicyUiModel.parseNavigationValue( it.arguments?.getString(POLICY_DETAIL_YOUTH_POLICY_ARGUMENT) ?: "" ) - // val arguments = requireNotNull(it.arguments) - // val policy: YouthPolicyUiModel = - // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - // requireNotNull( - // arguments.getSerializable( - // POLICY_DETAIL_YOUTH_POLICY_ARGUMENT, - // YouthPolicyUiModel::class.java, - // ), - // ) - // } else ({ - // arguments.getSerializable(POLICY_DETAIL_YOUTH_POLICY_ARGUMENT) - // }) as YouthPolicyUiModel PolicyDetailRoute( onShowSnackBar = onShowSnackBar, diff --git a/feature/privacypolicy/.gitignore b/feature/privacypolicy/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/privacypolicy/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/privacypolicy/build.gradle.kts b/feature/privacypolicy/build.gradle.kts new file mode 100644 index 00000000..fab857db --- /dev/null +++ b/feature/privacypolicy/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id("convention.feature") +} + +android { + namespace = "com.withpeace.withpeace.feature.privacypolicy" +} + +dependencies { +} \ No newline at end of file diff --git a/feature/privacypolicy/consumer-rules.pro b/feature/privacypolicy/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/feature/privacypolicy/proguard-rules.pro b/feature/privacypolicy/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/privacypolicy/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/privacypolicy/src/androidTest/java/com/withpeace/withpeace/feature/privacypolicy/ExampleInstrumentedTest.kt b/feature/privacypolicy/src/androidTest/java/com/withpeace/withpeace/feature/privacypolicy/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..7bf7ba43 --- /dev/null +++ b/feature/privacypolicy/src/androidTest/java/com/withpeace/withpeace/feature/privacypolicy/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.withpeace.withpeace.feature.privacypolicy + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.withpeace.withpeace.feature.privacypolicy.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/feature/privacypolicy/src/main/AndroidManifest.xml b/feature/privacypolicy/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/privacypolicy/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/privacypolicy/src/main/java/com/withpeace/withpeace/feature/privacypolicy/PrivacyPolicyScreen.kt b/feature/privacypolicy/src/main/java/com/withpeace/withpeace/feature/privacypolicy/PrivacyPolicyScreen.kt new file mode 100644 index 00000000..785ddcd7 --- /dev/null +++ b/feature/privacypolicy/src/main/java/com/withpeace/withpeace/feature/privacypolicy/PrivacyPolicyScreen.kt @@ -0,0 +1,44 @@ +package com.withpeace.withpeace.feature.privacypolicy + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.withpeace.withpeace.core.designsystem.theme.WithpeaceTheme +import com.withpeace.withpeace.core.designsystem.ui.WithPeaceBackButtonTopAppBar +import com.withpeace.withpeace.core.ui.common.WithpeaceWebView + +@Composable +fun PrivacyPolicyRoute( + onShowSnackBar: (String) -> Unit, + onClickBackButton: () -> Unit, +) { + PrivacyPolicyScreen(onClickBackButton = onClickBackButton) +} + +@Composable +fun PrivacyPolicyScreen( + modifier: Modifier = Modifier, + onClickBackButton: () -> Unit, +) { + Column { + WithPeaceBackButtonTopAppBar( + onClickBackButton = onClickBackButton, + title = { + Text( + text = "개인정보처리방침", + style = WithpeaceTheme.typography.title1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = modifier.padding(end = 24.dp), + ) + }, + ) + WithpeaceWebView(url = "https://pointy-shampoo-9c7.notion.site/3671c1dbd4664645831707152f29105a") + } +} + + diff --git a/feature/privacypolicy/src/main/java/com/withpeace/withpeace/feature/privacypolicy/navigation/PrivacyPolicyNavigation.kt b/feature/privacypolicy/src/main/java/com/withpeace/withpeace/feature/privacypolicy/navigation/PrivacyPolicyNavigation.kt new file mode 100644 index 00000000..69581cd5 --- /dev/null +++ b/feature/privacypolicy/src/main/java/com/withpeace/withpeace/feature/privacypolicy/navigation/PrivacyPolicyNavigation.kt @@ -0,0 +1,24 @@ +package com.withpeace.withpeace.feature.privacypolicy.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.withpeace.withpeace.feature.privacypolicy.PrivacyPolicyRoute + +const val PRIVACY_POLICY_ROUTE = "privacy_policy_route" + +fun NavController.navigateToPrivacyPolicy(navOptions: NavOptions? = null) = + navigate(PRIVACY_POLICY_ROUTE, navOptions) + +fun NavGraphBuilder.privacyPolicyGraph( + onShowSnackBar: (String) -> Unit, + onClickBackButton: () -> Unit, +) { + composable(PRIVACY_POLICY_ROUTE) { + PrivacyPolicyRoute( + onShowSnackBar = onShowSnackBar, + onClickBackButton = onClickBackButton, + ) + } +} diff --git a/feature/privacypolicy/src/test/java/com/withpeace/withpeace/feature/privacypolicy/ExampleUnitTest.kt b/feature/privacypolicy/src/test/java/com/withpeace/withpeace/feature/privacypolicy/ExampleUnitTest.kt new file mode 100644 index 00000000..fbf06e18 --- /dev/null +++ b/feature/privacypolicy/src/test/java/com/withpeace/withpeace/feature/privacypolicy/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.withpeace.withpeace.feature.privacypolicy + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/feature/registerpost/src/main/java/com/withpeace/withpeace/feature/registerpost/RegisterPostScreen.kt b/feature/registerpost/src/main/java/com/withpeace/withpeace/feature/registerpost/RegisterPostScreen.kt index 29a11b1b..046561f1 100644 --- a/feature/registerpost/src/main/java/com/withpeace/withpeace/feature/registerpost/RegisterPostScreen.kt +++ b/feature/registerpost/src/main/java/com/withpeace/withpeace/feature/registerpost/RegisterPostScreen.kt @@ -262,7 +262,6 @@ fun RegisterPostTopAppBar( ) { Column { WithPeaceBackButtonTopAppBar( - modifier = modifier.padding(end = 24.dp), onClickBackButton = onClickBackButton, title = { Text( @@ -272,6 +271,7 @@ fun RegisterPostTopAppBar( }, actions = { WithPeaceCompleteButton( + modifier = modifier.padding(end = 24.dp), onClick = onCompleteRegisterPost, enabled = !isLoading, ) diff --git a/feature/termsofservice/.gitignore b/feature/termsofservice/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/termsofservice/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/termsofservice/build.gradle.kts b/feature/termsofservice/build.gradle.kts new file mode 100644 index 00000000..fd99f016 --- /dev/null +++ b/feature/termsofservice/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id("convention.feature") +} + +android { + namespace = "com.withpeace.withpeace.feature.termsofservice" +} + +dependencies { +} \ No newline at end of file diff --git a/feature/termsofservice/consumer-rules.pro b/feature/termsofservice/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/feature/termsofservice/proguard-rules.pro b/feature/termsofservice/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/termsofservice/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/termsofservice/src/androidTest/java/com/withpeace/withpeace/feature/termsofservice/ExampleInstrumentedTest.kt b/feature/termsofservice/src/androidTest/java/com/withpeace/withpeace/feature/termsofservice/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..60a8b70b --- /dev/null +++ b/feature/termsofservice/src/androidTest/java/com/withpeace/withpeace/feature/termsofservice/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.withpeace.withpeace.feature.termsofservice + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.withpeace.withpeace.feature.termsofservice.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/feature/termsofservice/src/main/AndroidManifest.xml b/feature/termsofservice/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/termsofservice/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/termsofservice/src/main/java/com/withpeace/withpeace/feature/termsofservice/TermsOfServiceScreen.kt b/feature/termsofservice/src/main/java/com/withpeace/withpeace/feature/termsofservice/TermsOfServiceScreen.kt new file mode 100644 index 00000000..18d6f59c --- /dev/null +++ b/feature/termsofservice/src/main/java/com/withpeace/withpeace/feature/termsofservice/TermsOfServiceScreen.kt @@ -0,0 +1,42 @@ +package com.withpeace.withpeace.feature.termsofservice + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.withpeace.withpeace.core.designsystem.theme.WithpeaceTheme +import com.withpeace.withpeace.core.designsystem.ui.WithPeaceBackButtonTopAppBar +import com.withpeace.withpeace.core.ui.common.WithpeaceWebView + +@Composable +fun TermsOfServiceRoute( + onShowSnackBar: (String) -> Unit, + onClickBackButton: () -> Unit, +) { + TermsOfServiceScreen(onClickBackButton = onClickBackButton) +} + +@Composable +fun TermsOfServiceScreen( + modifier: Modifier = Modifier, + onClickBackButton: () -> Unit, +) { + Column { + WithPeaceBackButtonTopAppBar( + onClickBackButton = onClickBackButton, + title = { + Text( + text = "서비스 이용약관", + style = WithpeaceTheme.typography.title1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = modifier.padding(end = 24.dp), + ) + }, + ) + WithpeaceWebView(url = "https://pointy-shampoo-9c7.notion.site/099b85822b9a4394848888fc2bc96560") + } +} \ No newline at end of file diff --git a/feature/termsofservice/src/main/java/com/withpeace/withpeace/feature/termsofservice/navigation/TermsOfServiceNavigation.kt b/feature/termsofservice/src/main/java/com/withpeace/withpeace/feature/termsofservice/navigation/TermsOfServiceNavigation.kt new file mode 100644 index 00000000..96c79098 --- /dev/null +++ b/feature/termsofservice/src/main/java/com/withpeace/withpeace/feature/termsofservice/navigation/TermsOfServiceNavigation.kt @@ -0,0 +1,25 @@ +package com.withpeace.withpeace.feature.termsofservice.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.withpeace.withpeace.feature.termsofservice.TermsOfServiceRoute + +const val TERMS_OF_SERVICE_ROUTE = "terms_of_service_route" + +fun NavController.navigateToTermsOfService(navOptions: NavOptions? = null) = + navigate(TERMS_OF_SERVICE_ROUTE, navOptions) + +fun NavGraphBuilder.termsOfServiceGraph( + onShowSnackBar: (String) -> Unit, + onClickBackButton: () -> Unit, +) { + composable(TERMS_OF_SERVICE_ROUTE) { + + TermsOfServiceRoute( + onShowSnackBar = onShowSnackBar, + onClickBackButton = onClickBackButton, + ) + } +} \ No newline at end of file diff --git a/feature/termsofservice/src/test/java/com/withpeace/withpeace/feature/termsofservice/ExampleUnitTest.kt b/feature/termsofservice/src/test/java/com/withpeace/withpeace/feature/termsofservice/ExampleUnitTest.kt new file mode 100644 index 00000000..baa783bb --- /dev/null +++ b/feature/termsofservice/src/test/java/com/withpeace/withpeace/feature/termsofservice/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.withpeace.withpeace.feature.termsofservice + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 3c5031eb..f1f3942a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,8 +5,9 @@ # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. + # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4705ba96..365c04c0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -71,6 +71,8 @@ firebasePlugin = "4.3.15" firebaseBom = "32.7.4" firebaseCrashlytics = "2.9.9" +textflow = "1.1.2" + [libraries] android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" } android-desugarJdkLibs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "androidDesugarJdkLibs" } @@ -132,6 +134,7 @@ skydoves-landscapist-placeholder = { group = "com.github.skydoves", name = "land landscapist-animation = { group = "com.github.skydoves", name = "landscapist-animation" } compose-shimmer = { group = "com.valentinilk.shimmer", name = "compose-shimmer", version.ref = "composeShimmer" } skydoves-sandwich = { group = "com.github.skydoves", name = "sandwich", version.ref = "sandwich" } +text-flow = { group = "io.github.oleksandrbalan", name = "textflow", version.ref = "textflow" } truth = { group = "com.google.truth", name = "truth", version.ref = "truth" } junit4 = { group = "junit", name = "junit", version.ref = "junit4" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 92a328c4..8ea150ea 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -38,3 +38,6 @@ include(":feature:gallery") include(":feature:profileeditor") include(":core:analytics") include(":feature:policydetail") +include(":feature:policyconsent") +include(":feature:privacypolicy") +include(":feature:termsofservice")