Skip to content
Open
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,10 @@ Follow existing patterns first. Repo has minimal automated formatting/lint beyon
- Team conventions (external):
- https://github.com/EAT-SSU/Android/wiki/Android-convention
- https://github.com/EAT-SSU/Android/wiki/Git-convention

## Commit Message Convention
Format: `<type>: <Korean description>`
Examples: `fix: 00 고침`, `feat: 이력서 섹션 추가`, `style: 타이포 조정`
Keep the type in English (Conventional Commits style), and write the description in Korean.
간결하지만 자세한 작업 내용을 한글로 커밋한다.
커밋은 한 커밋에 하나의 일만 포함한다. 여러가지 일은 커밋을 쪼갠다.
Original file line number Diff line number Diff line change
Expand Up @@ -223,13 +223,15 @@ internal fun ReviewListScreen(
rating = 0.0,
)
)
Column(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier.fillMaxSize(),
) {
Spacer(
modifier = Modifier
.padding(vertical = 16.dp)
.fillMaxWidth() // 가로 전체 차지
.fillMaxWidth()
.height(16.dp)
.background(Gray100) // 배경색 적용
.background(Gray100)
)

Row(Modifier.padding(horizontal = 24.dp)) {
Expand All @@ -246,9 +248,9 @@ internal fun ReviewListScreen(
}
Box(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.fillMaxHeight()
.padding(top = 100.dp)
.fillMaxWidth()
.weight(1f),
contentAlignment = Alignment.Center
) {
DelayedLoadingIndicator(modifier = Modifier)
}
Expand All @@ -263,24 +265,20 @@ internal fun ReviewListScreen(
val isInitialLoading = loadState.refresh is LoadState.Loading
val isError = loadState.refresh is LoadState.Error

LazyColumn(
modifier = Modifier.fillMaxSize()
) {
item {
if (isInitialLoading || isError || reviewPagingItems.itemCount == 0) {
Column(
modifier = Modifier.fillMaxSize(),
) {
ReviewInfoContent(menuName, info)
}

item {
Spacer(
modifier = Modifier
.padding(vertical = 16.dp)
.fillMaxWidth()
.height(16.dp)
.background(Gray100)
)
}

item {
Row(Modifier.padding(horizontal = 24.dp)) {
Text(
stringResource(R.string.review),
Expand All @@ -293,27 +291,16 @@ internal fun ReviewListScreen(
style = EatssuTheme.typography.h2,
)
}
}

if (isInitialLoading) {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(top = 100.dp),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
contentAlignment = Alignment.Center
) {
if (isInitialLoading) {
DelayedLoadingIndicator(modifier = Modifier)
}
}
} else if (isError) {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(top = 100.dp),
contentAlignment = Alignment.Center
) {
} else if (isError) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
stringResource(R.string.toast_review_load_failed),
Expand All @@ -326,23 +313,46 @@ internal fun ReviewListScreen(
modifier = Modifier.width(100.dp)
)
}
} else if (reviewPagingItems.itemCount == 0) {
EmptyReviewContent(
modifier = Modifier.fillMaxWidth(),
)
}
}
} else if (reviewPagingItems.itemCount == 0) {
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
item {
ReviewInfoContent(menuName, info)
}

item {
Box(
Spacer(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.fillMaxHeight(),
contentAlignment = Alignment.Center
) {
EmptyReviewContent(
modifier = Modifier
.fillMaxWidth(),
.padding(vertical = 16.dp)
.fillMaxWidth()
.height(16.dp)
.background(Gray100)
)
}

item {
Row(Modifier.padding(horizontal = 24.dp)) {
Text(
stringResource(R.string.review),
style = EatssuTheme.typography.h2,
)
Spacer(modifier = Modifier.width(6.dp))
Text(
text = "${info?.reviewCnt}",
color = Primary,
style = EatssuTheme.typography.h2,
)
}
}
} else {

items(
count = reviewPagingItems.itemCount,
key = reviewPagingItems.itemKey { it.reviewId }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.eatssu.android.presentation.cafeteria.review.modify

import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
Expand All @@ -18,6 +19,9 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
Expand All @@ -36,6 +40,7 @@ import com.eatssu.common.UiState
import com.eatssu.common.enums.ScreenId
import com.eatssu.design_system.component.CloseTopBar
import com.eatssu.design_system.component.EatSsuButton
import com.eatssu.design_system.component.EatSsuDialog
import com.eatssu.design_system.component.RatingBarMedium
import com.eatssu.design_system.theme.EatssuTheme
import com.eatssu.design_system.theme.Gray100
Expand Down Expand Up @@ -78,6 +83,40 @@ fun ModifyReviewScreen(
}
}

var showExitDialog by remember { mutableStateOf(false) }

val hasUnsavedChanges = (ui as? UiState.Success)?.data?.let {
(it as? ModifyState.Editing)?.hasChanges ?: false
} ?: false
Comment on lines +88 to +90
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.

medium

hasUnsavedChanges가 recomposition이 일어날 때마다 계속해서 재계산되고 있습니다. remember를 사용하여 ui 상태가 변경될 때만 값을 다시 계산하도록 하여 불필요한 계산을 줄일 수 있습니다. 또한, 표현을 더 간결하게 만들 수 있습니다.

Suggested change
val hasUnsavedChanges = (ui as? UiState.Success)?.data?.let {
(it as? ModifyState.Editing)?.hasChanges ?: false
} ?: false
val hasUnsavedChanges = remember(ui) {
((ui as? UiState.Success)?.data as? ModifyState.Editing)?.hasChanges ?: false
}


val handleBack = {
if (hasUnsavedChanges) {
showExitDialog = true
} else {
onBack()
}
}

BackHandler(enabled = true) {
handleBack()
}

if (showExitDialog) {
EatSsuDialog(
title = stringResource(R.string.review_exit_dialog_title),
description = stringResource(R.string.review_exit_dialog_description),
confirmText = stringResource(R.string.review_exit_dialog_confirm),
dismissText = stringResource(R.string.review_exit_dialog_dismiss),
onConfirmClick = { showExitDialog = false },
onDismissButtonClick = {
showExitDialog = false
onBack()
},
onDismissRequest = { showExitDialog = false },
visible = showExitDialog
)
Comment on lines +105 to +117
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.

high

EatSsuDialog 사용법이 매우 혼란스럽습니다. confirmText에 '계속 작성'을, dismissText에 '나가기'를 전달하고 있습니다. EatSsuDialog의 구현을 보면 confirm 버튼이 주 액션(primary color) 버튼입니다. 현재 코드대로라면 '계속 작성'이 주 액션으로 강조되는데, 이는 사용자의 데이터 손실을 막기 위한 의도된 UX 디자인일 수 있습니다.

하지만 코드상으로는 '확인(confirm)'을 의미하는 파라미터에 '취소'에 해당하는 액션을, '취소(dismiss)'를 의미하는 파라미터에 '확인'에 해당하는 액션을 연결하고 있어 매우 혼란스럽고 잠재적인 버그의 원인이 될 수 있습니다.

의도된 UX가 '계속 작성'을 강조하는 것이 맞다면, EatSsuDialog 컴포넌트의 API를 더 명확하게 개선하는 것을 고려해야 합니다. 만약 '나가기'가 주 액션(primary)이 되어야 한다면, 호출부에서 confirmdismiss에 전달하는 텍스트와 콜백을 서로 바꾸어 의미를 일치시켜야 합니다.

}

when (val data = (ui as? UiState.Success)?.data) {
is ModifyState.Editing -> {
ModifyReviewScreen(
Expand All @@ -88,7 +127,7 @@ fun ModifyReviewScreen(
menuLikeInfos = data.menuLikeInfos,
isSubmitting = false,
canSubmit = data.canSubmit,
onBack = onBack,
onBack = handleBack,
onRatingChanged = viewModel::onRatingChanged,
onContentChanged = { new ->
if (new.length <= MAX_TEXT_COUNT) viewModel.onContentChanged(new)
Expand All @@ -108,7 +147,7 @@ fun ModifyReviewScreen(
menuLikeInfos = data.menuLikeInfos,
isSubmitting = true,
canSubmit = false,
onBack = onBack,
onBack = handleBack,
onRatingChanged = {}, // 수정 불가
onContentChanged = {}, // 수정 불가
onToggleLike = {}, // 수정 불가
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.eatssu.android.presentation.cafeteria.review.write

import android.net.Uri
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
Expand Down Expand Up @@ -30,6 +31,9 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
Expand All @@ -39,9 +43,9 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.compose.AsyncImage
import com.eatssu.android.R
import com.eatssu.android.domain.model.MenuMini
import com.eatssu.android.presentation.cafeteria.review.write.component.MenuLikeButtonItem
Expand All @@ -53,6 +57,7 @@ import com.eatssu.common.enums.MenuType
import com.eatssu.common.enums.ScreenId
import com.eatssu.design_system.component.CloseTopBar
import com.eatssu.design_system.component.EatSsuButton
import com.eatssu.design_system.component.EatSsuDialog
import com.eatssu.design_system.component.RatingBarMedium
import com.eatssu.design_system.theme.EatssuTheme
import com.eatssu.design_system.theme.Gray100
Expand All @@ -62,7 +67,7 @@ import com.eatssu.design_system.theme.Gray400
import com.eatssu.design_system.theme.Gray500
import com.eatssu.design_system.theme.Gray700
import com.eatssu.design_system.theme.Primary
import androidx.core.net.toUri
import coil.compose.AsyncImage

const val MAX_TEXT_COUNT = 300

Expand Down Expand Up @@ -102,6 +107,41 @@ fun WriteReviewScreen(
}
}

var showExitDialog by remember { mutableStateOf(false) }

val hasUnsavedChanges = when (val data = (ui as? UiState.Success)?.data) {
is WriteReviewState.Editing -> data.rating > 0 || data.content.isNotEmpty() || data.selectedImageUri != null
else -> false
}
Comment on lines +112 to +115
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.

medium

hasUnsavedChanges가 recomposition이 일어날 때마다 계속해서 재계산되고 있습니다. remember를 사용하여 ui 상태가 변경될 때만 값을 다시 계산하도록 하여 불필요한 계산을 줄일 수 있습니다.

    val hasUnsavedChanges = remember(ui) {
        when (val data = (ui as? UiState.Success)?.data) {
            is WriteReviewState.Editing -> data.rating > 0 || data.content.isNotEmpty() || data.selectedImageUri != null
            else -> false
        }
    }


val handleBack = {
if (hasUnsavedChanges) {
showExitDialog = true
} else {
onBack()
}
}

BackHandler(enabled = true) {
handleBack()
}

if (showExitDialog) {
EatSsuDialog(
title = stringResource(R.string.review_exit_dialog_title),
description = stringResource(R.string.review_exit_dialog_description),
confirmText = stringResource(R.string.review_exit_dialog_confirm),
dismissText = stringResource(R.string.review_exit_dialog_dismiss),
onConfirmClick = { showExitDialog = false },
onDismissButtonClick = {
showExitDialog = false
onBack()
},
onDismissRequest = { showExitDialog = false },
visible = showExitDialog
)
Comment on lines +130 to +142
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.

high

EatSsuDialog 사용법이 매우 혼란스럽습니다. confirmText에 '계속 작성'을, dismissText에 '나가기'를 전달하고 있습니다. EatSsuDialog의 구현을 보면 confirm 버튼이 주 액션(primary color) 버튼입니다. 현재 코드대로라면 '계속 작성'이 주 액션으로 강조되는데, 이는 사용자의 데이터 손실을 막기 위한 의도된 UX 디자인일 수 있습니다.

하지만 코드상으로는 '확인(confirm)'을 의미하는 파라미터에 '취소'에 해당하는 액션을, '취소(dismiss)'를 의미하는 파라미터에 '확인'에 해당하는 액션을 연결하고 있어 매우 혼란스럽고 잠재적인 버그의 원인이 될 수 있습니다.

의도된 UX가 '계속 작성'을 강조하는 것이 맞다면, EatSsuDialog 컴포넌트의 API를 더 명확하게 개선하는 것을 고려해야 합니다. 만약 '나가기'가 주 액션(primary)이 되어야 한다면, 호출부에서 confirmdismiss에 전달하는 텍스트와 콜백을 서로 바꾸어 의미를 일치시켜야 합니다.

}

when (val data = (ui as? UiState.Success)?.data) {
is WriteReviewState.Editing -> {
WriteReviewScreen(
Expand All @@ -113,7 +153,7 @@ fun WriteReviewScreen(
likedMenuIds = data.likedMenuIds,
selectedImageUri = data.selectedImageUri,
isPosting = false,
onBack = onBack,
onBack = handleBack,
onRatingChanged = viewModel::onRatingChanged,
onContentChanged = { new ->
if (new.length <= MAX_TEXT_COUNT) viewModel.onContentChanged(new)
Expand All @@ -135,7 +175,7 @@ fun WriteReviewScreen(
likedMenuIds = data.likedMenuIds,
selectedImageUri = data.selectedImageUri,
isPosting = true,
onBack = onBack,
onBack = handleBack,
onRatingChanged = {}, // 비활성
onContentChanged = {}, // 비활성
onToggleLike = {}, // 비활성
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@
<string name="review_settings">리뷰 설정</string>
<string name="review_error_occurred">에러가 발생했습니다.</string>
<string name="review_write">리뷰 작성하기</string>
<string name="review_exit_dialog_title">나가시겠어요?</string>
<string name="review_exit_dialog_description">지금 나가면 작성한 내용이 저장되지 않습니다.</string>
<string name="review_exit_dialog_confirm">계속 작성</string>
<string name="review_exit_dialog_dismiss">나가기</string>

<!-- ========================== -->
<!-- Review List -->
Expand Down
Loading
Loading