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 fa50490..2eb8211 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 @@ -2,8 +2,10 @@ package com.hsLink.hslink.data.di import com.hsLink.hslink.data.remote.datasource.CommunityPostDataSource 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.PostDataSourceImpl +import com.hsLink.hslink.data.remote.datasourceimpl.SearchDataSourceImpl import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -21,4 +23,9 @@ interface DataSourceModule { abstract fun bindsPostLocalDataSource( communityPostDataSourceImpl: CommunityPostDataSourceImpl, ): CommunityPostDataSource + + @Binds + abstract fun bindsSearchDataSource( + searchDataSourceImpl: SearchDataSourceImpl, + ): SearchDataSource } \ 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 506224b..18a6b30 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,10 +4,12 @@ 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.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.search.SearchRepository import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -38,4 +40,9 @@ interface RepositoryModule { fun bindsCommunityPostRepository( communityPostRepositoryImpl: CommunityRepositoryImpl, ): CommunityRepository + + @Binds + fun bindsSearchRepository( + searchRepositoryImpl: SearchRepositoryImpl, + ): SearchRepository } diff --git a/app/src/main/java/com/hsLink/hslink/data/di/ServiceModule.kt b/app/src/main/java/com/hsLink/hslink/data/di/ServiceModule.kt index 87df03e..694d57e 100644 --- a/app/src/main/java/com/hsLink/hslink/data/di/ServiceModule.kt +++ b/app/src/main/java/com/hsLink/hslink/data/di/ServiceModule.kt @@ -1,6 +1,7 @@ package com.hsLink.hslink.data.di import com.hsLink.hslink.data.service.DummyService +import com.hsLink.hslink.data.service.search.SearchService import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -17,4 +18,10 @@ object ServiceModule { fun providesDummyService(retrofit: Retrofit): DummyService = retrofit.create(DummyService::class.java) + @Provides + @Singleton + fun provideSearchService(retrofit: Retrofit): SearchService { + return retrofit.create(SearchService::class.java) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/data/dto/request/SocialLoginRequest.kt b/app/src/main/java/com/hsLink/hslink/data/dto/request/SocialLoginRequestDto.kt similarity index 82% rename from app/src/main/java/com/hsLink/hslink/data/dto/request/SocialLoginRequest.kt rename to app/src/main/java/com/hsLink/hslink/data/dto/request/SocialLoginRequestDto.kt index 4b372a1..fe22c4e 100644 --- a/app/src/main/java/com/hsLink/hslink/data/dto/request/SocialLoginRequest.kt +++ b/app/src/main/java/com/hsLink/hslink/data/dto/request/SocialLoginRequestDto.kt @@ -3,7 +3,7 @@ package com.hsLink.hslink.data.dto.request import kotlinx.serialization.Serializable @Serializable -data class SocialLoginRequest( +data class SocialLoginRequestDto( val provider: String, val accessToken: String ) \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/data/dto/response/SearchResponseDto.kt b/app/src/main/java/com/hsLink/hslink/data/dto/response/SearchResponseDto.kt new file mode 100644 index 0000000..b5c6e88 --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/data/dto/response/SearchResponseDto.kt @@ -0,0 +1,28 @@ +package com.hsLink.hslink.data.dto.response + +import kotlinx.serialization.Serializable + +@Serializable +data class MentorListResponseDto( + val isSuccess: Boolean, + val code: String, + val message: String, + val result: MentorListResultDto +) +@Serializable +data class MentorListResultDto( + val totalMentorCount: Long, + val page: Int, + val size: Int, + val totalPages: Int, + val items: List +) +@Serializable +data class MentorItemDto( + val userId: Long, + val name: String, + val major: String, + val jobSeeking: Boolean, + val employed: Boolean, + val academicStatus: String +) \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/data/dto/response/SocialLoginResponse.kt b/app/src/main/java/com/hsLink/hslink/data/dto/response/SocialLoginResponseDto.kt similarity index 86% rename from app/src/main/java/com/hsLink/hslink/data/dto/response/SocialLoginResponse.kt rename to app/src/main/java/com/hsLink/hslink/data/dto/response/SocialLoginResponseDto.kt index 42b4fb3..b1ef367 100644 --- a/app/src/main/java/com/hsLink/hslink/data/dto/response/SocialLoginResponse.kt +++ b/app/src/main/java/com/hsLink/hslink/data/dto/response/SocialLoginResponseDto.kt @@ -3,7 +3,7 @@ package com.hsLink.hslink.data.dto.response import kotlinx.serialization.Serializable @Serializable -data class SocialLoginResponse( +data class SocialLoginResponseDto( val accessToken: String, val refreshToken: String, val isNewUser: Boolean, diff --git a/app/src/main/java/com/hsLink/hslink/data/dto/response/UserProfileResponseDto.kt b/app/src/main/java/com/hsLink/hslink/data/dto/response/UserProfileResponseDto.kt new file mode 100644 index 0000000..6ebf15c --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/data/dto/response/UserProfileResponseDto.kt @@ -0,0 +1,40 @@ +package com.hsLink.hslink.data.dto.response + +import kotlinx.serialization.Serializable + +@Serializable +data class UserProfileResponseDto( + val isSuccess: Boolean, + val code: String, + val message: String, + val result: UserProfileDto +) +@Serializable +data class UserProfileDto( + val userId: Long, + val name: String, + val studentNumberPrefix: String, + val major: String, + val email: String, + val jobSeeking: Boolean, + val employed: Boolean, + val academicStatus: String, + val careers: List, + val links: List +) +@Serializable +data class CareerItemDto( + val id: Long, + val companyName: String, + val position: String, + val jobType: String, + val employed: Boolean, + val startYm: String, + val endYm: String? +) +@Serializable +data class LinkItemDto( + val id: Long, + val type: String, + val url: String +) \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/data/remote/datasource/SearchDataSource.kt b/app/src/main/java/com/hsLink/hslink/data/remote/datasource/SearchDataSource.kt new file mode 100644 index 0000000..9737b0d --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/data/remote/datasource/SearchDataSource.kt @@ -0,0 +1,9 @@ +package com.hsLink.hslink.data.remote.datasource + +import com.hsLink.hslink.data.dto.response.MentorListResponseDto +import com.hsLink.hslink.data.dto.response.UserProfileResponseDto + +interface SearchDataSource { + suspend fun getMentors(page: Int, size: Int): MentorListResponseDto + suspend fun getUserProfile(userId: Long): UserProfileResponseDto +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/data/remote/datasourceimpl/SearchDataSourceImpl.kt b/app/src/main/java/com/hsLink/hslink/data/remote/datasourceimpl/SearchDataSourceImpl.kt new file mode 100644 index 0000000..e105d13 --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/data/remote/datasourceimpl/SearchDataSourceImpl.kt @@ -0,0 +1,20 @@ +package com.hsLink.hslink.data.remote.datasourceimpl + +import com.hsLink.hslink.data.dto.response.MentorListResponseDto +import com.hsLink.hslink.data.dto.response.UserProfileResponseDto +import com.hsLink.hslink.data.remote.datasource.SearchDataSource +import com.hsLink.hslink.data.service.search.SearchService +import javax.inject.Inject + +class SearchDataSourceImpl @Inject constructor( + private val searchService: SearchService +) : SearchDataSource { + + override suspend fun getMentors(page: Int, size: Int): MentorListResponseDto { + return searchService.getMentors(page, size) + } + + override suspend fun getUserProfile(userId: Long): UserProfileResponseDto { + return searchService.getUserProfile(userId) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/data/repositoryimpl/AuthRepositoryImpl.kt b/app/src/main/java/com/hsLink/hslink/data/repositoryimpl/AuthRepositoryImpl.kt index 4e46295..3a499ec 100644 --- a/app/src/main/java/com/hsLink/hslink/data/repositoryimpl/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/hsLink/hslink/data/repositoryimpl/AuthRepositoryImpl.kt @@ -1,7 +1,7 @@ package com.hsLink.hslink.data.repositoryimpl -import com.hsLink.hslink.data.dto.request.SocialLoginRequest -import com.hsLink.hslink.data.dto.response.SocialLoginResponse +import com.hsLink.hslink.data.dto.request.SocialLoginRequestDto +import com.hsLink.hslink.data.dto.response.SocialLoginResponseDto import com.hsLink.hslink.data.service.login.AuthService import com.hsLink.hslink.domain.repository.AuthRepository import javax.inject.Inject @@ -13,9 +13,9 @@ class AuthRepositoryImpl @Inject constructor( override suspend fun loginWithSocialToken( provider: String, accessToken: String - ): Result { + ): Result { return try { - val request = SocialLoginRequest(provider, accessToken) + val request = SocialLoginRequestDto(provider, accessToken) val response = authService.socialLogin(request) if (response.isSuccessful) { diff --git a/app/src/main/java/com/hsLink/hslink/data/repositoryimpl/search/SearchRepositoryImpl.kt b/app/src/main/java/com/hsLink/hslink/data/repositoryimpl/search/SearchRepositoryImpl.kt new file mode 100644 index 0000000..70a13b5 --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/data/repositoryimpl/search/SearchRepositoryImpl.kt @@ -0,0 +1,93 @@ +package com.hsLink.hslink.data.repositoryimpl.search + +import android.util.Log +import com.hsLink.hslink.data.remote.datasource.SearchDataSource +import com.hsLink.hslink.domain.model.search.MentorListEntity +import com.hsLink.hslink.domain.model.search.MentorEntity +import com.hsLink.hslink.domain.model.search.UserProfileEntity +import com.hsLink.hslink.domain.model.search.CareerEntity +import com.hsLink.hslink.domain.model.search.LinkEntity +import com.hsLink.hslink.domain.repository.search.SearchRepository +import javax.inject.Inject + +class SearchRepositoryImpl @Inject constructor( + private val searchDataSource: SearchDataSource +) : SearchRepository { + + override suspend fun getMentors(page: Int, size: Int): Result { + Log.d("SearchRepository", "getMentors 호출: page=$page, size=$size") + return try { + val response = searchDataSource.getMentors(page, size) + Log.d("SearchRepository", "API 응답: isSuccess=${response.isSuccess}, message=${response.message}") + + if (response.isSuccess) { + val mentorListEntity = MentorListEntity( + totalMentorCount = response.result.totalMentorCount, + page = response.result.page, + size = response.result.size, + totalPages = response.result.totalPages, + mentors = response.result.items.map { dto -> + MentorEntity( + userId = dto.userId, + name = dto.name, + major = dto.major, + jobSeeking = dto.jobSeeking, + employed = dto.employed, + academicStatus = dto.academicStatus + ) + } + ) + Log.d("SearchRepository", "변환 완료: ${mentorListEntity.mentors.size}개 멘토") + Result.success(mentorListEntity) + } else { + Log.e("SearchRepository", "API 실패: ${response.message}") + Result.failure(Exception(response.message)) + } + } catch (e: Exception) { + Log.e("SearchRepository", "예외 발생: ${e.message}", e) + Result.failure(e) + } + } + + + override suspend fun getUserProfile(userId: Long): Result { + return try { + val response = searchDataSource.getUserProfile(userId) + if (response.isSuccess) { + val userProfileEntity = UserProfileEntity( + userId = response.result.userId, + name = response.result.name, + studentNumberPrefix = response.result.studentNumberPrefix, + major = response.result.major, + email = response.result.email, + jobSeeking = response.result.jobSeeking, + employed = response.result.employed, + academicStatus = response.result.academicStatus, + careers = response.result.careers.map { dto -> + CareerEntity( + id = dto.id, + companyName = dto.companyName, + position = dto.position, + jobType = dto.jobType, + employed = dto.employed, + startYm = dto.startYm, + endYm = dto.endYm + ) + }, + links = response.result.links.map { dto -> + LinkEntity( + id = dto.id, + type = dto.type, + url = dto.url + ) + } + ) + Result.success(userProfileEntity) + } else { + Result.failure(Exception(response.message)) + } + } catch (e: Exception) { + Result.failure(e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/data/service/login/AuthService.kt b/app/src/main/java/com/hsLink/hslink/data/service/login/AuthService.kt index 0985bf1..6b63311 100644 --- a/app/src/main/java/com/hsLink/hslink/data/service/login/AuthService.kt +++ b/app/src/main/java/com/hsLink/hslink/data/service/login/AuthService.kt @@ -1,12 +1,12 @@ package com.hsLink.hslink.data.service.login -import com.hsLink.hslink.data.dto.request.SocialLoginRequest -import com.hsLink.hslink.data.dto.response.SocialLoginResponse +import com.hsLink.hslink.data.dto.request.SocialLoginRequestDto +import com.hsLink.hslink.data.dto.response.SocialLoginResponseDto import retrofit2.Response import retrofit2.http.Body import retrofit2.http.POST interface AuthService { @POST("auth/login") - suspend fun socialLogin(@Body request: SocialLoginRequest): Response + suspend fun socialLogin(@Body request: SocialLoginRequestDto): Response } \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/data/service/search/SearchService.kt b/app/src/main/java/com/hsLink/hslink/data/service/search/SearchService.kt new file mode 100644 index 0000000..39e83df --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/data/service/search/SearchService.kt @@ -0,0 +1,21 @@ +package com.hsLink.hslink.data.service.search + +import com.hsLink.hslink.data.dto.response.MentorListResponseDto +import com.hsLink.hslink.data.dto.response.UserProfileResponseDto +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Query + +interface SearchService { + + @GET("users/mentors") + suspend fun getMentors( + @Query("page") page: Int = 0, + @Query("size") size: Int = 15 + ): MentorListResponseDto + + @GET("users/profiles/{userId}") + suspend fun getUserProfile( + @Path("userId") userId: Long + ): UserProfileResponseDto +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/domain/model/search/SearchEntity.kt b/app/src/main/java/com/hsLink/hslink/domain/model/search/SearchEntity.kt new file mode 100644 index 0000000..ac72753 --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/domain/model/search/SearchEntity.kt @@ -0,0 +1,18 @@ +package com.hsLink.hslink.domain.model.search + +data class MentorListEntity( + val totalMentorCount: Long, + val page: Int, + val size: Int, + val totalPages: Int, + val mentors: List +) + +data class MentorEntity( + val userId: Long, + val name: String, + val major: String, + val jobSeeking: Boolean, + val employed: Boolean, + val academicStatus: String +) \ 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 new file mode 100644 index 0000000..9b14d2c --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/domain/model/search/UserProfileEntity.kt @@ -0,0 +1,30 @@ +package com.hsLink.hslink.domain.model.search + +data class UserProfileEntity( + val userId: Long, + val name: String, + val studentNumberPrefix: String, + val major: String, + val email: String, + val jobSeeking: Boolean, + val employed: Boolean, + val academicStatus: String, + val careers: List, + val links: List +) + +data class CareerEntity( + val id: Long, + val companyName: String, + val position: String, + val jobType: String, + val employed: Boolean, + val startYm: String, + val endYm: String? +) + +data class LinkEntity( + val id: Long, + val type: String, + val url: String +) \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/domain/repository/AuthRepository.kt b/app/src/main/java/com/hsLink/hslink/domain/repository/AuthRepository.kt index a64fcfe..acc0dab 100644 --- a/app/src/main/java/com/hsLink/hslink/domain/repository/AuthRepository.kt +++ b/app/src/main/java/com/hsLink/hslink/domain/repository/AuthRepository.kt @@ -1,10 +1,10 @@ package com.hsLink.hslink.domain.repository -import com.hsLink.hslink.data.dto.response.SocialLoginResponse +import com.hsLink.hslink.data.dto.response.SocialLoginResponseDto interface AuthRepository { suspend fun loginWithSocialToken( provider: String, accessToken: String - ): Result + ): Result } \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/domain/repository/search/SearchRepository.kt b/app/src/main/java/com/hsLink/hslink/domain/repository/search/SearchRepository.kt new file mode 100644 index 0000000..7a2acb8 --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/domain/repository/search/SearchRepository.kt @@ -0,0 +1,9 @@ +package com.hsLink.hslink.domain.repository.search + +import com.hsLink.hslink.domain.model.search.MentorListEntity +import com.hsLink.hslink.domain.model.search.UserProfileEntity + +interface SearchRepository { + suspend fun getMentors(page: Int, size: Int): Result + suspend fun getUserProfile(userId: Long): Result +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/presentation/main/MainNavHost.kt b/app/src/main/java/com/hsLink/hslink/presentation/main/MainNavHost.kt index 9958277..0acad41 100644 --- a/app/src/main/java/com/hsLink/hslink/presentation/main/MainNavHost.kt +++ b/app/src/main/java/com/hsLink/hslink/presentation/main/MainNavHost.kt @@ -8,12 +8,13 @@ import com.hsLink.hslink.presentation.community.navigation.main.communityNavGrap import com.hsLink.hslink.presentation.community.navigation.post.communityPostNavGraph import com.hsLink.hslink.presentation.community.navigation.write.communityWriteNavGraph import com.hsLink.hslink.presentation.home.navigation.homeNavGraph -import com.hsLink.hslink.presentation.home.navigation.searchNavGraph +import com.hsLink.hslink.presentation.search.navigation.searchNavGraph import com.hsLink.hslink.presentation.login.navigation.loginNavGraph import com.hsLink.hslink.presentation.mypage.navigation.main.mypageNavGraph import com.hsLink.hslink.presentation.mypage.navigation.profile.profileEditNavGraph import com.hsLink.hslink.presentation.mypage.navigation.career.careerNavGraph import com.hsLink.hslink.presentation.mypage.navigation.sns.snsNavGraph +import com.hsLink.hslink.presentation.search.navigation.profileNavGraph @Composable fun MainNavHost( @@ -34,7 +35,19 @@ fun MainNavHost( ) homeNavGraph(padding) - searchNavGraph(padding) + + searchNavGraph( + paddingValues = padding, + onNavigateToProfile = { userId -> + navigator.navigateToProfile(userId) + } + ) + + profileNavGraph( + paddingValues = padding, + onNavigateBack = { navigator.navigateUp() } + ) + communityNavGraph( padding, navigateUp = navigator::navigateUp, diff --git a/app/src/main/java/com/hsLink/hslink/presentation/main/MainNavigator.kt b/app/src/main/java/com/hsLink/hslink/presentation/main/MainNavigator.kt index 778bb86..6f88b50 100644 --- a/app/src/main/java/com/hsLink/hslink/presentation/main/MainNavigator.kt +++ b/app/src/main/java/com/hsLink/hslink/presentation/main/MainNavigator.kt @@ -14,7 +14,8 @@ import com.hsLink.hslink.presentation.community.navigation.post.navigateToCommun import com.hsLink.hslink.presentation.community.navigation.write.navigateToWriteCommunity import com.hsLink.hslink.presentation.home.navigation.Home import com.hsLink.hslink.presentation.home.navigation.navigateToHome -import com.hsLink.hslink.presentation.home.navigation.navigateToSearch +import com.hsLink.hslink.presentation.search.navigation.navigateToSearch +import com.hsLink.hslink.presentation.search.navigation.navigateToProfile import com.hsLink.hslink.presentation.mypage.navigation.main.navigateToMypage class MainNavigator( @@ -27,7 +28,6 @@ class MainNavigator( @Composable get() = navController .currentBackStackEntryAsState().value?.destination - val currentTab: MainTab? @Composable get() = MainTab.find { tabRoute -> currentDestination?.hasRoute(tabRoute::class) == true @@ -73,6 +73,10 @@ class MainNavigator( navController.navigateToHome(navOptions) } + fun navigateToProfile(userId: Long, navOptions: NavOptions? = null) { + navController.navigateToProfile(userId, navOptions) + } + @Composable fun showBottomNavigator() = MainTab.contains { currentDestination?.hasRoute(it::class) == true diff --git a/app/src/main/java/com/hsLink/hslink/presentation/main/MainTab.kt b/app/src/main/java/com/hsLink/hslink/presentation/main/MainTab.kt index 3b851c2..8dbbe79 100644 --- a/app/src/main/java/com/hsLink/hslink/presentation/main/MainTab.kt +++ b/app/src/main/java/com/hsLink/hslink/presentation/main/MainTab.kt @@ -7,7 +7,7 @@ import com.hsLink.hslink.R import com.hsLink.hslink.core.navigation.MainTabRoute import com.hsLink.hslink.presentation.community.navigation.main.Community import com.hsLink.hslink.presentation.home.navigation.Home -import com.hsLink.hslink.presentation.home.navigation.Search +import com.hsLink.hslink.presentation.search.navigation.Search import com.hsLink.hslink.presentation.mypage.navigation.main.Mypage enum class MainTab( diff --git a/app/src/main/java/com/hsLink/hslink/presentation/search/SearchScreen.kt b/app/src/main/java/com/hsLink/hslink/presentation/search/SearchScreen.kt deleted file mode 100644 index 62e8a7a..0000000 --- a/app/src/main/java/com/hsLink/hslink/presentation/search/SearchScreen.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.hsLink.hslink.presentation.search - -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier - - -@Composable -fun SearchRoute( - paddingValues: PaddingValues, -) { - SearchScreen(paddingValues) -} - -@Composable -fun SearchScreen( - paddingValues: PaddingValues, - modifier: Modifier = Modifier, -) { - Text( - text = "Search", - modifier = modifier - .padding(paddingValues) - ) - -} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/presentation/search/component/CareerSection.kt b/app/src/main/java/com/hsLink/hslink/presentation/search/component/CareerSection.kt new file mode 100644 index 0000000..b8e950a --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/presentation/search/component/CareerSection.kt @@ -0,0 +1,95 @@ +package com.hsLink.hslink.presentation.search.component + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.hsLink.hslink.core.designsystem.theme.HsLinkTheme +import com.hsLink.hslink.domain.model.search.CareerEntity + +@Composable +fun CareerCard( + careers: List, + modifier: Modifier = Modifier +) { + Column(modifier = modifier) { + Text( + text = "커리어", + style = HsLinkTheme.typography.title_16Strong, + color = HsLinkTheme.colors.Grey700, + modifier = Modifier.padding(bottom = 12.dp) + ) + + Card( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + colors = CardDefaults.cardColors( + containerColor = HsLinkTheme.colors.Common + ), + border = BorderStroke( + width = 1.dp, + color = HsLinkTheme.colors.Grey200 + ) + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + careers.forEachIndexed { index, career -> + CareerItem(career = career) + if (index < careers.size - 1) { + HorizontalDivider( + modifier = Modifier.padding(vertical = 12.dp), + color = HsLinkTheme.colors.Grey200 + ) + } + } + } + } + } +} + +@Composable +private fun CareerItem( + career: CareerEntity, + modifier: Modifier = Modifier +) { + Column(modifier = modifier) { + Text( + text = career.companyName, + style = HsLinkTheme.typography.body_16Strong, + color = HsLinkTheme.colors.Grey700 + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = career.position, + style = HsLinkTheme.typography.body_16Strong, + color = HsLinkTheme.colors.Grey700 + ) + + Spacer(modifier = Modifier.height(4.dp)) + + val period = if (career.endYm != null) { + "${career.startYm} - ${career.endYm}" + } else { + "${career.startYm} - 현재" + } + + Text( + text = period, + style = HsLinkTheme.typography.body_16Strong, + color = HsLinkTheme.colors.Grey700 + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/presentation/search/component/HsLinkIconActionButton.kt b/app/src/main/java/com/hsLink/hslink/presentation/search/component/HsLinkIconActionButton.kt new file mode 100644 index 0000000..3f02854 --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/presentation/search/component/HsLinkIconActionButton.kt @@ -0,0 +1,91 @@ +// HsLinkIconActionButton.kt (새로 생성) +package com.hsLink.hslink.core.designsystem.component + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.hsLink.hslink.core.designsystem.theme.HsLinkTheme +import com.hsLink.hslink.core.util.noRippleClickable + +@Composable +fun HsLinkIconActionButton( + label: String, + @DrawableRes iconRes: Int, + onClick: () -> Unit, + size: HsLinkActionButtonSize, + modifier: Modifier = Modifier, + isEnabled: Boolean = true, +) { + val textStyle = size.getTextStyle() + + val textColor: Color + val backgroundColor: Color + + if (isEnabled) { + textColor = when(size){ + HsLinkActionButtonSize.Large -> HsLinkTheme.colors.Common + HsLinkActionButtonSize.Medium -> HsLinkTheme.colors.Common + HsLinkActionButtonSize.Small -> HsLinkTheme.colors.Grey500 + } + backgroundColor = when (size) { + HsLinkActionButtonSize.Large -> HsLinkTheme.colors.DeepBlue500 + HsLinkActionButtonSize.Medium -> HsLinkTheme.colors.SkyBlue400 + HsLinkActionButtonSize.Small -> HsLinkTheme.colors.Grey100 + } + } else { + textColor = when (size) { + HsLinkActionButtonSize.Large -> HsLinkTheme.colors.Common + HsLinkActionButtonSize.Medium -> HsLinkTheme.colors.Common + HsLinkActionButtonSize.Small -> HsLinkTheme.colors.Grey500 + } + backgroundColor = HsLinkTheme.colors.Grey200 + } + + Box( + modifier = modifier + .background( + color = backgroundColor, + shape = RoundedCornerShape(8.dp) + ) + .noRippleClickable( + onClick = onClick, + enabled = isEnabled, + ) + .padding( + horizontal = size.horizontalPadding, + vertical = size.verticalPadding + ), + contentAlignment = Alignment.Center + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Icon( + painter = painterResource(id = iconRes), + contentDescription = null, + tint = textColor + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = label, + color = textColor, + style = textStyle + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/presentation/search/component/LinkCard.kt b/app/src/main/java/com/hsLink/hslink/presentation/search/component/LinkCard.kt new file mode 100644 index 0000000..c45afac --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/presentation/search/component/LinkCard.kt @@ -0,0 +1,88 @@ +package com.hsLink.hslink.presentation.search.component + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.hsLink.hslink.core.designsystem.theme.HsLinkTheme +import com.hsLink.hslink.domain.model.search.LinkEntity + +@Composable +fun LinkCard( + links: List, + modifier: Modifier = Modifier +) { + Column(modifier = modifier) { + Text( + text = "자기소개 링크", + style = HsLinkTheme.typography.title_16Strong, + color = HsLinkTheme.colors.Grey700, + modifier = Modifier.padding(bottom = 12.dp) + ) + + Card( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + colors = CardDefaults.cardColors( + containerColor = HsLinkTheme.colors.Common + ), + border = BorderStroke( + width = 1.dp, + color = HsLinkTheme.colors.Grey200 + ) + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + links.forEachIndexed { index, link -> + LinkItem(link = link) + if (index < links.size - 1) { + Spacer(modifier = Modifier.height(16.dp)) + } + } + } + } + } +} + +@Composable +private fun LinkItem( + link: LinkEntity, + modifier: Modifier = Modifier +) { + Column(modifier = modifier) { + Text( + text = getLinkTypeText(link.type), + style = HsLinkTheme.typography.body_16Strong, + color = HsLinkTheme.colors.Grey700 + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = link.url, + style = HsLinkTheme.typography.body_16Strong, + color = HsLinkTheme.colors.DeepBlue500 + ) + } +} + +private fun getLinkTypeText(type: String): String { + return when (type) { + "LINKEDIN" -> "LinkedIn" + "GITHUB" -> "GitHub" + "INSTAGRAM" -> "Instagram" + "BLOG" -> "블로그" + else -> "기타" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/presentation/search/component/ProfileCard.kt b/app/src/main/java/com/hsLink/hslink/presentation/search/component/ProfileCard.kt new file mode 100644 index 0000000..57349bb --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/presentation/search/component/ProfileCard.kt @@ -0,0 +1,190 @@ +package com.hsLink.hslink.presentation.search.component + +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.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.hsLink.hslink.core.designsystem.theme.HsLinkTheme +import com.hsLink.hslink.domain.model.search.UserProfileEntity + +@Preview(showBackground = true) +@Composable +private fun ProfileCardPreview() { + HsLinkTheme { + ProfileCard( + profile = UserProfileEntity( + userId = 1L, + name = "송효재", + studentNumberPrefix = "21", + major = "회계재무경영", + email = "test@test.com", + jobSeeking = true, + employed = true, + academicStatus = "GRADUATED", + careers = emptyList(), + links = emptyList() + ), + modifier = Modifier.padding(16.dp) + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun ProfileCardPreviewNotEmployed() { + HsLinkTheme { + ProfileCard( + profile = UserProfileEntity( + userId = 2L, + name = "김민수", + studentNumberPrefix = "20", + major = "컴퓨터공학과", + email = "test2@test.com", + jobSeeking = false, + employed = false, + academicStatus = "ENROLLED", + careers = emptyList(), + links = emptyList() + ), + modifier = Modifier.padding(16.dp) + ) + } +} + +@Composable +private fun StatusItem( + title: String, + subtitle: String +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = title, + style = HsLinkTheme.typography.title_16Strong, + color = HsLinkTheme.colors.Grey700 + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = subtitle, + style = HsLinkTheme.typography.body_14Normal, + color = HsLinkTheme.colors.Grey500 + ) + } +} + +@Composable +fun ProfileCard( + profile: UserProfileEntity, + modifier: Modifier = Modifier +) { + Box( // 전체 감싸기 + modifier = modifier + .fillMaxWidth() + .background(HsLinkTheme.colors.SkyBlue100), // 전체 폭 배경 + contentAlignment = Alignment.TopCenter + ) { + // 파란 배경 + Column( + modifier = Modifier + .fillMaxWidth(0.92f) + .background( + color = HsLinkTheme.colors.SkyBlue100, + shape = RoundedCornerShape(12.dp) + ) + .padding(horizontal = 20.dp, vertical = 20.dp) + ) { + // 상단 정보 + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = profile.name, + style = HsLinkTheme.typography.title_24Strong, + color = HsLinkTheme.colors.Grey700 + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = "${profile.studentNumberPrefix}학번", + style = HsLinkTheme.typography.body_14Normal, + color = HsLinkTheme.colors.Grey500 + ) + } + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = profile.major, + style = HsLinkTheme.typography.body_16Normal, + color = HsLinkTheme.colors.Grey700 + ) + + Spacer(modifier = Modifier.height(40.dp)) // 하단 카드 걸칠 공간 확보 + } + + // 흰색 하단 박스 (겹치게 배치) + androidx.compose.material3.Surface( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .align(Alignment.BottomCenter) + .offset(y = 20.dp), // 파란 배경 아래로 20dp 걸치기 + shape = RoundedCornerShape(12.dp), + color = HsLinkTheme.colors.Common, + shadowElevation = 6.dp // 그림자 효과 + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 12.dp), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + StatusItem( + title = if (profile.jobSeeking) "구직 중" else "구직 안함", + subtitle = "구직 여부" + ) + VerticalDivider( + modifier = Modifier.height(40.dp), + color = HsLinkTheme.colors.Grey200 + ) + StatusItem( + title = if (profile.employed) "재직 중" else "미재직", + subtitle = "재직 여부" + ) + VerticalDivider( + modifier = Modifier.height(40.dp), + color = HsLinkTheme.colors.Grey200 + ) + StatusItem( + title = getAcademicStatusText(profile.academicStatus), + subtitle = "재학 여부" + ) + } + } + } +} + + +private fun getAcademicStatusText(status: String): String { + return when (status) { + "ENROLLED" -> "재학" + "GRADUATED" -> "졸업" + "ON_LEAVE" -> "휴학" + else -> "기타" + } +} diff --git a/app/src/main/java/com/hsLink/hslink/presentation/search/component/SearchUserItems.kt b/app/src/main/java/com/hsLink/hslink/presentation/search/component/SearchUserItems.kt new file mode 100644 index 0000000..7898d9b --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/presentation/search/component/SearchUserItems.kt @@ -0,0 +1,101 @@ +package com.hsLink.hslink.presentation.search.component + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color.Companion.Black +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.theme.HsLinkTheme + +@Preview(showBackground = true) +@Composable +private fun MyPageDetailItemContentPreview() { + HsLinkTheme { + SearchUserItems( + name ="송효재", + title = "21학번 회계재무경영", + subtitle = "구직 중 · 재직 중 · 졸업", + onClick = { } + ) + } +} + +data class SearchUserItemsData( + val id: String, + val name : String, + val title: String, + val subtitle: String, + val route: String, +) + +@Composable +fun SearchUserItems( + name : String, + title: String, + subtitle: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Card( + onClick = onClick, + modifier = modifier + .fillMaxWidth() + .padding(vertical = 4.dp, horizontal = 16.dp), + colors = CardDefaults.cardColors( + containerColor = HsLinkTheme.colors.Common + ), + border = BorderStroke( + width = 1.dp, + color = HsLinkTheme.colors.Grey200 + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 20.dp, horizontal = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = name, + color = Black, + style = HsLinkTheme.typography.title_20Strong + ) + + Text( + text = title, + color = Black, + style = HsLinkTheme.typography.body_14Normal + ) + Text( + text = subtitle, + color = HsLinkTheme.colors.Grey400, + style = HsLinkTheme.typography.btm_M + ) + } + + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_mypage_item_lefrarrow), + contentDescription = null, + tint = Black + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/presentation/search/navigation/ProfileNavigation.kt b/app/src/main/java/com/hsLink/hslink/presentation/search/navigation/ProfileNavigation.kt new file mode 100644 index 0000000..9ce7e93 --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/presentation/search/navigation/ProfileNavigation.kt @@ -0,0 +1,35 @@ +package com.hsLink.hslink.presentation.search.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import androidx.navigation.toRoute +import com.hsLink.hslink.core.navigation.Route +import com.hsLink.hslink.presentation.search.screen.ProfileRoute +import kotlinx.serialization.Serializable + +fun NavController.navigateToProfile( + userId: Long, + navOptions: NavOptions? = null +) { + navigate(Profile(userId = userId), navOptions) +} + +fun NavGraphBuilder.profileNavGraph( + paddingValues: PaddingValues, + onNavigateBack: () -> Unit +) { + composable { backStackEntry -> + val profile = backStackEntry.toRoute() + ProfileRoute( + userId = profile.userId, + paddingValues = paddingValues, + onNavigateBack = onNavigateBack + ) + } +} + +@Serializable +data class Profile(val userId: Long) : Route \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/presentation/search/navigation/SearchNavigation.kt b/app/src/main/java/com/hsLink/hslink/presentation/search/navigation/SearchNavigation.kt index cf4c9ab..845d97d 100644 --- a/app/src/main/java/com/hsLink/hslink/presentation/search/navigation/SearchNavigation.kt +++ b/app/src/main/java/com/hsLink/hslink/presentation/search/navigation/SearchNavigation.kt @@ -1,4 +1,4 @@ -package com.hsLink.hslink.presentation.home.navigation +package com.hsLink.hslink.presentation.search.navigation import androidx.compose.foundation.layout.PaddingValues import androidx.navigation.NavController @@ -6,7 +6,7 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.hsLink.hslink.core.navigation.MainTabRoute -import com.hsLink.hslink.presentation.search.SearchRoute +import com.hsLink.hslink.presentation.search.screen.SearchRoute import kotlinx.serialization.Serializable fun NavController.navigateToSearch(navOptions: NavOptions? = null) { @@ -14,10 +14,14 @@ fun NavController.navigateToSearch(navOptions: NavOptions? = null) { } fun NavGraphBuilder.searchNavGraph( - padding: PaddingValues, + paddingValues: PaddingValues, + onNavigateToProfile: (Long) -> Unit = {} ) { composable { - SearchRoute(padding) + SearchRoute( + paddingValues = paddingValues, + onNavigateToProfile = onNavigateToProfile + ) } } diff --git a/app/src/main/java/com/hsLink/hslink/presentation/search/screen/ProfileScreen.kt b/app/src/main/java/com/hsLink/hslink/presentation/search/screen/ProfileScreen.kt new file mode 100644 index 0000000..795d1c3 --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/presentation/search/screen/ProfileScreen.kt @@ -0,0 +1,248 @@ +package com.hsLink.hslink.presentation.search.screen + +import android.content.Intent +import android.net.Uri +import androidx.compose.foundation.BorderStroke +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.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.hsLink.hslink.R +import com.hsLink.hslink.core.designsystem.component.HsLinkActionButton +import com.hsLink.hslink.core.designsystem.component.HsLinkActionButtonSize +import com.hsLink.hslink.core.designsystem.component.HsLinkIconActionButton +import com.hsLink.hslink.core.designsystem.component.HsLinkTopBar +import com.hsLink.hslink.core.designsystem.theme.HsLinkTheme +import com.hsLink.hslink.domain.model.search.CareerEntity +import com.hsLink.hslink.domain.model.search.LinkEntity +import com.hsLink.hslink.domain.model.search.UserProfileEntity +import com.hsLink.hslink.presentation.search.component.CareerCard +import com.hsLink.hslink.presentation.search.component.LinkCard +import com.hsLink.hslink.presentation.search.component.ProfileCard +import com.hsLink.hslink.presentation.search.state.ProfileIntent +import com.hsLink.hslink.presentation.search.state.ProfileSideEffect +import com.hsLink.hslink.presentation.search.state.ProfileUiState +import com.hsLink.hslink.presentation.search.viewmodel.ProfileViewModel + +@Composable +fun ProfileRoute( + userId: Long, + paddingValues: PaddingValues, + onNavigateBack: () -> Unit, + viewModel: ProfileViewModel = hiltViewModel() +) { + val uiState by viewModel.uiState.collectAsState() + + LaunchedEffect(userId) { + viewModel.handleIntent(ProfileIntent.LoadProfile(userId)) + } + + LaunchedEffect(viewModel.sideEffect) { + viewModel.sideEffect.collect { sideEffect -> + when (sideEffect) { + is ProfileSideEffect.NavigateBack -> { + onNavigateBack() + } + is ProfileSideEffect.ShowError -> { + // TODO: Toast 또는 SnackBar 처리 + } + } + } + } + + ProfileScreen( + paddingValues = paddingValues, + uiState = uiState, + onIntent = viewModel::handleIntent + ) +} + +@Composable +fun ProfileScreen( + modifier: Modifier = Modifier, + paddingValues: PaddingValues, + uiState: ProfileUiState, + onIntent: (ProfileIntent) -> Unit, +) { + LazyColumn( + modifier = modifier + .fillMaxSize() + .background(color = HsLinkTheme.colors.Common) + .padding(paddingValues), + ) { + item { + HsLinkTopBar( + modifier = Modifier.padding(horizontal = 16.dp), + title = { + Text( + text = "한성인 찾기", + style = HsLinkTheme.typography.title_20Strong, + color = HsLinkTheme.colors.Grey600 + ) + }, + leftIcon = R.drawable.ic_topbar_arrowleft, + onLeftIconClick = { onIntent(ProfileIntent.NavigateBack) }, + ) + } + + if (uiState.isLoading) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .height(200.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + } + + uiState.userProfile?.let { profile -> + // 프로필 헤더 추가 + item { + Spacer(modifier = Modifier.height(32.dp)) + ProfileCard( + profile = profile, + //modifier = Modifier.padding(16.dp) + ) + } + + // 커리어 섹션 추가 + if (profile.careers.isNotEmpty()) { + item { + Spacer(modifier = Modifier.height(32.dp)) + CareerCard( + careers = profile.careers, + modifier = Modifier.padding(horizontal = 16.dp) + ) + } + } + + // 링크 섹션 추가 + if (profile.links.isNotEmpty()) { + item { + Spacer(modifier = Modifier.height(32.dp)) + LinkCard( + links = profile.links, + modifier = Modifier.padding(horizontal = 16.dp) + ) + } + } + + // 하단 버튼 추가 + item { + Spacer(modifier = Modifier.height(32.dp)) + val context = LocalContext.current + HsLinkIconActionButton( + label = "멘토링 메일 보내기", + iconRes = R.drawable.ic_search_mail, + onClick = { + val intent = Intent(Intent.ACTION_SENDTO).apply { + data = Uri.parse("mailto:${profile.email}") // 프로필의 이메일 주소 사용 + putExtra(Intent.EXTRA_SUBJECT, "멘토링 문의드립니다.") + putExtra(Intent.EXTRA_TEXT, "안녕하세요, ${profile.name}님.\n\n멘토링 관련하여 문의드립니다.") + } + context.startActivity(intent) + }, + size = HsLinkActionButtonSize.Large, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 32.dp) + ) + } + } + } +} + + + +@Preview(showBackground = true) +@Composable +private fun ProfileScreenPreview() { + HsLinkTheme { + ProfileScreen( + paddingValues = PaddingValues(), + uiState = ProfileUiState( + userProfile = UserProfileEntity( + userId = 1L, + name = "송효재", + studentNumberPrefix = "21", + major = "회계재무경영", + email = "test@test.com", + jobSeeking = true, + employed = true, + academicStatus = "GRADUATED", + careers = listOf( + CareerEntity( + id = 1L, + companyName = "Sebp", + position = "컨설턴트 · 인턴", + jobType = "PERMANENT", + employed = true, + startYm = "25.08", + endYm = "재직 중" + ), + CareerEntity( + id = 2L, + companyName = "한국생산성본부", + position = "영업직 · 인턴", + jobType = "PERMANENT", + employed = false, + startYm = "25.01", + endYm = "25.06" + ) + ), + links = listOf( + LinkEntity( + id = 1L, + type = "INSTAGRAM", + url = "https://www.instagram.com/02_sing_song/" + ), + LinkEntity( + id = 2L, + type = "BLOG", + url = "https://www.instagram.com/02_sing_song/" + ) + ) + ) + ), + onIntent = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/presentation/search/screen/SearchScreen.kt b/app/src/main/java/com/hsLink/hslink/presentation/search/screen/SearchScreen.kt new file mode 100644 index 0000000..2adbd21 --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/presentation/search/screen/SearchScreen.kt @@ -0,0 +1,231 @@ +package com.hsLink.hslink.presentation.search.screen + +import androidx.compose.foundation.Image +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.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.hsLink.hslink.R +import com.hsLink.hslink.core.designsystem.component.HsLinkTopBar +import com.hsLink.hslink.core.designsystem.theme.HsLinkTheme +import com.hsLink.hslink.domain.model.search.MentorEntity +import com.hsLink.hslink.presentation.search.component.SearchUserItems +import com.hsLink.hslink.presentation.search.state.SearchIntent +import com.hsLink.hslink.presentation.search.state.SearchSideEffect +import com.hsLink.hslink.presentation.search.state.SearchUiState +import com.hsLink.hslink.presentation.search.viewmodel.SearchViewModel + +@Composable +@Preview(showBackground = true) +private fun SearchScreenPreview() { + HsLinkTheme { + SearchScreen( + paddingValues = PaddingValues(), + uiState = SearchUiState( + isLoading = false, + totalMentorCount = 13564, + mentors = listOf( + MentorEntity( + userId = 1L, + name = "송효재", + major = "회계재무경영", + jobSeeking = true, + employed = false, + academicStatus = "GRADUATED" + ), + MentorEntity( + userId = 2L, + name = "김민수", + major = "컴퓨터공학과", + jobSeeking = false, + employed = true, + academicStatus = "ENROLLED" + ), + MentorEntity( + userId = 3L, + name = "박지영", + major = "경영학과", + jobSeeking = true, + employed = true, + academicStatus = "GRADUATED" + ) + ) + ), + onNavigateToProfile = {} + ) + } +} + +@Composable +fun SearchRoute( + paddingValues: PaddingValues, + onNavigateToProfile: (Long) -> Unit, + viewModel: SearchViewModel = hiltViewModel() +) { + val uiState by viewModel.uiState.collectAsState() + + LaunchedEffect(Unit) { + viewModel.handleIntent(SearchIntent.LoadMentors) + } + + LaunchedEffect(viewModel.sideEffect) { + viewModel.sideEffect.collect { sideEffect -> + when (sideEffect) { + is SearchSideEffect.NavigateToProfile -> { + onNavigateToProfile(sideEffect.userId) + } + is SearchSideEffect.ShowError -> { + // TODO: Toast 또는 SnackBar 처리 + } + } + } + } + + SearchScreen( + paddingValues = paddingValues, + uiState = uiState, + onIntent = viewModel::handleIntent, + onNavigateToProfile = onNavigateToProfile + ) +} + +@Composable +fun SearchScreen( + modifier: Modifier = Modifier, + paddingValues: PaddingValues, + uiState: SearchUiState = SearchUiState(), + onIntent: (SearchIntent) -> Unit = {}, + onNavigateToProfile: (Long) -> Unit = {}, +) { + LazyColumn( + modifier = modifier + .fillMaxSize() + .background(color = HsLinkTheme.colors.Common) + .padding(paddingValues), + ) { + item { + HsLinkTopBar( + modifier = Modifier.padding(start = 12.dp), + title = { + Image( + painter = painterResource(id = R.drawable.img_home_logo), + contentDescription = null, + modifier = Modifier + .height(48.dp) + .width(92.dp), + contentScale = ContentScale.FillBounds, + ) + }, + rightIconFirst = null, + rightIconSecond = null, + leftIcon = null + + ) + HorizontalDivider( + thickness = 1.dp, + color = HsLinkTheme.colors.Grey100 + ) + Column( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 16.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Bottom + ) { + Text( + text = "멘토링을 받고 싶은\n한성인을 찾아보세요", + color = HsLinkTheme.colors.DeepBlue500, + style = HsLinkTheme.typography.title_24Strong, + modifier = Modifier.weight(1f) + ) + + Text( + text = "전체 ${uiState.totalMentorCount}명", + color = HsLinkTheme.colors.Grey600, + style = HsLinkTheme.typography.body_16Normal + ) + } + } + } + + if (uiState.isLoading && uiState.mentors.isEmpty()) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .height(200.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + } + + items( + items = uiState.mentors, + key = { it.userId } + ) { mentor -> + SearchUserItems( + name = mentor.name, + title = mentor.major, // 학번 없이 전공만 + subtitle = buildStatusText(mentor.jobSeeking, mentor.employed, mentor.academicStatus), + onClick = { onIntent(SearchIntent.NavigateToProfile(mentor.userId)) } + ) + } + + if (uiState.isLoadingMore) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .height(60.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + } + } +} + +private fun buildStatusText(jobSeeking: Boolean, employed: Boolean, academicStatus: String): String { + val status = mutableListOf() + if (jobSeeking) status.add("구직 중") + if (employed) status.add("재직 중") + status.add(getAcademicStatusText(academicStatus)) + return status.joinToString(" · ") +} + +private fun getAcademicStatusText(status: String): String { + return when (status) { + "ENROLLED" -> "재학" + "GRADUATED" -> "졸업" + "ON_LEAVE" -> "휴학" + else -> "기타" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/presentation/search/state/ProfileContract.kt b/app/src/main/java/com/hsLink/hslink/presentation/search/state/ProfileContract.kt new file mode 100644 index 0000000..62f0566 --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/presentation/search/state/ProfileContract.kt @@ -0,0 +1,20 @@ +package com.hsLink.hslink.presentation.search.state + +import com.hsLink.hslink.domain.model.search.UserProfileEntity + +data class ProfileUiState( + val isLoading: Boolean = false, + val userProfile: UserProfileEntity? = null, + val errorMessage: String? = null +) + +sealed class ProfileIntent { + data class LoadProfile(val userId: Long) : ProfileIntent() + object ClearError : ProfileIntent() + object NavigateBack : ProfileIntent() +} + +sealed class ProfileSideEffect { + object NavigateBack : ProfileSideEffect() + data class ShowError(val message: String) : ProfileSideEffect() +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/presentation/search/state/SearchUiState.kt b/app/src/main/java/com/hsLink/hslink/presentation/search/state/SearchUiState.kt new file mode 100644 index 0000000..616b71c --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/presentation/search/state/SearchUiState.kt @@ -0,0 +1,24 @@ +package com.hsLink.hslink.presentation.search.state + +import com.hsLink.hslink.domain.model.search.MentorEntity + +data class SearchUiState( + val isLoading: Boolean = false, + val mentors: List = emptyList(), + val totalMentorCount: Long = 0, + val currentPage: Int = 0, + val isLoadingMore: Boolean = false, + val errorMessage: String? = null +) + +sealed class SearchIntent { + object LoadMentors : SearchIntent() + object LoadMoreMentors : SearchIntent() + data class NavigateToProfile(val userId: Long) : SearchIntent() + object ClearError : SearchIntent() +} + +sealed class SearchSideEffect { + data class NavigateToProfile(val userId: Long) : SearchSideEffect() + data class ShowError(val message: String) : SearchSideEffect() +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/presentation/search/viewmodel/ProfileViewModel.kt b/app/src/main/java/com/hsLink/hslink/presentation/search/viewmodel/ProfileViewModel.kt new file mode 100644 index 0000000..5bc4892 --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/presentation/search/viewmodel/ProfileViewModel.kt @@ -0,0 +1,79 @@ +package com.hsLink.hslink.presentation.search.viewmodel + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.hsLink.hslink.domain.repository.search.SearchRepository +import com.hsLink.hslink.presentation.search.state.ProfileIntent +import com.hsLink.hslink.presentation.search.state.ProfileSideEffect +import com.hsLink.hslink.presentation.search.state.ProfileUiState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class ProfileViewModel @Inject constructor( + private val searchRepository: SearchRepository +) : ViewModel() { + + private val _uiState = MutableStateFlow(ProfileUiState()) + val uiState = _uiState.asStateFlow() + + private val _sideEffect = MutableSharedFlow() + val sideEffect = _sideEffect.asSharedFlow() + + fun handleIntent(intent: ProfileIntent) { + Log.d("ProfileViewModel", "handleIntent: $intent") + when (intent) { + is ProfileIntent.LoadProfile -> loadProfile(intent.userId) + is ProfileIntent.ClearError -> clearError() + is ProfileIntent.NavigateBack -> navigateBack() + } + } + + private fun loadProfile(userId: Long) { + Log.d("ProfileViewModel", "loadProfile 시작: userId=$userId") + viewModelScope.launch { + _uiState.value = _uiState.value.copy( + isLoading = true, + errorMessage = null + ) + Log.d("ProfileViewModel", "Loading 상태 설정 완료") + + searchRepository.getUserProfile(userId) + .onSuccess { userProfile -> + Log.d("ProfileViewModel", "프로필 로드 성공: ${userProfile.name}") + _uiState.value = _uiState.value.copy( + isLoading = false, + userProfile = userProfile + ) + } + .onFailure { exception -> + Log.e("ProfileViewModel", "프로필 로드 실패: ${exception.message}", exception) + _uiState.value = _uiState.value.copy( + isLoading = false, + errorMessage = exception.message + ) + postSideEffect(ProfileSideEffect.ShowError(exception.message ?: "Unknown error")) + } + } + } + + private fun clearError() { + _uiState.value = _uiState.value.copy(errorMessage = null) + } + + private fun navigateBack() { + viewModelScope.launch { + postSideEffect(ProfileSideEffect.NavigateBack) + } + } + + private suspend fun postSideEffect(sideEffect: ProfileSideEffect) { + _sideEffect.emit(sideEffect) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/presentation/search/viewmodel/SearchViewModel.kt b/app/src/main/java/com/hsLink/hslink/presentation/search/viewmodel/SearchViewModel.kt new file mode 100644 index 0000000..0a65c64 --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/presentation/search/viewmodel/SearchViewModel.kt @@ -0,0 +1,103 @@ +package com.hsLink.hslink.presentation.search.viewmodel + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.hsLink.hslink.domain.repository.search.SearchRepository +import com.hsLink.hslink.presentation.search.state.SearchIntent +import com.hsLink.hslink.presentation.search.state.SearchSideEffect +import com.hsLink.hslink.presentation.search.state.SearchUiState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SearchViewModel @Inject constructor( + private val searchRepository: SearchRepository +) : ViewModel() { + + private val _uiState = MutableStateFlow(SearchUiState()) + val uiState = _uiState.asStateFlow() + + private val _sideEffect = MutableSharedFlow() + val sideEffect = _sideEffect.asSharedFlow() + + fun handleIntent(intent: SearchIntent) { + Log.d("SearchViewModel", "handleIntent: $intent") + when (intent) { + is SearchIntent.LoadMentors -> loadMentors() + is SearchIntent.LoadMoreMentors -> loadMoreMentors() + is SearchIntent.NavigateToProfile -> navigateToProfile(intent.userId) + is SearchIntent.ClearError -> clearError() + } + } + + private fun loadMentors() { + Log.d("SearchViewModel", "loadMentors 시작") + viewModelScope.launch { + _uiState.value = _uiState.value.copy( + isLoading = true, + errorMessage = null + ) + Log.d("SearchViewModel", "Loading 상태 설정 완료") + + searchRepository.getMentors(page = 0, size = 15) + .onSuccess { mentorList -> + Log.d("SearchViewModel", "API 성공: ${mentorList.mentors.size}개 멘토 로드") + _uiState.value = _uiState.value.copy( + isLoading = false, + mentors = mentorList.mentors, + totalMentorCount = mentorList.totalMentorCount, + currentPage = 0 + ) + } + .onFailure { exception -> + Log.e("SearchViewModel", "API 실패: ${exception.message}", exception) + _uiState.value = _uiState.value.copy( + isLoading = false, + errorMessage = exception.message + ) + postSideEffect(SearchSideEffect.ShowError(exception.message ?: "Unknown error")) + } + } + } + private fun loadMoreMentors() { + val currentState = _uiState.value + if (currentState.isLoadingMore) return + + viewModelScope.launch { + _uiState.value = currentState.copy(isLoadingMore = true) + + searchRepository.getMentors(page = currentState.currentPage + 1, size = 15) + .onSuccess { mentorList -> + _uiState.value = _uiState.value.copy( + isLoadingMore = false, + mentors = currentState.mentors + mentorList.mentors, + currentPage = currentState.currentPage + 1 + ) + } + .onFailure { exception -> + _uiState.value = _uiState.value.copy(isLoadingMore = false) + postSideEffect(SearchSideEffect.ShowError(exception.message ?: "Load more failed")) + } + } + } + + private fun navigateToProfile(userId: Long) { + viewModelScope.launch { + postSideEffect(SearchSideEffect.NavigateToProfile(userId)) + } + } + + private fun clearError() { + _uiState.value = _uiState.value.copy(errorMessage = null) + } + + private suspend fun postSideEffect(sideEffect: SearchSideEffect) { + _sideEffect.emit(sideEffect) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_search_mail.xml b/app/src/main/res/drawable/ic_search_mail.xml new file mode 100644 index 0000000..2572b3a --- /dev/null +++ b/app/src/main/res/drawable/ic_search_mail.xml @@ -0,0 +1,20 @@ + + + +