diff --git a/app/src/main/java/com/hsLink/hslink/core/navigation/AppRoute.kt b/app/src/main/java/com/hsLink/hslink/core/navigation/AppRoute.kt index 0a3bdeb..07d039a 100644 --- a/app/src/main/java/com/hsLink/hslink/core/navigation/AppRoute.kt +++ b/app/src/main/java/com/hsLink/hslink/core/navigation/AppRoute.kt @@ -4,6 +4,3 @@ import kotlinx.serialization.Serializable @Serializable data object AppMain : Route // 전체 앱의 메인 (기존 MainScreen) - -@Serializable -data object Onboarding : Route \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/data/di/DataSourceModule.kt b/app/src/main/java/com/hsLink/hslink/data/di/DataSourceModule.kt index 2eb8211..765c315 100644 --- a/app/src/main/java/com/hsLink/hslink/data/di/DataSourceModule.kt +++ b/app/src/main/java/com/hsLink/hslink/data/di/DataSourceModule.kt @@ -1,9 +1,11 @@ package com.hsLink.hslink.data.di import com.hsLink.hslink.data.remote.datasource.CommunityPostDataSource +import com.hsLink.hslink.data.remote.datasource.OnboardingDataSource import com.hsLink.hslink.data.remote.datasource.PostDataSource import com.hsLink.hslink.data.remote.datasource.SearchDataSource import com.hsLink.hslink.data.remote.datasourceimpl.CommunityPostDataSourceImpl +import com.hsLink.hslink.data.remote.datasourceimpl.OnboardingDataSourceImpl import com.hsLink.hslink.data.remote.datasourceimpl.PostDataSourceImpl import com.hsLink.hslink.data.remote.datasourceimpl.SearchDataSourceImpl import dagger.Binds @@ -28,4 +30,11 @@ interface DataSourceModule { abstract fun bindsSearchDataSource( searchDataSourceImpl: SearchDataSourceImpl, ): SearchDataSource + + + @Binds + abstract fun bindsOnboardingDataSource( + onboardingDataSourceImpl: OnboardingDataSourceImpl + ): OnboardingDataSource + } \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/data/di/NetworkModule.kt b/app/src/main/java/com/hsLink/hslink/data/di/NetworkModule.kt index dcf1cd2..55b58a2 100644 --- a/app/src/main/java/com/hsLink/hslink/data/di/NetworkModule.kt +++ b/app/src/main/java/com/hsLink/hslink/data/di/NetworkModule.kt @@ -5,6 +5,7 @@ import com.hsLink.hslink.data.remote.AuthInterceptor import com.hsLink.hslink.data.service.commuunity.CommunityPostService import com.hsLink.hslink.data.service.home.PostService import com.hsLink.hslink.data.service.login.AuthService +import com.hsLink.hslink.data.service.onboarding.OnboardingService import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory import dagger.Module import dagger.Provides @@ -69,4 +70,10 @@ object NetworkModule { fun provideAuthService(retrofit: Retrofit): AuthService { return retrofit.create(AuthService::class.java) } + + @Provides + @Singleton + fun provideOnboardingService(retrofit: Retrofit): OnboardingService { + return retrofit.create(OnboardingService::class.java) + } } \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/data/di/RepositoryModule.kt b/app/src/main/java/com/hsLink/hslink/data/di/RepositoryModule.kt index 18a6b30..8778287 100644 --- a/app/src/main/java/com/hsLink/hslink/data/di/RepositoryModule.kt +++ b/app/src/main/java/com/hsLink/hslink/data/di/RepositoryModule.kt @@ -4,11 +4,13 @@ import com.hsLink.hslink.data.repositoryimpl.AuthRepositoryImpl import com.hsLink.hslink.data.repositoryimpl.CommunityRepositoryImpl import com.hsLink.hslink.data.repositoryimpl.DummyRepositoryImpl import com.hsLink.hslink.data.repositoryimpl.home.PostRepositoryImpl +import com.hsLink.hslink.data.repositoryimpl.onboarding.OnboardingRepositoryImpl import com.hsLink.hslink.data.repositoryimpl.search.SearchRepositoryImpl import com.hsLink.hslink.domain.DummyRepository import com.hsLink.hslink.domain.repository.AuthRepository import com.hsLink.hslink.domain.repository.community.CommunityRepository import com.hsLink.hslink.domain.repository.home.PostRepository +import com.hsLink.hslink.domain.repository.onboarding.OnboardingRepository import com.hsLink.hslink.domain.repository.search.SearchRepository import dagger.Binds import dagger.Module @@ -45,4 +47,9 @@ interface RepositoryModule { fun bindsSearchRepository( searchRepositoryImpl: SearchRepositoryImpl, ): SearchRepository + + @Binds + fun bindsOnboardingRepository( + onboardingRepositoryImpl: OnboardingRepositoryImpl, + ): OnboardingRepository } diff --git a/app/src/main/java/com/hsLink/hslink/data/dto/request/onboarding/CareerRequest.kt b/app/src/main/java/com/hsLink/hslink/data/dto/request/onboarding/CareerRequest.kt new file mode 100644 index 0000000..b0d0f58 --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/data/dto/request/onboarding/CareerRequest.kt @@ -0,0 +1,30 @@ +package com.hsLink.hslink.data.dto.request.onboarding + +import com.hsLink.hslink.presentation.onboarding.model.JobType +import com.hsLink.hslink.presentation.onboarding.model.LinkType +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class CareerRequest( + @SerialName("companyName") + val companyName: String, + @SerialName("position") + val position: String, + @SerialName("jobType") + val jobType: JobType, + @SerialName("startYm") + val startYm: String, + @SerialName("endYm") + val endYm: String? = null, + @SerialName("employed") + val employed: Boolean +) + +@Serializable +data class LinkRequest( + @SerialName("type") + val type: LinkType, + @SerialName("url") + val url: String +) \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/data/dto/request/onboarding/OnboardingRequest.kt b/app/src/main/java/com/hsLink/hslink/data/dto/request/onboarding/OnboardingRequest.kt new file mode 100644 index 0000000..b47ad4e --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/data/dto/request/onboarding/OnboardingRequest.kt @@ -0,0 +1,20 @@ +package com.hsLink.hslink.data.dto.request.onboarding + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class OnboardingRequest ( + @SerialName("name") + val name: String, + @SerialName("major") + val major: String, + @SerialName("studentNumber") + val studentNumber: String, + @SerialName("jobSeeking") + val jobSeeking : Boolean, + @SerialName("mentor") + val mentor : Boolean, + @SerialName("email") + val email : String + ) \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/data/dto/response/CareerResponse.kt b/app/src/main/java/com/hsLink/hslink/data/dto/response/CareerResponse.kt new file mode 100644 index 0000000..985b98d --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/data/dto/response/CareerResponse.kt @@ -0,0 +1,42 @@ +package com.hsLink.hslink.data.dto.response.onboarding + +import com.hsLink.hslink.domain.model.search.CareerItemEntity +import com.hsLink.hslink.domain.model.search.LinkItemEntity +import com.hsLink.hslink.presentation.onboarding.model.JobType +import com.hsLink.hslink.presentation.onboarding.model.LinkType +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class CareerResponse( + @SerialName("id") val id: Int, + @SerialName("companyName") val companyName: String, + @SerialName("position") val position: String, + @SerialName("jobType") val jobType: JobType, + @SerialName("employed") val employed: Boolean, + @SerialName("startYm") val startYm: String, + @SerialName("endYm") val endYm: String?, +) + +@Serializable +data class LinkResponse( + @SerialName("id") val id: Long, + @SerialName("type") val type: LinkType, + @SerialName("url") val url: String, +) + +typealias CareerListResponseDto = List + +@Serializable +data class LinkListResponseDto( + @SerialName("links") val links: List, +) + +fun CareerResponse.toEntity(): CareerItemEntity = CareerItemEntity( + id = id, companyName = companyName, position = position, + jobType = jobType, employed = employed, startYm = startYm, endYm = endYm +) + +fun LinkResponse.toEntity(): LinkItemEntity = LinkItemEntity( + id = id, type = type, url = url +) \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/data/remote/datasource/OnboardingDataSource.kt b/app/src/main/java/com/hsLink/hslink/data/remote/datasource/OnboardingDataSource.kt new file mode 100644 index 0000000..9ea25e3 --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/data/remote/datasource/OnboardingDataSource.kt @@ -0,0 +1,13 @@ +package com.hsLink.hslink.data.remote.datasource + +import com.hsLink.hslink.core.network.BaseResponse +import com.hsLink.hslink.data.dto.request.onboarding.* +import com.hsLink.hslink.data.dto.response.onboarding.* + +interface OnboardingDataSource { + suspend fun submitOnboarding(request: OnboardingRequest): BaseResponse + suspend fun getCareers(): BaseResponse> + suspend fun submitCareer(request: CareerRequest): BaseResponse + suspend fun getLinks(): BaseResponse + suspend fun submitLink(request: LinkRequest): LinkResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/data/remote/datasourceimpl/OnboardingDataSourceImpl.kt b/app/src/main/java/com/hsLink/hslink/data/remote/datasourceimpl/OnboardingDataSourceImpl.kt new file mode 100644 index 0000000..7992d52 --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/data/remote/datasourceimpl/OnboardingDataSourceImpl.kt @@ -0,0 +1,27 @@ +package com.hsLink.hslink.data.remote.datasourceimpl + +import com.hsLink.hslink.core.network.BaseResponse +import com.hsLink.hslink.data.dto.request.onboarding.* +import com.hsLink.hslink.data.remote.datasource.OnboardingDataSource +import com.hsLink.hslink.data.service.onboarding.OnboardingService +import com.hsLink.hslink.data.dto.response.onboarding.* +import javax.inject.Inject + +class OnboardingDataSourceImpl @Inject constructor( + private val onboardingService: OnboardingService +) : OnboardingDataSource { + override suspend fun submitOnboarding(request: OnboardingRequest): BaseResponse = + onboardingService.onboarding(request) + + override suspend fun getCareers(): BaseResponse> = + onboardingService.getCareers() + + override suspend fun submitCareer(request: CareerRequest): BaseResponse = + onboardingService.submitCareer(request) + + override suspend fun getLinks(): BaseResponse = + onboardingService.getLinks() + + override suspend fun submitLink(request: LinkRequest): LinkResponse = + onboardingService.submitLink(request) +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/data/repositoryimpl/onboarding/OnboardingRepositoryImpl.kt b/app/src/main/java/com/hsLink/hslink/data/repositoryimpl/onboarding/OnboardingRepositoryImpl.kt new file mode 100644 index 0000000..d84b771 --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/data/repositoryimpl/onboarding/OnboardingRepositoryImpl.kt @@ -0,0 +1,43 @@ +package com.hsLink.hslink.data.repositoryimpl.onboarding + +import com.hsLink.hslink.data.dto.request.onboarding.* +import com.hsLink.hslink.data.dto.response.onboarding.toEntity +import com.hsLink.hslink.data.remote.datasource.OnboardingDataSource +import com.hsLink.hslink.domain.model.search.* +import com.hsLink.hslink.domain.repository.onboarding.OnboardingRepository +import javax.inject.Inject + +class OnboardingRepositoryImpl @Inject constructor( + private val onboardingDataSource: OnboardingDataSource, +) : OnboardingRepository { + override suspend fun submitOnboarding(request: OnboardingRequest): Result = runCatching { + val response = onboardingDataSource.submitOnboarding(request) + if (response.isSuccess) response.result + else throw Exception(response.message) + } + + override suspend fun getCareers(): Result> = runCatching { + val response = onboardingDataSource.getCareers() + if (response.isSuccess && response.result != null) { + response.result.map { it.toEntity() } + } else throw Exception(response.message ?: "Failed to get careers") + } + + override suspend fun submitCareer(request: CareerRequest): Result = runCatching { + val response = onboardingDataSource.submitCareer(request) + if (response.isSuccess && response.result != null) { + response.result.toEntity() + } else throw Exception(response.message ?: "Failed to submit career") + } + + override suspend fun getLinks(): Result> = runCatching { + val response = onboardingDataSource.getLinks() + if (response.isSuccess && response.result != null) { + response.result.links.map { it.toEntity() } + } else throw Exception(response.message ?: "Failed to get links") + } + + override suspend fun submitLink(request: LinkRequest): Result = runCatching { + onboardingDataSource.submitLink(request).toEntity() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/data/service/onboarding/OnboardingService.kt b/app/src/main/java/com/hsLink/hslink/data/service/onboarding/OnboardingService.kt new file mode 100644 index 0000000..556b012 --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/data/service/onboarding/OnboardingService.kt @@ -0,0 +1,23 @@ +package com.hsLink.hslink.data.service.onboarding + +import com.hsLink.hslink.core.network.BaseResponse +import com.hsLink.hslink.data.dto.request.onboarding.* +import com.hsLink.hslink.data.dto.response.onboarding.* +import retrofit2.http.* + +interface OnboardingService { + @POST("auth/onboarding") + suspend fun onboarding(@Body request: OnboardingRequest): BaseResponse + + @GET("careers/mycareers") + suspend fun getCareers(): BaseResponse> + + @POST("careers") + suspend fun submitCareer(@Body request: CareerRequest): BaseResponse + + @GET("links/mylinks") + suspend fun getLinks(): BaseResponse + + @POST("links") + suspend fun submitLink(@Body request: LinkRequest): LinkResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/domain/model/search/UserProfileEntity.kt b/app/src/main/java/com/hsLink/hslink/domain/model/search/UserProfileEntity.kt index 9b14d2c..085aefa 100644 --- a/app/src/main/java/com/hsLink/hslink/domain/model/search/UserProfileEntity.kt +++ b/app/src/main/java/com/hsLink/hslink/domain/model/search/UserProfileEntity.kt @@ -1,5 +1,8 @@ package com.hsLink.hslink.domain.model.search +import com.hsLink.hslink.presentation.onboarding.model.JobType +import com.hsLink.hslink.presentation.onboarding.model.LinkType + data class UserProfileEntity( val userId: Long, val name: String, @@ -13,6 +16,16 @@ data class UserProfileEntity( val links: List ) +data class CareerItemEntity( + val id: Int, + val companyName: String, + val position: String, + val jobType: JobType, + val employed: Boolean, + val startYm: String, + val endYm: String? +) + data class CareerEntity( val id: Long, val companyName: String, @@ -27,4 +40,10 @@ data class LinkEntity( val id: Long, val type: String, val url: String +) + +data class LinkItemEntity( + val id: Long, + val type: LinkType, + val url: String ) \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/domain/repository/onboarding/OnboardingRepository.kt b/app/src/main/java/com/hsLink/hslink/domain/repository/onboarding/OnboardingRepository.kt new file mode 100644 index 0000000..2caf352 --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/domain/repository/onboarding/OnboardingRepository.kt @@ -0,0 +1,15 @@ +package com.hsLink.hslink.domain.repository.onboarding + +import com.hsLink.hslink.data.dto.request.onboarding.CareerRequest +import com.hsLink.hslink.data.dto.request.onboarding.LinkRequest +import com.hsLink.hslink.data.dto.request.onboarding.OnboardingRequest +import com.hsLink.hslink.domain.model.search.CareerItemEntity +import com.hsLink.hslink.domain.model.search.LinkItemEntity + +interface OnboardingRepository { + suspend fun submitOnboarding(request: OnboardingRequest): Result + suspend fun getCareers(): Result> + suspend fun submitCareer(request: CareerRequest): Result + suspend fun getLinks(): Result> + suspend fun submitLink(request: LinkRequest): Result +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/presentation/main/MainActivity.kt b/app/src/main/java/com/hsLink/hslink/presentation/main/MainActivity.kt index cc81aab..fb26e40 100644 --- a/app/src/main/java/com/hsLink/hslink/presentation/main/MainActivity.kt +++ b/app/src/main/java/com/hsLink/hslink/presentation/main/MainActivity.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -20,9 +21,12 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import androidx.navigation.navigation import com.hsLink.hslink.core.designsystem.theme.HsLinkTheme import com.hsLink.hslink.presentation.login.screen.KaKaoLoginScreen import com.kakao.sdk.common.KakaoSdk @@ -76,12 +80,19 @@ class MainActivity : ComponentActivity() { } } +@Serializable +data object AuthGraph + @Serializable data object Login +@Serializable +data object Onboarding + @Serializable data object AppMain + @Composable private fun AppNavigation() { val navController = rememberNavController() @@ -93,42 +104,69 @@ private fun AppNavigation() { isLoginChecked = true } - if (!isLoginChecked) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() + Scaffold { paddingValues -> + if (!isLoginChecked) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } else { + NavHost( + navController = navController, + startDestination = if (isLoggedIn) AppMain else AuthGraph + ) { + authNavGraph( + navController = navController, + paddingValues = paddingValues, + onNavigateToOnboarding = { + navController.navigate(Onboarding) + }, + onNavigateToAppMain = { + navController.navigate(AppMain) { + popUpTo(AuthGraph) { inclusive = true } + } + } + ) + + composable { + MainScreen() + } + } } - return } +} + + + +fun NavGraphBuilder.authNavGraph( + navController: NavHostController, + paddingValues: PaddingValues, + onNavigateToOnboarding: () -> Unit, + onNavigateToAppMain: () -> Unit, +) { + navigation(startDestination = Login) { - NavHost( - navController = navController, - startDestination = if (isLoggedIn) AppMain else Login - ) { composable { KaKaoLoginScreen( - paddingValues = PaddingValues(), - onNavigateToHome = { - navController.navigate(AppMain) { - popUpTo(Login) { inclusive = true } - } - }, - onNavigateToOnboarding = { - navController.navigate(AppMain) { - popUpTo(Login) { inclusive = true } - } - } + paddingValues = paddingValues, + onNavigateToHome = onNavigateToOnboarding, + onNavigateToOnboarding = onNavigateToOnboarding ) } - composable { - MainScreen() + composable { + com.hsLink.hslink.presentation.onboarding.OnboardingRoute( + paddingValues = paddingValues, + navigateUp = { navController.popBackStack() }, + navigateToHome = onNavigateToAppMain + ) } } } + private suspend fun checkLoginStatus(): Boolean { return false } \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/presentation/mypage/screen/career/CareerEditScreen.kt b/app/src/main/java/com/hsLink/hslink/presentation/mypage/screen/career/CareerEditScreen.kt index 0338a01..7f3b2a0 100644 --- a/app/src/main/java/com/hsLink/hslink/presentation/mypage/screen/career/CareerEditScreen.kt +++ b/app/src/main/java/com/hsLink/hslink/presentation/mypage/screen/career/CareerEditScreen.kt @@ -71,15 +71,13 @@ fun CareerEditScreen( onSaveClick: () -> Unit, modifier: Modifier = Modifier, ) { - // State 관리 var startDate by remember { mutableStateOf("2024.04") } var endDate by remember { mutableStateOf("2024.08") } var isCurrentlyEmployed by remember { mutableStateOf(false) } var companyName by remember { mutableStateOf("한성대학교") } var jobName by remember { mutableStateOf("영업직") } - var selectedJobType by remember { mutableStateOf(JobType.FULL_TIME) } + var selectedJobType by remember { mutableStateOf(JobType.PERMANENT) } - // Focus state들 var companyFocused by remember { mutableStateOf(false) } var jobNameFocused by remember { mutableStateOf(false) } var startDateFocused by remember { mutableStateOf(false) } @@ -87,16 +85,14 @@ fun CareerEditScreen( var showExitDialog by remember { mutableStateOf(false) } - // 변경사항이 있는지 체크하는 함수 (함수 내부로 이동) fun hasUnsavedChanges(): Boolean { return startDate != "2024.04" || endDate != "2024.08" || companyName != "한성대학교" || jobName != "영업직" || - selectedJobType != JobType.FULL_TIME + selectedJobType != JobType.PERMANENT } - // 나가기 처리 함수 fun handleExit() { if (hasUnsavedChanges()) { showExitDialog = true @@ -129,8 +125,8 @@ fun CareerEditScreen( }, leftIcon = R.drawable.ic_topbar_arrowleft, rightIconFirst = R.drawable.ic_topbar_close, - onLeftIconClick = { handleExit() }, // ← 수정 - onRightIconFirstClick = { handleExit() } // ← 수정 + onLeftIconClick = { handleExit() }, + onRightIconFirstClick = { handleExit() } ) } @@ -142,7 +138,6 @@ fun CareerEditScreen( ) } - // 현재 재직 여부 (날짜 범위) item { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Text( @@ -188,7 +183,6 @@ fun CareerEditScreen( } } - // 회사명 item { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Text( @@ -214,7 +208,6 @@ fun CareerEditScreen( } } - // 직무명 item { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Text( @@ -240,7 +233,6 @@ fun CareerEditScreen( } } -// 재직 형태 (4개 버튼) item { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Text( @@ -262,24 +254,23 @@ fun CareerEditScreen( ) { HsLinkSelectButton( modifier = Modifier.weight(1f), - label = JobType.FULL_TIME.label, - onClick = { selectedJobType = JobType.FULL_TIME }, + label = JobType.PERMANENT.label, + onClick = { selectedJobType = JobType.PERMANENT }, size = HsLinkButtonSize.Large, isEnabled = true, - isSelected = selectedJobType == JobType.FULL_TIME + isSelected = selectedJobType == JobType.PERMANENT ) HsLinkSelectButton( modifier = Modifier.weight(1f), - label = JobType.CONTRACT.label, - onClick = { selectedJobType = JobType.CONTRACT }, + label = JobType.TEMPORARY.label, + onClick = { selectedJobType = JobType.TEMPORARY }, size = HsLinkButtonSize.Large, isEnabled = true, - isSelected = selectedJobType == JobType.CONTRACT + isSelected = selectedJobType == JobType.TEMPORARY ) } Row( - // ← 이 Row가 위 Row와 같은 레벨에 있어야 함 horizontalArrangement = Arrangement.spacedBy(8.dp), ) { HsLinkSelectButton( @@ -301,9 +292,9 @@ fun CareerEditScreen( } } } - } // ← 여기서 재직 형태 item 종료 + } - item { // ← 새로운 item으로 분리 + item { HsLinkActionButton( label = "수정완료", onClick = onSaveClick, diff --git a/app/src/main/java/com/hsLink/hslink/presentation/onboarding/OnboardingScreen.kt b/app/src/main/java/com/hsLink/hslink/presentation/onboarding/OnboardingScreen.kt index 5820cf8..b384ace 100644 --- a/app/src/main/java/com/hsLink/hslink/presentation/onboarding/OnboardingScreen.kt +++ b/app/src/main/java/com/hsLink/hslink/presentation/onboarding/OnboardingScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.text.AnnotatedString @@ -23,6 +24,7 @@ import com.hsLink.hslink.core.designsystem.component.HsLinkActionButtonSize import com.hsLink.hslink.core.designsystem.theme.HsLinkTheme import com.hsLink.hslink.presentation.onboarding.component.OnboardingProgressBar import com.hsLink.hslink.presentation.onboarding.component.screen.CareerScreen +import com.hsLink.hslink.presentation.onboarding.component.screen.EmailScreen import com.hsLink.hslink.presentation.onboarding.component.screen.EmploymentStatusScreen import com.hsLink.hslink.presentation.onboarding.component.screen.JobInfoScreen import com.hsLink.hslink.presentation.onboarding.component.screen.JobSeekingScreen @@ -31,6 +33,7 @@ import com.hsLink.hslink.presentation.onboarding.component.screen.MajorScreen import com.hsLink.hslink.presentation.onboarding.component.screen.MentorshipScreen import com.hsLink.hslink.presentation.onboarding.component.screen.NameScreen import com.hsLink.hslink.presentation.onboarding.component.screen.StudentIdScreen +import com.hsLink.hslink.presentation.onboarding.component.screen.LinkAddScreen // 새로운 폼 임포트 import com.hsLink.hslink.presentation.onboarding.model.OnboardingStep import com.hsLink.hslink.presentation.onboarding.viewmodel.OnboardingViewModel @@ -79,6 +82,7 @@ fun OnboardingRoute( ) } + OnboardingStep.EMPLOYMENT_STATUS -> { EmploymentStatusScreen( selectedStatus = state.employmentStatus, @@ -91,37 +95,43 @@ fun OnboardingRoute( } OnboardingStep.CAREER -> { + CareerScreen( - selectedStatus = state.employmentStatus, + selectedCareer = state.career, + careerList = state.careerList, progress = state.currentStep.progress, paddingValues = paddingValues, - onStatusSelect = {}, + onCareerSelect = viewModel::updateCareer, onPreviousClick = viewModel::moveToPreviousStep, - onNextClick = viewModel::moveToNextStep + onNextClick = viewModel::moveToNextStep, + onAddCareerClick = viewModel::openJobInfoForm ) } OnboardingStep.JOB_INFO -> { JobInfoScreen( - startDate = state.startDate, - endDate = state.endDate, - isCurrentlyEmployed = state.isCurrentlyEmployed, - companyName = state.companyName, - jobName = state.jobName, - selectedJobType = state.jobType, + companyName = state.tempCompanyName, + position = state.tempPosition, + department = state.tempDepartment, + selectedJobType = state.tempJobType, + startYm = state.tempStartYm, + endYm = state.tempEndYm, + isCurrentlyEmployed = state.tempIsEmployed, progress = state.currentStep.progress, paddingValues = paddingValues, - onStartDateChange = viewModel::updateStartDate, - onEndDateChange = viewModel::updateEndDate, - onCurrentlyEmployedChange = viewModel::updateIsCurrentlyEmployed, - onCompanyNameChange = viewModel::updateCompanyName, - onJobNameChange = viewModel::updateJobName, - onJobTypeSelect = viewModel::updateJobType, - onCancelClick = viewModel::moveToPreviousStep, - onSaveClick = viewModel::moveToNextStep + onCompanyNameChange = viewModel::updateTempCompanyName, + onPositionChange = viewModel::updateTempPosition, + onDepartmentChange = viewModel::updateTempDepartment, + onJobTypeSelect = viewModel::updateTempJobType, + onStartDateChange = viewModel::updateTempStartYm, + onEndDateChange = viewModel::updateTempEndYm, + onCurrentlyEmployedChange = viewModel::updateTempIsEmployed, + onPreviousClick = viewModel::moveToPreviousStep, + onNextClick = viewModel::submitJobInfoAndReturnToCareerList ) } + OnboardingStep.JOB_SEEKING -> { JobSeekingScreen( isJobSeeking = state.isJobSeeking, @@ -144,18 +154,41 @@ fun OnboardingRoute( ) } - OnboardingStep.LINKS -> { + OnboardingStep.EMAIL -> { + EmailScreen( + email = state.email, + progress = state.currentStep.progress, + paddingValues = paddingValues, + onEmailChange = viewModel::updateEmail, + onPreviousClick = viewModel::moveToPreviousStep, + onNextClick = viewModel::moveToNextStep + ) + } + + OnboardingStep.LINKS_LIST -> { LinksScreen( - links = state.links, + linkList = state.linkList, progress = state.currentStep.progress, paddingValues = paddingValues, - onAddLink = viewModel::addLink, - onRemoveLink = viewModel::removeLink, onPreviousClick = viewModel::moveToPreviousStep, onNextClick = { viewModel.submitOnboarding() navigateToHome() - } + }, + onAddLinkClick = viewModel::openLinkForm + ) + } + + OnboardingStep.LINKS -> { + LinkAddScreen( + type = state.tempLinkType, + url = state.tempLinkUrl, + progress = state.currentStep.progress, + paddingValues = paddingValues, + onTypeSelect = viewModel::updateTempLinkType, + onUrlChange = viewModel::updateTempLinkUrl, + onPreviousClick = viewModel::moveToPreviousStep, + onNextClick = viewModel::submitLinkAndReturnToLinkList ) } } @@ -225,6 +258,7 @@ fun OnboardingScreen( Spacer(modifier = Modifier.height(32.dp)) + // Content Column( modifier = Modifier .weight(1f) @@ -288,4 +322,4 @@ fun OnboardingScreen( onNextClick = onNextClick, content = content ) -} +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/presentation/onboarding/component/YearMonthTransform.kt b/app/src/main/java/com/hsLink/hslink/presentation/onboarding/component/YearMonthTransform.kt new file mode 100644 index 0000000..96c1954 --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/presentation/onboarding/component/YearMonthTransform.kt @@ -0,0 +1,70 @@ +package com.hsLink.hslink.presentation.onboarding.component + +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation + +/** + * 날짜 입력을 yyyy-MM 형식으로 자동 포맷팅하는 VisualTransformation + * 예: "202401" -> "2024-01" + */ +class YearMonthVisualTransformation : VisualTransformation { + override fun filter(text: AnnotatedString): TransformedText { + val trimmed = text.text.filter { it.isDigit() }.take(6) + + val formatted = buildString { + trimmed.forEachIndexed { index, char -> + append(char) + if (index == 3 && trimmed.length > 4) { + append("-") + } + } + } + + val offsetMapping = object : OffsetMapping { + override fun originalToTransformed(offset: Int): Int { + return when { + offset <= 4 -> offset + else -> offset + 1 + } + } + + override fun transformedToOriginal(offset: Int): Int { + return when { + offset <= 4 -> offset + else -> offset - 1 + } + } + } + + return TransformedText(AnnotatedString(formatted), offsetMapping) + } +} + +/** + * 입력값을 숫자만 허용하고 최대 6자리로 제한 + */ +fun String.toYearMonthFormat(): String { + val digits = this.filter { it.isDigit() }.take(6) + return when { + digits.length <= 4 -> digits + else -> "${digits.substring(0, 4)}-${digits.substring(4)}" + } +} + +/** + * yyyy-MM 또는 yyyy.MM 형식 검증 + */ +fun String.isValidYearMonth(): Boolean { + val regex = """^\d{4}[-./]\d{2}$""".toRegex() + if (!regex.matches(this)) return false + + val parts = this.split("""[-./]""".toRegex()) + if (parts.size != 2) return false + + val year = parts[0].toIntOrNull() ?: return false + val month = parts[1].toIntOrNull() ?: return false + + return year in 1900..2100 && month in 1..12 +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/presentation/onboarding/component/screen/CareerScreen.kt b/app/src/main/java/com/hsLink/hslink/presentation/onboarding/component/screen/CareerScreen.kt index 034715a..f7f83bd 100644 --- a/app/src/main/java/com/hsLink/hslink/presentation/onboarding/component/screen/CareerScreen.kt +++ b/app/src/main/java/com/hsLink/hslink/presentation/onboarding/component/screen/CareerScreen.kt @@ -2,15 +2,9 @@ package com.hsLink.hslink.presentation.onboarding.component.screen import androidx.compose.foundation.background import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -24,68 +18,139 @@ import androidx.compose.ui.unit.dp import com.hsLink.hslink.R import com.hsLink.hslink.core.designsystem.theme.HsLinkTheme import com.hsLink.hslink.core.util.noRippleClickable +import com.hsLink.hslink.domain.model.search.CareerItemEntity import com.hsLink.hslink.presentation.onboarding.OnboardingScreen -import com.hsLink.hslink.presentation.onboarding.model.EmploymentStatus @Composable fun CareerScreen( - selectedStatus: EmploymentStatus?, + selectedCareer: Boolean?, + careerList: List, progress: Float, paddingValues: PaddingValues, - onStatusSelect: (EmploymentStatus) -> Unit, + onCareerSelect: (Boolean) -> Unit, onPreviousClick: () -> Unit, onNextClick: () -> Unit, + onAddCareerClick: () -> Unit, modifier: Modifier = Modifier, ) { + val onSkipPathClick = { + onCareerSelect(false) + onNextClick() + } + + val onExperiencedPathClick = { + onCareerSelect(true) + onAddCareerClick() + } + OnboardingScreen( title = "지금까지의 커리어를 알려주세요", progress = progress, - subtitle = "아직 경력이 없다면 넘겨도 괜찮아요", + subtitle = "아직 경력이 없다면 '다음' 버튼으로 넘겨도 괜찮아요.", paddingValues = paddingValues, showPreviousButton = true, nextButtonEnabled = true, onPreviousClick = onPreviousClick, - onNextClick = onNextClick + onNextClick = onSkipPathClick ) { - Box( - modifier = modifier - .fillMaxSize() - .background( - color = HsLinkTheme.colors.Common, - shape = RoundedCornerShape(12.dp) - ) - .border( - width = 1.dp, - color = HsLinkTheme.colors.Grey100, - shape = RoundedCornerShape(12.dp) - ) + Column( + modifier = modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(16.dp) ) { - Column( - modifier = Modifier.align(Alignment.Center), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp) + + Box( + modifier = Modifier + .weight(1f) + .fillMaxWidth() + ) { + if (careerList.isNotEmpty()) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(careerList) { career -> + CareerItem(career) + } + } + } + } + + Box( + modifier = Modifier + .fillMaxWidth() + .background( + color = HsLinkTheme.colors.Common, + shape = RoundedCornerShape(12.dp) + ) + .border( + width = 1.dp, + color = HsLinkTheme.colors.Grey100, + shape = RoundedCornerShape(12.dp) + ) + .padding(vertical = 40.dp) ) { - Text( - text = "아직 등록된 커리어가 없어요", - style = HsLinkTheme.typography.title_16Strong, - color = HsLinkTheme.colors.Grey700 - ) - Text( - text = "더 많은 정보를 얻기 위해 커리어를 등록해요", - style = HsLinkTheme.typography.body_14Normal, - color = HsLinkTheme.colors.Grey500 - ) - CareerSelectButton( - modifier = Modifier.padding(top = 16.dp), - onClick = {}, - label = "커리어 추가하기", - isEnabled = true - ) + Column( + modifier = Modifier.align(Alignment.Center), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = "경력을 등록하고 동문들과 정보를 나눠보세요!", + style = HsLinkTheme.typography.title_16Strong, + color = HsLinkTheme.colors.Grey700 + ) + Text( + text = "커리어를 등록하면 더 많은 정보를 얻을 수 있어요.", + style = HsLinkTheme.typography.body_14Normal, + color = HsLinkTheme.colors.Grey500 + ) + CareerSelectButton( + modifier = Modifier.padding(top = 16.dp), + onClick = onExperiencedPathClick, + label = "커리어 추가하기", + isEnabled = true + ) + } } } } } +@Composable +private fun CareerItem( + career: CareerItemEntity, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .fillMaxWidth() + .background( + color = HsLinkTheme.colors.Grey100, + shape = RoundedCornerShape(8.dp) + ) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = career.companyName, + style = HsLinkTheme.typography.title_16Strong, + color = HsLinkTheme.colors.Grey700 + ) + Text( + text = career.position, + style = HsLinkTheme.typography.body_14Normal, + color = HsLinkTheme.colors.Grey500 + ) + career.jobType?.let { + Text( + text = it.label, + style = HsLinkTheme.typography.body_14Normal, + color = HsLinkTheme.colors.Grey400 + ) + } + } +} + @Composable private fun CareerSelectButton( modifier: Modifier = Modifier, @@ -103,10 +168,7 @@ private fun CareerSelectButton( onClick = onClick, enabled = isEnabled, ) - .padding( - horizontal = 68.dp, - vertical = 9.dp - ), + .padding(horizontal = 68.dp, vertical = 9.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { @@ -124,19 +186,4 @@ private fun CareerSelectButton( style = HsLinkTheme.typography.btm_M ) } -} - -@Preview(showBackground = true) -@Composable -private fun ReviewCareerScreen() { - HsLinkTheme { - CareerScreen( - selectedStatus = null, - progress = 0.5f, - paddingValues = PaddingValues(0.dp), - onStatusSelect = {}, - onPreviousClick = {}, - onNextClick = {} - ) - } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/presentation/onboarding/component/screen/EmailScreen.kt b/app/src/main/java/com/hsLink/hslink/presentation/onboarding/component/screen/EmailScreen.kt new file mode 100644 index 0000000..22e674d --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/presentation/onboarding/component/screen/EmailScreen.kt @@ -0,0 +1,95 @@ +package com.hsLink.hslink.presentation.onboarding.component.screen + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.hsLink.hslink.core.designsystem.component.HsLinkTextField +import com.hsLink.hslink.core.designsystem.theme.HsLinkTheme +import com.hsLink.hslink.presentation.onboarding.OnboardingScreen + +@Composable +fun EmailScreen( + email: String, + progress: Float, + paddingValues: PaddingValues, + onEmailChange: (String) -> Unit, + onPreviousClick: () -> Unit, + onNextClick: () -> Unit, +) { + var isFocused by remember { mutableStateOf(false) } + + OnboardingScreen( + title = buildAnnotatedString { + append("이메일을 입력해주세요 ") + withStyle(style = SpanStyle(color = HsLinkTheme.colors.Red500)) { + append("*") + } + }, + subtitle = "본인 확인 및 계정 분실 시 사용됩니다.", + progress = progress, + paddingValues = paddingValues, + showPreviousButton = true, + nextButtonEnabled = email.isNotEmpty(), + onPreviousClick = onPreviousClick, + onNextClick = onNextClick + ) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = buildAnnotatedString { + append("이메일 ") + withStyle(style = SpanStyle(color = HsLinkTheme.colors.Red500)) { + append("*") + } + }, + color = HsLinkTheme.colors.Grey700, + style = HsLinkTheme.typography.title_14Strong + ) + HsLinkTextField( + value = email, + placeholder = "이메일을 입력해주세요", + onValueChanged = onEmailChange, + borderColor = if (isFocused) HsLinkTheme.colors.DeepBlue500 + else HsLinkTheme.colors.Grey300, + backgroundColor = HsLinkTheme.colors.Common, + onFocusChanged = { isFocused = it }, + modifier = Modifier.fillMaxWidth(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email), + imeAction = ImeAction.Done, + onDoneAction = { if (email.isNotEmpty()) onNextClick() } + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun EmailScreenPreview() { + HsLinkTheme { + EmailScreen( + email = "hsu@connect.com", + progress = 0.9f, + paddingValues = PaddingValues(), + onEmailChange = {}, + onPreviousClick = {}, + onNextClick = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/presentation/onboarding/component/screen/JobInfoScreen.kt b/app/src/main/java/com/hsLink/hslink/presentation/onboarding/component/screen/JobInfoScreen.kt index 1e2bc5b..3df8c31 100644 --- a/app/src/main/java/com/hsLink/hslink/presentation/onboarding/component/screen/JobInfoScreen.kt +++ b/app/src/main/java/com/hsLink/hslink/presentation/onboarding/component/screen/JobInfoScreen.kt @@ -1,21 +1,14 @@ package com.hsLink.hslink.presentation.onboarding.component.screen -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -26,40 +19,41 @@ import com.hsLink.hslink.core.designsystem.theme.HsLinkTheme import com.hsLink.hslink.presentation.onboarding.OnboardingScreen import com.hsLink.hslink.presentation.onboarding.model.JobType -@OptIn(ExperimentalLayoutApi::class) @Composable fun JobInfoScreen( - startDate: String, - endDate: String, - isCurrentlyEmployed: Boolean, companyName: String, - jobName: String, + position: String, + department: String, selectedJobType: JobType?, + startYm: String, + endYm: String?, + isCurrentlyEmployed: Boolean, progress: Float, paddingValues: PaddingValues, - onStartDateChange: (String) -> Unit, - onEndDateChange: (String) -> Unit, - onCurrentlyEmployedChange: (Boolean) -> Unit, onCompanyNameChange: (String) -> Unit, - onJobNameChange: (String) -> Unit, + onPositionChange: (String) -> Unit, + onDepartmentChange: (String) -> Unit, onJobTypeSelect: (JobType) -> Unit, - onCancelClick: () -> Unit, - onSaveClick: () -> Unit, + onStartDateChange: (String) -> Unit, + onEndDateChange: (String?) -> Unit, + onCurrentlyEmployedChange: (Boolean) -> Unit, + onPreviousClick: () -> Unit, + onNextClick: () -> Unit, ) { var companyFocused by remember { mutableStateOf(false) } - var jobNameFocused by remember { mutableStateOf(false) } + var positionFocused by remember { mutableStateOf(false) } var startDateFocused by remember { mutableStateOf(false) } var endDateFocused by remember { mutableStateOf(false) } - val isFormValid = companyName.isNotEmpty() && - jobName.isNotEmpty() && - startDate.isNotEmpty() && - (endDate.isNotEmpty() || isCurrentlyEmployed) && - selectedJobType != null + val isFormValid = companyName.isNotBlank() && + position.isNotBlank() && + selectedJobType != null && + startYm.matches("""^\d{4}-\d{2}$""".toRegex()) && + (isCurrentlyEmployed || (endYm != null && endYm.matches("""^\d{4}-\d{2}$""".toRegex()))) OnboardingScreen( title = buildAnnotatedString { - append("아래의 정보를 등록해주세요 ") + append("재직 정보를 등록해주세요 ") withStyle(style = SpanStyle(color = HsLinkTheme.colors.Red500)) { append("*") } @@ -69,16 +63,16 @@ fun JobInfoScreen( showPreviousButton = true, nextButtonEnabled = isFormValid, nextButtonLabel = "저장하기", - onPreviousClick = onCancelClick, - onNextClick = onSaveClick + onPreviousClick = onPreviousClick, + onNextClick = onNextClick ) { Column( - verticalArrangement = Arrangement.spacedBy(20.dp) + verticalArrangement = Arrangement.spacedBy(36.dp) ) { - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Text( text = buildAnnotatedString { - append("현재 재직 여부 (형식 : 24.04) ") + append("재직 기간 ") withStyle(style = SpanStyle(color = HsLinkTheme.colors.Red500)) { append("*") } @@ -86,32 +80,40 @@ fun JobInfoScreen( style = HsLinkTheme.typography.title_14Strong, color = HsLinkTheme.colors.Grey700 ) + Row( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically ) { - HsLinkTextField( - value = startDate, - placeholder = "근무시작일", - onValueChanged = onStartDateChange, + YearMonthTextField( + value = startYm, + placeholder = "YYYY-MM", modifier = Modifier.weight(1f), - borderColor = if (startDateFocused) HsLinkTheme.colors.SkyBlue500 else HsLinkTheme.colors.Grey300, - backgroundColor = HsLinkTheme.colors.Common, - onFocusChanged = { startDateFocused = it }, + onValueChanged = onStartDateChange, + isFocused = startDateFocused, + onFocusChanged = { startDateFocused = it } ) + Text(text = "~", style = HsLinkTheme.typography.body_16Normal) - HsLinkTextField( - value = endDate, - placeholder = "근무종료일", - onValueChanged = onEndDateChange, + + YearMonthTextField( + value = endYm ?: "", + placeholder = "YYYY-MM", modifier = Modifier.weight(1f), - borderColor = if (endDateFocused) HsLinkTheme.colors.SkyBlue500 else HsLinkTheme.colors.Grey300, - backgroundColor = HsLinkTheme.colors.Common, + onValueChanged = onEndDateChange, + isFocused = endDateFocused, onFocusChanged = { endDateFocused = it }, + enabled = !isCurrentlyEmployed ) + HsLinkSelectButton( label = "재직중", - onClick = { onCurrentlyEmployedChange(!isCurrentlyEmployed) }, + onClick = { + onCurrentlyEmployedChange(!isCurrentlyEmployed) + if (!isCurrentlyEmployed) { + onEndDateChange(null) + } + }, size = HsLinkButtonSize.Medium, isSelected = isCurrentlyEmployed ) @@ -153,13 +155,13 @@ fun JobInfoScreen( color = HsLinkTheme.colors.Grey700 ) HsLinkTextField( - value = jobName, - placeholder = "직무명을 입력해주세요", - onValueChanged = onJobNameChange, - borderColor = if (jobNameFocused) HsLinkTheme.colors.SkyBlue500 + value = position, + placeholder = "직무명을 입력해주세요 (예: Android 개발자)", + onValueChanged = onPositionChange, + borderColor = if (positionFocused) HsLinkTheme.colors.SkyBlue500 else HsLinkTheme.colors.Grey300, backgroundColor = HsLinkTheme.colors.Common, - onFocusChanged = { jobNameFocused = it }, + onFocusChanged = { positionFocused = it }, modifier = Modifier.fillMaxWidth() ) } @@ -179,36 +181,28 @@ fun JobInfoScreen( modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(12.dp), ) { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { HsLinkSelectButton( modifier = Modifier.weight(1f), - label = JobType.FULL_TIME.label, - onClick = { onJobTypeSelect(JobType.FULL_TIME) }, + label = JobType.PERMANENT.label, + onClick = { onJobTypeSelect(JobType.PERMANENT) }, size = HsLinkButtonSize.Large, - isEnabled = true, - isSelected = selectedJobType == JobType.FULL_TIME + isSelected = selectedJobType == JobType.PERMANENT ) HsLinkSelectButton( modifier = Modifier.weight(1f), - label = JobType.CONTRACT.label, - onClick = { onJobTypeSelect(JobType.CONTRACT) }, + label = JobType.TEMPORARY.label, + onClick = { onJobTypeSelect(JobType.TEMPORARY) }, size = HsLinkButtonSize.Large, - isEnabled = true, - isSelected = selectedJobType == JobType.CONTRACT + isSelected = selectedJobType == JobType.TEMPORARY ) } - - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { HsLinkSelectButton( modifier = Modifier.weight(1f), label = JobType.INTERN.label, onClick = { onJobTypeSelect(JobType.INTERN) }, size = HsLinkButtonSize.Large, - isEnabled = true, isSelected = selectedJobType == JobType.INTERN ) HsLinkSelectButton( @@ -216,39 +210,71 @@ fun JobInfoScreen( label = JobType.FREELANCER.label, onClick = { onJobTypeSelect(JobType.FREELANCER) }, size = HsLinkButtonSize.Large, - isEnabled = true, isSelected = selectedJobType == JobType.FREELANCER ) } - } } } } } +@Composable +private fun YearMonthTextField( + value: String, + placeholder: String, + modifier: Modifier = Modifier, + onValueChanged: (String) -> Unit, + isFocused: Boolean, + onFocusChanged: (Boolean) -> Unit, + enabled: Boolean = true +) { + HsLinkTextField( + value = value, + placeholder = placeholder, + modifier = modifier, + onValueChanged = { newValue -> + val filtered = newValue.filter { it.isDigit() || it == '-' } + val formatted = when { + filtered.length <= 4 -> filtered + filtered.length == 5 && !filtered.contains("-") -> + "${filtered.substring(0, 4)}-${filtered.substring(4)}" + filtered.length > 7 -> filtered.take(7) + else -> filtered + } + onValueChanged(formatted) + }, + borderColor = if (isFocused) HsLinkTheme.colors.SkyBlue500 + else HsLinkTheme.colors.Grey300, + backgroundColor = if (enabled) HsLinkTheme.colors.Common + else HsLinkTheme.colors.Grey100, + onFocusChanged = onFocusChanged, + ) +} @Preview(showBackground = true) @Composable private fun JobInfoScreenPreview() { HsLinkTheme { JobInfoScreen( - startDate = "24.01", - endDate = "", - isCurrentlyEmployed = true, companyName = "에이치스 링크", - jobName = "Android 개발자", - selectedJobType = JobType.FULL_TIME, + position = "Android 개발자", + department = "개발팀", + selectedJobType = JobType.PERMANENT, + startYm = "2024-01", + endYm = null, + isCurrentlyEmployed = true, progress = 0.5f, paddingValues = PaddingValues(), + onCompanyNameChange = {}, + onPositionChange = {}, + onDepartmentChange = {}, + onJobTypeSelect = {}, onStartDateChange = {}, onEndDateChange = {}, onCurrentlyEmployedChange = {}, - onCompanyNameChange = {}, - onJobNameChange = {}, - onJobTypeSelect = {}, - onCancelClick = {}, - onSaveClick = {} + onPreviousClick = {}, + onNextClick = {} ) } } \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/presentation/onboarding/component/screen/LinkAddScreen.kt b/app/src/main/java/com/hsLink/hslink/presentation/onboarding/component/screen/LinkAddScreen.kt new file mode 100644 index 0000000..d48c98d --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/presentation/onboarding/component/screen/LinkAddScreen.kt @@ -0,0 +1,141 @@ +package com.hsLink.hslink.presentation.onboarding.component.screen + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.hsLink.hslink.core.designsystem.component.HsLinkButtonSize +import com.hsLink.hslink.core.designsystem.component.HsLinkSelectButton +import com.hsLink.hslink.core.designsystem.component.HsLinkTextField +import com.hsLink.hslink.core.designsystem.theme.HsLinkTheme +import com.hsLink.hslink.presentation.onboarding.OnboardingScreen +import com.hsLink.hslink.presentation.onboarding.model.LinkType + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun LinkAddScreen( + type: LinkType?, + url: String, + progress: Float, + paddingValues: PaddingValues, + onTypeSelect: (LinkType) -> Unit, + onUrlChange: (String) -> Unit, + onPreviousClick: () -> Unit, + onNextClick: () -> Unit, + modifier: Modifier = Modifier, +) { + var urlFocused by remember { mutableStateOf(false) } + + val isFormValid = type != null && url.isNotBlank() && + (url.startsWith("http://") || url.startsWith("https://")) + + OnboardingScreen( + title = buildAnnotatedString { + append("링크 등록") + }, + progress = progress, + paddingValues = paddingValues, + subtitle = "나중에 멘토링 할 때 기본 정보로 활용됩니다.", + showPreviousButton = true, + nextButtonEnabled = isFormValid, + nextButtonLabel = "저장하기", + onPreviousClick = onPreviousClick, + onNextClick = onNextClick + ) { + Column( + verticalArrangement = Arrangement.spacedBy(20.dp), + modifier = modifier.fillMaxWidth() + ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + text = buildAnnotatedString { + append("링크 유형 구분 ") + withStyle(style = SpanStyle(color = HsLinkTheme.colors.Red500)) { + append("*") + } + }, + style = HsLinkTheme.typography.title_14Strong, + color = HsLinkTheme.colors.Grey700 + ) + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + LinkType.entries.forEach { linkType -> + HsLinkSelectButton( + label = linkType.label, + onClick = { onTypeSelect(linkType) }, + size = HsLinkButtonSize.Medium, + isSelected = type == linkType + ) + } + } + } + + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + text = buildAnnotatedString { + append("URL ") + withStyle(style = SpanStyle(color = HsLinkTheme.colors.Red500)) { + append("*") + } + }, + style = HsLinkTheme.typography.title_14Strong, + color = HsLinkTheme.colors.Grey700 + ) + HsLinkTextField( + value = url, + placeholder = "https://example.com", + onValueChanged = onUrlChange, + modifier = Modifier + .fillMaxWidth() + .onFocusChanged { urlFocused = it.isFocused }, + borderColor = if (urlFocused) HsLinkTheme.colors.SkyBlue500 else HsLinkTheme.colors.Grey300, + backgroundColor = HsLinkTheme.colors.Common, + onFocusChanged = { } + ) + + if (url.isNotBlank() && !url.startsWith("http://") && !url.startsWith("https://")) { + Text( + text = "URL은 http:// 또는 https://로 시작해야 합니다", + style = HsLinkTheme.typography.body_14Normal, + color = HsLinkTheme.colors.Red500 + ) + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun LinkAddScreenPreview() { + HsLinkTheme { + LinkAddScreen( + type = LinkType.GITHUB, + url = "https://github.com/hsu-link", + progress = 0.9f, + paddingValues = PaddingValues(0.dp), + onTypeSelect = {}, + onUrlChange = {}, + onPreviousClick = {}, + onNextClick = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/presentation/onboarding/component/screen/LinkScreen.kt b/app/src/main/java/com/hsLink/hslink/presentation/onboarding/component/screen/LinkScreen.kt index aa8cb3e..2e55095 100644 --- a/app/src/main/java/com/hsLink/hslink/presentation/onboarding/component/screen/LinkScreen.kt +++ b/app/src/main/java/com/hsLink/hslink/presentation/onboarding/component/screen/LinkScreen.kt @@ -10,222 +10,162 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.hsLink.hslink.R -import com.hsLink.hslink.core.designsystem.component.HsLinkButtonSize -import com.hsLink.hslink.core.designsystem.component.HsLinkSelectButton import com.hsLink.hslink.core.designsystem.theme.HsLinkTheme import com.hsLink.hslink.core.util.noRippleClickable +import com.hsLink.hslink.domain.model.search.LinkItemEntity import com.hsLink.hslink.presentation.onboarding.OnboardingScreen -import com.hsLink.hslink.presentation.onboarding.model.ExternalLink -import com.hsLink.hslink.presentation.onboarding.model.LinkType -import com.hsLink.hslink.presentation.onboarding.screen.AddLinkDialog @Composable fun LinksScreen( - links: List, + linkList: List, progress: Float, paddingValues: PaddingValues, - onAddLink: (ExternalLink) -> Unit, - onRemoveLink: (ExternalLink) -> Unit, onPreviousClick: () -> Unit, onNextClick: () -> Unit, + onAddLinkClick: () -> Unit, + modifier: Modifier = Modifier, ) { - var showLinkDialog by remember { mutableStateOf(false) } - OnboardingScreen( - title = "자신을 소개할 수 있는\n외부 링크를 추가해주세요", + title = "외부 링크를 등록해보세요", progress = progress, - subtitle = "자신을 소개할 수 있는 링크가 없으면 넘겨도 괜찮아요", + subtitle = "블로그, 깃허브, 포트폴리오 등을 등록하면 동문들과 더 쉽게 연결될 수 있어요.", paddingValues = paddingValues, showPreviousButton = true, nextButtonEnabled = true, + nextButtonLabel = "완료", onPreviousClick = onPreviousClick, onNextClick = onNextClick ) { Column( - modifier = Modifier.fillMaxSize(), + modifier = modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(16.dp) ) { - - if (links.isEmpty()) { - Box( - modifier = Modifier - .fillMaxWidth() - .height(180.dp) - .background( - color = HsLinkTheme.colors.Common, - shape = RoundedCornerShape(12.dp) - ) - .border( - width = 1.dp, - color = HsLinkTheme.colors.Grey100, - shape = RoundedCornerShape(12.dp) - ) - ) { - Column( - modifier = Modifier.align(Alignment.Center), - horizontalAlignment = Alignment.CenterHorizontally, + Box( + modifier = Modifier + .weight(1f) + .fillMaxWidth() + ) { + if (linkList.isNotEmpty()) { + LazyColumn( + modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(12.dp) ) { - Text( - text = "등록된 링크가 없어요", - style = HsLinkTheme.typography.title_16Strong, - color = HsLinkTheme.colors.Grey700 - ) - Text( - text = "자신을 소개할 수 있는 링크를 추가해보세요", - style = HsLinkTheme.typography.body_14Normal, - color = HsLinkTheme.colors.Grey500 - ) + items(linkList) { link -> + LinkItem(link) + } } } + } - } else { + Box( + modifier = Modifier + .fillMaxWidth() + .background( + color = HsLinkTheme.colors.Common, + shape = RoundedCornerShape(12.dp) + ) + .border( + width = 1.dp, + color = HsLinkTheme.colors.Grey100, + shape = RoundedCornerShape(12.dp) + ) + .padding(vertical = 40.dp) + ) { Column( - verticalArrangement = Arrangement.spacedBy(12.dp) + modifier = Modifier.align(Alignment.Center), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) ) { - links.forEach { link -> - LinkItem( - link = link, - onClick = { onRemoveLink(link) } - ) - } + Text( + text = "외부 링크를 추가해보세요", + style = HsLinkTheme.typography.title_16Strong, + color = HsLinkTheme.colors.Grey700 + ) + Text( + text = "블로그, 깃허브, 포트폴리오 등을 등록할 수 있어요.", + style = HsLinkTheme.typography.body_14Normal, + color = HsLinkTheme.colors.Grey500 + ) + LinkAddButton( + modifier = Modifier.padding(top = 16.dp), + onClick = onAddLinkClick, + label = "링크 추가하기" + ) } } - - Spacer(modifier = Modifier.height(8.dp)) - - HsLinkSelectButton( - label = "+ 추가하기", - onClick = { showLinkDialog = true }, - size = HsLinkButtonSize.Large, - modifier = Modifier.fillMaxWidth(), - isEnabled = true, - isSelected = false - ) } } - - if (showLinkDialog) { - AddLinkDialog( - onDismiss = { showLinkDialog = false }, - onConfirm = { link -> - onAddLink(link) - showLinkDialog = false - } - ) - } } - @Composable -private fun LinkItem( - link: ExternalLink, - onClick: () -> Unit -) { - Row( - modifier = Modifier +private fun LinkItem(link: LinkItemEntity, modifier: Modifier = Modifier) { + Column( + modifier = modifier .fillMaxWidth() - .background(HsLinkTheme.colors.Common) - .noRippleClickable(onClick = onClick) - .padding(vertical = 16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - Text( - text = link.type.label, - style = HsLinkTheme.typography.body_16Normal, - color = HsLinkTheme.colors.Grey600 + .background( + color = HsLinkTheme.colors.Grey100, + shape = RoundedCornerShape(8.dp) ) - + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + link.type?.let { Text( - text = link.url.extractDomain(), - style = HsLinkTheme.typography.body_14Normal, - color = HsLinkTheme.colors.Grey400 + text = it.label, + style = HsLinkTheme.typography.title_14Strong, + color = HsLinkTheme.colors.Grey700 ) } - - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_home_post_arrow), - contentDescription = "상세보기", - tint = HsLinkTheme.colors.Grey300 + Text( + text = link.url, + style = HsLinkTheme.typography.body_14Normal, + color = HsLinkTheme.colors.SkyBlue500 ) } } -private fun String.extractDomain(): String { - return try { - val withoutProtocol = this.removePrefix("https://") - .removePrefix("http://") - .removePrefix("www.") - - val domain = withoutProtocol.split("/")[0] - - when { - domain.contains("instagram.com") -> "인스타그램" - domain.contains("github.com") -> "깃허브" - domain.contains("linkedin.com") -> "링크드인" - domain.contains("notion.so") || domain.contains("notion.site") -> "노션" - else -> domain.take(30) - } - } catch (e: Exception) { - this.take(30) - } -} - -@Preview(showBackground = true) @Composable -private fun LinksScreenWithDataPreview() { - HsLinkTheme { - LinksScreen( - links = listOf( - ExternalLink(LinkType.GITHUB, "https://instagram.com/username"), - ExternalLink(LinkType.PORTFOLIO, "https://drive.google.com/...") - ), - progress = 0.9f, - paddingValues = PaddingValues(), - onAddLink = {}, - onRemoveLink = {}, - onPreviousClick = {}, - onNextClick = {} +private fun LinkAddButton( + modifier: Modifier = Modifier, + onClick: () -> Unit, + label: String, + isEnabled: Boolean = true, +) { + Row( + modifier = modifier + .background( + color = HsLinkTheme.colors.Grey100, + shape = RoundedCornerShape(8.dp) + ) + .noRippleClickable(onClick = onClick, enabled = isEnabled) + .padding(horizontal = 68.dp, vertical = 9.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_onboarding_plus), + contentDescription = "링크 추가하기", + tint = HsLinkTheme.colors.Grey500 ) - } -} - -@Preview(showBackground = true) -@Composable -private fun LinksScreenEmptyPreview() { - HsLinkTheme { - LinksScreen( - links = emptyList(), - progress = 0.9f, - paddingValues = PaddingValues(), - onAddLink = {}, - onRemoveLink = {}, - onPreviousClick = {}, - onNextClick = {} + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = label, + color = HsLinkTheme.colors.Grey500, + style = HsLinkTheme.typography.btm_M ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/hsLink/hslink/presentation/onboarding/model/OnboardingState.kt b/app/src/main/java/com/hsLink/hslink/presentation/onboarding/model/OnboardingState.kt index 1547736..5ec10ae 100644 --- a/app/src/main/java/com/hsLink/hslink/presentation/onboarding/model/OnboardingState.kt +++ b/app/src/main/java/com/hsLink/hslink/presentation/onboarding/model/OnboardingState.kt @@ -1,38 +1,66 @@ package com.hsLink.hslink.presentation.onboarding.model +import com.hsLink.hslink.domain.model.search.CareerItemEntity +import com.hsLink.hslink.domain.model.search.LinkItemEntity + data class OnboardingState( val studentId: String = "", val name: String = "", val major: String = "", val majorQuery: String = "", val employmentStatus: EmploymentStatus? = null, + val career: Boolean? = null, + val isExperiencedPath: Boolean = false, val companyName: String = "", - val jobName: String = "", - val startDate: String = "", - val endDate: String = "", - val isCurrentlyEmployed: Boolean = false, + val position: String = "", + val department: String = "", val jobType: JobType? = null, + val careerList: List = emptyList(), + val linkList: List = emptyList(), + val tempCompanyName: String = "", + val tempPosition: String = "", + val tempDepartment: String = "", + val tempJobType: JobType? = null, + val tempStartYm: String = "", + val tempEndYm: String? = null, + val tempIsEmployed: Boolean = false, + val tempLinkType: LinkType? = null, + val tempLinkUrl: String = "", val wantsMentorship: Boolean? = null, val isJobSeeking: Boolean? = null, - val links: List = emptyList(), val currentStep: OnboardingStep = OnboardingStep.STUDENT_ID, + val email: String = "", + val isLoading: Boolean = false, + val apiError: String? = null ) -enum class OnboardingStep(val stepNumber: Int, val totalSteps: Int = 9) { + +enum class OnboardingStep(val stepNumber: Int, val totalSteps: Int = 11) { STUDENT_ID(1), NAME(2), MAJOR(3), EMPLOYMENT_STATUS(4), - CAREER(5), JOB_INFO(6), - MENTORSHIP(7), - LINKS(8), - JOB_SEEKING(9); + JOB_SEEKING(7), + MENTORSHIP(8), + EMAIL(9), + LINKS_LIST(10), + LINKS(11); val progress: Float get() = stepNumber.toFloat() / totalSteps.toFloat() + + fun next(): OnboardingStep? { + val nextStepNumber = this.stepNumber + 1 + return entries.find { it.stepNumber == nextStepNumber } + } + + fun previous(): OnboardingStep? { + val previousStepNumber = this.stepNumber - 1 + return entries.find { it.stepNumber == previousStepNumber } + } } @@ -45,8 +73,8 @@ enum class EmploymentStatus(val label: String) { } enum class JobType(val label: String) { - FULL_TIME("정규직"), - CONTRACT("계약직"), + PERMANENT("정규직"), + TEMPORARY("계약직"), INTERN("인턴"), FREELANCER("프리랜서") } diff --git a/app/src/main/java/com/hsLink/hslink/presentation/onboarding/viewmodel/OnboardingViewModel.kt b/app/src/main/java/com/hsLink/hslink/presentation/onboarding/viewmodel/OnboardingViewModel.kt index 64d9edb..913282e 100644 --- a/app/src/main/java/com/hsLink/hslink/presentation/onboarding/viewmodel/OnboardingViewModel.kt +++ b/app/src/main/java/com/hsLink/hslink/presentation/onboarding/viewmodel/OnboardingViewModel.kt @@ -2,14 +2,17 @@ package com.hsLink.hslink.presentation.onboarding.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.hsLink.hslink.data.dto.request.onboarding.CareerRequest +import com.hsLink.hslink.data.dto.request.onboarding.LinkRequest +import com.hsLink.hslink.data.dto.request.onboarding.OnboardingRequest +import com.hsLink.hslink.domain.repository.onboarding.OnboardingRepository import com.hsLink.hslink.presentation.onboarding.model.EmploymentStatus -import com.hsLink.hslink.presentation.onboarding.model.ExternalLink import com.hsLink.hslink.presentation.onboarding.model.JobType +import com.hsLink.hslink.presentation.onboarding.model.LinkType import com.hsLink.hslink.presentation.onboarding.model.OnboardingState import com.hsLink.hslink.presentation.onboarding.model.OnboardingStep import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -17,114 +20,233 @@ import javax.inject.Inject @HiltViewModel class OnboardingViewModel @Inject constructor( - // private val userRepository: UserRepository + private val onboardingRepository: OnboardingRepository ) : ViewModel() { private val _state = MutableStateFlow(OnboardingState()) - val state: StateFlow = _state.asStateFlow() + val state = _state.asStateFlow() - fun updateStudentId(studentId: String) { - _state.update { it.copy(studentId = studentId) } - } - - fun updateName(name: String) { - _state.update { it.copy(name = name) } + init { + viewModelScope.launch { + fetchCareersAndLinks() + } } - - fun updateMajorQuery(query: String) { - _state.update { it.copy(majorQuery = query) } + fun refreshCareersAndLinks() { + viewModelScope.launch { + fetchCareersAndLinks() + } } - fun updateMajor(major: String) { - _state.update { it.copy(major = major) } - } - fun updateEmploymentStatus(status: EmploymentStatus) { - _state.update { it.copy(employmentStatus = status) } - } + private suspend fun fetchCareersAndLinks() { + _state.update { it.copy(isLoading = true) } - fun updateCompanyName(companyName: String) { - _state.update { it.copy(companyName = companyName) } - } + val careersResult = onboardingRepository.getCareers() + val linksResult = onboardingRepository.getLinks() + _state.update { currentState -> + val newCareerList = careersResult.getOrNull() ?: currentState.careerList + val newLinkList = linksResult.getOrNull() ?: currentState.linkList - fun updateJobName(name: String) { - _state.value = _state.value.copy(jobName = name) + currentState.copy( + isLoading = false, + careerList = newCareerList, + linkList = newLinkList, + apiError = careersResult.exceptionOrNull()?.message + ?: linksResult.exceptionOrNull()?.message + ) + } } - fun updateJobType(jobType: JobType) { - _state.value = _state.value.copy(jobType = jobType) - } - fun updateStartDate(date: String) { - _state.value = _state.value.copy(startDate = date) - } + fun moveToNextStep() { + val currentState = _state.value + val nextStep = when (currentState.currentStep) { + OnboardingStep.CAREER -> { + if (currentState.isExperiencedPath) { + OnboardingStep.JOB_SEEKING + } else { + OnboardingStep.MENTORSHIP + } + } + OnboardingStep.EMAIL -> OnboardingStep.LINKS_LIST + OnboardingStep.LINKS_LIST -> currentState.currentStep.next() + OnboardingStep.LINKS -> null + else -> currentState.currentStep.next() + } - fun updateEndDate(date: String) { - _state.value = _state.value.copy(endDate = date) + nextStep?.let { + _state.update { it.copy(currentStep = nextStep) } + } } - fun updateIsCurrentlyEmployed(isEmployed: Boolean) { - _state.value = _state.value.copy( - isCurrentlyEmployed = isEmployed, - endDate = if (isEmployed) "" else _state.value.endDate - ) - } + fun moveToPreviousStep() { + val currentState = _state.value + val previousStep = when (currentState.currentStep) { + OnboardingStep.MENTORSHIP -> { + if (currentState.isExperiencedPath) OnboardingStep.JOB_SEEKING else OnboardingStep.CAREER + } + OnboardingStep.LINKS_LIST -> OnboardingStep.EMAIL + OnboardingStep.LINKS -> OnboardingStep.LINKS_LIST + OnboardingStep.JOB_INFO -> OnboardingStep.CAREER + else -> currentState.currentStep.previous() + } - fun updateMentorship(wantsMentorship: Boolean) { - _state.update { it.copy(wantsMentorship = wantsMentorship) } + previousStep?.let { + _state.update { it.copy(currentStep = previousStep) } + } } - fun updateJobSeeking(isJobSeeking: Boolean) { - _state.update { it.copy(isJobSeeking = isJobSeeking) } + fun openJobInfoForm() { + _state.update { + it.copy( + currentStep = OnboardingStep.JOB_INFO, + tempCompanyName = "", tempPosition = "", tempDepartment = "", tempJobType = null, + tempStartYm = "", tempEndYm = null, tempIsEmployed = false + ) + } } - fun addLink(link: ExternalLink) { - _state.update { it.copy(links = it.links + link) } + fun submitJobInfoAndReturnToCareerList() { + viewModelScope.launch { + val s = _state.value + val request = CareerRequest( + companyName = s.tempCompanyName, + position = s.tempPosition, + jobType = s.tempJobType ?: run { + return@launch + }, + startYm = s.tempStartYm, + endYm = s.tempEndYm, + employed = s.tempIsEmployed + ) + + _state.update { it.copy(isLoading = true) } + + val result = onboardingRepository.submitCareer(request) + + if (result.isSuccess) { + + fetchCareersAndLinks() + + _state.update { + it.copy( + currentStep = OnboardingStep.CAREER, + isLoading = false, + tempCompanyName = "", + tempPosition = "", + tempDepartment = "", + tempJobType = null, + tempStartYm = "", + tempEndYm = null, + tempIsEmployed = false, + apiError = null + ) + } + + } else { + _state.update { + it.copy( + apiError = result.exceptionOrNull()?.message, + isLoading = false + ) + } + } + } } - fun removeLink(link: ExternalLink) { - _state.update { it.copy(links = it.links - link) } - } + fun openLinkForm() { - fun moveToNextStep() { _state.update { - val nextStep = when (it.currentStep) { - OnboardingStep.STUDENT_ID -> OnboardingStep.NAME - OnboardingStep.NAME -> OnboardingStep.MAJOR - OnboardingStep.MAJOR -> OnboardingStep.EMPLOYMENT_STATUS - OnboardingStep.EMPLOYMENT_STATUS -> OnboardingStep.CAREER - OnboardingStep.CAREER -> OnboardingStep.JOB_INFO - OnboardingStep.JOB_INFO -> OnboardingStep.MENTORSHIP - OnboardingStep.MENTORSHIP -> OnboardingStep.LINKS - OnboardingStep.LINKS -> OnboardingStep.JOB_SEEKING - OnboardingStep.JOB_SEEKING -> OnboardingStep.MENTORSHIP - } - it.copy(currentStep = nextStep) + it.copy( + currentStep = OnboardingStep.LINKS, + tempLinkType = null, + tempLinkUrl = "" + ) } } - fun moveToPreviousStep() { - _state.update { - val previousStep = when (it.currentStep) { - OnboardingStep.STUDENT_ID -> OnboardingStep.STUDENT_ID - OnboardingStep.NAME -> OnboardingStep.STUDENT_ID - OnboardingStep.MAJOR -> OnboardingStep.NAME - OnboardingStep.EMPLOYMENT_STATUS -> OnboardingStep.MAJOR - OnboardingStep.CAREER -> OnboardingStep.EMPLOYMENT_STATUS - OnboardingStep.JOB_INFO -> OnboardingStep.CAREER - OnboardingStep.MENTORSHIP -> OnboardingStep.JOB_INFO - OnboardingStep.LINKS -> OnboardingStep.MENTORSHIP - OnboardingStep.JOB_SEEKING -> OnboardingStep.LINKS + fun submitLinkAndReturnToLinkList() { + viewModelScope.launch { + val s = _state.value + val request = LinkRequest( + type = s.tempLinkType ?: run { + return@launch + }, + url = s.tempLinkUrl + ) + + _state.update { it.copy(isLoading = true) } + + val result = onboardingRepository.submitLink(request) + + if (result.isSuccess) { + + fetchCareersAndLinks() + + _state.update { + it.copy( + currentStep = OnboardingStep.LINKS_LIST, + isLoading = false, + tempLinkType = null, + tempLinkUrl = "", + apiError = null + ) + } + + } else { + _state.update { + it.copy( + apiError = result.exceptionOrNull()?.message, + isLoading = false + ) + } } - it.copy(currentStep = previousStep) } } + fun updateTempCompanyName(name: String) { _state.update { it.copy(tempCompanyName = name) } } + fun updateTempPosition(position: String) { _state.update { it.copy(tempPosition = position) } } + fun updateTempDepartment(department: String) { _state.update { it.copy(tempDepartment = department) } } + fun updateTempJobType(jobType: JobType) { _state.update { it.copy(tempJobType = jobType) } } + fun updateTempStartYm(date: String) { _state.update { it.copy(tempStartYm = date) } } + fun updateTempEndYm(date: String?) { _state.update { it.copy(tempEndYm = date) } } + fun updateTempIsEmployed(isEmployed: Boolean) { _state.update { it.copy(tempIsEmployed = isEmployed) } } + + fun updateTempLinkType(type: LinkType) { _state.update { it.copy(tempLinkType = type) } } + fun updateTempLinkUrl(url: String) { _state.update { it.copy(tempLinkUrl = url) } } + + + fun updateStudentId(studentId: String) { _state.update { it.copy(studentId = studentId) } } + fun updateName(name: String) { _state.update { it.copy(name = name) } } + fun updateMajor(major: String) { _state.update { it.copy(major = major) } } + fun updateMajorQuery(query: String) { _state.update { it.copy(majorQuery = query) } } + fun updateEmploymentStatus(status: EmploymentStatus) { _state.update { it.copy(employmentStatus = status) } } + fun updateCareer(isExperienced: Boolean) { + _state.update { it.copy(career = isExperienced, isExperiencedPath = isExperienced) } + } + fun updateJobSeeking(isJobSeeking: Boolean) { _state.update { it.copy(isJobSeeking = isJobSeeking) } } + fun updateMentorship(wantsMentorship: Boolean) { _state.update { it.copy(wantsMentorship = wantsMentorship) } } + fun updateEmail(email: String) { _state.update { it.copy(email = email) } } + fun submitOnboarding() { viewModelScope.launch { - // userRepository.submitOnboarding(state.value) + val currentState = _state.value + val request = OnboardingRequest( + name = currentState.name, major = currentState.major, studentNumber = currentState.studentId, + jobSeeking = currentState.isJobSeeking ?: false, mentor = currentState.wantsMentorship ?: false, + email = currentState.email + ) + + onboardingRepository.submitOnboarding(request) + .onSuccess { + println("Onboarding Success") + // TODO: Navigate to home or show success message + } + .onFailure { + println("Onboarding Failed: ${it.message}") + // TODO: Show error message to user + } } } - } \ No newline at end of file