Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Expand Up @@ -18,6 +18,9 @@ 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.graphics.ColorFilter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
Expand All @@ -33,17 +36,20 @@ import com.umcspot.spot.ui.extension.screenWidthDp

@Composable
fun AcceptModal(
painter: Painter,
painterTint: Color,
modalTitle : String,
modalDes : String,
modalDes : String?,
okButtonText : String,
noButtonText: String?,
modifier: Modifier = Modifier,
onClick: () -> Unit = {},
onDismiss:() -> Unit= {}
onDismiss:() -> Unit = {}
) {
Card(
modifier = modifier
.width(screenWidthDp(326.dp))
.height(screenHeightDp(249.dp)),
.wrapContentHeight(),
shape = SpotShapes.Round,
colors = CardDefaults.elevatedCardColors(
containerColor = SpotTheme.colors.white
Expand All @@ -70,10 +76,11 @@ fun AcceptModal(
}

Image(
painter = painterResource(R.drawable.ic_check),
painter = painter,
contentDescription = null,
modifier = Modifier
.size(screenWidthDp(33.dp))
.size(screenWidthDp(33.dp)),
colorFilter = ColorFilter.tint(painterTint)
)

Spacer(Modifier.height(screenHeightDp(7.dp)))
Expand All @@ -88,46 +95,68 @@ fun AcceptModal(

Spacer(Modifier.height(screenHeightDp(20.dp)))

Text(
text = modalDes,
style = SpotTheme.typography.regular_500,
textAlign = TextAlign.Center
)
if(modalDes != null) {
Text(
text = modalDes,
style = SpotTheme.typography.regular_500,
textAlign = TextAlign.Center
)

Spacer(Modifier.height(screenHeightDp(20.dp)))
Spacer(Modifier.height(screenHeightDp(20.dp)))
}

TextButton(
modifier = Modifier
.width(screenWidthDp(156.dp))
.height(screenHeightDp(39.dp)),
text = okButtonText,
style = SpotTheme.typography.h5,
onClick = onClick,
shape = SpotShapes.Soft,
state = TextButtonState.B500State,
)
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
TextButton(
modifier = Modifier
.width(screenWidthDp(141.dp))
.height(screenHeightDp(39.dp)),
text = okButtonText,
style = SpotTheme.typography.h5,
onClick = onClick,
shape = SpotShapes.Soft,
state = TextButtonState.B500State,
)

if(noButtonText != null) {
TextButton(
modifier = Modifier
.width(screenWidthDp(141.dp))
.height(screenHeightDp(39.dp)),
text = noButtonText,
style = SpotTheme.typography.h5,
onClick = onDismiss,
shape = SpotShapes.Soft,
state = TextButtonState.G500State,
)
}
}
}
}
}

@Composable
fun AcceptDialog(
visible: Boolean,
painter: Painter = painterResource(R.drawable.ic_check),
painterTint: Color = Color.Unspecified,
modalTitle : String,
modalDes : String,
modalDes : String?,
okButtonText : String,
noButtonText: String?,
onClick: () -> Unit,
onDismiss: () -> Unit,
) {
if (!visible) return
Dialog(onDismissRequest = onDismiss) {
AcceptModal(
painter = painter,
painterTint = painterTint,
modalTitle = modalTitle,
modalDes = modalDes,
okButtonText = okButtonText,
noButtonText = noButtonText,
onClick = onClick,
onDismiss = onDismiss
onDismiss = onDismiss,
)
}
}
Expand All @@ -140,6 +169,7 @@ private fun AcceptDialog_Preview() {
visible = true,
modalTitle = "스터디원 신고 완료",
modalDes = "스터디원 신고가 완료되었어요.\n쾌적한 서비스 이용을 위해 항상 노력하겠습니다.",
noButtonText = "취소",
okButtonText = "확인",
onClick = {},
onDismiss = {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ 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.graphics.ColorFilter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
Expand All @@ -27,17 +30,20 @@ import com.umcspot.spot.designsystem.R
import com.umcspot.spot.designsystem.component.button.TextButton
import com.umcspot.spot.designsystem.component.button.TextButtonState
import com.umcspot.spot.designsystem.shapes.SpotShapes
import com.umcspot.spot.designsystem.theme.R500
import com.umcspot.spot.designsystem.theme.SpotTheme
import com.umcspot.spot.ui.extension.screenHeightDp
import com.umcspot.spot.ui.extension.screenWidthDp


@Composable
fun RejectModal(
painter : Painter,
painterTint : Color,
modalTitle : String,
modalDes : String,
modalDes : String?,
okButtonText : String,
noButtonText : String,
noButtonText : String?,
modifier: Modifier = Modifier,
onClick: () -> Unit = {},
onCancel:() -> Unit= {},
Expand All @@ -46,7 +52,7 @@ fun RejectModal(
Card(
modifier = modifier
.width(screenWidthDp(326.dp))
.height(screenHeightDp(227.dp)),
.wrapContentHeight(),
shape = SpotShapes.Round,
colors = CardDefaults.elevatedCardColors(
containerColor = SpotTheme.colors.white
Expand All @@ -73,10 +79,11 @@ fun RejectModal(
}

Image(
painter = painterResource(R.drawable.emoji_sad),
painter = painter,
contentDescription = null,
modifier = Modifier
.size(screenWidthDp(33.dp))
.size(screenWidthDp(33.dp)),
colorFilter = ColorFilter.tint(painterTint)
)
Comment on lines 81 to 87
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Color.Unspecified 사용 시 이미지가 보이지 않는 버그

painterTint의 기본값이 Color.Unspecified일 때 ColorFilter.tint()를 적용하면 이미지가 투명하게 렌더링되어 보이지 않습니다. Color.Unspecified는 내부적으로 0x00000000 값을 가지므로, tint 필터가 이미지를 사실상 숨기게 됩니다.

🐛 colorFilter 조건부 적용 제안
         Image(
             painter = painter,
             contentDescription = null,
             modifier = Modifier
                 .size(screenWidthDp(33.dp)),
-            colorFilter = ColorFilter.tint(painterTint)
+            colorFilter = if (painterTint != Color.Unspecified) ColorFilter.tint(painterTint) else null
         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Image(
painter = painterResource(R.drawable.emoji_sad),
painter = painter,
contentDescription = null,
modifier = Modifier
.size(screenWidthDp(33.dp))
.size(screenWidthDp(33.dp)),
colorFilter = ColorFilter.tint(painterTint)
)
Image(
painter = painter,
contentDescription = null,
modifier = Modifier
.size(screenWidthDp(33.dp)),
colorFilter = if (painterTint != Color.Unspecified) ColorFilter.tint(painterTint) else null
)
🤖 Prompt for AI Agents
In
`@core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/modal/RejectModal.kt`
around lines 80 - 86, The image tinting currently applies
ColorFilter.tint(painterTint) even when painterTint is Color.Unspecified, which
renders the image transparent; update the Image usage (the Image composable
where painter = painter and colorFilter = ColorFilter.tint(painterTint)) to only
apply a colorFilter when painterTint != Color.Unspecified (or make painterTint
default to null) so that ColorFilter.tint is not used for Color.Unspecified
values.


Spacer(Modifier.height(screenHeightDp(7.dp)))
Expand All @@ -90,13 +97,15 @@ fun RejectModal(

Spacer(Modifier.height(screenHeightDp(20.dp)))

Text(
text = modalDes,
style = SpotTheme.typography.regular_500,
textAlign = TextAlign.Center
)
if(modalDes != null) {
Text(
text = modalDes,
style = SpotTheme.typography.regular_500,
textAlign = TextAlign.Center
)

Spacer(Modifier.height(screenHeightDp(20.dp)))
Spacer(Modifier.height(screenHeightDp(20.dp)))
}

Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
TextButton(
Expand All @@ -110,16 +119,18 @@ fun RejectModal(
state = TextButtonState.R500State,
)

TextButton(
modifier = Modifier
.width(screenWidthDp(141.dp))
.height(screenHeightDp(39.dp)),
text = noButtonText,
style = SpotTheme.typography.h5,
onClick = onCancel,
shape = SpotShapes.Soft,
state = TextButtonState.G500State,
)
if(noButtonText != null) {
TextButton(
modifier = Modifier
.width(screenWidthDp(141.dp))
.height(screenHeightDp(39.dp)),
text = noButtonText,
style = SpotTheme.typography.h5,
onClick = onCancel,
shape = SpotShapes.Soft,
state = TextButtonState.G500State,
)
}
}
}
}
Expand All @@ -128,17 +139,21 @@ fun RejectModal(
@Composable
fun RejectDialog(
visible: Boolean,
painter : Painter = painterResource(R.drawable.emoji_sad),
painterTint : Color = Color.Unspecified,
modalTitle : String,
modalDes : String,
modalDes : String?,
okButtonText : String,
noButtonText : String,
onDismiss: () -> Unit,
noButtonText : String?,
onDismiss: () -> Unit = {},
onClick: () -> Unit,
onCancel: () -> Unit
onCancel: () -> Unit = {}
) {
if (!visible) return
Dialog(onDismissRequest = onDismiss) {
RejectModal(
painter = painter,
painterTint = painterTint,
modalTitle = modalTitle,
modalDes = modalDes,
okButtonText = okButtonText,
Expand All @@ -156,6 +171,8 @@ private fun RejectDialog_Preview() {
SpotTheme {
RejectDialog(
visible = true,
painter = painterResource(R.drawable.emoji_sad),
painterTint = SpotTheme.colors.R500,
modalTitle = "나가시겠어요?",
modalDes = "지금 나가면, 쓰던 글은 저장되지 않아요.",
okButtonText = "네",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,8 @@ private fun StudyListItemPreview() {
isLiked = false,
hitCount = 1200,
profileImageUrl = ImageRef.Name("spot_logo"),
isOwner = false
isOwner = false,
isAlone = false
),
modifier = Modifier.padding(10.dp),
onClick = {},
Expand Down
9 changes: 9 additions & 0 deletions core/designsystem/src/main/res/drawable/error.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="33dp"
android:height="33dp"
android:viewportWidth="33"
android:viewportHeight="33">
<path
android:pathData="M16.5,2.75C24.094,2.75 30.25,8.907 30.25,16.5C30.25,24.093 24.094,30.25 16.5,30.25C8.906,30.25 2.75,24.093 2.75,16.5C2.75,8.907 8.906,2.75 16.5,2.75ZM16.5,5.042C10.182,5.042 5.042,10.182 5.042,16.5C5.042,22.818 10.182,27.958 16.5,27.958C22.818,27.958 27.958,22.818 27.958,16.5C27.958,10.182 22.818,5.042 16.5,5.042ZM16.499,19.94C16.863,19.94 17.212,20.085 17.47,20.343C17.728,20.6 17.872,20.95 17.872,21.314C17.872,21.678 17.728,22.028 17.47,22.285C17.212,22.543 16.863,22.688 16.499,22.688C16.134,22.688 15.785,22.543 15.527,22.285C15.27,22.028 15.125,21.678 15.125,21.314C15.125,20.95 15.27,20.6 15.527,20.343C15.785,20.085 16.134,19.94 16.499,19.94ZM16.492,9.625C16.741,9.625 16.982,9.715 17.17,9.879C17.358,10.042 17.481,10.269 17.515,10.516L17.524,10.655L17.53,16.845C17.53,17.107 17.431,17.358 17.253,17.549C17.075,17.741 16.83,17.857 16.57,17.875C16.309,17.893 16.051,17.811 15.848,17.646C15.645,17.482 15.513,17.246 15.477,16.987L15.467,16.847L15.462,10.658C15.462,10.522 15.488,10.388 15.54,10.263C15.592,10.137 15.668,10.023 15.763,9.928C15.859,9.832 15.973,9.756 16.098,9.704C16.223,9.652 16.356,9.625 16.492,9.625Z"
android:fillColor="#F34343"/>
</vector>
4 changes: 2 additions & 2 deletions core/model/src/main/java/com/umcspot/spot/model/Global.kt
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ enum class StudyTheme(
OTHER("기타");

companion object {
fun from(value: String): StudyTheme? =
values().firstOrNull { it.name == value }
fun from(value: String): StudyTheme =
StudyTheme.entries.first { it.name == value }
Comment on lines +66 to +67
Copy link

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:

cat core/model/src/main/java/com/umcspot/spot/model/Global.kt | head -120

Repository: SPOTeam/Android-V2

Length of output: 2292


🏁 Script executed:

# Search for all calls to StudyTheme.from()
rg -n "StudyTheme\.from" --type=kt -C3

Repository: SPOTeam/Android-V2

Length of output: 88


🏁 Script executed:

# Check SocialLoginType.from() for comparison
rg -n "SocialLoginType\.from" --type=kt -A5

Repository: SPOTeam/Android-V2

Length of output: 88


🏁 Script executed:

rg -n "StudyTheme\.from" -C3

Repository: SPOTeam/Android-V2

Length of output: 693


🏁 Script executed:

# Also search for any try-catch blocks around StudyTheme.from
rg -n "StudyTheme\.from" -C5

Repository: SPOTeam/Android-V2

Length of output: 903


🏁 Script executed:

# Search in all kotlin files
fd "\.kt$" | xargs grep -n "StudyTheme.from" -C3

Repository: SPOTeam/Android-V2

Length of output: 693


🏁 Script executed:

# Search for all StudyTheme.from occurrences (ensure we found them all)
rg "StudyTheme\.from"

Repository: SPOTeam/Android-V2

Length of output: 189


🏁 Script executed:

# Look at UserPreferredCategoryResponseDto definition to understand data source
fd "UserPreferredCategoryResponseDto" | head -5

Repository: SPOTeam/Android-V2

Length of output: 156


🏁 Script executed:

# Find and examine the DTO definition
rg -n "UserPreferredCategoryResponseDto" --type=kotlin | head -10

Repository: SPOTeam/Android-V2

Length of output: 1440


🏁 Script executed:

# Search in all files for UserPreferredCategoryResponseDto
fd "\." | xargs grep -l "UserPreferredCategoryResponseDto" 2>/dev/null | head -5

Repository: SPOTeam/Android-V2

Length of output: 450


🏁 Script executed:

cat data/user/src/main/java/com/umcspot/spot/user/dto/response/UserPreferredCategoryResponseDto.kt

Repository: SPOTeam/Android-V2

Length of output: 446


🏁 Script executed:

# Check the mapping function context
cat data/user/src/main/java/com/umcspot/spot/user/mapper/UserMapper.kt | head -70

Repository: SPOTeam/Android-V2

Length of output: 2282


🏁 Script executed:

# Check if there's any error handling at the service/repository level
rg -n "getUserPreferredCategory|toDomain" data/user/src/main/java/com/umcspot/spot/user/service/ -A5 -B5

Repository: SPOTeam/Android-V2

Length of output: 1178


API 응답 매핑 시 런타임 크래시 위험

StudyTheme.from().first { }를 사용하므로 일치하는 항목이 없으면 NoSuchElementException을 발생시킵니다. 현재 UserMapper.kt:56에서 API 응답 데이터(UserPreferredCategoryResponseDto.categories)를 직접 변환할 때 이 함수를 호출하고 있는데, API가 예상치 못한 값을 반환하거나 데이터가 손상된 경우 앱이 크래시됩니다.

같은 파일의 SocialLoginType.from() (lines 51-56)은 .firstOrNull { } ?: KAKAO로 안전하게 기본값을 반환하는 패턴을 사용하고 있어 일관성이 없습니다.

🔧 안전한 기본값 반환 제안
 companion object {
-    fun from(value: String): StudyTheme =
-        StudyTheme.entries.first { it.name == value }
+    fun from(value: String): StudyTheme =
+        StudyTheme.entries.firstOrNull { it.name == value } ?: OTHER
 }
🤖 Prompt for AI Agents
In `@core/model/src/main/java/com/umcspot/spot/model/Global.kt` around lines 66 -
67, StudyTheme.from currently uses StudyTheme.entries.first { it.name == value }
which will throw NoSuchElementException for unknown API values; update it to
mirror SocialLoginType.from by returning a safe default (e.g., KOREAN or another
appropriate StudyTheme constant) when no match is found: replace the .first {
... } call with .firstOrNull { it.name == value } ?: <DEFAULT_THEME>. Also check
call sites such as UserMapper.kt where
UserPreferredCategoryResponseDto.categories are mapped to use the safe-from
behavior to prevent runtime crashes on malformed API data.

}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ data class Study (
@SerialName("isOwner")
val isOwner: Boolean,

@SerialName("isAlone")
val isAlone: Boolean,

@SerialName("hitCount")
val hitCount: Int = 0,
Comment on lines +48 to 52
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

역호환을 위해 기본값 추가 필요
Line 48-49의 isAlone가 non-null인데 기본값이 없어, 서버가 필드를 누락하면 역직렬화 실패 가능성이 있습니다. 배포 순서가 엇갈릴 경우 앱 크래시로 이어질 수 있어요.

🛠 제안 수정
 `@SerialName`("isAlone")
-    val isAlone: Boolean,
+    val isAlone: Boolean = false,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@SerialName("isAlone")
val isAlone: Boolean,
@SerialName("hitCount")
val hitCount: Int = 0,
`@SerialName`("isAlone")
val isAlone: Boolean = false,
`@SerialName`("hitCount")
val hitCount: Int = 0,
🤖 Prompt for AI Agents
In
`@data/study/src/main/java/com/umcspot/spot/study/dto/response/StudyResponseDto.kt`
around lines 48 - 52, The isAlone property in StudyResponseDto is non-nullable
without a default, which will break deserialization if the server omits the
field; add a safe default (e.g., isAlone: Boolean = false) to the
StudyResponseDto data class so missing or older-server payloads deserialize
safely, keeping the `@SerialName`("isAlone") annotation intact.


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ fun Study.toDomain() : StudyResult =
likeCount = this.likeCount,
isLiked = this.isLiked,
isOwner = this.isOwner,
isAlone = this.isAlone,
hitCount = this.hitCount,
profileImageUrl = this.profileImageUrl.toImageRef()
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ interface UserDataSource {

suspend fun getMyPageInfo() : BaseResponse<MyPageResponseDto>
suspend fun getUserPreferredCategory() : BaseResponse<UserPreferredCategoryResponseDto>

suspend fun leaveSpot() : NullResultResponse
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,7 @@ class UserDataSourceImpl @Inject constructor(

override suspend fun getUserPreferredCategory(): BaseResponse<UserPreferredCategoryResponseDto> =
userService.getUserPreferredCategory()

override suspend fun leaveSpot(): NullResultResponse =
userService.leaveSpot()
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.umcspot.spot.user.repositoryimpl
import android.content.Context
import android.util.Log
import com.umcspot.spot.common.location.LocationStore
import com.umcspot.spot.common.location.mapRegionCodesToFullNames
import com.umcspot.spot.model.StudyTheme
import com.umcspot.spot.user.datasource.UserDataSource
import com.umcspot.spot.user.mapper.toDomain
Expand All @@ -18,7 +17,7 @@ import javax.inject.Inject

class UserRepositoryImpl @Inject constructor(
private val userDataSource: UserDataSource,
@ApplicationContext private val appContext: Context, // @ApplicationContext 로 주입 추천
@ApplicationContext private val appContext: Context,
) : UserRepository {
override suspend fun getUserName(): Result<UserResult> =
runCatching {
Expand Down Expand Up @@ -87,4 +86,11 @@ class UserRepositoryImpl @Inject constructor(
}.onFailure {
Log.e("UserRepository", "getUserPreferredCategory failed", it)
}

override suspend fun leaveSpot() : Result<String> =
runCatching {
userDataSource.leaveSpot().code
}.onFailure {
Log.e("UserRepository", "leaveSpot failed", it)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.umcspot.spot.user.dto.response.UserPreferredCategoryResponseDto
import com.umcspot.spot.user.dto.response.UserPreferredRegionResponseDto
import com.umcspot.spot.user.dto.response.UserResponseDto
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.GET
import retrofit2.http.POST

Expand Down Expand Up @@ -44,4 +45,8 @@ interface UserService {
@GET("/api/members/prefer-categories")
suspend fun getUserPreferredCategory(
): BaseResponse<UserPreferredCategoryResponseDto>

@DELETE("/api/members/me")
suspend fun leaveSpot(
): NullResultResponse
}
Loading