Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e83ab48
[TNT-261] feat: feature:trainee:modifymyinfo 모듈 최초 생성
SeonJeongk Jun 12, 2025
6d308bf
[TNT-261] feat: 트레이니 개인 정보 수정 화면 연결
SeonJeongk Jul 6, 2025
b05cab4
[TNT-261] refactor: 공통 문자열 리소스 정의
SeonJeongk Aug 9, 2025
bfd662e
[TNT-261] feat: 트레이니 개인 정보 수정 화면 구현
SeonJeongk Aug 9, 2025
074ed0a
[TNT-261] feat: 트레이니 PT 목적 수정 UI 구현
SeonJeongk Aug 9, 2025
524d579
[TNT-261] fix: 트레이니 개인 정보 수정 화면 네이밍 통일 및 UI 수정
SeonJeongk Sep 13, 2025
36747d3
[TNT-261] feat: 트레이니 프로필 사진 수정 바텀시트 UI 구현
SeonJeongk Sep 13, 2025
5ed3f82
[TNT-261] feat: 트레이니 개인 정보 수정 완료 버튼 구현
SeonJeongk Sep 13, 2025
6336357
[TNT-261] feat: 트레이니 개인 정보 수정 화면 뒤로가기 팝업 UI 구현
SeonJeongk Sep 13, 2025
6c3b331
[TNT-261] feat: 트레이니 개인 정보 수정 화면 진입 시 정보 불러오기
SeonJeongk Oct 25, 2025
1da663b
[TNT-261] feat: 트레이니 개인 정보 수정 사항 확인 기능 구현
SeonJeongk Oct 25, 2025
74c7d86
[TNT-261] feat: 트레이니 프로필 정책 및 주의사항 유효성 검사 추가
SeonJeongk Oct 29, 2025
0ba0ff4
[TNT-000] fix: SignUpRequest 프로퍼티 수정
SeonJeongk Oct 30, 2025
2160965
[TNT-261] feat: 트레이니 개인 정보 수정 '완료' 버튼 활성화 조건 설정
SeonJeongk Oct 30, 2025
0e086d6
[TNT-000] fix: UpdateUserInfoRequest 프로퍼티 수정
SeonJeongk Nov 3, 2025
3b27e3d
[TNT-261] feat: 트레이니 개인 정보 수정 API 호출 구현
SeonJeongk Nov 7, 2025
11545b3
[TNT-261] feat: 트레이니 개인 정보 수정 API 연동
SeonJeongk Nov 7, 2025
f2bfd2a
[TNT-261] feat: 트레이니 유저 정보 캐싱 처리
SeonJeongk Nov 10, 2025
2267c50
[TNT-261] feat: 트레이니 캐시된 유저 정보 갱신 및 초기화 기능 구현
SeonJeongk Nov 19, 2025
23de98b
[TNT-261] refactor: 트레이니 로그아웃/탈퇴 시 유저 정보 캐시 초기화
SeonJeongk Nov 19, 2025
986c8db
[TNT-261] fix: 트레이너 연결 시 유저 정보 캐시 갱신
SeonJeongk Nov 19, 2025
d882063
[TNT-261] fix: request 에러 수정
SeonJeongk Nov 20, 2025
f827ae1
[TNT-261] Merge branch 'develop' into feature/TNT-261
SeonJeongk Nov 21, 2025
5be6e31
[TNT-261] refactor: 트레이니 개인 정보 수정 화면 신규 TextField 적용
SeonJeongk Nov 21, 2025
58d3654
[TNT-000] fix: 회원 초대 코드 복사 스낵바 아이콘 수정
SeonJeongk Nov 21, 2025
a1e6e8a
[TNT-000] refactor: 트레이너 마이페이지 공통 문자열 리소스 적용
SeonJeongk Nov 21, 2025
4583001
[TNT-261] refactor: 기본 날짜 DEFAULT_DATE로 관리
SeonJeongk Nov 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ sealed interface Route {
@Serializable
data object TraineeMyPage : Route

@Serializable
data object TraineeModifyMyInfo : Route

@Serializable
data object TraineeNotification : Route

Expand Down
13 changes: 13 additions & 0 deletions core/ui/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

<string name="core_entered_wrong_text">잘못된 수치를 입력했어요</string>
<string name="core_text_length_and_format_warning">%s자 미만의 한글 또는 영문으로 입력해주세요</string>
<string name="core_text_length_warning">%s자 미만으로 입력해주세요</string>

<string name="core_trainee">트레이니</string>
<string name="core_trainer">트레이너</string>
Expand Down Expand Up @@ -67,6 +68,14 @@
<!-- PT Session -->
<string name="core_pt_session">%d회차 수업</string>

<!-- PT Purpose -->
<string name="core_loss_weight">체중 감량</string>
<string name="core_strength_improvement">근력 향상</string>
<string name="core_health_care">건강 관리</string>
<string name="core_flexibility">유연성 향상</string>
<string name="core_body_profile">바디프로필</string>
<string name="core_posture_correction">자세 교정</string>

<!-- MyPage -->
<string name="core_modifying_personal_info">개인정보 수정</string>
<string name="core_app_push_notification">앱 푸시 알림</string>
Expand All @@ -81,6 +90,10 @@
<string name="core_logout_content">언제든지 다시 로그인 할 수 있어요!</string>
<string name="core_logout_complete_title">로그아웃이 완료되었어요</string>

<string name="core_delete_account_title">계정을 탈퇴할까요?</string>
<string name="core_delete_account_complete_title">계정 탈퇴가 완료되었어요</string>
<string name="core_delete_account_complete_content">다음에 더 폭발적인 케미로 다시 만나요! 💣</string>

<string name="core_length_warning">%d자 미만으로 입력해주세요.</string>
<string name="core_no_records_yet">아직 등록된 기록이 없어요</string>
<string name="core_confirm_modify_info_exit">"정보 수정을 종료할까요?"</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ data class UpdateUserInfoRequest(
val removeImage: Boolean,
val memberType: MemberType,
val name: String,
val birthDay: String? = null,
val birthday: String? = null,
val height: Double? = null,
val weight: Double? = null,
val cautionNote: String? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,11 @@ class TraineeRemoteDataSource @Inject constructor(
suspend fun getMealRecord(dietId: Long) = networkHandler {
apiService.getMealRecord(dietId)
}

suspend fun putUserInfo(
profileImage: MultipartBody.Part?,
request: RequestBody,
) = networkHandler {
apiService.putMyInfo(profileImage, request)
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
package co.kr.data.repository

import co.kr.data.network.model.UpdateUserInfoRequest
import co.kr.data.network.model.enum.MemberType
import co.kr.data.network.model.toDomain
import co.kr.data.network.model.trainee.MealRecordRequest
import co.kr.data.network.model.trainee.toDomain
import co.kr.data.network.source.TraineeRemoteDataSource
import co.kr.data.network.source.UserRemoteDataSource
import co.kr.tnt.domain.model.ProfileImageUpdatePolicy
import co.kr.tnt.domain.model.User
import co.kr.tnt.domain.model.trainee.TraineeDailyRecord
import co.kr.tnt.domain.model.trainee.TraineeDailyRecordStatus
import co.kr.tnt.domain.model.trainee.TraineeMealRecordDetail
import co.kr.tnt.domain.repository.TraineeRepository
import co.kr.tnt.domain.utils.DateFormatter
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.onStart
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
Expand All @@ -30,10 +35,15 @@ internal class TraineeRepositoryImpl @Inject constructor(
private val dateFormatter: DateFormatter,
private val json: Json,
) : TraineeRepository {
override suspend fun getMyInfo(): User.Trainee {
val user = userRemoteDataSource.getMyInfo().toDomain(dateFormatter)
require(user is User.Trainee)
return user
private val cacheUserInfo = MutableStateFlow(User.Trainee.EMPTY)

override suspend fun getMyInfo(): Flow<User.Trainee> {
return cacheUserInfo
.onStart {
if (cacheUserInfo.value == User.Trainee.EMPTY) {
cacheUserInfo.value = fetchUserInfo()
}
}
}

override suspend fun getWeeklyRecordedDate(
Expand Down Expand Up @@ -67,19 +77,71 @@ internal class TraineeRepositoryImpl @Inject constructor(
dietType = mealType,
memo = memo,
)
val requestBody = mealRecordRequest.toRequestBody()
val requestBody = json
.encodeToString(mealRecordRequest)
.toRequestBody("application/json".toMediaTypeOrNull())

traineeRemoteDataSource.postMealRecord(
dietImage = imagePart,
request = requestBody,
)
}

private fun MealRecordRequest.toRequestBody(): RequestBody {
val jsonString = json.encodeToString(this)
return jsonString.toRequestBody("application/json".toMediaTypeOrNull())
}

override suspend fun getMealRecord(dietId: Long): TraineeMealRecordDetail =
traineeRemoteDataSource.getMealRecord(dietId).toDomain(dateFormatter)

override suspend fun updateUserInfo(
profileImageUpdatePolicy: ProfileImageUpdatePolicy,
userInfo: User.Trainee,
) {
val (profileImage, isRemoveProfileImage) = when (profileImageUpdatePolicy) {
is ProfileImageUpdatePolicy.Change -> profileImageUpdatePolicy.newProfileImage to false
ProfileImageUpdatePolicy.Keep -> null to false
ProfileImageUpdatePolicy.Remove -> null to true
}
val imagePart = profileImage?.let {
val requestFile = it.asRequestBody("image/*".toMediaTypeOrNull())
MultipartBody.Part.createFormData("profileImage", it.name, requestFile)
}
val selectedDate = userInfo.birthday?.let { dateFormatter.format(it, "yyyy-MM-dd") }

val request = UpdateUserInfoRequest(
removeImage = isRemoveProfileImage,
memberType = MemberType.TRAINEE,
name = userInfo.name,
birthday = selectedDate,
height = userInfo.height?.toDouble(),
weight = userInfo.weight,
cautionNote = userInfo.caution,
ptGoals = userInfo.ptPurpose,
)
val requestBody = json
.encodeToString(request)
.toRequestBody("application/json".toMediaTypeOrNull())

runCatching {
traineeRemoteDataSource.putUserInfo(
profileImage = imagePart,
request = requestBody,
)
}.onSuccess {
refreshCachedUserInfo()
}.onFailure { failure ->
throw failure
}
}

private suspend fun fetchUserInfo(): User.Trainee {
val user = userRemoteDataSource.getMyInfo().toDomain(dateFormatter)
require(user is User.Trainee)
return user
}

override suspend fun refreshCachedUserInfo() {
cacheUserInfo.value = fetchUserInfo()
}

override suspend fun clearCachedUserInfo() {
cacheUserInfo.value = User.Trainee.EMPTY
}
}
12 changes: 12 additions & 0 deletions domain/src/main/java/co/kr/tnt/domain/Policy.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ object UserProfilePolicy {
// TnT 에서 사용자가 입력할 수 있는 이름의 최대 길이는 15자이다.
const val USER_NAME_MAX_LENGTH = 15

// TnT 에서 사용자가 입력할 수 있는 키의 최대 길이는 3자이다.
const val USER_HEIGHT_MAX_LENGTH = 3

// TnT 에서 사용자가 입력할 수 있는 몸무게의 최대 길이는 5자이다. (000.0)
const val USER_WEIGHT_MAX_LENGTH = 5

// TnT 에서 사용자가 입력할 수 있는 주의사항의 최대 길이는 100자이다.
const val USER_CAUTION_MAX_LENGTH = 100

// TnT 에서 사용자가 입력할 수 있는 이름은 한글, 영어, 공백만 허용한다.
val USER_NAME_REGEX = Regex("^[a-zA-Zㄱ-ㅎㅏ-ㅣ가-힣 ]+\$")

// TnT 에서 사용자가 입력할 수 있는 몸무게는 소수점 이하 한 자리까지만 허용한다. (000, 00, 00.0, 000.0)
val USER_WEIGHT_REGEX = Regex("^(\\d{1,3}(\\.\\d)?)?\$")
}
10 changes: 10 additions & 0 deletions domain/src/main/java/co/kr/tnt/domain/model/PtPurpose.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package co.kr.tnt.domain.model

enum class PtPurpose {
LOSS_WEIGHT,
STRENGTH,
HEALTH_CARE,
FLEXIBILITY,
BODY_PROFILE,
POSTURE_CORRECTION,
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package co.kr.tnt.domain.repository

import co.kr.tnt.domain.model.ProfileImageUpdatePolicy
import co.kr.tnt.domain.model.User
import co.kr.tnt.domain.model.trainee.TraineeDailyRecord
import co.kr.tnt.domain.model.trainee.TraineeDailyRecordStatus
import co.kr.tnt.domain.model.trainee.TraineeMealRecordDetail
import kotlinx.coroutines.flow.Flow
import java.io.File
import java.time.LocalDate

interface TraineeRepository {
suspend fun getMyInfo(): User.Trainee
suspend fun getMyInfo(): Flow<User.Trainee>
suspend fun postMealRecord(
mealImage: File?,
date: String,
Expand All @@ -23,4 +25,10 @@ interface TraineeRepository {
suspend fun getMealRecord(
dietId: Long,
): TraineeMealRecordDetail
suspend fun updateUserInfo(
profileImageUpdatePolicy: ProfileImageUpdatePolicy,
userInfo: User.Trainee,
)
suspend fun refreshCachedUserInfo()
suspend fun clearCachedUserInfo()
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package co.kr.tnt.trainee.connect
import androidx.lifecycle.viewModelScope
import co.kr.tnt.core.ui.R.string.core_failed_to_server_request
import co.kr.tnt.domain.repository.ConnectRepository
import co.kr.tnt.domain.repository.TraineeRepository
import co.kr.tnt.trainee.connect.TraineeConnectContract.TraineeConnectPage
import co.kr.tnt.trainee.connect.TraineeConnectContract.TraineeConnectSideEffect
import co.kr.tnt.trainee.connect.TraineeConnectContract.TraineeConnectUiEvent
Expand All @@ -20,6 +21,7 @@ import javax.inject.Inject
@HiltViewModel
internal class TraineeConnectViewModel @Inject constructor(
private val connectRepository: ConnectRepository,
private val traineeRepository: TraineeRepository,
) :
BaseViewModel<TraineeConnectUiState, TraineeConnectUiEvent, TraineeConnectSideEffect>(
TraineeConnectUiState(),
Expand Down Expand Up @@ -100,6 +102,7 @@ internal class TraineeConnectViewModel @Inject constructor(
traineeImage = result.traineeImage,
)
}
refreshCachedUserInfo()
navigateToNext()
}.onFailure {
sendEffect(
Expand All @@ -113,6 +116,12 @@ internal class TraineeConnectViewModel @Inject constructor(
}
}

private fun refreshCachedUserInfo() {
viewModelScope.launch {
traineeRepository.refreshCachedUserInfo()
}
}

private fun navigateToBack() {
if (currentState.page == TraineeConnectPage.firstPage) {
handleDialogState()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest
import com.kizitonwose.calendar.compose.weekcalendar.WeekCalendarState
import com.kizitonwose.calendar.compose.weekcalendar.rememberWeekCalendarState
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import java.time.DayOfWeek
import java.time.LocalDate
Expand Down Expand Up @@ -145,7 +146,7 @@ internal fun TraineeHomeRoute(
}

LaunchedEffect(viewModel.effect) {
viewModel.effect.collect { effect ->
viewModel.effect.collectLatest { effect ->
when (effect) {
TraineeHomeEffect.NavigateToExerciseRecord -> {
showBottomSheet = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import co.kr.tnt.ui.base.BaseViewModel
import co.kr.tnt.ui.resource.DisplayText
import com.kizitonwose.calendar.core.yearMonth
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import java.time.Duration
import java.time.LocalDate
Expand Down Expand Up @@ -161,28 +164,32 @@ internal class TraineeHomeViewModel @Inject constructor(
cachedMonthlyRecordState.clear()
handleChangeVisibleMonth(currentState.selectedDay.yearMonth)
selectDay(currentState.selectedDay)
showConnectDialog()
getUserInfo()
}

private fun showConnectDialog() {
val currentDateTime = LocalDateTime.now()

private fun getUserInfo() {
viewModelScope.launch {
runCatching {
traineeRepository.getMyInfo()
}.onSuccess { result ->
updateState { copy(isConnected = result.isConnected) }
if (result.isConnected) {
return@launch
traineeRepository.getMyInfo()
.onEach { user ->
updateState { copy(isConnected = user.isConnected) }
if (user.isConnected.not()) {
showConnectDialog()
}
}
}.onFailure {
sendEffect(
TraineeHomeEffect.ShowToast(
DisplayText.Resource(core_failed_to_server_request),
),
)
}
.catch {
sendEffect(
TraineeHomeEffect.ShowToast(
DisplayText.Resource(core_failed_to_server_request),
),
)
}
.launchIn(viewModelScope)
}
}

private fun showConnectDialog() {
val currentDateTime = LocalDateTime.now()
viewModelScope.launch {
val lastHiddenDate = connectRepository.getExplicitDeniedConnectDate().firstOrNull()
val isHidden = lastHiddenDate != null &&
Duration.between(lastHiddenDate, currentDateTime).toHours() < DIALOG_HIDE_DURATION_HOURS
Expand Down
1 change: 1 addition & 0 deletions feature/trainee/main/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ dependencies {
implementation(projects.feature.trainee.notification)
implementation(projects.feature.trainee.mealrecord)
implementation(projects.feature.trainee.mealdetail)
implementation(projects.feature.trainee.modifymyinfo)

implementation(libs.kotlinx.immutable)
}
Loading