Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
@@ -0,0 +1,60 @@
package com.konkuk.medicarecall.ui.theme

import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.Dp

actual fun Modifier.figmaShadow(
group: ShadowGroup,
cornerRadius: Dp,
): Modifier = this.drawWithCache {
val radiusPx = cornerRadius.toPx()
val paint = Paint().asFrameworkPaint().apply {
isAntiAlias = true
color = 0
}

onDrawWithContent {
group.layers.forEach { layer ->
paint.setShadowLayer(
layer.blurRadius.toPx(),
layer.offsetX.toPx(),
layer.offsetY.toPx(),
layer.color.toArgb(),
)
drawIntoCanvas { canvas ->
val nc = canvas.nativeCanvas
nc.drawRoundRect(
0f,
0f,
size.width,
size.height,
radiusPx,
radiusPx,
paint,
)
}
}

paint.clearShadowLayer()

drawIntoCanvas { canvas ->
val nc = canvas.nativeCanvas
nc.drawRoundRect(
0f,
0f,
size.width,
size.height,
radiusPx,
radiusPx,
paint,
)
}

drawContent()
}
}
Comment on lines +8 to +19
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: ShadowLayer 및 ShadowGroup 정의 확인

# ShadowLayer 클래스/데이터 클래스 정의 검색
echo "=== ShadowLayer 정의 ==="
rg -n -A 10 'data class ShadowLayer|class ShadowLayer'

echo ""
echo "=== ShadowGroup 정의 ==="
rg -n -A 10 'data class ShadowGroup|class ShadowGroup'

Repository: Medicare-Call/Medicare-Call-KMP

Length of output: 4049


offset 속성 처리 필요해요 🎨

코드 구조는 깔끔하고 fold 패턴으로 여러 shadow layer를 쌓아가는 방식도 좋습니다. 근데 예상대로 ShadowLayer에는 offsetXoffsetY 속성이 있는데, 현재 구현에서는 이게 무시되고 있어요.

Modifier.shadow()는 x/y offset을 직접 지원하지 않기 때문에, offset 값들을 반영하려면 Modifier.offset()을 조합하거나 다른 접근이 필요합니다. Figma 디자인에서 offset이 중요하다면 다음처럼 처리할 수 있어요:

actual fun Modifier.figmaShadow(
    group: ShadowGroup,
    cornerRadius: Dp,
): Modifier = group.layers.fold(this) { acc, layer ->
    acc.offset(x = layer.offsetX, y = layer.offsetY)
        .shadow(
            elevation = layer.blurRadius,
            shape = RoundedCornerShape(cornerRadius),
            ambientColor = layer.color,
            spotColor = layer.color,
            clip = false
        )
}

offset 없이도 디자인이 괜찮다면 현재대로 두셔도 괜찮지만, Figma와의 정확한 일치가 필요하다면 위처럼 처리해주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@composeApp/src/androidMain/kotlin/com/konkuk/medicarecall/ui/theme/Effect.android.kt`
around lines 8 - 19, The current Modifier.figmaShadow implementation ignores
each ShadowLayer's offsetX/offsetY so shadows don't match Figma; update
Modifier.figmaShadow (the fold over group.layers) to apply layer offsets before
applying shadow (e.g., compose acc.offset(x = layer.offsetX, y = layer.offsetY)
then .shadow(...)) so the ShadowGroup layers' offsets are respected when
rendering; ensure you reference layer.offsetX and layer.offsetY from the
ShadowLayer and preserve existing cornerRadius, blurRadius, color, and clip
behavior.

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.konkuk.medicarecall.ui.feature.settings.mydata.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.konkuk.medicarecall.data.repository.UserRepository
import com.konkuk.medicarecall.domain.model.PushNotification
import com.konkuk.medicarecall.domain.model.UserInfo
import com.konkuk.medicarecall.domain.model.type.GenderType
import kotlinx.coroutines.flow.MutableStateFlow
Expand Down Expand Up @@ -56,7 +57,12 @@ class SettingsEditMyDataViewModel(
try {
userRepository.updateMyInfo(userInfo)
.onSuccess {
_uiState.update { it.copy(isUpdateSuccess = true) }
_uiState.update {
it.copy(
isUpdateSuccess = true,
myDataInfo = userInfo, // 저장 성공 시 최신 데이터로 동기화
)
}
onComplete?.invoke()
}
.onFailure { e ->
Expand All @@ -72,11 +78,6 @@ class SettingsEditMyDataViewModel(
}
}

// 화면 진입 시나 필요 시 상태 초기화
fun resetStatus() {
_uiState.update { it.copy(isUpdateSuccess = false, errorMessage = null) }
}

fun initializeNotificationSettings(myDataInfo: UserInfo) {
val masterOn = myDataInfo.pushNotification.isAllEnabled
_uiState.update {
Expand All @@ -89,42 +90,70 @@ class SettingsEditMyDataViewModel(
}
}

fun setMasterChecked(value: Boolean) {
_uiState.update {
it.copy(
masterChecked = value,
completeChecked = value,
abnormalChecked = value,
missedChecked = value,
)
}
}
// 토글 타입별로 새 상태를 계산하고 서버에 반영하는 함수
fun updateNotificationSettingByType(
type: String,
checked: Boolean,
) {
val currentInfo = uiState.value.myDataInfo ?: return
val currentState = uiState.value

fun setCompleteChecked(value: Boolean) {
_uiState.update {
it.copy(
completeChecked = value,
masterChecked = if (!value) false else it.masterChecked,
)
}
}
val newMasterChecked: Boolean
val newCompleteChecked: Boolean
val newAbnormalChecked: Boolean
val newMissedChecked: Boolean

fun setAbnormalChecked(value: Boolean) {
_uiState.update {
it.copy(
abnormalChecked = value,
masterChecked = if (!value) false else it.masterChecked,
)
when (type) {
"master" -> {
newMasterChecked = checked
newCompleteChecked = checked
newAbnormalChecked = checked
newMissedChecked = checked
}

"complete" -> {
newCompleteChecked = checked
newAbnormalChecked = currentState.abnormalChecked
newMissedChecked = currentState.missedChecked
newMasterChecked = checked && newAbnormalChecked && newMissedChecked
}

"abnormal" -> {
newCompleteChecked = currentState.completeChecked
newAbnormalChecked = checked
newMissedChecked = currentState.missedChecked
newMasterChecked = newCompleteChecked && checked && newMissedChecked
}

"missed" -> {
newCompleteChecked = currentState.completeChecked
newAbnormalChecked = currentState.abnormalChecked
newMissedChecked = checked
newMasterChecked = newCompleteChecked && newAbnormalChecked && checked
}

else -> return
}
}

fun setMissedChecked(value: Boolean) {
_uiState.update {
it.copy(
missedChecked = value,
masterChecked = if (!value) false else it.masterChecked,
masterChecked = newMasterChecked,
completeChecked = newCompleteChecked,
abnormalChecked = newAbnormalChecked,
missedChecked = newMissedChecked,
)
}

val updatedUserInfo = currentInfo.copy(
pushNotification = PushNotification(
all = if (newMasterChecked) "ON" else "OFF",
carecallCompleted = if (newCompleteChecked) "ON" else "OFF",
healthAlert = if (newAbnormalChecked) "ON" else "OFF",
carecallMissed = if (newMissedChecked) "ON" else "OFF",
),
)

updateUserData(updatedUserInfo)
Comment on lines 138 to +156
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

낙관적 업데이트(Optimistic Update) 시 에러 발생 시 롤백 로직 부재

UI 상태를 먼저 업데이트(Line 138-145)한 후 서버 호출(Line 156)을 하는 낙관적 업데이트 패턴을 사용하고 있는데, updateUserData가 실패하면 UI는 이미 변경된 상태로 남아있고 서버와 불일치하게 됩니다.

사용자 경험을 위해 에러 발생 시 이전 상태로 롤백하거나, 실패 토스트 메시지와 함께 상태를 복원하는 로직을 추가하면 좋을 것 같습니다.

🛡️ 롤백 로직 추가 예시
+        // 롤백을 위해 이전 상태 저장
+        val previousState = _uiState.value.copy()
+
         _uiState.update {
             it.copy(
                 masterChecked = newMasterChecked,
                 completeChecked = newCompleteChecked,
                 abnormalChecked = newAbnormalChecked,
                 missedChecked = newMissedChecked,
             )
         }

         val updatedUserInfo = currentInfo.copy(
             pushNotification = PushNotification(
                 all = if (newMasterChecked) "ON" else "OFF",
                 carecallCompleted = if (newCompleteChecked) "ON" else "OFF",
                 healthAlert = if (newAbnormalChecked) "ON" else "OFF",
                 carecallMissed = if (newMissedChecked) "ON" else "OFF",
             ),
         )

-        updateUserData(updatedUserInfo)
+        updateUserData(updatedUserInfo) {
+            // onComplete - 성공 시 아무것도 안 함
+        }
+        // 또는 실패 시 롤백하는 별도 콜백 파라미터 추가 고려

updateUserDataonFailure 핸들러에서 previousState로 복원하는 방식도 고려할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@composeApp/src/commonMain/kotlin/com/konkuk/medicarecall/ui/feature/settings/mydata/viewmodel/SettingsEditMyDataViewModel.kt`
around lines 138 - 156, The code performs an optimistic UI update via
_uiState.update then calls updateUserData without rollback; capture the previous
UI state and previous currentInfo before calling _uiState.update (e.g., store
prevState and prevInfo), perform the optimistic update, then call updateUserData
and handle failure (catch exception or use onFailure callback) to restore
_uiState to prevState and, if needed, reset currentInfo to prevInfo and show an
error/toast; implement this rollback inside SettingsEditMyDataViewModel around
the _uiState.update and updateUserData calls so failures revert the
PushNotification changes and UI flags back to their previous values.

}

fun initializeFormData(myDataInfo: UserInfo) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,18 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import org.jetbrains.compose.resources.painterResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.konkuk.medicarecall.resources.Res
import com.konkuk.medicarecall.resources.*
import com.konkuk.medicarecall.resources.ic_settings_back
import com.konkuk.medicarecall.ui.feature.settings.component.SettingsTopAppBar
import com.konkuk.medicarecall.domain.model.PushNotification
import com.konkuk.medicarecall.ui.feature.settings.notification.component.SwitchButton
import com.konkuk.medicarecall.ui.feature.settings.mydata.viewmodel.SettingsEditMyDataViewModel
import com.konkuk.medicarecall.ui.feature.settings.notification.component.SwitchButton
import com.konkuk.medicarecall.ui.theme.MediCareCallTheme
import org.jetbrains.compose.resources.painterResource
import org.koin.compose.viewmodel.koinViewModel

@Composable
Expand All @@ -50,21 +49,7 @@ fun SettingsNotificationScreen(
uiState.myDataInfo?.let { viewModel.initializeNotificationSettings(it) }
}

val updateSettings = {
uiState.myDataInfo?.let { info ->
viewModel.updateUserData(
userInfo = info.copy(
pushNotification = PushNotification(
all = if (uiState.masterChecked) "ON" else "OFF",
carecallCompleted = if (uiState.completeChecked) "ON" else "OFF",
healthAlert = if (uiState.abnormalChecked) "ON" else "OFF",
carecallMissed = if (uiState.missedChecked) "ON" else "OFF",
),
),
)
}
}

// ViewModel 내부에서 새 체크 상태 기준으로 저장
if (uiState.isLoading && uiState.myDataInfo == null) {
Box(
modifier = modifier
Expand Down Expand Up @@ -112,8 +97,10 @@ fun SettingsNotificationScreen(
SwitchButton(
checked = uiState.masterChecked,
onCheckedChange = { isChecked ->
viewModel.setMasterChecked(isChecked)
updateSettings()
viewModel.updateNotificationSettingByType(
type = "master",
checked = isChecked,
)
},
)
}
Expand All @@ -130,8 +117,10 @@ fun SettingsNotificationScreen(
SwitchButton(
checked = uiState.completeChecked,
onCheckedChange = { isChecked ->
viewModel.setCompleteChecked(isChecked)
updateSettings()
viewModel.updateNotificationSettingByType(
type = "complete",
checked = isChecked,
)
},
)
}
Expand All @@ -148,8 +137,10 @@ fun SettingsNotificationScreen(
SwitchButton(
checked = uiState.abnormalChecked,
onCheckedChange = { isChecked ->
viewModel.setAbnormalChecked(isChecked)
updateSettings()
viewModel.updateNotificationSettingByType(
type = "abnormal",
checked = isChecked,
)
},
)
}
Expand All @@ -166,8 +157,10 @@ fun SettingsNotificationScreen(
SwitchButton(
checked = uiState.missedChecked,
onCheckedChange = { isChecked ->
viewModel.setMissedChecked(isChecked)
updateSettings()
viewModel.updateNotificationSettingByType(
type = "missed",
checked = isChecked,
)
},
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@ package com.konkuk.medicarecall.ui.theme
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
Expand Down Expand Up @@ -39,15 +35,13 @@ val defaultMediCareCallShadow = MediCareCallShadows(
ShadowLayer(-4.dp, 0.dp, 8.dp, Color(0xFF222222).copy(alpha = 0.02f)),
),
),

shadow02 = ShadowGroup(
listOf(
ShadowLayer(0.dp, 4.dp, 12.dp, Color(0xFF222222).copy(alpha = 0.04f)),
ShadowLayer(4.dp, 0.dp, 12.dp, Color(0xFF222222).copy(alpha = 0.04f)),
ShadowLayer(-4.dp, 0.dp, 12.dp, Color(0xFF222222).copy(alpha = 0.04f)),
),
),

shadow03 = ShadowGroup(
listOf(
ShadowLayer(0.dp, 4.dp, 16.dp, Color(0xFF222222).copy(alpha = 0.04f)),
Expand All @@ -59,21 +53,7 @@ val defaultMediCareCallShadow = MediCareCallShadows(

val LocalMediCareCallShadowProvider = staticCompositionLocalOf { defaultMediCareCallShadow }

fun Modifier.figmaShadow(
expect fun Modifier.figmaShadow(
group: ShadowGroup,
cornerRadius: Dp = 14.dp,
): Modifier = this.drawWithCache {
val radiusPx = cornerRadius.toPx()

onDrawWithContent {
group.layers.forEach { layer ->
drawRoundRect(
color = layer.color,
topLeft = Offset(layer.offsetX.toPx(), layer.offsetY.toPx()),
size = Size(size.width, size.height),
cornerRadius = CornerRadius(radiusPx, radiusPx),
)
}
drawContent()
}
}
): Modifier
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ReadOnlyComposable
import com.konkuk.medicarecall.platform.platformDynamicColorScheme

Expand Down Expand Up @@ -32,11 +33,21 @@ fun MediCareCallTheme(
else -> LightColorScheme
}

MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content,
)
val mediCareCallTypography = createMediCareCallTypography()
val mediCareCallColors = defaultMediCareCallColors
val mediCareCallShadow = defaultMediCareCallShadow

CompositionLocalProvider(
LocalMediCareCallColorsProvider provides mediCareCallColors,
LocalMedicareCallTypographyProvider provides mediCareCallTypography,
LocalMediCareCallShadowProvider provides mediCareCallShadow,
) {
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content,
)
}
}

object MediCareCallTheme {
Expand Down
Loading
Loading