diff --git a/app/src/main/java/team/retum/jobisandroidv2/JobisNavigator.kt b/app/src/main/java/team/retum/jobisandroidv2/JobisNavigator.kt index 96ad93e94..ec355b63b 100644 --- a/app/src/main/java/team/retum/jobisandroidv2/JobisNavigator.kt +++ b/app/src/main/java/team/retum/jobisandroidv2/JobisNavigator.kt @@ -16,6 +16,7 @@ import team.retum.jobis.application.navigation.navigateToApplication import team.retum.jobis.change.password.navigation.navigateToComparePassword import team.retum.jobis.change.password.navigation.navigateToResetPassword import team.retum.jobis.interests.navigation.navigateToInterests +import team.retum.jobis.interests.navigation.navigateToInterestsComplete import team.retum.jobis.notice.navigation.navigateToNoticeDetails import team.retum.jobis.notice.navigation.navigateToNotices import team.retum.jobis.recruitment.navigation.navigateToRecruitmentDetails @@ -104,6 +105,10 @@ internal class JobisNavigator( navController.navigateToInterests() } + fun navigateToInterestsComplete(studentName: String) { + navController.navigateToInterestsComplete(studentName = studentName) + } + fun navigateToComparePassword() { navController.navigateToComparePassword() } diff --git a/app/src/main/java/team/retum/jobisandroidv2/navigation/MainNavigation.kt b/app/src/main/java/team/retum/jobisandroidv2/navigation/MainNavigation.kt index 0fb18cce5..8fb820644 100644 --- a/app/src/main/java/team/retum/jobisandroidv2/navigation/MainNavigation.kt +++ b/app/src/main/java/team/retum/jobisandroidv2/navigation/MainNavigation.kt @@ -10,6 +10,7 @@ import team.retum.employment.navigation.employment import team.retum.employment.navigation.employmentDetail import team.retum.jobis.application.navigation.application import team.retum.jobis.interests.navigation.interests +import team.retum.jobis.interests.navigation.interestsComplete import team.retum.jobis.notice.navigation.noticeDetails import team.retum.jobis.notice.navigation.notices import team.retum.jobis.recruitment.navigation.recruitmentDetails @@ -75,7 +76,13 @@ internal fun NavGraphBuilder.mainNavigation( onBackPressed = navigator::popBackStackIfNotHome, ) reportBug(onBackPressed = navigator::popBackStackIfNotHome) - interests(onBackPressed = navigator::popBackStackIfNotHome) + interests( + onBackPressed = navigator::popBackStackIfNotHome, + navigateToInterestsComplete = navigator::navigateToInterestsComplete, + ) + interestsComplete( + onBackPressed = navigator::popBackStackIfNotHome, + ) noticeDetails(onBackPressed = navigator::popBackStackIfNotHome) companies( onBackPressed = navigator::popBackStackIfNotHome, diff --git a/core/common/src/main/java/team/retum/common/utils/ResourceKeys.kt b/core/common/src/main/java/team/retum/common/utils/ResourceKeys.kt index cd94f4d58..e7c43a93b 100644 --- a/core/common/src/main/java/team/retum/common/utils/ResourceKeys.kt +++ b/core/common/src/main/java/team/retum/common/utils/ResourceKeys.kt @@ -22,4 +22,5 @@ object ResourceKeys { const val EMAIL = "@dsm.hs.kr" const val DATABASE_NAME = "jobis-database" const val CLASS_ID = "classId" + const val STUDENT_NAME = "studentName" } diff --git a/core/data/src/main/java/team/retum/data/di/RepositoryModule.kt b/core/data/src/main/java/team/retum/data/di/RepositoryModule.kt index 13ed8b589..d459c1a89 100644 --- a/core/data/src/main/java/team/retum/data/di/RepositoryModule.kt +++ b/core/data/src/main/java/team/retum/data/di/RepositoryModule.kt @@ -22,6 +22,8 @@ import team.retum.data.repository.company.CompanyRepository import team.retum.data.repository.company.CompanyRepositoryImpl import team.retum.data.repository.file.FileRepository import team.retum.data.repository.file.FileRepositoryImpl +import team.retum.data.repository.interests.InterestsRepository +import team.retum.data.repository.interests.InterestsRepositoryImpl import team.retum.data.repository.intern.WinterInterRepository import team.retum.data.repository.intern.WinterInternRepositoryImpl import team.retum.data.repository.notice.NoticeRepository @@ -104,4 +106,8 @@ abstract class RepositoryModule { @Binds @Singleton abstract fun bindServerStatusCheckRepository(serverStatusCheckRepositoryImpl: ServerStatusCheckRepositoryImpl): ServerStatusCheckRepository + + @Binds + @Singleton + abstract fun bindInterestsRepository(interestsRepositoryImpl: InterestsRepositoryImpl): InterestsRepository } diff --git a/core/data/src/main/java/team/retum/data/repository/interests/InterestsRepository.kt b/core/data/src/main/java/team/retum/data/repository/interests/InterestsRepository.kt new file mode 100644 index 000000000..c89f1f995 --- /dev/null +++ b/core/data/src/main/java/team/retum/data/repository/interests/InterestsRepository.kt @@ -0,0 +1,11 @@ +package team.retum.data.repository.interests + +import team.retum.network.model.request.interests.InterestsToggleRequest +import team.retum.network.model.response.interests.FetchInterestsRecruitmentsResponse +import team.retum.network.model.response.interests.FetchInterestsResponse + +interface InterestsRepository { + suspend fun setInterestsToggle(codes: InterestsToggleRequest) + suspend fun fetchInterests(): FetchInterestsResponse + suspend fun fetchInterestsSearchRecruitments(): FetchInterestsRecruitmentsResponse +} diff --git a/core/data/src/main/java/team/retum/data/repository/interests/InterestsRepositoryImpl.kt b/core/data/src/main/java/team/retum/data/repository/interests/InterestsRepositoryImpl.kt new file mode 100644 index 000000000..5ac1bc559 --- /dev/null +++ b/core/data/src/main/java/team/retum/data/repository/interests/InterestsRepositoryImpl.kt @@ -0,0 +1,21 @@ +package team.retum.data.repository.interests + +import team.retum.network.datasource.interests.InterestsDataSource +import team.retum.network.model.request.interests.InterestsToggleRequest +import team.retum.network.model.response.interests.FetchInterestsRecruitmentsResponse +import team.retum.network.model.response.interests.FetchInterestsResponse +import javax.inject.Inject + +class InterestsRepositoryImpl @Inject constructor( + private val interestsDataSource: InterestsDataSource, +) : InterestsRepository { + override suspend fun setInterestsToggle(codes: InterestsToggleRequest) { + interestsDataSource.setInterestsToggle(codes = codes) + } + + override suspend fun fetchInterests(): FetchInterestsResponse = + interestsDataSource.fetchInterests() + + override suspend fun fetchInterestsSearchRecruitments(): FetchInterestsRecruitmentsResponse = + interestsDataSource.fetchInterestsSearchRecruitments() +} diff --git a/core/design-system/src/main/res/drawable/success.xml b/core/design-system/src/main/res/drawable/success.xml new file mode 100644 index 000000000..5ede9dddd --- /dev/null +++ b/core/design-system/src/main/res/drawable/success.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/domain/src/main/java/team/retum/usecase/entity/interests/InterestsEntity.kt b/core/domain/src/main/java/team/retum/usecase/entity/interests/InterestsEntity.kt new file mode 100644 index 000000000..5d9c9bd18 --- /dev/null +++ b/core/domain/src/main/java/team/retum/usecase/entity/interests/InterestsEntity.kt @@ -0,0 +1,29 @@ +package team.retum.usecase.entity.interests + +import androidx.compose.runtime.Immutable +import team.retum.network.model.response.interests.FetchInterestsResponse + +@Immutable +data class InterestsEntity( + val studentName: String, + val interests: List, +) { + data class InterestMajorEntity( + val id: Int, + val studentId: Int, + val code: Int, + val keyword: String, + ) +} + +internal fun FetchInterestsResponse.toEntity() = InterestsEntity( + studentName = this.studentName, + interests = this.interests.map { it.toEntity() }, +) + +private fun FetchInterestsResponse.InterestMajor.toEntity() = InterestsEntity.InterestMajorEntity( + id = this.id, + studentId = this.studentId, + code = this.code, + keyword = this.keyword, +) diff --git a/core/domain/src/main/java/team/retum/usecase/entity/interests/InterestsRecruitmentEntity.kt b/core/domain/src/main/java/team/retum/usecase/entity/interests/InterestsRecruitmentEntity.kt new file mode 100644 index 000000000..8e33a8890 --- /dev/null +++ b/core/domain/src/main/java/team/retum/usecase/entity/interests/InterestsRecruitmentEntity.kt @@ -0,0 +1,34 @@ +package team.retum.usecase.entity.interests + +import androidx.compose.runtime.Immutable +import team.retum.network.model.response.interests.FetchInterestsRecruitmentsResponse + +@Immutable +data class InterestsRecruitmentsEntity( + val recruitments: List, +) { + data class InterestsRecruitmentEntity( + val id: Int, + val companyId: String, + val companyProfileUrl: String, + val trainPay: Int, + val militarySupport: Boolean, + val hiringJobs: String, + val bookmarked: Boolean, + ) +} + +internal fun FetchInterestsRecruitmentsResponse.toEntity() = InterestsRecruitmentsEntity( + recruitments = this.recruitments.map { it.toEntity() }, +) + +private fun FetchInterestsRecruitmentsResponse.InterestRecruitments.toEntity() = + InterestsRecruitmentsEntity.InterestsRecruitmentEntity( + id = this.id, + companyId = this.companyId, + companyProfileUrl = this.companyProfileUrl, + trainPay = this.trainPay, + militarySupport = this.militarySupport, + hiringJobs = this.hiringJobs, + bookmarked = this.bookmarked, + ) diff --git a/core/domain/src/main/java/team/retum/usecase/usecase/interests/FetchInterestsSearchRecruitmentsUseCase.kt b/core/domain/src/main/java/team/retum/usecase/usecase/interests/FetchInterestsSearchRecruitmentsUseCase.kt new file mode 100644 index 000000000..74b7ded64 --- /dev/null +++ b/core/domain/src/main/java/team/retum/usecase/usecase/interests/FetchInterestsSearchRecruitmentsUseCase.kt @@ -0,0 +1,13 @@ +package team.retum.usecase.usecase.interests + +import team.retum.data.repository.interests.InterestsRepository +import team.retum.usecase.entity.interests.toEntity +import javax.inject.Inject + +class FetchInterestsSearchRecruitmentsUseCase @Inject constructor( + private val interestsRepository: InterestsRepository, +) { + suspend operator fun invoke() = runCatching { + interestsRepository.fetchInterestsSearchRecruitments().toEntity() + } +} diff --git a/core/domain/src/main/java/team/retum/usecase/usecase/interests/FetchInterestsUseCase.kt b/core/domain/src/main/java/team/retum/usecase/usecase/interests/FetchInterestsUseCase.kt new file mode 100644 index 000000000..65a6be1aa --- /dev/null +++ b/core/domain/src/main/java/team/retum/usecase/usecase/interests/FetchInterestsUseCase.kt @@ -0,0 +1,13 @@ +package team.retum.usecase.usecase.interests + +import team.retum.data.repository.interests.InterestsRepository +import team.retum.usecase.entity.interests.toEntity +import javax.inject.Inject + +class FetchInterestsUseCase @Inject constructor( + private val interestsRepository: InterestsRepository, +) { + suspend operator fun invoke() = runCatching { + interestsRepository.fetchInterests().toEntity() + } +} diff --git a/core/domain/src/main/java/team/retum/usecase/usecase/interests/SetInterestsToggleUseCase.kt b/core/domain/src/main/java/team/retum/usecase/usecase/interests/SetInterestsToggleUseCase.kt new file mode 100644 index 000000000..caa4d9c24 --- /dev/null +++ b/core/domain/src/main/java/team/retum/usecase/usecase/interests/SetInterestsToggleUseCase.kt @@ -0,0 +1,13 @@ +package team.retum.usecase.usecase.interests + +import team.retum.data.repository.interests.InterestsRepository +import team.retum.network.model.request.interests.InterestsToggleRequest +import javax.inject.Inject + +class SetInterestsToggleUseCase @Inject constructor( + private val interestsRepository: InterestsRepository, +) { + suspend operator fun invoke(codes: InterestsToggleRequest) = runCatching { + interestsRepository.setInterestsToggle(codes = codes) + } +} diff --git a/core/network/src/main/java/team/retum/network/api/InterestsApi.kt b/core/network/src/main/java/team/retum/network/api/InterestsApi.kt new file mode 100644 index 000000000..f93b2792e --- /dev/null +++ b/core/network/src/main/java/team/retum/network/api/InterestsApi.kt @@ -0,0 +1,22 @@ +package team.retum.network.api + +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.PATCH +import team.retum.network.di.RequestUrls +import team.retum.network.model.request.interests.InterestsToggleRequest +import team.retum.network.model.response.interests.FetchInterestsRecruitmentsResponse +import team.retum.network.model.response.interests.FetchInterestsResponse + +interface InterestsApi { + @PATCH(RequestUrls.Interests.interests) + suspend fun setInterestsToggle( + @Body codes: InterestsToggleRequest, + ) + + @GET(RequestUrls.Interests.interests) + suspend fun fetchInterests(): FetchInterestsResponse + + @GET(RequestUrls.Interests.interestsRecruitments) + suspend fun fetchInterestsSearchRecruitments(): FetchInterestsRecruitmentsResponse +} diff --git a/core/network/src/main/java/team/retum/network/datasource/interests/InterestsDataSource.kt b/core/network/src/main/java/team/retum/network/datasource/interests/InterestsDataSource.kt new file mode 100644 index 000000000..11a76450f --- /dev/null +++ b/core/network/src/main/java/team/retum/network/datasource/interests/InterestsDataSource.kt @@ -0,0 +1,11 @@ +package team.retum.network.datasource.interests + +import team.retum.network.model.request.interests.InterestsToggleRequest +import team.retum.network.model.response.interests.FetchInterestsRecruitmentsResponse +import team.retum.network.model.response.interests.FetchInterestsResponse + +interface InterestsDataSource { + suspend fun setInterestsToggle(codes: InterestsToggleRequest) + suspend fun fetchInterests(): FetchInterestsResponse + suspend fun fetchInterestsSearchRecruitments(): FetchInterestsRecruitmentsResponse +} diff --git a/core/network/src/main/java/team/retum/network/datasource/interests/InterestsDataSourceImpl.kt b/core/network/src/main/java/team/retum/network/datasource/interests/InterestsDataSourceImpl.kt new file mode 100644 index 000000000..4aaddf22e --- /dev/null +++ b/core/network/src/main/java/team/retum/network/datasource/interests/InterestsDataSourceImpl.kt @@ -0,0 +1,30 @@ +package team.retum.network.datasource.interests + +import team.retum.network.api.InterestsApi +import team.retum.network.model.request.interests.InterestsToggleRequest +import team.retum.network.model.response.interests.FetchInterestsRecruitmentsResponse +import team.retum.network.model.response.interests.FetchInterestsResponse +import team.retum.network.util.RequestHandler +import javax.inject.Inject + +class InterestsDataSourceImpl @Inject constructor( + private val interestsApi: InterestsApi, +) : InterestsDataSource { + override suspend fun setInterestsToggle(codes: InterestsToggleRequest) { + RequestHandler().request { + interestsApi.setInterestsToggle(codes = codes) + } + } + + override suspend fun fetchInterests(): FetchInterestsResponse { + return RequestHandler().request { + interestsApi.fetchInterests() + } + } + + override suspend fun fetchInterestsSearchRecruitments(): FetchInterestsRecruitmentsResponse { + return RequestHandler().request { + interestsApi.fetchInterestsSearchRecruitments() + } + } +} diff --git a/core/network/src/main/java/team/retum/network/di/NetworkModule.kt b/core/network/src/main/java/team/retum/network/di/NetworkModule.kt index a5695edb8..89e779250 100644 --- a/core/network/src/main/java/team/retum/network/di/NetworkModule.kt +++ b/core/network/src/main/java/team/retum/network/di/NetworkModule.kt @@ -22,6 +22,7 @@ import team.retum.network.api.BugApi import team.retum.network.api.CodeApi import team.retum.network.api.CompanyApi import team.retum.network.api.FileApi +import team.retum.network.api.InterestsApi import team.retum.network.api.NoticeApi import team.retum.network.api.NotificationApi import team.retum.network.api.RecruitmentApi @@ -188,4 +189,10 @@ object NetworkModule { fun provideServerStatusCheckApi(retrofit: Retrofit): ServerStatusCheckApi { return retrofit.create(ServerStatusCheckApi::class.java) } + + @Provides + @Singleton + fun provideInterestsApi(retrofit: Retrofit): InterestsApi { + return retrofit.create(InterestsApi::class.java) + } } diff --git a/core/network/src/main/java/team/retum/network/di/RemoteDataSourceModule.kt b/core/network/src/main/java/team/retum/network/di/RemoteDataSourceModule.kt index 64350f1bb..87b045e8b 100644 --- a/core/network/src/main/java/team/retum/network/di/RemoteDataSourceModule.kt +++ b/core/network/src/main/java/team/retum/network/di/RemoteDataSourceModule.kt @@ -22,6 +22,8 @@ import team.retum.network.datasource.company.CompanyDataSource import team.retum.network.datasource.company.CompanyDataSourceImpl import team.retum.network.datasource.file.RemoteFileDataSource import team.retum.network.datasource.file.RemoteFileDataSourceImpl +import team.retum.network.datasource.interests.InterestsDataSource +import team.retum.network.datasource.interests.InterestsDataSourceImpl import team.retum.network.datasource.notice.NoticeDataSource import team.retum.network.datasource.notice.NoticeDataSourceImpl import team.retum.network.datasource.notification.NotificationDataSource @@ -104,4 +106,8 @@ abstract class RemoteDataSourceModule { @Binds @Singleton abstract fun bindServerStatusCheckDataSource(serverStatusCheckDataSourceImpl: RemoteServerStatusCheckDataSourceImpl): RemoteServerStatusCheckDataSource + + @Binds + @Singleton + abstract fun bindInterestsDataSource(interestsDataSourceImpl: InterestsDataSourceImpl): InterestsDataSource } diff --git a/core/network/src/main/java/team/retum/network/di/RequestUrls.kt b/core/network/src/main/java/team/retum/network/di/RequestUrls.kt index a78255c70..6e5074a59 100644 --- a/core/network/src/main/java/team/retum/network/di/RequestUrls.kt +++ b/core/network/src/main/java/team/retum/network/di/RequestUrls.kt @@ -133,4 +133,11 @@ internal object RequestUrls { const val checkServerStatus = path } + + data object Interests { + private const val path = "/interests" + + const val interests = path + const val interestsRecruitments = "$path/recruitment" + } } diff --git a/core/network/src/main/java/team/retum/network/model/request/interests/InterestsToggleRequest.kt b/core/network/src/main/java/team/retum/network/model/request/interests/InterestsToggleRequest.kt new file mode 100644 index 000000000..cc581b380 --- /dev/null +++ b/core/network/src/main/java/team/retum/network/model/request/interests/InterestsToggleRequest.kt @@ -0,0 +1,9 @@ +package team.retum.network.model.request.interests + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class InterestsToggleRequest( + @Json(name = "code_ids") val codeIds: List, +) diff --git a/core/network/src/main/java/team/retum/network/model/response/interests/FetchInterestsRecruitmentsResponse.kt b/core/network/src/main/java/team/retum/network/model/response/interests/FetchInterestsRecruitmentsResponse.kt new file mode 100644 index 000000000..107a791ad --- /dev/null +++ b/core/network/src/main/java/team/retum/network/model/response/interests/FetchInterestsRecruitmentsResponse.kt @@ -0,0 +1,19 @@ +package team.retum.network.model.response.interests + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class FetchInterestsRecruitmentsResponse( + @Json(name = "recruitments") val recruitments: List, +) { + data class InterestRecruitments( + @Json(name = "id") val id: Int, + @Json(name = "company_id") val companyId: String, + @Json(name = "company_profile_url") val companyProfileUrl: String, + @Json(name = "train_pay") val trainPay: Int, + @Json(name = "military_support") val militarySupport: Boolean, + @Json(name = "hiring_jobs") val hiringJobs: String, + @Json(name = "bookmarked") val bookmarked: Boolean, + ) +} diff --git a/core/network/src/main/java/team/retum/network/model/response/interests/FetchInterestsResponse.kt b/core/network/src/main/java/team/retum/network/model/response/interests/FetchInterestsResponse.kt new file mode 100644 index 000000000..79504b418 --- /dev/null +++ b/core/network/src/main/java/team/retum/network/model/response/interests/FetchInterestsResponse.kt @@ -0,0 +1,17 @@ +package team.retum.network.model.response.interests + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class FetchInterestsResponse( + @Json(name = "student_name") val studentName: String, + @Json(name = "interests") val interests: List, +) { + data class InterestMajor( + @Json(name = "id") val id: Int, + @Json(name = "student_id") val studentId: Int, + @Json(name = "code") val code: Int, + @Json(name = "keyword") val keyword: String, + ) +} diff --git a/feature/interests/build.gradle.kts b/feature/interests/build.gradle.kts index 70bac8f32..744c48ba4 100644 --- a/feature/interests/build.gradle.kts +++ b/feature/interests/build.gradle.kts @@ -21,6 +21,7 @@ android { dependencies { implementation(project(":core:common")) implementation(project(":core:domain")) + implementation(project(":core:network")) implementation(libs.kotlinx.collections.immutable) } diff --git a/feature/interests/src/main/java/team/retum/jobis/interests/navigation/InterestsCompleteNavigation.kt b/feature/interests/src/main/java/team/retum/jobis/interests/navigation/InterestsCompleteNavigation.kt new file mode 100644 index 000000000..71fd85dd9 --- /dev/null +++ b/feature/interests/src/main/java/team/retum/jobis/interests/navigation/InterestsCompleteNavigation.kt @@ -0,0 +1,32 @@ +package team.retum.jobis.interests.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import team.retum.common.utils.ResourceKeys +import team.retum.jobis.interests.ui.InterestsComplete + +const val NAVIGATION_INTERESTS_COMPLETE = "interestsComplete" +fun NavGraphBuilder.interestsComplete( + onBackPressed: () -> Unit, +) { + composable( + route = "$NAVIGATION_INTERESTS_COMPLETE/{${ResourceKeys.STUDENT_NAME}}", + arguments = listOf( + navArgument(ResourceKeys.STUDENT_NAME) { type = NavType.StringType }, + ), + ) { + val studentName = it.arguments?.getString(ResourceKeys.STUDENT_NAME) ?: "" + + InterestsComplete( + onBackPressed = onBackPressed, + studentName = studentName, + ) + } +} + +fun NavController.navigateToInterestsComplete(studentName: String) { + navigate("$NAVIGATION_INTERESTS_COMPLETE/$studentName") +} diff --git a/feature/interests/src/main/java/team/retum/jobis/interests/navigation/InterestsNavigation.kt b/feature/interests/src/main/java/team/retum/jobis/interests/navigation/InterestsNavigation.kt index b17abb0ee..9d7cfbccd 100644 --- a/feature/interests/src/main/java/team/retum/jobis/interests/navigation/InterestsNavigation.kt +++ b/feature/interests/src/main/java/team/retum/jobis/interests/navigation/InterestsNavigation.kt @@ -6,9 +6,15 @@ import androidx.navigation.compose.composable import team.retum.jobis.interests.ui.Interests const val NAVIGATION_INTERESTS = "interests" -fun NavGraphBuilder.interests(onBackPressed: () -> Unit) { +fun NavGraphBuilder.interests( + onBackPressed: () -> Unit, + navigateToInterestsComplete: (String) -> Unit, +) { composable(NAVIGATION_INTERESTS) { - Interests(onBackPressed = onBackPressed) + Interests( + onBackPressed = onBackPressed, + navigateToInterestsComplete = navigateToInterestsComplete, + ) } } diff --git a/feature/interests/src/main/java/team/retum/jobis/interests/ui/InterestsCompleteScreen.kt b/feature/interests/src/main/java/team/retum/jobis/interests/ui/InterestsCompleteScreen.kt new file mode 100644 index 000000000..5da745fbc --- /dev/null +++ b/feature/interests/src/main/java/team/retum/jobis/interests/ui/InterestsCompleteScreen.kt @@ -0,0 +1,79 @@ +package team.retum.jobis.interests.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import team.retum.jobis.interests.R +import team.retum.jobisdesignsystemv2.appbar.JobisSmallTopAppBar +import team.retum.jobisdesignsystemv2.foundation.JobisTheme +import team.retum.jobisdesignsystemv2.foundation.JobisTypography +import team.retum.jobisdesignsystemv2.text.JobisText + +@Composable +internal fun InterestsComplete( + onBackPressed: () -> Unit, + studentName: String, +) { + InterestsCompleteScreen( + studentName = studentName, + onBackPressed = onBackPressed, + ) +} + +@Composable +private fun InterestsCompleteScreen( + studentName: String, + onBackPressed: () -> Unit, +) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + JobisSmallTopAppBar( + title = stringResource(R.string.set_interests), + onBackPressed = onBackPressed, + ) + Column( + modifier = Modifier.fillMaxHeight(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Image( + painter = painterResource(team.retum.design_system.R.drawable.success), + contentDescription = "major_check_success", + ) + JobisText( + modifier = Modifier.padding( + top = 20.dp, + start = 24.dp, + end = 24.dp, + ), + text = stringResource(R.string.interests_check_title, studentName), + textAlign = TextAlign.Center, + style = JobisTypography.PageTitle, + color = JobisTheme.colors.onBackground, + ) + JobisText( + modifier = Modifier.padding( + top = 8.dp, + bottom = 20.dp, + start = 24.dp, + end = 24.dp, + ), + text = stringResource(R.string.interests_alarm), + textAlign = TextAlign.Center, + style = JobisTypography.SubBody, + ) + } + } +} diff --git a/feature/interests/src/main/java/team/retum/jobis/interests/ui/InterestsScreen.kt b/feature/interests/src/main/java/team/retum/jobis/interests/ui/InterestsScreen.kt index 5ab2ed111..12d00d563 100644 --- a/feature/interests/src/main/java/team/retum/jobis/interests/ui/InterestsScreen.kt +++ b/feature/interests/src/main/java/team/retum/jobis/interests/ui/InterestsScreen.kt @@ -1,48 +1,92 @@ package team.retum.jobis.interests.ui +import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.SnapshotStateList -import androidx.compose.runtime.toMutableStateList +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import kotlinx.collections.immutable.toPersistentList +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import team.retum.jobis.interests.R +import team.retum.jobis.interests.viewmodel.InterestsSideEffect +import team.retum.jobis.interests.viewmodel.InterestsState +import team.retum.jobis.interests.viewmodel.InterestsViewmodel import team.retum.jobisdesignsystemv2.appbar.JobisSmallTopAppBar +import team.retum.jobisdesignsystemv2.button.ButtonColor +import team.retum.jobisdesignsystemv2.button.JobisButton +import team.retum.jobisdesignsystemv2.foundation.JobisIcon import team.retum.jobisdesignsystemv2.foundation.JobisTheme -import team.retum.jobisdesignsystemv2.skills.Skills -import team.retum.jobisdesignsystemv2.tab.TabBar -import team.retum.jobisdesignsystemv2.textfield.JobisTextField +import team.retum.jobisdesignsystemv2.foundation.JobisTypography +import team.retum.jobisdesignsystemv2.text.JobisText +import team.retum.jobisdesignsystemv2.toast.JobisToast +import team.retum.jobisdesignsystemv2.utils.clickable +import team.retum.usecase.entity.CodesEntity @Composable -internal fun Interests(onBackPressed: () -> Unit) { - InterestsScreen(onBackPressed = onBackPressed) +internal fun Interests( + onBackPressed: () -> Unit, + navigateToInterestsComplete: (String) -> Unit, + interestsViewmodel: InterestsViewmodel = hiltViewModel(), +) { + val state by interestsViewmodel.state.collectAsStateWithLifecycle() + val context = LocalContext.current + + LaunchedEffect(Unit) { + interestsViewmodel.sideEffect.collect { + when (it) { + is InterestsSideEffect.PatchMajorFail -> { + JobisToast.create( + context = context, + message = context.getString(R.string.cannot_submit_interests_major), + drawable = JobisIcon.Error, + ) + } + + is InterestsSideEffect.MoveToInterestsComplete -> { + navigateToInterestsComplete(it.studentName) + } + } + } + } + + InterestsScreen( + onBackPressed = onBackPressed, + state = state, + setSelectedMajor = interestsViewmodel::setMajor, + patchInterestsMajor = interestsViewmodel::patchInterestsMajor, + setButtonState = interestsViewmodel::setButtonState, + ) } @Composable -private fun InterestsScreen(onBackPressed: () -> Unit) { - // TODO 뷰모델로 옮기기 - var content by remember { mutableStateOf("") } - var selectedCategoryIndex by remember { mutableIntStateOf(0) } - val checkedSkills = remember { mutableStateListOf() } - val categories = remember { - mutableStateListOf( - "Android", - "Back-end", - "Front-end", - ) +private fun InterestsScreen( + onBackPressed: () -> Unit, + state: InterestsState, + setSelectedMajor: (Long) -> Unit, + patchInterestsMajor: () -> Unit, + setButtonState: (Boolean) -> Unit, +) { + val buttonText = if (state.selectedMajorCount > 0) { + setButtonState(true) + stringResource(R.string.select_interests_button_count, state.selectedMajorCount) + } else { + setButtonState(false) + stringResource(R.string.select_interests_button) } Column( @@ -54,54 +98,130 @@ private fun InterestsScreen(onBackPressed: () -> Unit) { title = stringResource(id = R.string.set_interests), onBackPressed = onBackPressed, ) + InterestsTitle(studentName = state.studentName) InterestsInput( - content = { content }, - onContentChange = { content = it }, - categories = categories, - selectedCategoryIndex = selectedCategoryIndex, - onSelectCategory = { selectedCategoryIndex = it }, - checkedSkills = checkedSkills, + categories = state.majorList, + selectedMajorCodes = state.selectedMajorCodes, + onSelectCategory = setSelectedMajor, + onUnselectCategory = { setSelectedMajor(it) }, + ) + Spacer(modifier = Modifier.weight(1f)) + JobisButton( + text = buttonText, + color = ButtonColor.Primary, + onClick = { patchInterestsMajor() }, + enabled = state.buttonEnable, ) - Spacer(modifier = Modifier.height(16.dp)) } } +@OptIn(ExperimentalLayoutApi::class) @Composable private fun InterestsInput( - content: () -> String, - onContentChange: (String) -> Unit, - selectedCategoryIndex: Int, - categories: SnapshotStateList, - onSelectCategory: (Int) -> Unit, - checkedSkills: SnapshotStateList, + categories: List, + selectedMajorCodes: List, + onSelectCategory: (Long) -> Unit, + onUnselectCategory: (Long) -> Unit, +) { + FlowRow( + modifier = Modifier.padding( + horizontal = 24.dp, + vertical = 36.dp, + ), + maxItemsInEachRow = 5, + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + categories.forEach { + MajorContent( + major = it.keyword, + majorId = it.code, + selected = selectedMajorCodes.contains(it.code), + onClick = { major -> + when (selectedMajorCodes.contains(it.code)) { + true -> { onUnselectCategory(major) } + false -> { onSelectCategory(major) } + } + }, + ) + } + } +} + +@Composable +private fun InterestsTitle( + studentName: String, ) { - Column(modifier = Modifier.padding(vertical = 16.dp)) { - JobisTextField( - hint = stringResource(id = R.string.hint_keyword), - value = content, - onValueChange = onContentChange, + Column( + modifier = Modifier.padding(top = 34.dp), + ) { + JobisText( + modifier = Modifier.padding( + top = 20.dp, + start = 24.dp, + end = 24.dp, + ), + text = stringResource(R.string.interests_select_title, studentName), + style = JobisTypography.PageTitle, + color = JobisTheme.colors.onBackground, ) - TabBar( - selectedTabIndex = selectedCategoryIndex, - tabs = categories.toPersistentList(), - onSelectTab = onSelectCategory, + JobisText( + modifier = Modifier.padding( + top = 8.dp, + bottom = 20.dp, + start = 24.dp, + end = 24.dp, + ), + text = stringResource(R.string.interests_alarm), + style = JobisTypography.SubBody, ) - // TODO 더미 데이터 제거 - Skills( - skills = listOf( - "Kotlin", - "Java", - ).toMutableStateList(), - checkedSkills = checkedSkills.toPersistentList(), - onCheckedChange = { index, checked, _ -> - // TODO 뷰모델로 함수 옮기기 - checkedSkills.run { - when (checked) { - true -> add(index) - false -> remove(index) - } - } - }, + } +} + +@Composable +private fun MajorContent( + modifier: Modifier = Modifier, + major: String, + majorId: Long, + selected: Boolean, + onClick: (Long) -> Unit, +) { + val background by animateColorAsState( + targetValue = if (selected) { + JobisTheme.colors.onPrimary + } else { + JobisTheme.colors.inverseSurface + }, + label = "", + ) + val textColor by animateColorAsState( + targetValue = if (selected) { + JobisTheme.colors.background + } else { + JobisTheme.colors.onPrimaryContainer + }, + label = "", + ) + + Box( + modifier = modifier + .clickable( + enabled = true, + onClick = { onClick(majorId) }, + onPressed = {}, + ) + .clip(RoundedCornerShape(30.dp)) + .background(background), + contentAlignment = Alignment.Center, + ) { + JobisText( + modifier = modifier.padding( + horizontal = 16.dp, + vertical = 6.dp, + ), + text = major, + style = JobisTypography.Body, + color = textColor, ) } } diff --git a/feature/interests/src/main/java/team/retum/jobis/interests/viewmodel/InterestsViewmodel.kt b/feature/interests/src/main/java/team/retum/jobis/interests/viewmodel/InterestsViewmodel.kt new file mode 100644 index 000000000..0f707d4ac --- /dev/null +++ b/feature/interests/src/main/java/team/retum/jobis/interests/viewmodel/InterestsViewmodel.kt @@ -0,0 +1,139 @@ +package team.retum.jobis.interests.viewmodel + +import androidx.compose.runtime.Immutable +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import team.retum.common.base.BaseViewModel +import team.retum.common.enums.CodeType +import team.retum.network.model.request.interests.InterestsToggleRequest +import team.retum.usecase.entity.CodesEntity +import team.retum.usecase.entity.interests.InterestsEntity +import team.retum.usecase.entity.interests.InterestsRecruitmentsEntity +import team.retum.usecase.usecase.code.FetchCodeUseCase +import team.retum.usecase.usecase.interests.FetchInterestsSearchRecruitmentsUseCase +import team.retum.usecase.usecase.interests.FetchInterestsUseCase +import team.retum.usecase.usecase.interests.SetInterestsToggleUseCase +import javax.inject.Inject + +@HiltViewModel +internal class InterestsViewmodel @Inject constructor( + private val fetchCodeUseCase: FetchCodeUseCase, + private val fetchInterestsUseCase: FetchInterestsUseCase, + private val fetchInterestsSearchRecruitmentsUseCase: FetchInterestsSearchRecruitmentsUseCase, + private val setInterestsToggleUseCase: SetInterestsToggleUseCase, +) : BaseViewModel(InterestsState.getInitialState()) { + + init { + fetchInterests() + fetchInterestsSearchRecruitments() + fetchCodes() + } + + private fun fetchCodes() { + viewModelScope.launch(Dispatchers.IO) { + fetchCodeUseCase( + keyword = null, + type = CodeType.JOB, + parentCode = null, + ).onSuccess { + setState { + state.value.copy( + majorList = it.codes, + ) + } + } + } + } + + private fun fetchInterests() { + viewModelScope.launch(Dispatchers.IO) { + fetchInterestsUseCase().onSuccess { + setState { + state.value.copy( + studentName = it.studentName, + interestsMajorList = it.interests, + ) + } + } + } + } + + private fun fetchInterestsSearchRecruitments() { + viewModelScope.launch(Dispatchers.IO) { + fetchInterestsSearchRecruitmentsUseCase().onSuccess { + setState { + state.value.copy( + interestsRecruitments = it, + ) + } + } + } + } + + internal fun patchInterestsMajor() { + viewModelScope.launch(Dispatchers.IO) { + setInterestsToggleUseCase( + codes = InterestsToggleRequest(codeIds = state.value.selectedMajorCodes.toMutableList()), + ).onSuccess { + postSideEffect(sideEffect = InterestsSideEffect.MoveToInterestsComplete(state.value.studentName)) + }.onFailure { + postSideEffect(sideEffect = InterestsSideEffect.PatchMajorFail) + } + } + } + + internal fun setMajor(majorId: Long) { + viewModelScope.launch { + val currentSelected = state.value.selectedMajorCodes.toMutableList() + if (currentSelected.contains(majorId)) { + currentSelected.remove(majorId) + } else { + currentSelected.add(majorId) + } + setState { + state.value.copy( + selectedMajorCodes = currentSelected, + selectedMajorCount = currentSelected.size, + ) + } + } + } + + internal fun setButtonState(buttonState: Boolean) { + setState { + state.value.copy( + buttonEnable = buttonState, + ) + } + } +} + +@Immutable +internal data class InterestsState( + val studentName: String, + val majorList: List, + val interestsMajorList: List, + val interestsRecruitments: InterestsRecruitmentsEntity?, + val selectedMajorCodes: List, + val selectedMajorCount: Int, + val buttonEnable: Boolean, +) { + companion object { + fun getInitialState() = InterestsState( + studentName = "", + majorList = emptyList(), + interestsMajorList = emptyList(), + interestsRecruitments = null, + selectedMajorCodes = emptyList(), + selectedMajorCount = 0, + buttonEnable = false, + ) + } +} + +internal sealed class InterestsSideEffect { + data class MoveToInterestsComplete(val studentName: String) : InterestsSideEffect() + data object PatchMajorFail : InterestsSideEffect() +} diff --git a/feature/interests/src/main/res/values/strings.xml b/feature/interests/src/main/res/values/strings.xml index 60967a638..1ee4e9d6d 100644 --- a/feature/interests/src/main/res/values/strings.xml +++ b/feature/interests/src/main/res/values/strings.xml @@ -1,5 +1,11 @@ - 관심사 설정 - 검색어를 입력해주세요 + 관심 분야 설정 + %s님의\n관심사를 선택해주세요 + %s님의\n관심사를 확인했어요! + 관심 분야를 선택해주세요! + %s개 선택 + 관심사에 맞는 모집 의뢰서가 업로드되면 \n알림을 드립니다! + + 관심 분야를 등록하지 못했어요 diff --git a/feature/landing/src/main/java/team/retum/landing/LandingViewModel.kt b/feature/landing/src/main/java/team/retum/landing/LandingViewModel.kt new file mode 100644 index 000000000..a7b68e0e6 --- /dev/null +++ b/feature/landing/src/main/java/team/retum/landing/LandingViewModel.kt @@ -0,0 +1,3 @@ +package team.retum.landing + +class LandingViewModel diff --git a/feature/mypage/src/main/java/team/retum/jobis/navigation/MyPageNavigation.kt b/feature/mypage/src/main/java/team/retum/jobis/navigation/MyPageNavigation.kt index 44eb27503..1012b8de8 100644 --- a/feature/mypage/src/main/java/team/retum/jobis/navigation/MyPageNavigation.kt +++ b/feature/mypage/src/main/java/team/retum/jobis/navigation/MyPageNavigation.kt @@ -30,5 +30,11 @@ fun NavGraphBuilder.myPage( } fun NavController.navigateToMyPage() { - navigate(NAVIGATION_MY_PAGE) + navigate(NAVIGATION_MY_PAGE) { + popUpTo(0) { + saveState = true + } + launchSingleTop = true + restoreState = true + } } diff --git a/feature/mypage/src/main/java/team/retum/jobis/ui/MyPageScreen.kt b/feature/mypage/src/main/java/team/retum/jobis/ui/MyPageScreen.kt index 244d4b235..1451dfc61 100644 --- a/feature/mypage/src/main/java/team/retum/jobis/ui/MyPageScreen.kt +++ b/feature/mypage/src/main/java/team/retum/jobis/ui/MyPageScreen.kt @@ -235,7 +235,7 @@ private fun MyPageScreen( imageResource = painterResource(id = JobisIcon.Code), description = "interest field icon", contentTitle = stringResource(id = R.string.interest_field), - onClick = showUpdateLaterToast, + onClick = onSelectInterestClick, iconColor = JobisTheme.colors.onPrimary, ), ListItemInfo(