Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9a44a04
✨ Feat: 갤러리 사진 선택 기능 추가
chanho0908 Jan 27, 2026
634cf7a
✨ Feat: ObserveAsEvents 컴포저블 함수 추가
chanho0908 Jan 27, 2026
c98e7bb
✨ Feat: 사진 촬영, 업로드 실패시 에러 메시지 출력 기능 추가
chanho0908 Jan 30, 2026
3993ff4
✨ Feat: 공용 Round Button 컴포저블 추가
chanho0908 Jan 30, 2026
60c6028
✨ Feat: 사진 다시찍기, 갤러리 버튼 이미지 추가
chanho0908 Jan 30, 2026
41f034d
✨ Feat: 사진 다시찍기 기능 추가
chanho0908 Jan 30, 2026
822a200
♻️ Refactor: 사진 촬영 실패시 에러처리 추가
chanho0908 Jan 30, 2026
59f7be2
✨ Feat: 리플 효과 제거 함수에 enable 파라미터 추가
chanho0908 Jan 30, 2026
afadfd8
✨ Feat: 사진 촬영시 촬영,갤러리, 렌즈 토글 버튼 비활성화 기능 추가
chanho0908 Jan 30, 2026
131e0dc
♻️ Refactor: 갤러리에서 선택된 이미지가 null일시 에러처리 제거
chanho0908 Jan 30, 2026
bff39f5
♻️ Refactor: 하위 컴포저블에서 사용중인 부모 컴포저블의 modifier 수정
chanho0908 Jan 31, 2026
45ae310
♻️ Refactor: 공용 round 버튼 패키지 수정
chanho0908 Jan 31, 2026
213270a
♻️ Refactor: 재촬영 이미지 수정
chanho0908 Jan 31, 2026
7a33d2e
♻️ Refactor: 업로드 버튼 중앙 정렬이 맞지 않는 문제 수정
chanho0908 Jan 31, 2026
f27a62d
✨ Merge branch 'feat/#38-task-certification' into feat/tast-certifica…
chanho0908 Feb 3, 2026
2fab411
♻️ Refactor: ObserveAsEvents의 인자명 수정, withContext 제거
chanho0908 Feb 3, 2026
12297dd
♻️ Refactor: AppRoundButton 리뷰 반영
chanho0908 Feb 3, 2026
87440bc
♻️ Refactor: 하드코딩된 업로드 리소스 분리
chanho0908 Feb 3, 2026
a242104
♻️ Refactor: 촬영 버튼 비활성화 시점을 콜백보다 먼저 하도록 수정
chanho0908 Feb 3, 2026
ca84073
♻️ Refactor: 촬영 버튼 활성화 플래그 변수 remember로 수정
chanho0908 Feb 3, 2026
8f12e12
♻️ Refactor: stringResource를 사용하도록 수정
chanho0908 Feb 3, 2026
f809031
♻️ Refactor: import 누락 수정
chanho0908 Feb 3, 2026
3091682
✨Merge branch 'develop' into feat/tast-certification-gallery-pick
chanho0908 Feb 5, 2026
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
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.

지금도 좋긴 한데 내용물이 항상 Text 기반이니까 content를 받지 말고 text, textColor를 추가하는 것도 괜찮을 거 같아요. 그리고 지금 border가 없는 버튼의 경우는 이 컴포저블로 표현이 안되는 거 같아요 borderColor랑 backgroundColor를 같은 색으로 맞춰서 표현한다고 해도 버튼 높이가 다르고 내부 정렬이 안 맞아서 이 부분도 재사용 가능하게 수정 부탁드려요!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

border를 선택해서 전달하도록 수정해봤어 !

리뷰 반영 커밋 : 12297dd

Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package com.twix.designsystem.components.button

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.twix.designsystem.components.text.AppText
import com.twix.designsystem.theme.CommonColor
import com.twix.designsystem.theme.GrayColor
import com.twix.designsystem.theme.TwixTheme
import com.twix.domain.model.enums.AppTextStyle

@Composable
fun AppRoundButton(
text: String,
textColor: Color,
modifier: Modifier = Modifier,
textStyle: AppTextStyle = AppTextStyle.T2,
backgroundColor: Color,
borderColor: Color = GrayColor.C500,
hasBorder: Boolean = true,
) {
Box(
modifier = modifier,
) {
if (hasBorder) {
Box(
modifier =
Modifier
.fillMaxSize()
.offset(y = 4.dp)
.background(
color = borderColor,
shape = RoundedCornerShape(100),
),
)
}

Box(
modifier =
Modifier
.fillMaxSize()
.background(
color = backgroundColor,
shape = RoundedCornerShape(100),
).then(
if (hasBorder) {
Modifier.border(
width = 1.6.dp,
color = borderColor,
shape = RoundedCornerShape(100),
)
} else {
Modifier
},
),
contentAlignment = Alignment.Center,
) {
AppText(
style = textStyle,
color = textColor,
text = text,
)
}
}
}

@Preview(showBackground = true)
@Composable
fun AppRoundButtonPreview() {
TwixTheme {
Column {
AppRoundButton(
modifier =
Modifier
.width(330.dp)
.height(68.dp),
text = "버튼임니다",
textColor = GrayColor.C500,
backgroundColor = CommonColor.White,
)
Spacer(modifier = Modifier.height(10.dp))
AppRoundButton(
modifier =
Modifier
.width(330.dp)
.height(68.dp),
text = "버튼임니다",
textColor = CommonColor.White,
backgroundColor = GrayColor.C500,
hasBorder = false,
)
Spacer(modifier = Modifier.height(10.dp))
}
}
}
33 changes: 33 additions & 0 deletions core/ui/src/main/java/com/twix/ui/base/ObserveAsEvents.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.twix.ui.base

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.flow.Flow

/**
* [Flow]를 통해 전달되는 일회성 이벤트(Side Effect)를 Lifecycle에 안전하게 관찰하기 위한 Composable 함수입니다.
* * 주로 ViewModel의 Channel이나 SharedFlow를 통해 전달되는 네비게이션, 스낵바 표시 등의
* UI 이벤트를 처리할 때 사용됩니다.
*
* @param T 이벤트의 타입.
* @param flow 관찰할 [Flow] 인스턴스 (예: ViewModel의 events).
* @param onEvent 이벤트가 발생했을 때 실행할 suspend 콜백 함수.
*
*/
@Composable
fun <T> ObserveAsEvents(
event: Flow<T>,
onEvent: suspend (T) -> Unit,
) {
val lifecycleOwner = LocalLifecycleOwner.current
LaunchedEffect(event, lifecycleOwner) {
// lifecycleOwner가 STARTED 상태일 때만 수집을 시작
// STOPPED 상태가 되면 수집 코루틴을 자동으로 취소
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
event.collect(onEvent)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.twix.task_certification
import android.Manifest
import android.content.pm.PackageManager
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
Expand All @@ -26,6 +27,9 @@ import androidx.core.content.ContextCompat
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.twix.designsystem.components.text.AppText
import com.twix.designsystem.components.toast.ToastManager
import com.twix.designsystem.components.toast.model.ToastData
import com.twix.designsystem.components.toast.model.ToastType
import com.twix.designsystem.theme.GrayColor
import com.twix.designsystem.theme.TwixTheme
import com.twix.domain.model.enums.AppTextStyle
Expand All @@ -35,13 +39,16 @@ import com.twix.task_certification.component.CameraPreviewBox
import com.twix.task_certification.component.TaskCertificationTopBar
import com.twix.task_certification.model.CameraPreview
import com.twix.task_certification.model.TaskCertificationIntent
import com.twix.task_certification.model.TaskCertificationSideEffect
import com.twix.task_certification.model.TaskCertificationUiState
import com.twix.ui.base.ObserveAsEvents
import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel
import org.koin.compose.koinInject

@Composable
fun TaskCertificationRoute(
toastManager: ToastManager = koinInject(),
camera: Camera = koinInject(),
viewModel: TaskCertificationViewModel = koinViewModel(),
navigateToBack: () -> Unit,
Expand Down Expand Up @@ -69,6 +76,11 @@ fun TaskCertificationRoute(
hasPermission = granted
}

val pickMedia =
rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->
viewModel.dispatch(TaskCertificationIntent.PickPicture(uri))
}

LaunchedEffect(Unit) {
if (!hasPermission) {
permissionLauncher.launch(Manifest.permission.CAMERA)
Expand All @@ -93,6 +105,20 @@ fun TaskCertificationRoute(
}
}

val imageCaptureFailMessage = stringResource(R.string.task_certification_image_capture_fail)
ObserveAsEvents(viewModel.sideEffect) { event ->
when (event) {
TaskCertificationSideEffect.ShowImageCaptureFailToast -> {
toastManager.tryShow(
ToastData(
message = imageCaptureFailMessage,
type = ToastType.ERROR,
),
)
}
}
}

TaskCertificationScreen(
uiState = uiState,
cameraPreview = cameraPreview,
Expand All @@ -105,6 +131,8 @@ fun TaskCertificationRoute(
.takePicture()
.onSuccess {
viewModel.dispatch(TaskCertificationIntent.TakePicture(it))
}.onFailure {
viewModel.dispatch(TaskCertificationIntent.TakePicture(null))
}
}
},
Expand All @@ -114,6 +142,13 @@ fun TaskCertificationRoute(
onClickFlash = {
viewModel.dispatch(TaskCertificationIntent.ToggleTorch)
},
onClickGallery = {
pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
},
onClickRefresh = {
viewModel.dispatch(TaskCertificationIntent.RetakePicture)
},
onClickUpload = { },
)
}

Expand All @@ -125,6 +160,9 @@ private fun TaskCertificationScreen(
onCaptureClick: () -> Unit,
onToggleCameraClick: () -> Unit,
onClickFlash: () -> Unit,
onClickGallery: () -> Unit,
onClickRefresh: () -> Unit,
onClickUpload: () -> Unit,
) {
Column(
Modifier
Expand Down Expand Up @@ -157,8 +195,12 @@ private fun TaskCertificationScreen(
Spacer(modifier = Modifier.height(52.dp))

CameraControlBar(
capture = uiState.capture,
onCaptureClick = onCaptureClick,
onToggleCameraClick = onToggleCameraClick,
onClickGallery = onClickGallery,
onClickRefresh = onClickRefresh,
onClickUpload = onClickUpload,
)
}
}
Expand All @@ -174,6 +216,9 @@ fun TaskCertificationScreenPreview() {
onCaptureClick = {},
onToggleCameraClick = {},
onClickFlash = {},
onClickGallery = {},
onClickRefresh = {},
onClickUpload = {},
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,21 @@ class TaskCertificationViewModel :
reducePicture(intent.uri)
}

is TaskCertificationIntent.PickPicture -> {
reducePicture(intent.uri)
}

is TaskCertificationIntent.ToggleLens -> {
reduceLens()
}

is TaskCertificationIntent.ToggleTorch -> {
reduceTorch()
}

is TaskCertificationIntent.RetakePicture -> {
setupRetake()
}
}
}

Expand All @@ -47,4 +55,8 @@ class TaskCertificationViewModel :
private fun reduceTorch() {
reduce { toggleTorch() }
}

private fun setupRetake() {
reduce { removePicture() }
}
}
Loading