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 55b58a2..37694dd 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 @@ -2,6 +2,7 @@ package com.hsLink.hslink.data.di import com.hsLink.hslink.BuildConfig import com.hsLink.hslink.data.remote.AuthInterceptor +import com.hsLink.hslink.data.service.CareerService import com.hsLink.hslink.data.service.commuunity.CommunityPostService import com.hsLink.hslink.data.service.home.PostService import com.hsLink.hslink.data.service.login.AuthService @@ -76,4 +77,9 @@ object NetworkModule { fun provideOnboardingService(retrofit: Retrofit): OnboardingService { return retrofit.create(OnboardingService::class.java) } + @Provides + @Singleton + fun provideCareerService(retrofit: Retrofit): CareerService { + return retrofit.create(CareerService::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 8778287..b85a144 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 @@ -1,15 +1,19 @@ package com.hsLink.hslink.data.di import com.hsLink.hslink.data.repositoryimpl.AuthRepositoryImpl +import com.hsLink.hslink.data.repositoryimpl.CareerRepositoryImpl 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.mypage.MypageRepositoryImpl 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.CareerRepository import com.hsLink.hslink.domain.repository.community.CommunityRepository import com.hsLink.hslink.domain.repository.home.PostRepository +import com.hsLink.hslink.domain.repository.mypage.MypageRepository import com.hsLink.hslink.domain.repository.onboarding.OnboardingRepository import com.hsLink.hslink.domain.repository.search.SearchRepository import dagger.Binds @@ -48,8 +52,18 @@ interface RepositoryModule { searchRepositoryImpl: SearchRepositoryImpl, ): SearchRepository + @Binds + fun bindMypageRepository( + mypageRepositoryImpl: MypageRepositoryImpl + ): MypageRepository + @Binds fun bindsOnboardingRepository( onboardingRepositoryImpl: OnboardingRepositoryImpl, ): OnboardingRepository -} + + @Binds + fun bindCareerRepository( + careerRepositoryImpl: CareerRepositoryImpl + ): CareerRepository +} \ No newline at end of file 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 694d57e..81a5c0b 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.mypage.MypageService import com.hsLink.hslink.data.service.search.SearchService import dagger.Module import dagger.Provides @@ -23,5 +24,10 @@ object ServiceModule { fun provideSearchService(retrofit: Retrofit): SearchService { return retrofit.create(SearchService::class.java) } + @Provides + @Singleton + fun provideMypageService(retrofit: Retrofit): MypageService { + return retrofit.create(MypageService::class.java) + } } \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/data/dto/request/common/JobType.kt b/app/src/main/java/com/hsLink/hslink/data/dto/request/common/JobType.kt new file mode 100644 index 0000000..ac5cdd2 --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/data/dto/request/common/JobType.kt @@ -0,0 +1,12 @@ +package com.hsLink.hslink.data.dto.request.common + +import kotlinx.serialization.Serializable + +// data/dto/common/JobType.kt +@Serializable +enum class JobType { + PERMANENT, // 정규직 + TEMPORARY, // 계약직 + INTERN, // 인턴 + FREELANCER // 프리랜서 +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/data/dto/request/mypage/UpdateProfileRequestDto.kt b/app/src/main/java/com/hsLink/hslink/data/dto/request/mypage/UpdateProfileRequestDto.kt new file mode 100644 index 0000000..555a15f --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/data/dto/request/mypage/UpdateProfileRequestDto.kt @@ -0,0 +1,13 @@ +// data/dto/request/mypage/UpdateProfileRequestDto.kt +package com.hsLink.hslink.data.dto.request.mypage + +import kotlinx.serialization.Serializable + +@Serializable +data class UpdateProfileRequestDto( + val studentNumber: String? = null, + val name: String? = null, + val major: String? = null, + val mentor: Boolean? = null, + val jobSeeking: Boolean? = null +) \ No newline at end of file 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 index b0d0f58..2924d2f 100644 --- 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 @@ -27,4 +27,14 @@ data class LinkRequest( val type: LinkType, @SerialName("url") val url: String -) \ No newline at end of file +) +@Serializable +data class CareerUpdateRequestDto( + val companyName: String, + val position: String, + val jobType: JobType, + val startYm: String, + val endYm: String?, + val employed: Boolean +) + 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 index 985b98d..1be37d8 100644 --- 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 @@ -25,6 +25,19 @@ data class LinkResponse( @SerialName("url") val url: String, ) + +@Serializable +data class CareerDto( + val id: Long, + val companyName: String, + val position: String, + val jobType: JobType, + val employed: Boolean, + val startYm: String, + val endYm: String? +) + + typealias CareerListResponseDto = List @Serializable diff --git a/app/src/main/java/com/hsLink/hslink/data/dto/response/mypage/MyPageUserProfileDto.kt b/app/src/main/java/com/hsLink/hslink/data/dto/response/mypage/MyPageUserProfileDto.kt new file mode 100644 index 0000000..e4b1bce --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/data/dto/response/mypage/MyPageUserProfileDto.kt @@ -0,0 +1,16 @@ +package com.hsLink.hslink.data.dto.response.mypage + +import kotlinx.serialization.Serializable + +@Serializable +data class MyPageUserProfileDto( + val userId: Long, + val studentNumber: String, + val name: String, + val major: String, + val mentor: Boolean, + val jobSeeking: Boolean, + val academicStatus: String, + val careers: List, + val links: List +) \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/data/dto/response/mypage/MyPageUserSummaryDto.kt b/app/src/main/java/com/hsLink/hslink/data/dto/response/mypage/MyPageUserSummaryDto.kt new file mode 100644 index 0000000..742c287 --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/data/dto/response/mypage/MyPageUserSummaryDto.kt @@ -0,0 +1,15 @@ +package com.hsLink.hslink.data.dto.response.mypage + +import kotlinx.serialization.Serializable + + +@Serializable +data class MyPageUserSummaryDto( + val userId: Long, + val name: String, + val studentNumberPrefix: String, // 학번 앞 두 자리 (예: "21") + val major: String, + val jobSeeking: Boolean, + val academicStatus: String, // <- String으로 변경 + val employed: Boolean +) \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/data/dto/response/mypage/ProfileUpdateResponse.kt b/app/src/main/java/com/hsLink/hslink/data/dto/response/mypage/ProfileUpdateResponse.kt new file mode 100644 index 0000000..59b7e28 --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/data/dto/response/mypage/ProfileUpdateResponse.kt @@ -0,0 +1,10 @@ +package com.hsLink.hslink.data.dto.response.mypage + +import kotlinx.serialization.Serializable + +@Serializable +data class ProfileUpdateResponse( + val isSuccess: Boolean, + val code: String, + val message: String +) \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/data/repositoryimpl/CareerRepositoryImpl.kt b/app/src/main/java/com/hsLink/hslink/data/repositoryimpl/CareerRepositoryImpl.kt new file mode 100644 index 0000000..3d7c523 --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/data/repositoryimpl/CareerRepositoryImpl.kt @@ -0,0 +1,50 @@ +package com.hsLink.hslink.data.repositoryimpl + +import com.hsLink.hslink.data.dto.request.onboarding.CareerUpdateRequestDto +import com.hsLink.hslink.data.dto.response.onboarding.CareerDto +import com.hsLink.hslink.data.service.CareerService +import com.hsLink.hslink.domain.repository.CareerRepository +import javax.inject.Inject + +class CareerRepositoryImpl @Inject constructor( + private val careerService: CareerService +) : CareerRepository { + + override suspend fun getMyCareers(): Result> { + return try { + val response = careerService.getMyCareers() + if (response.isSuccess) { + Result.success(response.result) + } else { + Result.failure(Exception(response.message)) + } + } catch (e: Exception) { + Result.failure(e) + } + } + + override suspend fun updateCareer(careerId: Long, request: CareerUpdateRequestDto): Result { + return try { + val response = careerService.updateCareer(careerId, request) + if (response.isSuccess) { + Result.success(response.result) + } else { + Result.failure(Exception(response.message)) + } + } catch (e: Exception) { + Result.failure(e) + } + } + override suspend fun getCareer(careerId: Long): Result { + return try { + val response = careerService.getCareer(careerId) + if (response.isSuccess) { + Result.success(response.result) + } 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/repositoryimpl/mypage/MypageRepositoryImpl.kt b/app/src/main/java/com/hsLink/hslink/data/repositoryimpl/mypage/MypageRepositoryImpl.kt new file mode 100644 index 0000000..010eb2d --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/data/repositoryimpl/mypage/MypageRepositoryImpl.kt @@ -0,0 +1,60 @@ +// data/repositoryimpl/mypage/MypageRepositoryImpl.kt +package com.hsLink.hslink.data.repositoryimpl.mypage + +import com.hsLink.hslink.data.dto.request.mypage.UpdateProfileRequestDto +import com.hsLink.hslink.data.dto.response.mypage.MyPageUserProfileDto +import com.hsLink.hslink.data.dto.response.mypage.MyPageUserSummaryDto +import com.hsLink.hslink.data.dto.response.mypage.UserProfileDto +import com.hsLink.hslink.data.service.mypage.MypageService +import com.hsLink.hslink.domain.repository.mypage.MypageRepository +import javax.inject.Inject + +class MypageRepositoryImpl @Inject constructor( + private val mypageService: MypageService +) : MypageRepository { + + override suspend fun getUserProfile(): Result { + return try { + val response = mypageService.getUserProfile() + if (response.isSuccessful) { + response.body()?.let { baseResponse -> + if (baseResponse.isSuccess) { + Result.success(baseResponse.result) + } else { + Result.failure(Exception(baseResponse.message)) + } + } ?: Result.failure(Exception("응답이 비어있습니다")) + } else { + Result.failure(Exception("API 호출 실패: ${response.code()}")) + } + } catch (e: Exception) { + Result.failure(e) + } + } + + override suspend fun updateProfile(request: UpdateProfileRequestDto): Result { + return try { + val response = mypageService.updateProfile(request) + if (response.isSuccess) { + Result.success(Unit) + } else { + Result.failure(Exception(response.message)) + } + } catch (e: Exception) { + Result.failure(e) + } + } + + override suspend fun getUserSummary(): Result { + return try { + val response = mypageService.getUserSummary() + if (response.isSuccess) { + Result.success(response.result) + } 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/CareerService.kt b/app/src/main/java/com/hsLink/hslink/data/service/CareerService.kt new file mode 100644 index 0000000..c9628af --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/data/service/CareerService.kt @@ -0,0 +1,27 @@ +// data/service/CareerService.kt +package com.hsLink.hslink.data.service + +import com.hsLink.hslink.core.network.BaseResponse +import com.hsLink.hslink.data.dto.request.onboarding.CareerUpdateRequestDto +import com.hsLink.hslink.data.dto.response.onboarding.CareerDto +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.PUT +import retrofit2.http.Path + +interface CareerService { + + @GET("careers/mycareers") + suspend fun getMyCareers(): BaseResponse> + + @GET("careers/{careerId}") + suspend fun getCareer( + @Path("careerId") careerId: Long + ): BaseResponse + + @PUT("careers/{careerId}") + suspend fun updateCareer( + @Path("careerId") careerId: Long, + @Body request: CareerUpdateRequestDto + ): BaseResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/data/service/mypage/MypageService.kt b/app/src/main/java/com/hsLink/hslink/data/service/mypage/MypageService.kt new file mode 100644 index 0000000..8cbd1fe --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/data/service/mypage/MypageService.kt @@ -0,0 +1,26 @@ +// data/service/mypage/MypageService.kt +package com.hsLink.hslink.data.service.mypage + +import com.hsLink.hslink.core.network.BaseResponse +import com.hsLink.hslink.data.dto.request.mypage.UpdateProfileRequestDto +import com.hsLink.hslink.data.dto.response.mypage.MyPageUserProfileDto +import com.hsLink.hslink.data.dto.response.mypage.MyPageUserSummaryDto +import com.hsLink.hslink.data.dto.response.mypage.ProfileUpdateResponse +import com.hsLink.hslink.data.dto.response.mypage.UserProfileDto +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.PATCH + +interface MypageService { + @GET("users/myprofile") + suspend fun getUserProfile(): Response> + + @PATCH("users/myprofile") + suspend fun updateProfile( + @Body request: UpdateProfileRequestDto + ): ProfileUpdateResponse + + @GET("users/summary") + suspend fun getUserSummary(): BaseResponse +} diff --git a/app/src/main/java/com/hsLink/hslink/domain/repository/CareerRepository.kt b/app/src/main/java/com/hsLink/hslink/domain/repository/CareerRepository.kt new file mode 100644 index 0000000..f360b31 --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/domain/repository/CareerRepository.kt @@ -0,0 +1,10 @@ +package com.hsLink.hslink.domain.repository + +import com.hsLink.hslink.data.dto.request.onboarding.CareerUpdateRequestDto +import com.hsLink.hslink.data.dto.response.onboarding.CareerDto + +interface CareerRepository { + suspend fun getCareer(careerId: Long): Result + suspend fun getMyCareers(): Result> + suspend fun updateCareer(careerId: Long, request: CareerUpdateRequestDto): Result +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/domain/repository/mypage/MypageRepository.kt b/app/src/main/java/com/hsLink/hslink/domain/repository/mypage/MypageRepository.kt new file mode 100644 index 0000000..fc64f4e --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/domain/repository/mypage/MypageRepository.kt @@ -0,0 +1,16 @@ +// domain/repository/mypage/MypageRepository.kt +package com.hsLink.hslink.domain.repository.mypage + +import com.hsLink.hslink.data.dto.request.mypage.UpdateProfileRequestDto +import com.hsLink.hslink.data.dto.response.mypage.MyPageUserProfileDto +import com.hsLink.hslink.data.dto.response.mypage.MyPageUserSummaryDto +import com.hsLink.hslink.data.dto.response.mypage.UserProfileDto + +interface MypageRepository { + suspend fun getUserProfile(): Result + + suspend fun updateProfile(request: UpdateProfileRequestDto): Result + + suspend fun getUserSummary(): Result + +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/presentation/mypage/navigation/career/CareerEditNavigation.kt b/app/src/main/java/com/hsLink/hslink/presentation/mypage/navigation/career/CareerEditNavigation.kt index c0b1065..34b34ea 100644 --- a/app/src/main/java/com/hsLink/hslink/presentation/mypage/navigation/career/CareerEditNavigation.kt +++ b/app/src/main/java/com/hsLink/hslink/presentation/mypage/navigation/career/CareerEditNavigation.kt @@ -9,21 +9,29 @@ import com.hsLink.hslink.core.navigation.Route import com.hsLink.hslink.presentation.mypage.screen.career.CareerEditRoute import kotlinx.serialization.Serializable -fun NavController.navigateToCareerEdit(navOptions: NavOptions? = null) { - navigate(CareerEdit, navOptions) +// ← careerId 파라미터 추가 +fun NavController.navigateToCareerEdit(careerId: Long, navOptions: NavOptions? = null) { + navigate(CareerEdit(careerId = careerId), navOptions) } fun NavGraphBuilder.careerNavGraph( padding: PaddingValues, navController: NavController, ) { - composable { + composable { backStackEntry -> + val careerEdit = backStackEntry.arguments?.let { + // ← careerId 추출 + CareerEdit(careerId = it.getLong("careerId")) + } ?: CareerEdit(careerId = 0L) + CareerEditRoute( paddingValues = padding, - navController = navController + navController = navController, + careerId = careerEdit.careerId // ← careerId 전달 ) } } +// ← data object에서 data class로 변경 @Serializable -data object CareerEdit : Route \ No newline at end of file +data class CareerEdit(val careerId: Long) : Route \ 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 7f3b2a0..e9ca081 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 @@ -11,6 +11,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn 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.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -22,6 +24,7 @@ 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 androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import com.hsLink.hslink.R import com.hsLink.hslink.core.designsystem.component.HsLinkActionButton @@ -31,19 +34,22 @@ import com.hsLink.hslink.core.designsystem.component.HsLinkSelectButton import com.hsLink.hslink.core.designsystem.component.HsLinkTextField import com.hsLink.hslink.core.designsystem.component.HsLinkTopBar import com.hsLink.hslink.core.designsystem.theme.HsLinkTheme +import com.hsLink.hslink.data.dto.request.onboarding.CareerUpdateRequestDto +import com.hsLink.hslink.data.dto.response.onboarding.CareerDto import com.hsLink.hslink.presentation.mypage.component.career.UnsavedChangesDialog +import com.hsLink.hslink.presentation.mypage.viewmodel.CareerViewModel import com.hsLink.hslink.presentation.onboarding.model.JobType @Preview(showBackground = true) @Composable private fun CareerEditScreenPreview() { HsLinkTheme { - CareerEditScreen( - paddingValues = PaddingValues(), - onBackClick = { }, - onCloseClick = { }, - onSaveClick = { } - ) +// CareerEditScreen( +// paddingValues = PaddingValues(), +// onBackClick = { }, +// onCloseClick = { }, +// onSaveClick = { } +// ) } } @@ -51,13 +57,36 @@ private fun CareerEditScreenPreview() { fun CareerEditRoute( paddingValues: PaddingValues, navController: NavController, + careerId: Long?, // ← Long? + careerViewModel: CareerViewModel = hiltViewModel() ) { + val selectedCareer by careerViewModel.selectedCareer.collectAsState() + val isLoading by careerViewModel.isLoading.collectAsState() + + LaunchedEffect(careerId) { + careerId?.let { id -> + careerViewModel.loadCareer(id) + } + } + CareerEditScreen( paddingValues = paddingValues, + career = selectedCareer, + isLoading = isLoading, onBackClick = { navController.popBackStack() }, onCloseClick = { navController.popBackStack() }, - onSaveClick = { - // 저장 로직 후 이전 화면으로 + onSaveClick = { companyName, position, jobType, startYm, endYm, employed -> + careerId?.let { id -> // ← null 체크 + val requestDto = CareerUpdateRequestDto( // ← 실제 DTO 생성 + companyName = companyName, + position = position, + jobType = jobType, + startYm = startYm, + endYm = endYm, + employed = employed + ) + careerViewModel.updateCareer(id, requestDto) // ← 실제 API 호출 + } navController.popBackStack() } ) @@ -68,29 +97,36 @@ fun CareerEditScreen( paddingValues: PaddingValues, onBackClick: () -> Unit, onCloseClick: () -> Unit, - onSaveClick: () -> Unit, + onSaveClick: (String, String, JobType, String, String?, Boolean) -> Unit, modifier: Modifier = Modifier, + career: CareerDto? = null, // ← 추가 + isLoading: Boolean = false, // ← 추가 ) { - 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.PERMANENT) } + // 기존 하드코딩된 초기값들을 career 데이터로 교체 + var startDate by remember(career) { mutableStateOf(career?.startYm ?: "") } + var endDate by remember(career) { mutableStateOf(career?.endYm ?: "") } + var isCurrentlyEmployed by remember(career) { mutableStateOf(career?.employed ?: false) } + var companyName by remember(career) { mutableStateOf(career?.companyName ?: "") } + var jobName by remember(career) { mutableStateOf(career?.position ?: "") } + var selectedJobType by remember(career) { mutableStateOf(career?.jobType) } + // ← 누락된 Focus 상태 변수들 추가 var companyFocused by remember { mutableStateOf(false) } var jobNameFocused by remember { mutableStateOf(false) } var startDateFocused by remember { mutableStateOf(false) } var endDateFocused by remember { mutableStateOf(false) } + // ← 누락된 Dialog 상태 변수 추가 var showExitDialog by remember { mutableStateOf(false) } + // ← 수정된 hasUnsavedChanges 함수 fun hasUnsavedChanges(): Boolean { - return startDate != "2024.04" || - endDate != "2024.08" || - companyName != "한성대학교" || - jobName != "영업직" || - selectedJobType != JobType.PERMANENT + return startDate != (career?.startYm ?: "") || + endDate != (career?.endYm ?: "") || + companyName != (career?.companyName ?: "") || + jobName != (career?.position ?: "") || + selectedJobType != career?.jobType || + isCurrentlyEmployed != (career?.employed ?: false) } fun handleExit() { @@ -132,7 +168,7 @@ fun CareerEditScreen( item { Text( - text = "아래의 정보를 등록해주세요", + text = "아래의 정보를 수정해주세요", // ← 문구 수정 style = HsLinkTheme.typography.title_20Strong, color = HsLinkTheme.colors.Grey700 ) @@ -142,7 +178,7 @@ fun CareerEditScreen( Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Text( text = buildAnnotatedString { - append("현재 재직 여부 (형식 : 24.04) ") + append("재직 기간 (형식 : 24.04) ") withStyle(style = SpanStyle(color = HsLinkTheme.colors.Red500)) { append("*") } @@ -167,7 +203,11 @@ fun CareerEditScreen( HsLinkTextField( value = endDate, placeholder = "근무종료일", - onValueChanged = { endDate = it }, + onValueChanged = { + if (!isCurrentlyEmployed) { // ← 재직중이 아닐 때만 변경 허용 + endDate = it + } + }, modifier = Modifier.weight(1f), borderColor = if (endDateFocused) HsLinkTheme.colors.SkyBlue500 else HsLinkTheme.colors.Grey300, backgroundColor = HsLinkTheme.colors.Common, @@ -175,7 +215,10 @@ fun CareerEditScreen( ) HsLinkSelectButton( label = "재직중", - onClick = { isCurrentlyEmployed = !isCurrentlyEmployed }, + onClick = { + isCurrentlyEmployed = !isCurrentlyEmployed + if (isCurrentlyEmployed) endDate = "" // 재직중이면 종료일 초기화 + }, size = HsLinkButtonSize.Medium, isSelected = isCurrentlyEmployed ) @@ -297,13 +340,26 @@ fun CareerEditScreen( item { HsLinkActionButton( label = "수정완료", - onClick = onSaveClick, + onClick = { + // 폼 데이터 수집해서 onSaveClick에 전달 + selectedJobType?.let { jobType -> + onSaveClick( + companyName, // String + jobName, // String + jobType, // JobType + startDate, // String + if (isCurrentlyEmployed) null else endDate, // String? + isCurrentlyEmployed // Boolean + ) + } + }, size = HsLinkActionButtonSize.Large, - isEnabled = isFormValid, + isEnabled = isFormValid && !isLoading, modifier = Modifier.fillMaxWidth() ) } } + if (showExitDialog) { UnsavedChangesDialog( onDismiss = { showExitDialog = false }, @@ -313,5 +369,4 @@ fun CareerEditScreen( } ) } - } diff --git a/app/src/main/java/com/hsLink/hslink/presentation/mypage/screen/main/MypageScreen.kt b/app/src/main/java/com/hsLink/hslink/presentation/mypage/screen/main/MypageScreen.kt index 5cc119c..4fa3729 100644 --- a/app/src/main/java/com/hsLink/hslink/presentation/mypage/screen/main/MypageScreen.kt +++ b/app/src/main/java/com/hsLink/hslink/presentation/mypage/screen/main/MypageScreen.kt @@ -10,33 +10,77 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue 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 androidx.navigation.NavController +import androidx.navigation.compose.currentBackStackEntryAsState 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.data.dto.response.mypage.MyPageUserProfileDto +import com.hsLink.hslink.data.dto.response.mypage.MyPageUserSummaryDto +import com.hsLink.hslink.data.dto.response.mypage.UserProfileDto import com.hsLink.hslink.presentation.mypage.component.main.MyPageCardItemContainer import com.hsLink.hslink.presentation.mypage.component.main.MyPageDetailItemContent import com.hsLink.hslink.presentation.mypage.component.main.MyPageItemData import com.hsLink.hslink.presentation.mypage.navigation.profile.navigateToProfileEdit +import com.hsLink.hslink.presentation.mypage.viewmodel.MypageViewModel + +// 상태 텍스트 만드는 함수 +private fun buildStatusText(jobSeeking: Boolean, academicStatus: String, employed: Boolean): String { + val jobText = if (jobSeeking) "구직 중" else "구직 중 아님" + val employedText = if (employed) "재직 중" else "재직 중 아님" + val academicText = when (academicStatus) { + "ENROLLED" -> "재학중" + "GRADUATED" -> "졸업" + "EXPECTED_GRADUATION" -> "졸업예정" + "COMPLETED" -> "수료" + "LEAVE_OF_ABSENCE" -> "휴학" + else -> academicStatus + } + return "$jobText · $employedText · $academicText" +} @Preview(showBackground = true) @Composable private fun MypageScreenPreview() { - MypageScreen(paddingValues = PaddingValues()) } @Composable fun MypageRoute( paddingValues: PaddingValues, navController: NavController, + viewModel: MypageViewModel = hiltViewModel() ) { + val userSummary by viewModel.userSummary.collectAsState() // ← 변경 + val isLoading by viewModel.isLoading.collectAsState() + val error by viewModel.error.collectAsState() + + LaunchedEffect(Unit) { + viewModel.loadUserSummary() // 또는 loadMypage() + } + + // ← 프로필 수정 후 돌아왔을 때 새로고침 + val currentBackStackEntry by navController.currentBackStackEntryAsState() + LaunchedEffect(currentBackStackEntry) { + // 프로필 수정 화면에서 돌아왔을 때만 새로고침 + if (currentBackStackEntry?.destination?.route?.contains("mypage") == true) { + viewModel.loadUserSummary() + } + } + MypageScreen( paddingValues = paddingValues, + userSummary = userSummary, // ← 변경 + isLoading = isLoading, + error = error, onNavigateToProfile = { navController.navigateToProfileEdit() } @@ -47,11 +91,14 @@ fun MypageRoute( fun MypageScreen( modifier: Modifier = Modifier, paddingValues: PaddingValues, - onNavigateToProfile: () -> Unit = {}, // 프로필로 이동 - onNavigateToPosts: () -> Unit = {}, // 게시글로 이동 - onNavigateToSettings: () -> Unit = {}, // 설정으로 이동 - onLogout: () -> Unit = {}, // 로그아웃 - onQuit: () -> Unit = {}, // 탈퇴 + userSummary: MyPageUserSummaryDto? = null, // ← 변경 + isLoading: Boolean = false, + error: String? = null, + onNavigateToProfile: () -> Unit = {}, + onNavigateToPosts: () -> Unit = {}, + onNavigateToSettings: () -> Unit = {}, + onLogout: () -> Unit = {}, + onQuit: () -> Unit = {}, ) { LazyColumn( modifier = modifier @@ -81,26 +128,17 @@ fun MypageScreen( item { MyPageDetailItemContent( - name = "송효재", - title = "21학번 회계재무경영", - subtitle = "구직 중 · 재직 중 · 졸업", + name = userSummary?.name ?: "로딩중...", // ← 변경 + title = if (userSummary != null) { + "${userSummary.studentNumberPrefix}학번 ${userSummary.major}" // ← 변경 + } else "로딩중...", + subtitle = if (userSummary != null) { + buildStatusText(userSummary.jobSeeking, userSummary.academicStatus, userSummary.employed) // ← employed 추가 + } else "로딩중...", onClick = onNavigateToProfile ) } - item { - MyPageCardItemContainer( - text = "마이페이지", - items = listOf( - MyPageItemData(id = "1", title = "나의 게시글", route = "/posts"), - MyPageItemData(id = "2", title = "나의 댓글", route = "/settings") - ), - onItemClick = { item -> - // 클릭 처리 - } - ) - } - item { Spacer(modifier = Modifier.height(24.dp)) } diff --git a/app/src/main/java/com/hsLink/hslink/presentation/mypage/screen/profile/ProfileEditScreen.kt b/app/src/main/java/com/hsLink/hslink/presentation/mypage/screen/profile/ProfileEditScreen.kt index 030da98..d15faf2 100644 --- a/app/src/main/java/com/hsLink/hslink/presentation/mypage/screen/profile/ProfileEditScreen.kt +++ b/app/src/main/java/com/hsLink/hslink/presentation/mypage/screen/profile/ProfileEditScreen.kt @@ -1,5 +1,6 @@ package com.hsLink.hslink.presentation.mypage.screen.profile +import android.util.Log import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -18,6 +19,8 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon 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.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -30,6 +33,7 @@ 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 androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import com.hsLink.hslink.R import com.hsLink.hslink.core.designsystem.component.HsLinkActionButton @@ -39,10 +43,16 @@ import com.hsLink.hslink.core.designsystem.component.HsLinkSelectButton import com.hsLink.hslink.core.designsystem.component.HsLinkTextField import com.hsLink.hslink.core.designsystem.component.HsLinkTopBar import com.hsLink.hslink.core.designsystem.theme.HsLinkTheme +import com.hsLink.hslink.data.dto.response.mypage.MyPageUserProfileDto +import com.hsLink.hslink.data.dto.request.common.JobType // ← 경로 수정 +import com.hsLink.hslink.data.dto.response.onboarding.CareerDto +import com.hsLink.hslink.presentation.mypage.component.profile.CareerCard import com.hsLink.hslink.presentation.mypage.component.profile.CareerCard import com.hsLink.hslink.presentation.mypage.component.profile.SNSCard import com.hsLink.hslink.presentation.mypage.navigation.career.navigateToCareerEdit import com.hsLink.hslink.presentation.mypage.navigation.sns.navigateToSNSEdit +import com.hsLink.hslink.presentation.mypage.viewmodel.CareerViewModel +import com.hsLink.hslink.presentation.mypage.viewmodel.MypageViewModel enum class MajorType(val displayName: String) { ACCOUNTING("회계재무경영"), @@ -55,6 +65,8 @@ enum class MajorType(val displayName: String) { BRAND("브랜드 디자인"), } + + @Preview(showBackground = true) @Composable private fun ProfileEditScreenPreview() { @@ -67,22 +79,51 @@ private fun ProfileEditScreenPreview() { } } +// ProfileEditScreenRoute 수정 @Composable fun ProfileEditScreenRoute( paddingValues: PaddingValues, navController: NavController, onBackClick: () -> Unit, onCloseClick: () -> Unit, + viewModel: MypageViewModel = hiltViewModel(), + careerViewModel: CareerViewModel = hiltViewModel() // ← 추가 + ) { + val userProfile by viewModel.userProfile.collectAsState() + val isLoading by viewModel.isLoading.collectAsState() + val careers by careerViewModel.careers.collectAsState() // ← 추가 + val isCareerLoading by careerViewModel.isLoading.collectAsState() // ← 추가 + + // 프로필 수정 화면 진입 시 전체 프로필 데이터 로드 + LaunchedEffect(Unit) { + viewModel.loadUserProfile() + careerViewModel.loadMyCareers() + } + ProfileEditScreen( paddingValues = paddingValues, + userProfile = userProfile, + careers = careers, // ← 추가 + isLoading = isLoading, + isCareerLoading = isCareerLoading, // ← 추가 onBackClick = onBackClick, onCloseClick = onCloseClick, - onCareerClick = { - navController.navigateToCareerEdit() + onSaveClick = { studentNumber, name, major, mentor, jobSeeking -> + viewModel.updateProfile( + studentNumber = studentNumber, + name = name, + major = major, + mentor = mentor, + jobSeeking = jobSeeking + ) + onBackClick() + }, + onCareerClick = { career -> + navController.navigateToCareerEdit(careerId = career.id) }, onSNSClick = { - navController.navigateToSNSEdit() // ← 추가 + navController.navigateToSNSEdit() } ) } @@ -91,12 +132,17 @@ fun ProfileEditScreenRoute( fun ProfileEditScreen( modifier: Modifier = Modifier, paddingValues: PaddingValues, + userProfile: MyPageUserProfileDto? = null, + isLoading: Boolean = false, + careers: List = emptyList(), + isCareerLoading: Boolean = false, onBackClick: () -> Unit, onCloseClick: () -> Unit, - onSaveClick: () -> Unit = {}, - onCareerClick: () -> Unit = {}, + onSaveClick: (String?, String?, String?, Boolean?, Boolean?) -> Unit = { _, _, _, _, _ -> }, + onCareerClick: (CareerDto) -> Unit = {}, onSNSClick: () -> Unit = {}, ) { + var studentId by remember { mutableStateOf("") } var isStudentIdFocused by remember { mutableStateOf(false) } @@ -111,6 +157,19 @@ fun ProfileEditScreen( var selectedJobStatus by remember { mutableStateOf("") } + // userProfile이 변경될 때마다 상태 업데이트 + LaunchedEffect(userProfile) { + Log.d("ProfileEditScreen", "LaunchedEffect 실행: ${userProfile?.name}") + userProfile?.let { profile -> + studentId = profile.studentNumber + name = profile.name + selectedMajor = profile.major + selectedMentorType = if (profile.mentor) "mentor" else "mentee" + selectedJobStatus = if (profile.jobSeeking) "seeking" else "not_seeking" + Log.d("ProfileEditScreen", "상태 업데이트 완료: $name") + } + } + fun isFormValid(): Boolean { return studentId.isNotEmpty() && name.isNotEmpty() && @@ -162,7 +221,11 @@ fun ProfileEditScreen( HsLinkTextField( value = studentId, placeholder = "21311114", - onValueChanged = { studentId = it }, + onValueChanged = { + if (it.length <= 10) { // <- 길이 제한 + studentId = it + } + }, borderColor = if (isStudentIdFocused) { HsLinkTheme.colors.DeepBlue500 } else { @@ -292,12 +355,14 @@ fun ProfileEditScreen( } Box { - CareerCard( - name = "투썸플레이스", - title = "영업", - dateRange = "2024.02 ~ 2024.10", // ← subtitle을 dateRange로 변경 - onClick = onCareerClick - ) + careers.forEach { career -> + CareerCard( + name = career.companyName, + title = career.position, + dateRange = "${career.startYm} ~ ${career.endYm ?: "현재"}", + onClick = { onCareerClick(career) } + ) + } } } } @@ -327,7 +392,7 @@ fun ProfileEditScreen( selectedMentorType = "mentor" }, size = HsLinkButtonSize.Large, - isSelected = selectedMentorType == "", + isSelected = selectedMentorType == "mentor", modifier = Modifier.fillMaxWidth() ) @@ -389,7 +454,7 @@ fun ProfileEditScreen( selectedJobStatus = "seeking" }, size = HsLinkButtonSize.Large, - isSelected = selectedJobStatus == "", + isSelected = selectedJobStatus == "seeking", modifier = Modifier.fillMaxWidth() ) @@ -409,8 +474,22 @@ fun ProfileEditScreen( HsLinkActionButton( label = "수정완료", onClick = { - // 수정 완료 로직 - onSaveClick() + // 실제 값들 전달 + onSaveClick( + studentId.takeIf { it.isNotEmpty() }, + name.takeIf { it.isNotEmpty() }, + selectedMajor.takeIf { it.isNotEmpty() }, + when(selectedMentorType) { + "mentor" -> true + "mentee" -> false + else -> null + }, + when(selectedJobStatus) { + "seeking" -> true + "not_seeking" -> false + else -> null + } + ) }, size = HsLinkActionButtonSize.Large, isEnabled = isFormValid(), // ← 폼 유효성 검사 diff --git a/app/src/main/java/com/hsLink/hslink/presentation/mypage/viewmodel/CareerViewModel.kt b/app/src/main/java/com/hsLink/hslink/presentation/mypage/viewmodel/CareerViewModel.kt new file mode 100644 index 0000000..89572a6 --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/presentation/mypage/viewmodel/CareerViewModel.kt @@ -0,0 +1,92 @@ +package com.hsLink.hslink.presentation.mypage.viewmodel + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.hsLink.hslink.data.dto.request.onboarding.CareerUpdateRequestDto +import com.hsLink.hslink.data.dto.response.onboarding.CareerDto +import com.hsLink.hslink.domain.repository.CareerRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class CareerViewModel @Inject constructor( + private val careerRepository: CareerRepository +) : ViewModel() { + + private val _careers = MutableStateFlow>(emptyList()) + val careers: StateFlow> = _careers.asStateFlow() + + private val _selectedCareer = MutableStateFlow(null) + val selectedCareer: StateFlow = _selectedCareer.asStateFlow() + + private val _isLoading = MutableStateFlow(false) + val isLoading: StateFlow = _isLoading.asStateFlow() + + private val _error = MutableStateFlow(null) + val error: StateFlow = _error.asStateFlow() + + fun loadMyCareers() { + viewModelScope.launch { + _isLoading.value = true + Log.d("CareerViewModel", "커리어 목록 조회 시작") + + careerRepository.getMyCareers().fold( + onSuccess = { careerList -> + _careers.value = careerList + _error.value = null + Log.d("CareerViewModel", "커리어 조회 성공: ${careerList.size}개") + }, + onFailure = { exception -> + _error.value = exception.message + Log.e("CareerViewModel", "커리어 조회 실패", exception) + } + ) + _isLoading.value = false + } + } + + fun updateCareer(careerId: Long, request: CareerUpdateRequestDto) { + viewModelScope.launch { + _isLoading.value = true + Log.d("CareerViewModel", "커리어 수정 시작: $careerId") + + careerRepository.updateCareer(careerId, request).fold( + onSuccess = { + Log.d("CareerViewModel", "커리어 수정 성공") + // 수정 후 목록 다시 조회 + loadMyCareers() + }, + onFailure = { exception -> + _error.value = exception.message + Log.e("CareerViewModel", "커리어 수정 실패", exception) + } + ) + _isLoading.value = false + } + } + + fun loadCareer(careerId: Long) { + viewModelScope.launch { + _isLoading.value = true + Log.d("CareerViewModel", "커리어 단건 조회 시작: $careerId") + + careerRepository.getCareer(careerId).fold( + onSuccess = { career -> + _selectedCareer.value = career + _error.value = null + Log.d("CareerViewModel", "커리어 조회 성공: ${career.companyName}") + }, + onFailure = { exception -> + _error.value = exception.message + Log.e("CareerViewModel", "커리어 조회 실패", exception) + } + ) + _isLoading.value = false + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hsLink/hslink/presentation/mypage/viewmodel/MypageViewModel.kt b/app/src/main/java/com/hsLink/hslink/presentation/mypage/viewmodel/MypageViewModel.kt new file mode 100644 index 0000000..fbef525 --- /dev/null +++ b/app/src/main/java/com/hsLink/hslink/presentation/mypage/viewmodel/MypageViewModel.kt @@ -0,0 +1,109 @@ +// presentation/mypage/viewmodel/MypageViewModel.kt +package com.hsLink.hslink.presentation.mypage.viewmodel + +import android.content.ContentValues.TAG +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.hsLink.hslink.data.dto.request.mypage.UpdateProfileRequestDto +import com.hsLink.hslink.data.dto.response.mypage.MyPageUserProfileDto +import com.hsLink.hslink.data.dto.response.mypage.MyPageUserSummaryDto +import com.hsLink.hslink.data.dto.response.mypage.UserProfileDto +import com.hsLink.hslink.domain.repository.mypage.MypageRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class MypageViewModel @Inject constructor( + private val mypageRepository: MypageRepository +) : ViewModel() { + + private val _userProfile = MutableStateFlow(null) + val userProfile: StateFlow = _userProfile.asStateFlow() + + private val _isLoading = MutableStateFlow(false) + val isLoading: StateFlow = _isLoading.asStateFlow() + + private val _error = MutableStateFlow(null) + val error: StateFlow = _error.asStateFlow() + + init { + getUserSummary() // ← getUserProfile() 대신 변경 + } + + fun loadUserProfile() { + getUserProfile() + } + + // ← 새로 추가: public loadUserSummary 함수 + fun loadUserSummary() { + getUserSummary() + } + private fun getUserProfile() { + viewModelScope.launch { + _isLoading.value = true + Log.d("MypageViewModel", "API 호출 시작") // <- 로그 추가 + mypageRepository.getUserProfile() + .onSuccess { profile -> + Log.d("MypageViewModel", "API 성공: ${profile.name}") // <- 로그 추가 + _userProfile.value = profile + _error.value = null + } + .onFailure { exception -> + Log.e("MypageViewModel", "API 실패: ${exception.message}") // <- 로그 추가 + _error.value = exception.message + } + _isLoading.value = false + } + } + + fun updateProfile( + studentNumber: String? = null, + name: String? = null, + major: String? = null, + mentor: Boolean? = null, + jobSeeking: Boolean? = null + ) { + viewModelScope.launch { + _isLoading.value = true + val request = UpdateProfileRequestDto(studentNumber, name, major, mentor, jobSeeking) + mypageRepository.updateProfile(request) + .onSuccess { + Log.d("MypageViewModel", "프로필 수정 성공") + // 수정 후 다시 조회 + getUserProfile() // ← 이건 그대로 유지 (전체 정보 필요) + getUserSummary() + } + .onFailure { exception -> + Log.e("MypageViewModel", "프로필 수정 실패: ${exception.message}") + _error.value = exception.message + } + _isLoading.value = false + } + } + private val _userSummary = MutableStateFlow(null) + val userSummary: StateFlow = _userSummary.asStateFlow() + + private fun getUserSummary() { + viewModelScope.launch { + _isLoading.value = true + Log.d(TAG, "Summary API 호출 시작") + + mypageRepository.getUserSummary().fold( + onSuccess = { summary -> + _userSummary.value = summary + Log.d(TAG, "Summary API 성공: ${summary.name}") + }, + onFailure = { exception -> + Log.e(TAG, "Summary API 실패", exception) + } + ).also { + _isLoading.value = false + } + } + } +} \ No newline at end of file