Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
08cef7f
[TNT-260] chore: detekt UnusedParameter 검사 비활성화 처리
hoyahozz Mar 15, 2025
31467c7
[TNT-260] feat: feature:trainer:modifymyinfo 모듈 최초 생성
hoyahozz Mar 15, 2025
3b653f9
[TNT-260] feat: 트레이너 마이페이지 -> 개인정보 수정 화면 이동 구현
hoyahozz Mar 16, 2025
a2d22a5
[TNT-260] feat: 개인정보 수정 화면 Contract 정의
hoyahozz Mar 16, 2025
8889399
[TNT-260] feat: 개인정보 수정 화면 내 State, Event 연동
hoyahozz Mar 16, 2025
347fc6d
[TNT-260] feat: 최초 진입 시 내 정보 불러오기 API 연동 및 정보 수정 관찰 구현
hoyahozz Mar 16, 2025
9a46616
[TNT-260] fix: 변경사항이 있는 경우에만 '완료' 버튼이 활성화되도록 수정
hoyahozz Jul 8, 2025
bf465cd
[TNT-260] feat: 트레이너 정보 수정 API 호출 구현
hoyahozz Jul 8, 2025
a342a9d
[TNT-260] refactor: 프로필 이미지 업데이트 정책 도메인 모델 정의 및 적용
hoyahozz Jul 25, 2025
4ee47af
[TNT-260] refactor: 유저 프로필 정책 정의
hoyahozz Jul 25, 2025
cf5a630
[TNT-260] feat: 이름 유효성 검사 추가
hoyahozz Jul 25, 2025
757d38e
[TNT-260] fix: detekt apply
hoyahozz Jul 25, 2025
b8944c7
[TNT-260] feat: 유저 정보 캐싱 처리 및 마이페이지 내 실시간 업데이트 처리
hoyahozz Jul 29, 2025
7d0fe04
[TNT-260] Merge branch 'develop' into feature/TNT-260
hoyahozz Jul 29, 2025
fa108a1
[TNT-260] Merge branch 'develop' into feature/TNT-260
hoyahozz Aug 9, 2025
4f9d544
[TNT-260] refactor: 공통 문자열 리소스 적용
hoyahozz Aug 9, 2025
dab3669
[TNT-260] fix: 문자열 리소스 정의
hoyahozz Aug 9, 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
2 changes: 2 additions & 0 deletions config/detekt/detekt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,8 @@ style:
# active: true
UnusedPrivateMember:
active: false
UnusedParameter:
active: false
# allowedNames: '(_|ignored|expected|serialVersionUID)'
# UseArrayLiteralsInAnnotations:
# active: false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ sealed interface Route {
@Serializable
data object TrainerNotification : Route

@Serializable
data object TrainerModifyMyInfo : Route

@Serializable
data class AddPtSession(val selectedDate: String) : Route

Expand Down
4 changes: 2 additions & 2 deletions core/ui/src/main/java/co/kr/tnt/ui/utils/FileUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import android.graphics.BitmapFactory
import android.net.Uri
import android.provider.MediaStore
import android.util.Log
import co.kr.tnt.domain.IMAGE_MAX_SIZE
import co.kr.tnt.domain.UserProfilePolicy
import co.kr.tnt.ui.extensions.toResizedByteArray
import java.io.File

Expand All @@ -33,7 +33,7 @@ fun getRealPathFromUri(uri: Uri, context: Context): String? {

fun Uri.convertToAllowedImageFormat(
context: Context,
maxSizeInBytes: Int = IMAGE_MAX_SIZE,
maxSizeInBytes: Int = UserProfilePolicy.USER_IMAGE_MAX_SIZE,
): File {
val inputStream = context.contentResolver.openInputStream(this)
val bitmap = BitmapFactory.decodeStream(inputStream)
Expand Down
4 changes: 4 additions & 0 deletions core/ui/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

<string name="core_close">닫기</string>
<string name="core_ok">확인</string>
<string name="core_complete">완료</string>
<string name="core_move_to_setting">설정으로 이동</string>

<string name="core_entered_wrong_text">잘못된 수치를 입력했어요</string>
Expand All @@ -42,6 +43,7 @@
<string name="core_age_unit">세</string>
<string name="core_height_unit">cm</string>
<string name="core_weight_unit">kg</string>
<string name="core_name_placeholder">이름을 입력해주세요</string>

<string name="core_notification">알림</string>
<string name="core_no_recent_notifications">최근 받은 알림이 없어요</string>
Expand Down Expand Up @@ -81,4 +83,6 @@

<string name="core_length_warning">%d자 미만으로 입력해주세요.</string>
<string name="core_no_records_yet">아직 등록된 기록이 없어요</string>
<string name="core_confirm_modify_info_exit">"정보 수정을 종료할까요?"</string>
<string name="core_unsaved_changes_warning">"수정 사항이 저장되지 않아요!"</string>
</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package co.kr.data.network.model

import co.kr.data.network.model.enum.MemberType
import kotlinx.serialization.Serializable

@Serializable
data class UpdateUserInfoRequest(
val removeImage: Boolean,
val memberType: MemberType,
val name: String,
val birthDay: String? = null,
val height: Double? = null,
val weight: Double? = null,
val cautionNote: String? = null,
val goalContents: List<String>? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ enum class MemberType {
TRAINER,
TRAINEE,
UNREGISTERED,
;

companion object {
fun from(userType: UserType): MemberType {
return when (userType) {
UserType.TRAINER -> TRAINER
UserType.TRAINEE -> TRAINEE
}
}
}
}

fun MemberType.toDomain(): UserType =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ interface ApiService {
@GET("/members")
suspend fun getMyInfo(): UserResponse

@Multipart
@PUT("/members")
suspend fun putMyInfo(
@Part profileImage: MultipartBody.Part?,
@Part("request") request: RequestBody,
)

// Connect
@GET("/trainers/invitation-code")
suspend fun getInviteCode(): InviteCodeResponse
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
package co.kr.data.network.source

import co.kr.data.network.model.UpdateUserInfoRequest
import co.kr.data.network.model.trainer.ActiveTraineesResponse
import co.kr.data.network.model.trainer.DailyPtSessionsResponse
import co.kr.data.network.model.trainer.MonthlyPtSessionCountsResponse
import co.kr.data.network.model.trainer.PtSessionRequest
import co.kr.data.network.service.ApiService
import co.kr.data.network.util.networkHandler
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class TrainerRemoteDataSource @Inject constructor(
private val apiService: ApiService,
private val json: Json,
) {
suspend fun getMonthlyPtSessionCounts(
year: Int,
Expand Down Expand Up @@ -40,4 +49,26 @@ class TrainerRemoteDataSource @Inject constructor(
suspend fun putCompletePtSession(ptSessionId: String) = networkHandler {
apiService.putCompletePtSession(ptSessionId)
}

suspend fun putUserInfo(
profileImage: File?,
request: UpdateUserInfoRequest,
) {
// TODO 공통 로직 추출
val profileImagePart = profileImage?.let {
val requestFile = it.asRequestBody("image/*".toMediaTypeOrNull())
MultipartBody.Part.createFormData("profileImage", it.name, requestFile)
}

val requestBody = json
.encodeToString(request)
.toRequestBody("application/json".toMediaTypeOrNull())

networkHandler {
apiService.putMyInfo(
profileImage = profileImagePart,
request = requestBody,
)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
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.trainer.PtSessionRequest
import co.kr.data.network.model.trainer.toDomain
import co.kr.data.network.source.TrainerRemoteDataSource
import co.kr.data.network.source.UserRemoteDataSource
import co.kr.tnt.domain.model.MemberInfo
import co.kr.tnt.domain.model.ProfileImageUpdatePolicy
import co.kr.tnt.domain.model.User
import co.kr.tnt.domain.model.UserType
import co.kr.tnt.domain.model.trainer.TrainerDailyPtSession
import co.kr.tnt.domain.model.trainer.TrainerDailyPtSessionCount
import co.kr.tnt.domain.repository.TrainerRepository
import co.kr.tnt.domain.utils.DateFormatter
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.onStart
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.YearMonth
Expand All @@ -23,6 +30,8 @@ internal class TrainerRepositoryImpl @Inject constructor(
private val trainerRemoteDataSource: TrainerRemoteDataSource,
private val dateFormatter: DateFormatter,
) : TrainerRepository {
private val cacheUserInfo = MutableStateFlow(User.Trainer.EMPTY)

override suspend fun getMonthlyPtSessionCounts(yearMonth: YearMonth): List<TrainerDailyPtSessionCount> =
trainerRemoteDataSource.getMonthlyPtSessionCounts(
year = yearMonth.year,
Expand All @@ -31,10 +40,13 @@ internal class TrainerRepositoryImpl @Inject constructor(
response.toDomain(dateFormatter)
}

override suspend fun getMyInfo(): User.Trainer {
val user = userRemoteDataSource.getMyInfo().toDomain(dateFormatter)
require(user is User.Trainer)
return user
override suspend fun getMyInfo(): Flow<User.Trainer> {
return cacheUserInfo
.onStart {
if (cacheUserInfo.value == User.Trainer.EMPTY) {
cacheUserInfo.value = fetchUserInfo()
}
}
}

override suspend fun getDailyPtSessions(day: LocalDate): TrainerDailyPtSession =
Expand Down Expand Up @@ -65,4 +77,36 @@ internal class TrainerRepositoryImpl @Inject constructor(

override suspend fun postCompleteSession(ptSessionId: String) =
trainerRemoteDataSource.putCompletePtSession(ptSessionId)

override suspend fun updateUserInfo(
profileImageUpdatePolicy: ProfileImageUpdatePolicy,
name: String,
) {
val (profileImage, isRemoveProfileImage) = when (profileImageUpdatePolicy) {
is ProfileImageUpdatePolicy.Change -> profileImageUpdatePolicy.newProfileImage to false
ProfileImageUpdatePolicy.Keep -> null to false
ProfileImageUpdatePolicy.Remove -> null to true
}

runCatching {
trainerRemoteDataSource.putUserInfo(
profileImage = profileImage,
request = UpdateUserInfoRequest(
removeImage = isRemoveProfileImage,
memberType = MemberType.from(UserType.TRAINER),
name = name,
),
)
}.onSuccess {
cacheUserInfo.value = fetchUserInfo()
}.onFailure { failure ->
throw failure
}
}

private suspend fun fetchUserInfo(): User.Trainer {
val user = userRemoteDataSource.getMyInfo().toDomain(dateFormatter)
require(user is User.Trainer)
return user
}
}
4 changes: 0 additions & 4 deletions domain/src/main/java/co/kr/tnt/domain/Constants.kt

This file was deleted.

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
@@ -0,0 +1,12 @@
package co.kr.tnt.domain

object UserProfilePolicy {
// TnT 에서 사용자가 업로드할 수 있는 이미지의 최대 용량은 10MB이다.
const val USER_IMAGE_MAX_SIZE = 10 * 1024 * 1024

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

정책을 따로 분리해두니 훨씬 깔끔하고 좋네요..! 전부 적용하시느라 고생 많으셨습니다 👍

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package co.kr.tnt.domain.model

import java.io.File

sealed interface ProfileImageUpdatePolicy {
data object Keep : ProfileImageUpdatePolicy
data class Change(val newProfileImage: File) : ProfileImageUpdatePolicy
data object Remove : ProfileImageUpdatePolicy
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
package co.kr.tnt.domain.repository

import co.kr.tnt.domain.model.MemberInfo
import co.kr.tnt.domain.model.ProfileImageUpdatePolicy
import co.kr.tnt.domain.model.User
import co.kr.tnt.domain.model.trainer.TrainerDailyPtSession
import co.kr.tnt.domain.model.trainer.TrainerDailyPtSessionCount
import kotlinx.coroutines.flow.Flow
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.YearMonth

interface TrainerRepository {
suspend fun getMyInfo(): User.Trainer
suspend fun getMyInfo(): Flow<User.Trainer>
suspend fun getMonthlyPtSessionCounts(yearMonth: YearMonth): List<TrainerDailyPtSessionCount>
suspend fun getDailyPtSessions(day: LocalDate): TrainerDailyPtSession
suspend fun getActiveMembers(): List<MemberInfo>
Expand All @@ -19,5 +21,10 @@ interface TrainerRepository {
memo: String,
traineeId: Long,
)

suspend fun postCompleteSession(ptSessionId: String)
suspend fun updateUserInfo(
profileImageUpdatePolicy: ProfileImageUpdatePolicy,
name: String,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import co.kr.tnt.core.ui.R.string.core_next
import co.kr.tnt.designsystem.component.TnTProfileImage
import co.kr.tnt.designsystem.component.button.TnTBottomButton
import co.kr.tnt.designsystem.theme.TnTTheme
import co.kr.tnt.domain.IMAGE_MAX_SIZE
import co.kr.tnt.domain.UserProfilePolicy
import co.kr.tnt.domain.model.UserType
import co.kr.tnt.feature.trainee.connect.R
import co.kr.tnt.ui.coil.ResizeTransformation
Expand Down Expand Up @@ -130,7 +130,7 @@ private fun ProfileSection(
.data(image)
.placeholder(defaultImage)
.error(DefaultUserProfile.Trainer.image)
.transformations(ResizeTransformation(IMAGE_MAX_SIZE))
.transformations(ResizeTransformation(UserProfilePolicy.USER_IMAGE_MAX_SIZE))
.build(),
)
Column(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import co.kr.tnt.designsystem.component.card.TnTRecordCard
import co.kr.tnt.designsystem.component.card.TnTSessionRecordCard
import co.kr.tnt.designsystem.snackbar.LocalSnackbar
import co.kr.tnt.designsystem.theme.TnTTheme
import co.kr.tnt.domain.IMAGE_MAX_SIZE
import co.kr.tnt.domain.UserProfilePolicy
import co.kr.tnt.domain.model.DailyRecord
import co.kr.tnt.domain.model.RecordType
import co.kr.tnt.domain.model.RecordType.PTSessionType
Expand Down Expand Up @@ -344,7 +344,7 @@ private fun DailyPtSession(
model = ImageRequest.Builder(context)
.data(session.trainerImage)
.placeholder(DefaultUserProfile.Trainer.image)
.transformations(ResizeTransformation(IMAGE_MAX_SIZE))
.transformations(ResizeTransformation(UserProfilePolicy.USER_IMAGE_MAX_SIZE))
.build(),
)

Expand Down Expand Up @@ -399,7 +399,7 @@ private fun DailyRecords(
model = ImageRequest.Builder(context)
.data(record.recordImage)
.placeholder(co.kr.tnt.core.designsystem.R.drawable.img_default)
.transformations(ResizeTransformation(IMAGE_MAX_SIZE))
.transformations(ResizeTransformation(UserProfilePolicy.USER_IMAGE_MAX_SIZE))
.build(),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import co.kr.tnt.designsystem.component.TnTTopBarWithBackButton
import co.kr.tnt.designsystem.component.chip.TnTChip
import co.kr.tnt.designsystem.snackbar.LocalSnackbar
import co.kr.tnt.designsystem.theme.TnTTheme
import co.kr.tnt.domain.IMAGE_MAX_SIZE
import co.kr.tnt.domain.UserProfilePolicy
import co.kr.tnt.domain.model.RecordType.MealType
import co.kr.tnt.domain.utils.DateFormatter
import co.kr.tnt.trainee.mealdetail.TraineeMealDetailContract.TraineeMealDetailSideEffect
Expand Down Expand Up @@ -94,7 +94,7 @@ private fun TraineeMealDetailScreen(
model = ImageRequest.Builder(context)
.data(state.image)
.placeholder(R.drawable.img_default)
.transformations(ResizeTransformation(IMAGE_MAX_SIZE))
.transformations(ResizeTransformation(UserProfilePolicy.USER_IMAGE_MAX_SIZE))
.build(),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ import co.kr.tnt.designsystem.component.calendar.model.DayState
import co.kr.tnt.designsystem.component.calendar.utils.rememberMostVisibleMonth
import co.kr.tnt.designsystem.snackbar.LocalSnackbar
import co.kr.tnt.designsystem.theme.TnTTheme
import co.kr.tnt.domain.IMAGE_MAX_SIZE
import co.kr.tnt.domain.UserProfilePolicy
import co.kr.tnt.domain.model.RecordType.MealType
import co.kr.tnt.domain.utils.DateFormatter
import co.kr.tnt.feature.trainee.mealrecord.R
Expand Down Expand Up @@ -359,7 +359,7 @@ private fun MealImageSelector(
val painter = rememberAsyncImagePainter(
model = ImageRequest.Builder(context)
.data(imageUri)
.transformations(ResizeTransformation(IMAGE_MAX_SIZE))
.transformations(ResizeTransformation(UserProfilePolicy.USER_IMAGE_MAX_SIZE))
.build(),
)
Column(
Expand Down
Loading