diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 07bb8c66..8b1cf69c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { implementation(project(":feature:postdetail")) implementation(project(":feature:profileeditor")) implementation(project(":feature:policydetail")) + implementation(project(":feature:policybookmarks")) implementation(project(":core:ui")) implementation(project(":core:interceptor")) implementation(project(":core:data")) diff --git a/app/src/main/java/com/withpeace/withpeace/MainActivity.kt b/app/src/main/java/com/withpeace/withpeace/MainActivity.kt index 68801ddd..046b51d0 100644 --- a/app/src/main/java/com/withpeace/withpeace/MainActivity.kt +++ b/app/src/main/java/com/withpeace/withpeace/MainActivity.kt @@ -89,7 +89,7 @@ class MainActivity : ComponentActivity() { "네트워크 상태가 원활하지 않습니다.", Toast.LENGTH_SHORT, ).show() - finish() + finish() // 강제 업데이트 } MainUiState.Loading -> {} } diff --git a/app/src/main/java/com/withpeace/withpeace/WithpeaceApp.kt b/app/src/main/java/com/withpeace/withpeace/WithpeaceApp.kt index c3d6e419..f59d139f 100644 --- a/app/src/main/java/com/withpeace/withpeace/WithpeaceApp.kt +++ b/app/src/main/java/com/withpeace/withpeace/WithpeaceApp.kt @@ -34,6 +34,7 @@ fun WithpeaceApp( val snackBarHostState = remember { SnackbarHostState() } var snackBarState: SnackbarState = remember { SnackbarState("", SnackbarType.Normal) } + val coroutineScope = rememberCoroutineScope() fun showSnackBar(snackbarState: SnackbarState) = coroutineScope.launch { snackBarState = snackbarState 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 0cab81e9..5fadf336 100644 --- a/app/src/main/java/com/withpeace/withpeace/navigation/NavHost.kt +++ b/app/src/main/java/com/withpeace/withpeace/navigation/NavHost.kt @@ -41,6 +41,8 @@ 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 +import navigatePolicyBookmarks +import policyBookmarksNavGraph @Composable fun WithpeaceNavHost( @@ -166,7 +168,7 @@ fun WithpeaceNavHost( homeNavGraph( onShowSnackBar = { onShowSnackBar(SnackbarState(it)) }, onPolicyClick = { - navController.navigateToPolicyDetail(policy = it) + navController.navigateToPolicyDetail(policyId = it) }, ) policyDetailNavGraph( @@ -203,6 +205,9 @@ fun WithpeaceNavHost( onAuthExpired = { onAuthExpired(onShowSnackBar, navController) }, + onDibsOfPolicyClick = { + navController.navigatePolicyBookmarks() + }, ) profileEditorNavGraph( onShowSnackBar = { onShowSnackBar(SnackbarState(it)) }, @@ -224,6 +229,12 @@ fun WithpeaceNavHost( }, ) + policyBookmarksNavGraph( + onShowSnackBar = { onShowSnackBar(SnackbarState(it)) }, + onClickBackButton = { + navController.popBackStack() + } + ) } postDetailGraph( onShowSnackBar = { onShowSnackBar(SnackbarState(it)) }, diff --git a/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/mapper/YouthPolicyMapper.kt b/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/mapper/YouthPolicyMapper.kt deleted file mode 100644 index f3b6e5ec..00000000 --- a/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/mapper/YouthPolicyMapper.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.withpeace.withpeace.core.data.mapper - -import com.withpeace.withpeace.core.domain.model.policy.PolicyClassification -import com.withpeace.withpeace.core.domain.model.policy.PolicyRegion -import com.withpeace.withpeace.core.domain.model.policy.YouthPolicy -import com.withpeace.withpeace.core.network.di.response.YouthPolicyEntity - -fun YouthPolicyEntity.toDomain(): YouthPolicy { - return YouthPolicy( - id = id, - title = title ?: "", - introduce = introduce ?: "", - region = regionCode.codeToRegion(), - policyClassification = classification.codeToPolicyClassification(), - ageInfo = ageInfo ?: "", - applicationDetails = applicationDetails ?: "", - residenceAndIncome = residenceAndIncome ?: "", - education = education ?: "", - specialization = specialization ?: "", - additionalNotes = additionalNotes ?: "", - participationRestrictions = participationRestrictions ?: "", - applicationProcess = applicationProcess ?: "", - screeningAndAnnouncement = screeningAndAnnouncement ?: "", - applicationSite = applicationSite ?: "", - submissionDocuments = submissionDocuments ?: "", - additionalUsefulInformation = etc ?: "", - supervisingAuthority = managingInstitution ?: "", - operatingOrganization = operatingOrganization ?: "", - businessRelatedReferenceSite1 = businessReferenceSite1 ?: "", - businessRelatedReferenceSite2 = businessReferenceSite2 ?: "", - ) -} - -private fun String?.codeToRegion(): PolicyRegion { - if (this?.slice(0..5) == "003001") { - return PolicyRegion.중앙부처 - } - return when (this?.slice(0..8)) { - "003002001" -> PolicyRegion.서울 - "003002002" -> PolicyRegion.부산 - "003002003" -> PolicyRegion.대구 - "003002004" -> PolicyRegion.인천 - "003002005" -> PolicyRegion.광주 - "003002006" -> PolicyRegion.대전 - "003002007" -> PolicyRegion.울산 - "003002008" -> PolicyRegion.경기 - "003002009" -> PolicyRegion.강원 - "003002010" -> PolicyRegion.충북 - "003002011" -> PolicyRegion.충남 - "003002012" -> PolicyRegion.전북 - "003002013" -> PolicyRegion.전남 - "003002014" -> PolicyRegion.경북 - "003002015" -> PolicyRegion.경남 - "003002016" -> PolicyRegion.제주 - "003002017" -> PolicyRegion.세종 - else -> PolicyRegion.기타 - } -} - -fun PolicyRegion.toCode(): String { - return when (this) { - PolicyRegion.중앙부처 -> "003001" - PolicyRegion.서울 -> "003002001" - PolicyRegion.부산 -> "003002002" - PolicyRegion.대구 -> "003002003" - PolicyRegion.인천 -> "003002004" - PolicyRegion.광주 -> "003002005" - PolicyRegion.대전 -> "003002006" - PolicyRegion.울산 -> "003002007" - PolicyRegion.경기 -> "003002008" - PolicyRegion.강원 -> "003002009" - PolicyRegion.충북 -> "003002010" - PolicyRegion.충남 -> "003002011" - PolicyRegion.전북 -> "003002012" - PolicyRegion.전남 -> "003002013" - PolicyRegion.경북 -> "003002014" - PolicyRegion.경남 -> "003002015" - PolicyRegion.제주 -> "003002016" - PolicyRegion.세종 -> "003002017" - PolicyRegion.기타 -> throw IllegalStateException("") - } -} - -fun String?.codeToPolicyClassification(): PolicyClassification { - return when (this) { - "023010" -> PolicyClassification.JOB - "023020" -> PolicyClassification.RESIDENT - "023030" -> PolicyClassification.EDUCATION - "023040" -> PolicyClassification.WELFARE_AND_CULTURE - "023050" -> PolicyClassification.PARTICIPATION_AND_RIGHT - else -> PolicyClassification.ETC - } -} - -fun PolicyClassification.toCode(): String { - return when (this) { - PolicyClassification.JOB -> "023010" - PolicyClassification.RESIDENT -> "023020" - PolicyClassification.EDUCATION -> "023030" - PolicyClassification.WELFARE_AND_CULTURE -> "023040" - PolicyClassification.PARTICIPATION_AND_RIGHT -> "023050" - PolicyClassification.ETC -> throw IllegalStateException("") - } -} \ No newline at end of file diff --git a/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/mapper/youthpolicy/PolicyDetailMapper.kt b/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/mapper/youthpolicy/PolicyDetailMapper.kt new file mode 100644 index 00000000..3232b716 --- /dev/null +++ b/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/mapper/youthpolicy/PolicyDetailMapper.kt @@ -0,0 +1,31 @@ +package com.withpeace.withpeace.core.data.mapper.youthpolicy + +import com.withpeace.withpeace.core.domain.model.policy.PolicyClassification +import com.withpeace.withpeace.core.domain.model.policy.YouthPolicyDetail +import com.withpeace.withpeace.core.network.di.response.policy.PolicyDetailResponse + +internal fun PolicyDetailResponse.toDomain(): YouthPolicyDetail { + return YouthPolicyDetail( + id = id, + title = title, + introduce = introduce, + ageInfo = ageInfo, + classification = classification.codeToPolicyClassification(), + + applicationDetails = applicationDetails, + residenceAndIncome = residenceAndIncome, + education = education, + specialization = specialization, + additionalNotes = additionalNotes, + participationRestrictions = participationRestrictions, + applicationProcess = applicationProcess, + 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/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/mapper/youthpolicy/PolicyMapper.kt b/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/mapper/youthpolicy/PolicyMapper.kt new file mode 100644 index 00000000..e16e8a88 --- /dev/null +++ b/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/mapper/youthpolicy/PolicyMapper.kt @@ -0,0 +1,60 @@ +package com.withpeace.withpeace.core.data.mapper.youthpolicy + +import com.withpeace.withpeace.core.domain.model.policy.PolicyClassification +import com.withpeace.withpeace.core.domain.model.policy.PolicyRegion +import com.withpeace.withpeace.core.domain.model.policy.YouthPolicy +import com.withpeace.withpeace.core.network.di.response.policy.PolicyResponse + +internal fun PolicyResponse.toDomain(): YouthPolicy { + return YouthPolicy( + id = id, + title = title, + introduce = introduce, + region = region.codeToRegion(), + policyClassification = classification.codeToPolicyClassification(), + ageInfo = ageInfo, + ) +} + +internal fun String?.codeToRegion(): PolicyRegion { + return PolicyRegion.entries.find { it.toString() == this } ?: PolicyRegion.기타 +} + +internal fun PolicyRegion.toCode(): String { + return when (this) { + PolicyRegion.중앙부처 -> "003001" + PolicyRegion.서울 -> "003002001" + PolicyRegion.부산 -> "003002002" + PolicyRegion.대구 -> "003002003" + PolicyRegion.인천 -> "003002004" + PolicyRegion.광주 -> "003002005" + PolicyRegion.대전 -> "003002006" + PolicyRegion.울산 -> "003002007" + PolicyRegion.경기 -> "003002008" + PolicyRegion.강원 -> "003002009" + PolicyRegion.충북 -> "003002010" + PolicyRegion.충남 -> "003002011" + PolicyRegion.전북 -> "003002012" + PolicyRegion.전남 -> "003002013" + PolicyRegion.경북 -> "003002014" + PolicyRegion.경남 -> "003002015" + PolicyRegion.제주 -> "003002016" + PolicyRegion.세종 -> "003002017" + PolicyRegion.기타 -> throw IllegalStateException("찾을 수 없는 지역입니다.") + } +} + +internal fun String?.codeToPolicyClassification(): PolicyClassification { + return PolicyClassification.entries.find { it.toString() == this } ?: PolicyClassification.ETC +} + +internal fun PolicyClassification.toCode(): String { + return when (this) { + PolicyClassification.JOB -> "023010" + PolicyClassification.RESIDENT -> "023020" + PolicyClassification.EDUCATION -> "023030" + PolicyClassification.WELFARE_AND_CULTURE -> "023040" + PolicyClassification.PARTICIPATION_AND_RIGHT -> "023050" + PolicyClassification.ETC -> throw IllegalStateException("정책 분류를 찾을 수 없습니다.") + } +} \ No newline at end of file diff --git a/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/paging/YouthPolicyPagingSource.kt b/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/paging/YouthPolicyPagingSource.kt index c3559b96..81db4cd8 100644 --- a/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/paging/YouthPolicyPagingSource.kt +++ b/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/paging/YouthPolicyPagingSource.kt @@ -1,12 +1,10 @@ package com.withpeace.withpeace.core.data.paging -import android.util.Log import androidx.paging.PagingSource import androidx.paging.PagingState import com.skydoves.sandwich.ApiResponse -import com.withpeace.withpeace.core.data.BuildConfig -import com.withpeace.withpeace.core.data.mapper.toCode -import com.withpeace.withpeace.core.data.mapper.toDomain +import com.withpeace.withpeace.core.data.mapper.youthpolicy.toCode +import com.withpeace.withpeace.core.data.mapper.youthpolicy.toDomain import com.withpeace.withpeace.core.domain.model.error.CheonghaError import com.withpeace.withpeace.core.domain.model.error.ResponseError import com.withpeace.withpeace.core.domain.model.policy.PolicyFilters @@ -23,15 +21,12 @@ class YouthPolicyPagingSource( override suspend fun load(params: LoadParams): LoadResult { val pageIndex = params.key ?: 1 val response = youthPolicyService.getPolicies( - apiKey = BuildConfig.YOUTH_POLICY_API_KEY, - pageSize = params.loadSize, + display = params.loadSize, pageIndex = pageIndex, - classification = filterInfo.classifications.joinToString(",") { it.toCode() }, region = filterInfo.regions.joinToString(",") { it.toCode() }, + classification = filterInfo.classifications.joinToString(",") { it.toCode() }, ) - - if (response is ApiResponse.Failure) { val error: CheonghaError = if (response is ApiResponse.Failure.Exception) ResponseError.HTTP_EXCEPTION_ERROR @@ -40,11 +35,11 @@ class YouthPolicyPagingSource( return LoadResult.Error(IllegalStateException("API response error that $error")) } - val data = (response as ApiResponse.Success).data + val successResponse = (response as ApiResponse.Success).data return LoadResult.Page( - data = data.youthPolicyEntity?.map { it.toDomain() } ?: emptyList(), + data = successResponse.data.map { it.toDomain() }, prevKey = if (pageIndex == STARTING_PAGE_INDEX) null else pageIndex - 1, - nextKey = if (response.data.youthPolicyEntity.isNullOrEmpty()) null else pageIndex + (params.loadSize / pageSize), + nextKey = if (successResponse.data.isEmpty()) null else pageIndex + (params.loadSize / pageSize), ) } diff --git a/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/repository/DefaultYouthPolicyRepository.kt b/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/repository/DefaultYouthPolicyRepository.kt index d49d62ce..1ef9b693 100644 --- a/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/repository/DefaultYouthPolicyRepository.kt +++ b/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/repository/DefaultYouthPolicyRepository.kt @@ -3,17 +3,27 @@ package com.withpeace.withpeace.core.data.repository import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingData +import com.skydoves.sandwich.suspendMapSuccess +import com.skydoves.sandwich.suspendOnError +import com.withpeace.withpeace.core.data.mapper.youthpolicy.toDomain import com.withpeace.withpeace.core.data.paging.YouthPolicyPagingSource +import com.withpeace.withpeace.core.data.util.handleApiFailure import com.withpeace.withpeace.core.domain.model.error.CheonghaError +import com.withpeace.withpeace.core.domain.model.error.ClientError +import com.withpeace.withpeace.core.domain.model.error.ResponseError import com.withpeace.withpeace.core.domain.model.policy.PolicyFilters import com.withpeace.withpeace.core.domain.model.policy.YouthPolicy +import com.withpeace.withpeace.core.domain.model.policy.YouthPolicyDetail +import com.withpeace.withpeace.core.domain.repository.UserRepository import com.withpeace.withpeace.core.domain.repository.YouthPolicyRepository import com.withpeace.withpeace.core.network.di.service.YouthPolicyService import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow import javax.inject.Inject class DefaultYouthPolicyRepository @Inject constructor( private val youthPolicyService: YouthPolicyService, + private val userRepository: UserRepository, ) : YouthPolicyRepository { override fun getPolicies( filterInfo: PolicyFilters, @@ -30,6 +40,30 @@ class DefaultYouthPolicyRepository @Inject constructor( }, ).flow + override fun getPolicy( + policyId: String, + onError: suspend (CheonghaError) -> Unit, + ): Flow = flow { + youthPolicyService.getPolicyDetail(policyId).suspendMapSuccess { + emit(data.toDomain()) + }.handleApiFailure { + onErrorWithAuthExpired(it, onError) + } + } + + private suspend fun onErrorWithAuthExpired( + it: ResponseError, + onError: suspend (CheonghaError) -> Unit, + ) { + if (it == ResponseError.INVALID_TOKEN_ERROR) { + userRepository.logout(onError).collect { + onError(ClientError.AuthExpired) + } + } else { + onError(it) + } + } + companion object { private const val PAGE_SIZE = 10 } 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 ebefdfd8..84d2d176 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 @@ -31,12 +31,12 @@ fun WithPeaceBackButtonTopAppBar( navigationIcon = { Icon( modifier = - Modifier - .padding(start = 20.dp, bottom = 16.dp, top = 16.dp, end = 28.dp) - .clickable { - onClickBackButton() - } - .padding(4.dp), + Modifier + .padding(start = 20.dp, bottom = 16.dp, top = 16.dp, end = 28.dp) + .clickable { + onClickBackButton() + } + .padding(4.dp), painter = painterResource(id = R.drawable.ic_backarrow_left), contentDescription = "BackArrowLeft", ) diff --git a/core/designsystem/src/main/java/com/withpeace/withpeace/core/designsystem/ui/snackbar/NavigatorSnackbar.kt b/core/designsystem/src/main/java/com/withpeace/withpeace/core/designsystem/ui/snackbar/NavigatorSnackbar.kt index 3ab5d4e3..873c64a3 100644 --- a/core/designsystem/src/main/java/com/withpeace/withpeace/core/designsystem/ui/snackbar/NavigatorSnackbar.kt +++ b/core/designsystem/src/main/java/com/withpeace/withpeace/core/designsystem/ui/snackbar/NavigatorSnackbar.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.withpeace.withpeace.core.designsystem.theme.WithpeaceTheme @@ -41,4 +42,12 @@ fun NavigatorSnackbar(data: SnackbarState) { textDecoration = TextDecoration.Underline, ) } -} \ No newline at end of file +} + +@Composable +@Preview +fun NavigationSnackbarPreview() { + WithpeaceTheme { + NavigatorSnackbar(data = SnackbarState("장충동 왕족발 보쌈", SnackbarType.Navigator("먹기",{}))) + } +} diff --git a/core/domain/src/main/java/com/withpeace/withpeace/core/domain/model/policy/PolicyFilters.kt b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/model/policy/PolicyFilters.kt index c6ccc836..0bad0cc3 100644 --- a/core/domain/src/main/java/com/withpeace/withpeace/core/domain/model/policy/PolicyFilters.kt +++ b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/model/policy/PolicyFilters.kt @@ -4,25 +4,25 @@ data class PolicyFilters( val regions: List = emptyList(), val classifications: List = emptyList(), ) { - fun addRegion(region: PolicyRegion): PolicyFilters = copy(regions = regions + region) - - fun removeRegion(region: PolicyRegion): PolicyFilters = copy(regions = regions - region) - fun updateRegion(region: PolicyRegion): PolicyFilters { return if (!regions.contains(region)) addRegion(region) else removeRegion(region) } - fun addClassification(classification: PolicyClassification): PolicyFilters = - copy(classifications = classifications + classification) + private fun addRegion(region: PolicyRegion): PolicyFilters = copy(regions = regions + region) - fun removeClassification(classification: PolicyClassification): PolicyFilters = - copy(classifications = classifications - classification) + private fun removeRegion(region: PolicyRegion): PolicyFilters = copy(regions = regions - region) fun updateClassification(classification: PolicyClassification): PolicyFilters { return if (!classifications.contains(classification)) addClassification(classification) else removeClassification(classification) } + private fun addClassification(classification: PolicyClassification): PolicyFilters = + copy(classifications = classifications + classification) + + private fun removeClassification(classification: PolicyClassification): PolicyFilters = + copy(classifications = classifications - classification) + fun removeAll(): PolicyFilters = copy(regions = emptyList(), classifications = emptyList()) } \ No newline at end of file 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 74b670d0..74243635 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 @@ -7,21 +7,4 @@ data class YouthPolicy( val region: PolicyRegion, val policyClassification: PolicyClassification, val ageInfo: String, - - val applicationDetails: String, - val residenceAndIncome: String, - val education: String, - val specialization: String, - val additionalNotes: String, - val participationRestrictions: String, - val applicationProcess: String, - 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/model/policy/YouthPolicyDetail.kt b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/model/policy/YouthPolicyDetail.kt new file mode 100644 index 00000000..ee54c843 --- /dev/null +++ b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/model/policy/YouthPolicyDetail.kt @@ -0,0 +1,26 @@ +package com.withpeace.withpeace.core.domain.model.policy + +data class YouthPolicyDetail( + val id: String, + val title: String, + val introduce: String, + val ageInfo: String, + val classification: PolicyClassification, + + val applicationDetails: String, + val residenceAndIncome: String, + val education: String, + val specialization: String, + val additionalNotes: String, + val participationRestrictions: String, + val applicationProcess: String, + val screeningAndAnnouncement: String, + val applicationSite: String, + val submissionDocuments: String, + + val additionalUsefulInformation: String, + val supervisingAuthority: String, + val operatingOrganization: String, + val businessRelatedReferenceSite1: String, + val businessRelatedReferenceSite2: String, +) diff --git a/core/domain/src/main/java/com/withpeace/withpeace/core/domain/repository/YouthPolicyRepository.kt b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/repository/YouthPolicyRepository.kt index dced8a55..88f2dd47 100644 --- a/core/domain/src/main/java/com/withpeace/withpeace/core/domain/repository/YouthPolicyRepository.kt +++ b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/repository/YouthPolicyRepository.kt @@ -4,6 +4,7 @@ import androidx.paging.PagingData import com.withpeace.withpeace.core.domain.model.error.CheonghaError import com.withpeace.withpeace.core.domain.model.policy.PolicyFilters import com.withpeace.withpeace.core.domain.model.policy.YouthPolicy +import com.withpeace.withpeace.core.domain.model.policy.YouthPolicyDetail import kotlinx.coroutines.flow.Flow interface YouthPolicyRepository { @@ -11,4 +12,9 @@ interface YouthPolicyRepository { filterInfo: PolicyFilters, onError: suspend (CheonghaError) -> Unit, ): Flow> + + fun getPolicy( + policyId: String, + onError: suspend (CheonghaError) -> Unit, + ): Flow } \ No newline at end of file diff --git a/core/domain/src/main/java/com/withpeace/withpeace/core/domain/usecase/GetYouthPolicyDetailUseCase.kt b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/usecase/GetYouthPolicyDetailUseCase.kt new file mode 100644 index 00000000..dc90fe64 --- /dev/null +++ b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/usecase/GetYouthPolicyDetailUseCase.kt @@ -0,0 +1,18 @@ +package com.withpeace.withpeace.core.domain.usecase + +import com.withpeace.withpeace.core.domain.model.error.CheonghaError +import com.withpeace.withpeace.core.domain.model.policy.YouthPolicyDetail +import com.withpeace.withpeace.core.domain.repository.YouthPolicyRepository +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class GetYouthPolicyDetailUseCase @Inject constructor( + private val youthPolicyRepository: YouthPolicyRepository, +) { + operator fun invoke( + policyId: String, + onError: suspend (CheonghaError) -> Unit, + ): Flow { + return youthPolicyRepository.getPolicy(policyId = policyId, onError = onError) + } +} \ No newline at end of file diff --git a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/di/NetworkModule.kt b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/di/NetworkModule.kt index 8f29e2b5..7b7f1f39 100644 --- a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/di/NetworkModule.kt +++ b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/di/NetworkModule.kt @@ -108,20 +108,4 @@ object NetworkModule { .addCallAdapterFactory(ApiResponseCallAdapterFactory.create()) .build() } - - @Named("youth_policy") - @Provides - @Singleton - fun provideXmlRetrofitClient( - @Named("logging_client") okHttpClient: OkHttpClient, - ): Retrofit { - val parser = TikXml.Builder().exceptionOnUnreadXml(false).build() - - return Retrofit.Builder() - .client(okHttpClient) - .baseUrl("https://www.youthcenter.go.kr/opi/") - .addConverterFactory(TikXmlConverterFactory.create(parser)) - .addCallAdapterFactory(ApiResponseCallAdapterFactory.create()) - .build() - } } 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 9c482745..ab703e83 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 @@ -39,6 +39,6 @@ object ServiceModule { @Provides @Singleton - fun providesYouthPolicyService(@Named("youth_policy") retrofit: Retrofit): YouthPolicyService = + fun providesYouthPolicyService(@Named("general") retrofit: Retrofit): YouthPolicyService = retrofit.create(YouthPolicyService::class.java) } 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 deleted file mode 100644 index 9358a03e..00000000 --- a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/response/YouthPolicyListResponse.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.withpeace.withpeace.core.network.di.response - -import com.tickaroo.tikxml.annotation.Element -import com.tickaroo.tikxml.annotation.PropertyElement -import com.tickaroo.tikxml.annotation.Xml - -@Xml(name = "youthPolicyList") -data class YouthPolicyListResponse( - @PropertyElement - val pageIndex: Int, - @PropertyElement(name = "totalCount") - val totalDataCount: Int, - @Element - val youthPolicyEntity: List?, -) - -@Xml(name = "youthPolicy") -data class YouthPolicyEntity( - @PropertyElement(name = "bizId", writeAsCData = true) - val id: String, - @PropertyElement(name = "polyBizSjnm", writeAsCData = true) // XML에서는 String 형식을 CData라고 정의함 - val title: String?, // API 데이터를 넣는 상황에, 휴먼에러를 고려하여 nullable 설정 - @PropertyElement(name = "polyItcnCn", writeAsCData = true) - val introduce: String?, - @PropertyElement(name = "polyRlmCd", writeAsCData = true) - val classification: String?, - @PropertyElement(name = "polyBizSecd") - val regionCode: String?, - @PropertyElement(name = "ageInfo", writeAsCData = true) - val ageInfo: String?, - - - @PropertyElement(name = "sporCn", writeAsCData = true) - val applicationDetails: String?, - @PropertyElement(name = "prcpCn", writeAsCData = true) - val residenceAndIncome: String?, - @PropertyElement(name = "accrRqisCn", writeAsCData = true) - val education: String?, - @PropertyElement(name = "splzRlmRqisCn", writeAsCData = true) - val specialization: String?, - @PropertyElement(name = "aditRscn", writeAsCData = true) - val additionalNotes: String?, - @PropertyElement(name = "prcpLmttTrgtCn", writeAsCData = true) - val participationRestrictions: String?, - @PropertyElement(name = "rqutProcCn", writeAsCData = true) - val applicationProcess: String?, - @PropertyElement(name = "jdgnPresCn", writeAsCData = true) - val screeningAndAnnouncement: String?, - @PropertyElement(name = "rqutUrla", writeAsCData = true) - val applicationSite: String?, - @PropertyElement(name = "pstnPaprCn", writeAsCData = true) - 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/response/policy/PolicyDetailResponse.kt b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/response/policy/PolicyDetailResponse.kt new file mode 100644 index 00000000..06d4b583 --- /dev/null +++ b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/response/policy/PolicyDetailResponse.kt @@ -0,0 +1,27 @@ +package com.withpeace.withpeace.core.network.di.response.policy + +import kotlinx.serialization.Serializable + +@Serializable +data class PolicyDetailResponse( + val id: String, // 정책 id + val title: String, // 정책 제목 + val introduce: String, // 정책 소개 + val classification: String, // 정책 분야 + val applicationDetails: String, // 지원 내용 + val ageInfo: String, // 연령 + val residenceAndIncome: String, // 거주지 및 소득 + val education: String, // 학력 + val specialization: String, // 특화 분야 + val additionalNotes: String, // 추가 단서 사항 + val participationRestrictions: String, // 참여 제한 대상 + val applicationProcess: String, // 신청 절차 + val screeningAndAnnouncement: String, // 심사 및 발표 + val applicationSite: String, // 신청 사이트 + val submissionDocuments: String, // 제출 서류 + val additionalUsefulInformation: String, // 기타 유익 정보 + val supervisingAuthority: String, // 주관 기관 + val operatingOrganization: String, // 운영 기관 + val businessRelatedReferenceSite1: String, // 사업관련 참고 사이트 1 + val businessRelatedReferenceSite2: String, // 사업관련 참고 사이트 2 +) diff --git a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/response/policy/PolicyResponse.kt b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/response/policy/PolicyResponse.kt new file mode 100644 index 00000000..97603ff4 --- /dev/null +++ b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/response/policy/PolicyResponse.kt @@ -0,0 +1,13 @@ +package com.withpeace.withpeace.core.network.di.response.policy + +import kotlinx.serialization.Serializable + +@Serializable +data class PolicyResponse( + val id: String, + val title: String, + val introduce: String, + val classification: String, + val region: String, + val ageInfo: String +) diff --git a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/service/PolicyService.kt b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/service/PolicyService.kt new file mode 100644 index 00000000..e01e4717 --- /dev/null +++ b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/service/PolicyService.kt @@ -0,0 +1,30 @@ +package com.withpeace.withpeace.core.network.di.service + +import com.skydoves.sandwich.ApiResponse +import com.withpeace.withpeace.core.network.di.response.BaseResponse +import com.withpeace.withpeace.core.network.di.response.policy.PolicyDetailResponse +import com.withpeace.withpeace.core.network.di.response.policy.PolicyResponse +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path +import retrofit2.http.Query + +interface PolicyService { + @GET("/api/v1/policies") + suspend fun getPolicies( + @Query("region") region: String, + @Query("classification") classification: String, + @Query("pageIndex") pageIndex: Int, + @Query("display") display: Int, + ): ApiResponse>> + + @GET("/api/v1/policies/{policyId}") + suspend fun getPolicyDetail(@Path("policyId") policyId: String): ApiResponse> + + @POST("/api/v1/policies/{policyId}/favorites") + suspend fun bookmarkPolicy(@Path("policyId") policyId: String): ApiResponse> + + @DELETE("/api/v1/policies/{policyId}/favorites") + suspend fun unBookmarkPolicy(@Path("policyId") policyId: String): ApiResponse> +} \ No newline at end of file diff --git a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/service/YouthPolicyService.kt b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/service/YouthPolicyService.kt index cae65855..179014a7 100644 --- a/core/network/src/main/java/com/withpeace/withpeace/core/network/di/service/YouthPolicyService.kt +++ b/core/network/src/main/java/com/withpeace/withpeace/core/network/di/service/YouthPolicyService.kt @@ -1,17 +1,30 @@ package com.withpeace.withpeace.core.network.di.service import com.skydoves.sandwich.ApiResponse -import com.withpeace.withpeace.core.network.di.response.YouthPolicyListResponse +import com.withpeace.withpeace.core.network.di.response.BaseResponse +import com.withpeace.withpeace.core.network.di.response.policy.PolicyDetailResponse +import com.withpeace.withpeace.core.network.di.response.policy.PolicyResponse +import retrofit2.http.DELETE import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path import retrofit2.http.Query interface YouthPolicyService { - @GET("youthPlcyList.do") + @GET("/api/v1/policies") suspend fun getPolicies( - @Query("openApiVlak") apiKey: String, - @Query("display") pageSize: Int, + @Query("region") region: String, + @Query("classification") classification: String, @Query("pageIndex") pageIndex: Int, - @Query("bizTycdSel") classification: String?, - @Query("srchPolyBizSecd") region: String?, - ): ApiResponse + @Query("display") display: Int, + ): ApiResponse>> + + @GET("/api/v1/policies/{policyId}") + suspend fun getPolicyDetail(@Path("policyId") policyId: String): ApiResponse> + + @POST("/api/v1/policies/{policyId}/favorites") + suspend fun bookmarkPolicy(@Path("policyId") policyId: String): ApiResponse> + + @DELETE("/api/v1/policies/{policyId}/favorites") + suspend fun unBookmarkPolicy(@Path("policyId") policyId: String): ApiResponse> } \ No newline at end of file diff --git a/core/ui/src/main/java/com/withpeace/withpeace/core/ui/bookmark/BookmarkButton.kt b/core/ui/src/main/java/com/withpeace/withpeace/core/ui/bookmark/BookmarkButton.kt new file mode 100644 index 00000000..e23809c6 --- /dev/null +++ b/core/ui/src/main/java/com/withpeace/withpeace/core/ui/bookmark/BookmarkButton.kt @@ -0,0 +1,39 @@ +package com.withpeace.withpeace.core.ui.bookmark + +import androidx.compose.foundation.Image +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import com.withpeace.withpeace.core.designsystem.theme.WithpeaceTheme +import com.withpeace.withpeace.core.ui.R + +@Composable +fun BookmarkButton( + modifier: Modifier = Modifier, + isClick: Boolean = false, +) { + Image( + modifier = modifier, + painter = painterResource( + id = if (isClick) R.drawable.ic_heart else com.withpeace.withpeace.core.ui.R.drawable.ic_empty_heart, + ), + contentDescription = "찜하기", + ) +} + +@Composable +@Preview() +fun BookmarkButtonPreview() { + WithpeaceTheme { + BookmarkButton() + } +} + +@Composable +@Preview() +fun BookmarkButtonClickedPreview() { + WithpeaceTheme { + BookmarkButton(isClick = true) + } +} \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_empty_heart.xml b/core/ui/src/main/res/drawable/ic_empty_heart.xml new file mode 100644 index 00000000..afaf4ea0 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_empty_heart.xml @@ -0,0 +1,11 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_heart.xml b/core/ui/src/main/res/drawable/ic_heart.xml new file mode 100644 index 00000000..12440bd4 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_heart.xml @@ -0,0 +1,10 @@ + + + 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 637bb229..43b1c85d 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 @@ -47,24 +47,27 @@ import androidx.constraintlayout.compose.Dimension import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.paging.LoadState +import androidx.paging.PagingData import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems 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.core.ui.bookmark.BookmarkButton import com.withpeace.withpeace.core.ui.policy.ClassificationUiModel import com.withpeace.withpeace.core.ui.policy.RegionUiModel -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 com.withpeace.withpeace.feature.home.uistate.YouthPolicyUiModel +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch @Composable fun HomeRoute( onShowSnackBar: (message: String) -> Unit = {}, viewModel: HomeViewModel = hiltViewModel(), - onPolicyClick: (YouthPolicyUiModel) -> Unit, + onPolicyClick: (String) -> Unit, ) { val youthPolicyPagingData = viewModel.youthPolicyPagingFlow.collectAsLazyPagingItems() val selectedFilterUiState = viewModel.selectingFilters.collectAsStateWithLifecycle() @@ -92,7 +95,7 @@ fun HomeScreen( onFilterAllOff: () -> Unit, onSearchWithFilter: () -> Unit, onCloseFilter: () -> Unit, - onPolicyClick: (YouthPolicyUiModel) -> Unit, + onPolicyClick: (String) -> Unit, ) { Column(modifier = modifier.fillMaxSize()) { HomeHeader( @@ -169,7 +172,7 @@ fun HomeScreen( private fun PolicyItems( modifier: Modifier, youthPolicies: LazyPagingItems, - onPolicyClick: (YouthPolicyUiModel) -> Unit, + onPolicyClick: (String) -> Unit, ) { Column( modifier = modifier @@ -179,7 +182,9 @@ private fun PolicyItems( ) { Spacer(modifier = modifier.height(8.dp)) LazyColumn( - modifier = modifier.fillMaxSize().testTag("home:policies"), + modifier = modifier + .fillMaxSize() + .testTag("home:policies"), contentPadding = PaddingValues(bottom = 16.dp), ) { items( @@ -304,7 +309,7 @@ private fun HomeHeader( private fun YouthPolicyCard( modifier: Modifier, youthPolicy: YouthPolicyUiModel, - onPolicyClick: (YouthPolicyUiModel) -> Unit, + onPolicyClick: (String) -> Unit, ) { Card( modifier = modifier @@ -320,7 +325,7 @@ private fun YouthPolicyCard( borderRadius = 10.dp, ) .clickable { - onPolicyClick(youthPolicy) + onPolicyClick(youthPolicy.id) }, ) { ConstraintLayout( @@ -331,7 +336,7 @@ private fun YouthPolicyCard( ) { val ( title, content, - region, ageRange, thumbnail, + region, ageRange, thumbnail, heart, ) = createRefs() Text( @@ -367,6 +372,15 @@ private fun YouthPolicyCard( overflow = TextOverflow.Ellipsis, maxLines = 2, ) + BookmarkButton( + modifier = modifier.constrainAs( + heart, + ) { + top.linkTo(content.bottom, margin = 8.dp) + start.linkTo(parent.start) + bottom.linkTo(parent.bottom) + }, + ) Text( text = youthPolicy.region.name, @@ -375,9 +389,9 @@ private fun YouthPolicyCard( .constrainAs( region, constrainBlock = { - top.linkTo(content.bottom, margin = 8.dp) - start.linkTo(parent.start) - bottom.linkTo(parent.bottom) + top.linkTo(heart.top) + start.linkTo(heart.end, margin = 8.dp) + bottom.linkTo(heart.bottom) }, ) .background( @@ -429,9 +443,33 @@ private fun YouthPolicyCard( } @Composable -@Preview +@Preview(showBackground = true) fun HomePreview() { WithpeaceTheme { - // HomeScreen() + HomeScreen( + youthPolicies = + flowOf( + PagingData.from( + listOf( + YouthPolicyUiModel( + id = "deterruisset", + title = "epicurei", + content = "interdum", + region = RegionUiModel.대구, + ageInfo = "quo", + classification = ClassificationUiModel.JOB, + ), + ), + ), + ).collectAsLazyPagingItems(), + selectedFilterUiState = PolicyFiltersUiModel(), + onDismissRequest = { }, + onClassificationCheckChanged = {}, + onRegionCheckChanged = {}, + onFilterAllOff = {}, + onSearchWithFilter = {}, + onCloseFilter = {}, + onPolicyClick = {}, + ) } -} \ No newline at end of file +} diff --git a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeViewModel.kt b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeViewModel.kt index 4fcb301c..2eeebf66 100644 --- a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeViewModel.kt +++ b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/HomeViewModel.kt @@ -5,14 +5,14 @@ import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import androidx.paging.cachedIn import androidx.paging.map +import com.withpeace.withpeace.core.domain.model.error.ResponseError import com.withpeace.withpeace.core.domain.model.policy.PolicyFilters import com.withpeace.withpeace.core.domain.usecase.GetYouthPoliciesUseCase import com.withpeace.withpeace.core.ui.policy.ClassificationUiModel import com.withpeace.withpeace.core.ui.policy.RegionUiModel import com.withpeace.withpeace.core.ui.policy.toDomain import com.withpeace.withpeace.feature.home.uistate.PolicyFiltersUiModel -import com.withpeace.withpeace.core.ui.policy.YouthPolicyUiModel -import com.withpeace.withpeace.core.ui.policy.toUiModel +import com.withpeace.withpeace.feature.home.uistate.YouthPolicyUiModel import com.withpeace.withpeace.feature.home.uistate.toDomain import com.withpeace.withpeace.feature.home.uistate.toUiModel import dagger.hilt.android.lifecycle.HiltViewModel @@ -54,6 +54,14 @@ class HomeViewModel @Inject constructor( youthPoliciesUseCase( filterInfo = completedFilters, onError = { + when (it) { + ResponseError.EXPIRED_TOKEN_ERROR -> { + //TODO 로그아웃, 네트워크 에러 + } + + else -> { + } + } }, ).map { it.map { youthPolicy -> diff --git a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/navigation/HomeNavigation.kt b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/navigation/HomeNavigation.kt index 0795283e..c2bc6397 100644 --- a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/navigation/HomeNavigation.kt +++ b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/navigation/HomeNavigation.kt @@ -4,7 +4,7 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable -import com.withpeace.withpeace.core.ui.policy.YouthPolicyUiModel +import com.withpeace.withpeace.feature.home.uistate.YouthPolicyUiModel import com.withpeace.withpeace.feature.home.HomeRoute const val HOME_ROUTE = "homeRoute" @@ -15,7 +15,7 @@ fun NavController.navigateHome(navOptions: NavOptions? = null) { fun NavGraphBuilder.homeNavGraph( onShowSnackBar: (message: String) -> Unit, - onPolicyClick: (YouthPolicyUiModel) -> Unit, + onPolicyClick: (String) -> Unit, ) { composable(route = HOME_ROUTE) { HomeRoute( diff --git a/feature/home/src/main/java/com/withpeace/withpeace/feature/home/uistate/YouthPolicyUiModel.kt b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/uistate/YouthPolicyUiModel.kt new file mode 100644 index 00000000..5cb39e33 --- /dev/null +++ b/feature/home/src/main/java/com/withpeace/withpeace/feature/home/uistate/YouthPolicyUiModel.kt @@ -0,0 +1,40 @@ +package com.withpeace.withpeace.feature.home.uistate + +import com.withpeace.withpeace.core.domain.model.policy.YouthPolicy +import com.withpeace.withpeace.core.ui.policy.ClassificationUiModel +import com.withpeace.withpeace.core.ui.policy.RegionUiModel +import com.withpeace.withpeace.core.ui.policy.toDomain +import com.withpeace.withpeace.core.ui.policy.toUiModel + +data class YouthPolicyUiModel( + val id: String, + val title: String, + val content: String, + val region: RegionUiModel, + val ageInfo: String, + val classification: ClassificationUiModel, +) + +fun YouthPolicy.toUiModel(): YouthPolicyUiModel { + return YouthPolicyUiModel( + id = id, + title = title, + content = introduce, + region = region.toUiModel(), + ageInfo = ageInfo, + classification = policyClassification.toUiModel(), + ) +} + +fun YouthPolicyUiModel.toDomain(): YouthPolicy { + return YouthPolicy( + id = id, + title = title, + introduce = content, + region = region.toDomain(), + policyClassification = classification.toDomain(), + ageInfo = ageInfo, + ) +} + +// 오류처리(Response 값 확인)+ 에러 상황 처리 logout(home 포함) \ No newline at end of file diff --git a/feature/mypage/src/main/java/com/withpeace/withpeace/feature/mypage/MyPageScreen.kt b/feature/mypage/src/main/java/com/withpeace/withpeace/feature/mypage/MyPageScreen.kt index 525555cf..12042b19 100644 --- a/feature/mypage/src/main/java/com/withpeace/withpeace/feature/mypage/MyPageScreen.kt +++ b/feature/mypage/src/main/java/com/withpeace/withpeace/feature/mypage/MyPageScreen.kt @@ -55,6 +55,7 @@ fun MyPageRoute( onLogoutSuccess: () -> Unit, onWithdrawSuccess: () -> Unit, onAuthExpired: () -> Unit, + onDibsOfPolicyClick: () -> Unit, ) { val profileInfo by viewModel.profileUiState.collectAsStateWithLifecycle() LaunchedEffect(viewModel.myPageUiEvent) { @@ -85,6 +86,7 @@ fun MyPageRoute( }, onWithdrawClick = viewModel::withdraw, profileInfo = profileInfo, + onDibsOfPolicyClick = onDibsOfPolicyClick ) } @@ -95,6 +97,7 @@ fun MyPageScreen( onLogoutClick: () -> Unit, onWithdrawClick: () -> Unit, profileInfo: ProfileUiState, + onDibsOfPolicyClick: () -> Unit, ) { Column { TitleBar(title = stringResource(R.string.my_page)) @@ -106,6 +109,7 @@ fun MyPageScreen( onEditProfile, onLogoutClick, onWithdrawClick, + onDibsOfPolicyClick = onDibsOfPolicyClick ) } @@ -138,6 +142,7 @@ private fun MyPageContent( onEditProfile: (ProfileInfoUiModel) -> Unit, onLogoutClick: () -> Unit, onWithdrawClick: () -> Unit, + onDibsOfPolicyClick: () -> Unit, ) { val scrollState = rememberScrollState() Column(modifier = modifier.verticalScroll(scrollState)) { @@ -194,6 +199,7 @@ private fun MyPageContent( onLogoutClick = onLogoutClick, onWithdrawClick = onWithdrawClick, email = profileInfo.email, + onDibsOfPolicyClick = onDibsOfPolicyClick, ) } } @@ -203,6 +209,7 @@ fun MyPageSections( modifier: Modifier, onLogoutClick: () -> Unit, onWithdrawClick: () -> Unit, + onDibsOfPolicyClick: () -> Unit, email: String, ) { Column(modifier = modifier) { @@ -213,6 +220,13 @@ fun MyPageSections( .height(1.dp), color = WithpeaceTheme.colors.SystemGray3, ) + FavoriteSection(onDibsOfPolicyClick = onDibsOfPolicyClick) + HorizontalDivider( + modifier = modifier + .fillMaxWidth() + .height(1.dp), + color = WithpeaceTheme.colors.SystemGray3, + ) EtcSection(modifier, onLogoutClick = onLogoutClick, onWithdrawClick = onWithdrawClick) } } @@ -223,7 +237,12 @@ private fun AccountSection(modifier: Modifier, email: String) { title = stringResource(R.string.account), ) { Spacer(modifier = modifier.height(16.dp)) - Row(modifier = modifier.fillMaxWidth().padding(horizontal = WithpeaceTheme.padding.BasicHorizontalPadding), horizontalArrangement = Arrangement.SpaceBetween) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = WithpeaceTheme.padding.BasicHorizontalPadding), + horizontalArrangement = Arrangement.SpaceBetween, + ) { Text( text = stringResource(R.string.connected_account), style = WithpeaceTheme.typography.body, @@ -236,6 +255,30 @@ private fun AccountSection(modifier: Modifier, email: String) { ) } } + Spacer(modifier = modifier.height(16.dp)) +} + +@Composable +private fun FavoriteSection( + modifier: Modifier = Modifier, + onDibsOfPolicyClick: () -> Unit, +) { + Section(title = stringResource(R.string.interested_list)) { + Spacer(modifier = modifier.height(8.dp)) + Text( + text = stringResource(R.string.my_dibs_of_policy), + style = WithpeaceTheme.typography.body, + color = WithpeaceTheme.colors.SystemBlack, + modifier = modifier + .fillMaxWidth() + .clickable { + onDibsOfPolicyClick() + } + .padding(horizontal = WithpeaceTheme.padding.BasicHorizontalPadding) + .padding(vertical = 8.dp), + ) + } + Spacer(modifier = modifier.height(8.dp)) } @Composable @@ -255,7 +298,8 @@ private fun EtcSection( .fillMaxWidth() .clickable { onLogoutClick() - }.padding(horizontal = WithpeaceTheme.padding.BasicHorizontalPadding) + } + .padding(horizontal = WithpeaceTheme.padding.BasicHorizontalPadding) .padding(vertical = 8.dp), ) Text( @@ -266,7 +310,8 @@ private fun EtcSection( .fillMaxWidth() .clickable { showDialog = true - }.padding(horizontal = WithpeaceTheme.padding.BasicHorizontalPadding) + } + .padding(horizontal = WithpeaceTheme.padding.BasicHorizontalPadding) .padding(vertical = 8.dp), ) } @@ -296,7 +341,6 @@ private fun Section( Spacer(modifier = modifier.height(16.dp)) Text(text = title, style = WithpeaceTheme.typography.caption, color = Color(0xFF858585), modifier = modifier.padding(horizontal = WithpeaceTheme.padding.BasicHorizontalPadding)) content() - Spacer(modifier = modifier.height(16.dp)) } } @@ -312,6 +356,7 @@ fun MyPagePreview() { onLogoutClick = {}, onWithdrawClick = {}, profileInfo = ProfileUiState.Success(ProfileInfoUiModel("닉네임닉네임", "", "abc@gmail.com")), + onDibsOfPolicyClick = {} ) } } \ No newline at end of file diff --git a/feature/mypage/src/main/java/com/withpeace/withpeace/feature/mypage/navigation/MyPageNavigation.kt b/feature/mypage/src/main/java/com/withpeace/withpeace/feature/mypage/navigation/MyPageNavigation.kt index e68aee7e..2455ade0 100644 --- a/feature/mypage/src/main/java/com/withpeace/withpeace/feature/mypage/navigation/MyPageNavigation.kt +++ b/feature/mypage/src/main/java/com/withpeace/withpeace/feature/mypage/navigation/MyPageNavigation.kt @@ -22,6 +22,7 @@ fun NavGraphBuilder.myPageNavGraph( onLogoutSuccess: () -> Unit, onWithdrawSuccess: () -> Unit, onAuthExpired: () -> Unit, + onDibsOfPolicyClick: () -> Unit, ) { composable(route = MY_PAGE_ROUTE) { val nickname = it.savedStateHandle.get(MY_PAGE_CHANGED_NICKNAME_ARGUMENT) @@ -34,7 +35,8 @@ fun NavGraphBuilder.myPageNavGraph( onLogoutSuccess = onLogoutSuccess, onWithdrawSuccess = onWithdrawSuccess, viewModel = viewModel, - onAuthExpired = onAuthExpired + onAuthExpired = onAuthExpired, + onDibsOfPolicyClick = onDibsOfPolicyClick, ) } } \ No newline at end of file diff --git a/feature/mypage/src/main/res/values/strings.xml b/feature/mypage/src/main/res/values/strings.xml index 8ff6c4d3..ef45d086 100644 --- a/feature/mypage/src/main/res/values/strings.xml +++ b/feature/mypage/src/main/res/values/strings.xml @@ -10,4 +10,6 @@ 서버와의 오류가 발생했습니다. 다시 시도해주세요. 탈퇴하시면 같은 계정으로\n 14일 동안 계정 생성을 할 수 없습니다.\n그래도 탈퇴하시겠습니까? 나가기 + 내가 찜한 정책 + 관심 목록 \ No newline at end of file diff --git a/feature/policybookmarks/.gitignore b/feature/policybookmarks/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/policybookmarks/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/policybookmarks/build.gradle.kts b/feature/policybookmarks/build.gradle.kts new file mode 100644 index 00000000..e4975ee2 --- /dev/null +++ b/feature/policybookmarks/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id("convention.feature") +} + +android { + namespace = "com.withpeace.withpeace.feature.policybookmarks" +} + +dependencies { +} \ No newline at end of file diff --git a/feature/policybookmarks/consumer-rules.pro b/feature/policybookmarks/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/feature/policybookmarks/proguard-rules.pro b/feature/policybookmarks/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/policybookmarks/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/policybookmarks/src/androidTest/java/com/withpeace/withpeace/feature/policybookmarks/ExampleInstrumentedTest.kt b/feature/policybookmarks/src/androidTest/java/com/withpeace/withpeace/feature/policybookmarks/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..b0621beb --- /dev/null +++ b/feature/policybookmarks/src/androidTest/java/com/withpeace/withpeace/feature/policybookmarks/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.withpeace.withpeace.feature.policybookmarks + +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.policybookmarks.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/feature/policybookmarks/src/main/AndroidManifest.xml b/feature/policybookmarks/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/policybookmarks/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/policybookmarks/src/main/java/com/withpeace/withpeace/feature/policybookmarks/PolicyBookmarksScreen.kt b/feature/policybookmarks/src/main/java/com/withpeace/withpeace/feature/policybookmarks/PolicyBookmarksScreen.kt new file mode 100644 index 00000000..f7ecd335 --- /dev/null +++ b/feature/policybookmarks/src/main/java/com/withpeace/withpeace/feature/policybookmarks/PolicyBookmarksScreen.kt @@ -0,0 +1,236 @@ +package com.withpeace.withpeace.feature.policybookmarks + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +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.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import com.withpeace.withpeace.core.designsystem.theme.WithpeaceTheme +import com.withpeace.withpeace.core.designsystem.ui.WithPeaceBackButtonTopAppBar +import com.withpeace.withpeace.core.designsystem.util.dropShadow +import com.withpeace.withpeace.core.ui.bookmark.BookmarkButton +import com.withpeace.withpeace.core.ui.policy.ClassificationUiModel +import com.withpeace.withpeace.core.ui.policy.RegionUiModel +import com.withpeace.withpeace.feature.policybookmarks.uistate.YouthPolicyUiModel + +@Composable +fun PolicyBookmarksRoute( + onShowSnackBar: (message: String) -> Unit, + onClickBackButton: () -> Unit, +) { + PolicyBookmarksScreen(onClickBackButton = onClickBackButton) +} + +@Composable +fun PolicyBookmarksScreen( + modifier: Modifier = Modifier, + onClickBackButton: () -> Unit, +) { + Column(modifier = modifier.fillMaxSize()) { + WithPeaceBackButtonTopAppBar( + onClickBackButton = { + onClickBackButton() + }, + title = { + Text( + text = "내가 찜한 정책", + style = WithpeaceTheme.typography.title1, + color = WithpeaceTheme.colors.SnackbarBlack, + ) + }, + ) + + Column( + modifier = modifier + .fillMaxSize() + .background(Color(0xFFF8F9FB)) + .padding(horizontal = 24.dp) + ) { + Spacer(modifier = modifier.height(8.dp)) + LazyColumn(contentPadding = PaddingValues(bottom = 8.dp)) { + items(20) { + Spacer(modifier = modifier.height(8.dp)) + YouthPolicyCard( + youthPolicy = YouthPolicyUiModel( + id = "conubia", + title = "reprehendunt", + content = "sem", + region = RegionUiModel.대구, + ageInfo = "consetetur", + classification = ClassificationUiModel.JOB, + ), + onPolicyClick = {}, + ) + } + } + } + } +} + +@Composable +private fun YouthPolicyCard( + modifier: Modifier = Modifier, + youthPolicy: YouthPolicyUiModel, + onPolicyClick: (String) -> Unit, +) { + Card( + modifier = modifier + .fillMaxWidth() + .background( + color = WithpeaceTheme.colors.SystemWhite, + shape = RoundedCornerShape(size = 10.dp), + ) + .dropShadow( + color = Color(0x1A000000), + blurRadius = 4.dp, + spreadRadius = 2.dp, + borderRadius = 10.dp, + ) + .clickable { + onPolicyClick(youthPolicy.id) + }, + ) { + ConstraintLayout( + modifier = modifier + .fillMaxWidth() + .background(WithpeaceTheme.colors.SystemWhite) + .padding(16.dp), + ) { + val ( + title, content, + region, ageRange, thumbnail, heart, + ) = createRefs() + + Text( + text = youthPolicy.title, + modifier.constrainAs( + title, + constrainBlock = { + top.linkTo(parent.top) + start.linkTo(parent.start) + end.linkTo(thumbnail.start, margin = 8.dp) + width = Dimension.fillToConstraints + }, + ), + color = WithpeaceTheme.colors.SystemBlack, + style = WithpeaceTheme.typography.homePolicyTitle, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + Text( + text = youthPolicy.content, + modifier = modifier + .constrainAs( + content, + constrainBlock = { + top.linkTo(title.bottom, margin = 8.dp) + start.linkTo(parent.start) + end.linkTo(thumbnail.start, margin = 8.dp) + width = Dimension.fillToConstraints + }, + ), + color = WithpeaceTheme.colors.SystemBlack, + style = WithpeaceTheme.typography.homePolicyContent, + overflow = TextOverflow.Ellipsis, + maxLines = 2, + ) + BookmarkButton( + modifier = modifier.constrainAs( + heart, + ) { + top.linkTo(content.bottom, margin = 8.dp) + start.linkTo(parent.start) + bottom.linkTo(parent.bottom) + }, + ) + + Text( + text = youthPolicy.region.name, + color = WithpeaceTheme.colors.MainPurple, + modifier = modifier + .constrainAs( + region, + constrainBlock = { + top.linkTo(heart.top) + start.linkTo(heart.end, margin = 8.dp) + bottom.linkTo(heart.bottom) + }, + ) + .background( + color = WithpeaceTheme.colors.SubPurple, + shape = RoundedCornerShape(size = 5.dp), + ) + .padding(horizontal = 8.dp, vertical = 4.dp), + style = WithpeaceTheme.typography.homePolicyTag, + ) + + Text( + text = youthPolicy.ageInfo, + color = WithpeaceTheme.colors.SystemGray1, + modifier = modifier + .constrainAs( + ageRange, + constrainBlock = { + top.linkTo(region.top) + start.linkTo(region.end, margin = 8.dp) + bottom.linkTo(region.bottom) + }, + ) + .background( + color = WithpeaceTheme.colors.SystemGray3, + shape = RoundedCornerShape(size = 5.dp), + ) + .padding(horizontal = 8.dp, vertical = 4.dp), + style = WithpeaceTheme.typography.homePolicyTag, + ) + + + Image( + modifier = modifier + .size(57.dp) + .clip(RoundedCornerShape(10.dp)) + .constrainAs( + ref = thumbnail, + constrainBlock = { + start.linkTo(title.end) + end.linkTo(parent.end) + top.linkTo(parent.top) + }, + ), + painter = painterResource(id = youthPolicy.classification.drawableResId), + contentDescription = stringResource(id = youthPolicy.classification.stringResId), + ) + } + } +} + +@Composable +@Preview(showBackground = true) +fun PolicyBookmarksPreview() { + PolicyBookmarksScreen( + onClickBackButton = { + + }, + ) +} diff --git a/feature/policybookmarks/src/main/java/com/withpeace/withpeace/feature/policybookmarks/navigation/PolicyBookmarksNavigation.kt b/feature/policybookmarks/src/main/java/com/withpeace/withpeace/feature/policybookmarks/navigation/PolicyBookmarksNavigation.kt new file mode 100644 index 00000000..43cd46a2 --- /dev/null +++ b/feature/policybookmarks/src/main/java/com/withpeace/withpeace/feature/policybookmarks/navigation/PolicyBookmarksNavigation.kt @@ -0,0 +1,23 @@ +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.withpeace.withpeace.feature.policybookmarks.PolicyBookmarksRoute + +const val POLICY_BOOKMARKS_ROUTE = "POLICY_BOOKMARKS_ROUTE" + +fun NavController.navigatePolicyBookmarks(navOptions: NavOptions? = null) { + navigate(POLICY_BOOKMARKS_ROUTE, navOptions) +} + +fun NavGraphBuilder.policyBookmarksNavGraph( + onShowSnackBar: (message: String) -> Unit, + onClickBackButton: () -> Unit, +) { + composable(route = POLICY_BOOKMARKS_ROUTE) { + PolicyBookmarksRoute( + onShowSnackBar = onShowSnackBar, + onClickBackButton = onClickBackButton, + ) + } +} \ No newline at end of file diff --git a/feature/policybookmarks/src/main/java/com/withpeace/withpeace/feature/policybookmarks/uistate/YouthPolicyUiModel.kt b/feature/policybookmarks/src/main/java/com/withpeace/withpeace/feature/policybookmarks/uistate/YouthPolicyUiModel.kt new file mode 100644 index 00000000..7bd2a4f5 --- /dev/null +++ b/feature/policybookmarks/src/main/java/com/withpeace/withpeace/feature/policybookmarks/uistate/YouthPolicyUiModel.kt @@ -0,0 +1,38 @@ +package com.withpeace.withpeace.feature.policybookmarks.uistate + +import com.withpeace.withpeace.core.domain.model.policy.YouthPolicy +import com.withpeace.withpeace.core.ui.policy.ClassificationUiModel +import com.withpeace.withpeace.core.ui.policy.RegionUiModel +import com.withpeace.withpeace.core.ui.policy.toDomain +import com.withpeace.withpeace.core.ui.policy.toUiModel + +data class YouthPolicyUiModel( + val id: String, + val title: String, + val content: String, + val region: RegionUiModel, + val ageInfo: String, + val classification: ClassificationUiModel, +) + +fun YouthPolicy.toUiModel(): YouthPolicyUiModel { + return YouthPolicyUiModel( + id = id, + title = title, + content = introduce, + region = region.toUiModel(), + ageInfo = ageInfo, + classification = policyClassification.toUiModel(), + ) +} + +fun YouthPolicyUiModel.toDomain(): YouthPolicy { + return YouthPolicy( + id = id, + title = title, + introduce = content, + region = region.toDomain(), + policyClassification = classification.toDomain(), + ageInfo = ageInfo, + ) +} \ No newline at end of file diff --git a/feature/policybookmarks/src/test/java/com/withpeace/withpeace/feature/policybookmarks/ExampleUnitTest.kt b/feature/policybookmarks/src/test/java/com/withpeace/withpeace/feature/policybookmarks/ExampleUnitTest.kt new file mode 100644 index 00000000..c85b3112 --- /dev/null +++ b/feature/policybookmarks/src/test/java/com/withpeace/withpeace/feature/policybookmarks/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.withpeace.withpeace.feature.policybookmarks + +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/src/main/java/com/withpeace/withpeace/feature/policydetail/PolicyDetailScreen.kt b/feature/policydetail/src/main/java/com/withpeace/withpeace/feature/policydetail/PolicyDetailScreen.kt index 7b12ae66..b4d1580b 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,7 +11,6 @@ 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 @@ -31,38 +30,53 @@ 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 +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.withpeace.withpeace.core.designsystem.theme.WithpeaceTheme import com.withpeace.withpeace.core.designsystem.ui.WithPeaceBackButtonTopAppBar import com.withpeace.withpeace.core.ui.policy.ClassificationUiModel -import com.withpeace.withpeace.core.ui.policy.RegionUiModel -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 com.withpeace.withpeace.feature.policydetail.uistate.YouthPolicyDetailUiModel +import com.withpeace.withpeace.feature.policydetail.uistate.YouthPolicyDetailUiState import eu.wewox.textflow.TextFlow import eu.wewox.textflow.TextFlowObstacleAlignment @Composable fun PolicyDetailRoute( - policy: YouthPolicyUiModel, onShowSnackBar: (message: String) -> Unit, viewModel: PolicyDetailViewModel = hiltViewModel(), onClickBackButton: () -> Unit, ) { + val policyDetailUiState = viewModel.policyDetailUiState.collectAsStateWithLifecycle() PolicyDetailScreen( onClickBackButton = onClickBackButton, - policy = policy, + policyUiState = policyDetailUiState.value, ) } @Composable fun PolicyDetailScreen( - policy: YouthPolicyUiModel, + policyUiState: YouthPolicyDetailUiState, modifier: Modifier = Modifier, onClickBackButton: () -> Unit, +) { + when (policyUiState) { + is YouthPolicyDetailUiState.Success -> { + PolicyDetailContent(modifier, onClickBackButton, policyUiState.youthPolicyDetail) + } + + YouthPolicyDetailUiState.Failure -> {} + YouthPolicyDetailUiState.Loading -> {} + } +} + +@Composable +private fun PolicyDetailContent( + modifier: Modifier = Modifier, + onClickBackButton: () -> Unit, + policy: YouthPolicyDetailUiModel, ) { val scrollState = rememberScrollState() val position = remember { @@ -147,7 +161,7 @@ fun PolicyDetailScreen( @Composable private fun TitleSection( modifier: Modifier, - policy: YouthPolicyUiModel, + policy: YouthPolicyDetailUiModel, ) { Column( modifier = modifier.padding(horizontal = 24.dp), @@ -189,7 +203,7 @@ private fun TitleSection( } @Composable -fun PolicySummarySection(modifier: Modifier = Modifier, policy: YouthPolicyUiModel) { +fun PolicySummarySection(modifier: Modifier = Modifier, policy: YouthPolicyDetailUiModel) { Column(modifier = modifier.padding(horizontal = 24.dp)) { Spacer(modifier = modifier.height(24.dp)) Text( @@ -218,7 +232,7 @@ fun PolicySummarySection(modifier: Modifier = Modifier, policy: YouthPolicyUiMod @Composable fun ApplyQualificationSection( modifier: Modifier = Modifier, - policy: YouthPolicyUiModel, + policy: YouthPolicyDetailUiModel, ) { Column(modifier = modifier.padding(horizontal = 24.dp)) { Spacer(modifier = modifier.height(24.dp)) @@ -257,7 +271,7 @@ fun ApplyQualificationSection( @Composable fun ApplicationGuideSection( modifier: Modifier = Modifier, - policy: YouthPolicyUiModel, + policy: YouthPolicyDetailUiModel, ) { Column(modifier = modifier.padding(horizontal = 24.dp)) { Spacer(modifier = modifier.height(24.dp)) @@ -294,7 +308,7 @@ fun ApplicationGuideSection( @Composable fun AdditionalInfoSection( modifier: Modifier = Modifier, - policy: YouthPolicyUiModel, + policy: YouthPolicyDetailUiModel, ) { Column(modifier = modifier.padding(horizontal = 24.dp)) { Spacer(modifier = modifier.height(24.dp)) @@ -337,12 +351,11 @@ fun AdditionalInfoSection( @Composable fun PolicyDetailPreview() { WithpeaceTheme { - PolicyDetailScreen( - policy = YouthPolicyUiModel( + PolicyDetailContent( + policy = YouthPolicyDetailUiModel( id = "sociosqu", title = "facilis", content = "가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바가나다라마사바", - region = RegionUiModel.대구, ageInfo = "cum", applicationDetails = "지원내용들.....", residenceAndIncome = "sale", @@ -361,7 +374,7 @@ fun PolicyDetailPreview() { businessRelatedReferenceSite1 = "noluisse", businessRelatedReferenceSite2 = "quo", ), - ) { - } + onClickBackButton = {}, + ) } } diff --git a/feature/policydetail/src/main/java/com/withpeace/withpeace/feature/policydetail/PolicyDetailViewModel.kt b/feature/policydetail/src/main/java/com/withpeace/withpeace/feature/policydetail/PolicyDetailViewModel.kt index 31bbdcf0..7fb4c632 100644 --- a/feature/policydetail/src/main/java/com/withpeace/withpeace/feature/policydetail/PolicyDetailViewModel.kt +++ b/feature/policydetail/src/main/java/com/withpeace/withpeace/feature/policydetail/PolicyDetailViewModel.kt @@ -2,11 +2,50 @@ package com.withpeace.withpeace.feature.policydetail import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.withpeace.withpeace.core.domain.usecase.GetYouthPolicyDetailUseCase +import com.withpeace.withpeace.feature.policydetail.navigation.POLICY_DETAIL_YOUTH_POLICY_ID_ARGUMENT +import com.withpeace.withpeace.feature.policydetail.uistate.YouthPolicyDetailUiEvent +import com.withpeace.withpeace.feature.policydetail.uistate.YouthPolicyDetailUiState +import com.withpeace.withpeace.feature.policydetail.uistate.toUiModel 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 PolicyDetailViewModel @Inject constructor( savedStateHandle: SavedStateHandle, + private val getPolicyDetailUseCase: GetYouthPolicyDetailUseCase, ) : ViewModel() { + private val policyId = savedStateHandle.get(POLICY_DETAIL_YOUTH_POLICY_ID_ARGUMENT) + ?: throw IllegalArgumentException("Id를 불러올 수 없습니다.") + + private val _policyDetailUiState: MutableStateFlow = + MutableStateFlow(YouthPolicyDetailUiState.Loading) + val policyDetailUiState = _policyDetailUiState.asStateFlow() + + private val _uiEvent = Channel() + val uiEvent = _uiEvent.receiveAsFlow() + + init { + fetchPolicyDetail(policyId) + } + + private fun fetchPolicyDetail(policyId: String) { + viewModelScope.launch { + getPolicyDetailUseCase( + policyId = policyId, + onError = { + + }, + ).collect { data -> + _policyDetailUiState.update { YouthPolicyDetailUiState.Success(data.toUiModel()) } + } + } + } } \ No newline at end of file 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 653eff07..9f907a35 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 @@ -6,23 +6,18 @@ import androidx.navigation.NavOptions import androidx.navigation.NavType import androidx.navigation.compose.composable import androidx.navigation.navArgument -import com.withpeace.withpeace.core.ui.policy.YouthPolicyUiModel -import com.withpeace.withpeace.core.ui.serializable.toNavigationValue import com.withpeace.withpeace.feature.policydetail.PolicyDetailRoute -import java.net.URLDecoder -import java.nio.charset.StandardCharsets const val POLICY_DETAIL_ROUTE = "policyDetailRoute" -const val POLICY_DETAIL_YOUTH_POLICY_ARGUMENT = "youthPolicy_argument" +const val POLICY_DETAIL_YOUTH_POLICY_ID_ARGUMENT = "youthPolicy_argument" const val POLICY_DETAIL_ROUTE_WITH_ARGUMENT = - "$POLICY_DETAIL_ROUTE/{$POLICY_DETAIL_YOUTH_POLICY_ARGUMENT}" + "$POLICY_DETAIL_ROUTE/{$POLICY_DETAIL_YOUTH_POLICY_ID_ARGUMENT}" fun NavController.navigateToPolicyDetail( navOptions: NavOptions? = null, - policy: YouthPolicyUiModel, + policyId: String, ) { - val policyDetail = YouthPolicyUiModel.toNavigationValue(policy) - navigate("$POLICY_DETAIL_ROUTE/${policyDetail}", navOptions) + navigate("$POLICY_DETAIL_ROUTE/${policyId}", navOptions) } fun NavGraphBuilder.policyDetailNavGraph( onShowSnackBar: (message: String) -> Unit, @@ -31,20 +26,15 @@ fun NavGraphBuilder.policyDetailNavGraph( composable( route = POLICY_DETAIL_ROUTE_WITH_ARGUMENT, arguments = listOf( - navArgument(POLICY_DETAIL_YOUTH_POLICY_ARGUMENT) { + navArgument(POLICY_DETAIL_YOUTH_POLICY_ID_ARGUMENT) { type = NavType.StringType }, ), ) { - val policy: YouthPolicyUiModel = YouthPolicyUiModel.parseNavigationValue( - it.arguments?.getString(POLICY_DETAIL_YOUTH_POLICY_ARGUMENT) ?: "" - ) - PolicyDetailRoute( onShowSnackBar = onShowSnackBar, onClickBackButton = onClickBackButton, - policy = policy, ) } } \ No newline at end of file diff --git a/feature/policydetail/src/main/java/com/withpeace/withpeace/feature/policydetail/uistate/YouthPolicyDetailUiEvent.kt b/feature/policydetail/src/main/java/com/withpeace/withpeace/feature/policydetail/uistate/YouthPolicyDetailUiEvent.kt new file mode 100644 index 00000000..1496d3f3 --- /dev/null +++ b/feature/policydetail/src/main/java/com/withpeace/withpeace/feature/policydetail/uistate/YouthPolicyDetailUiEvent.kt @@ -0,0 +1,6 @@ +package com.withpeace.withpeace.feature.policydetail.uistate + +sealed interface YouthPolicyDetailUiEvent { + data object UnAuthorizedError : YouthPolicyDetailUiEvent + data object ResponseError : YouthPolicyDetailUiEvent +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/withpeace/withpeace/core/ui/policy/YouthPolicyUiModel.kt b/feature/policydetail/src/main/java/com/withpeace/withpeace/feature/policydetail/uistate/YouthPolicyDetailUiModel.kt similarity index 67% rename from core/ui/src/main/java/com/withpeace/withpeace/core/ui/policy/YouthPolicyUiModel.kt rename to feature/policydetail/src/main/java/com/withpeace/withpeace/feature/policydetail/uistate/YouthPolicyDetailUiModel.kt index 49fcb60d..1c754aed 100644 --- a/core/ui/src/main/java/com/withpeace/withpeace/core/ui/policy/YouthPolicyUiModel.kt +++ b/feature/policydetail/src/main/java/com/withpeace/withpeace/feature/policydetail/uistate/YouthPolicyDetailUiModel.kt @@ -1,22 +1,16 @@ -package com.withpeace.withpeace.core.ui.policy +package com.withpeace.withpeace.feature.policydetail.uistate -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 -import com.withpeace.withpeace.core.ui.serializable.parseNavigationValue -import com.withpeace.withpeace.core.ui.serializable.toNavigationValue -import java.io.Serializable +import com.withpeace.withpeace.core.domain.model.policy.YouthPolicyDetail +import com.withpeace.withpeace.core.ui.policy.ClassificationUiModel +import com.withpeace.withpeace.core.ui.policy.toDomain +import com.withpeace.withpeace.core.ui.policy.toUiModel -@SuppressLint("SupportAnnotationUsage") -@kotlinx.serialization.Serializable -data class YouthPolicyUiModel( +data class YouthPolicyDetailUiModel( val id: String, val title: String, val content: String, - val region: RegionUiModel, val ageInfo: String, - @DrawableRes val classification: ClassificationUiModel, + val classification: ClassificationUiModel, val applicationDetails: String, val residenceAndIncome: String, @@ -35,24 +29,15 @@ data class YouthPolicyUiModel( val operatingOrganization: String, val businessRelatedReferenceSite1: String, val businessRelatedReferenceSite2: String, -): Serializable { - companion object { - fun toNavigationValue(value: YouthPolicyUiModel): String = - value.toNavigationValue() +) - fun parseNavigationValue(value: String): YouthPolicyUiModel = - value.parseNavigationValue() - } -} - -fun YouthPolicy.toUiModel(): YouthPolicyUiModel { - return YouthPolicyUiModel( +fun YouthPolicyDetail.toUiModel(): YouthPolicyDetailUiModel { + return YouthPolicyDetailUiModel( id = id, title = title, content = introduce, - region = region.toUiModel(), ageInfo = ageInfo, - classification = policyClassification.toUiModel(), + classification = classification.toUiModel(), applicationDetails = applicationDetails, residenceAndIncome = residenceAndIncome, @@ -72,14 +57,13 @@ fun YouthPolicy.toUiModel(): YouthPolicyUiModel { ) } -fun YouthPolicyUiModel.toDomain(): YouthPolicy { - return YouthPolicy( +fun YouthPolicyDetailUiModel.toDomain(): YouthPolicyDetail { + return YouthPolicyDetail( id = id, title = title, introduce = content, - region = region.toDomain(), - policyClassification = PolicyClassification.JOB, // Assuming this is a default or fixed value ageInfo = ageInfo, + classification = classification.toDomain(), applicationDetails = applicationDetails, residenceAndIncome = residenceAndIncome, diff --git a/feature/policydetail/src/main/java/com/withpeace/withpeace/feature/policydetail/uistate/YouthPolicyDetailUiState.kt b/feature/policydetail/src/main/java/com/withpeace/withpeace/feature/policydetail/uistate/YouthPolicyDetailUiState.kt new file mode 100644 index 00000000..0081c2ab --- /dev/null +++ b/feature/policydetail/src/main/java/com/withpeace/withpeace/feature/policydetail/uistate/YouthPolicyDetailUiState.kt @@ -0,0 +1,7 @@ +package com.withpeace.withpeace.feature.policydetail.uistate + +sealed interface YouthPolicyDetailUiState { + data object Loading : YouthPolicyDetailUiState + data class Success(val youthPolicyDetail: YouthPolicyDetailUiModel) : YouthPolicyDetailUiState + data object Failure : YouthPolicyDetailUiState +} \ No newline at end of file diff --git a/feature/profileeditor/src/main/java/com/app/profileeditor/ProfileEditorScreen.kt b/feature/profileeditor/src/main/java/com/app/profileeditor/ProfileEditorScreen.kt index b260d8b3..7d21a5c2 100644 --- a/feature/profileeditor/src/main/java/com/app/profileeditor/ProfileEditorScreen.kt +++ b/feature/profileeditor/src/main/java/com/app/profileeditor/ProfileEditorScreen.kt @@ -3,7 +3,6 @@ package com.app.profileeditor import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.border -import androidx.compose.foundation.gestures.scrollable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -61,6 +60,8 @@ fun ProfileEditorRoute( BackHandler { if (profileInfo.isChanged) { showAlertDialog = true + } else { + onClickBackButton() } } if (showAlertDialog) { @@ -210,7 +211,8 @@ private fun EditCompletedButton( end = WithpeaceTheme.padding.BasicHorizontalPadding, start = WithpeaceTheme.padding.BasicHorizontalPadding, ) - .fillMaxWidth().imePadding(), + .fillMaxWidth() + .imePadding(), colors = ButtonDefaults.buttonColors(containerColor = if (isClickable) WithpeaceTheme.colors.MainPurple else WithpeaceTheme.colors.SystemGray2), shape = RoundedCornerShape(9.dp), ) { @@ -234,7 +236,8 @@ fun ModifySaveDialog( Surface( modifier = modifier .width(327.dp) - .clip(RoundedCornerShape(10.dp)), + .clip(RoundedCornerShape(10.dp)) + .background(WithpeaceTheme.colors.SystemWhite), ) { ModifySaveDialogContent( modifier = modifier, diff --git a/settings.gradle.kts b/settings.gradle.kts index 6e8ded44..038fae1e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -42,3 +42,4 @@ include(":feature:policyconsent") include(":feature:privacypolicy") include(":feature:termsofservice") include(":benchmark") +include(":feature:policybookmarks")