Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
81957de
♻️ Refactor: TaskCertificationViewModel의 when 문 정리
chanho0908 Feb 9, 2026
f3c1f0e
♻️ Refactor: goalId를 ViewModel에서 SavedStateHandle로 주입받도록 수정
chanho0908 Feb 10, 2026
599accf
♻️ Refactor: 인증샷 화면 문구 수정
chanho0908 Feb 10, 2026
885adc9
♻️ Refactor: `CommentUiModel`에서 사용되지 않는 `isEmpty` 속성을 제거
chanho0908 Feb 10, 2026
c0eefba
✨ Feat: Uri를 ByteArray로 변환하는 확장 함수 추가
chanho0908 Feb 10, 2026
6f5225c
✨ Feat: goalId 부재 시 예외 처리 추가
chanho0908 Feb 10, 2026
91a6a9a
✨ Feat: 인증샷 업로드 API 연동
chanho0908 Feb 10, 2026
e9d433b
♻️ Refactor: 불필요한 id 전달 제거
chanho0908 Feb 10, 2026
2b79c6c
✨ Feat: TaskCertificationRefreshBus 추가
chanho0908 Feb 10, 2026
dfb8dc8
✨ Feat: UI 인증샷 업로드 기능 구현
chanho0908 Feb 10, 2026
a99943a
♻️ Refactor: GoalRefreshBus를 주입받아 인증글 변동 시 목표 리프레쉬 이벤트 발행하도록 수정
chanho0908 Feb 10, 2026
6c3f622
♻️ Refactor: ktlintformat 적용
chanho0908 Feb 10, 2026
aeba355
♻️ Refactor: SideEffect가 생성만 되고 실제로 emit되지 않는 문제 수정
chanho0908 Feb 10, 2026
37a3a40
♻️ Refactor: uriToByteArray 리소스 관리 및 예외 처리 개선
chanho0908 Feb 10, 2026
b75bec0
✨ Feat: 이미지 변환 실패 시 예외 처리 추가
chanho0908 Feb 10, 2026
dff1a2e
✨ Feat: 인증 상세 화면에서 코멘트가 없을 경우 표시하지 않도록 수정
chanho0908 Feb 10, 2026
204b7ce
♻️ Refactor: safeContentPadding을 windowInsetsPadding으로 변경
chanho0908 Feb 10, 2026
44c6f52
♻️ Refactor: 인증 상세 화면의 수정 기능 임시 비활성화
chanho0908 Feb 10, 2026
f291771
✨ Feat: 빈 리스트 응답 임시 예외 처리 구현
chanho0908 Feb 10, 2026
fa08f3d
♻️ Refactor: 주석 처리 누락 수정
chanho0908 Feb 10, 2026
2fd68a5
✨ Feat: 챌린지 상세 화면의 리액션 버튼 임시 제거
chanho0908 Feb 10, 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
12 changes: 8 additions & 4 deletions app/src/main/java/com/yapp/twix/main/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.safeContentPadding
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.core.view.WindowCompat
Expand All @@ -31,8 +34,9 @@ class MainActivity : ComponentActivity() {
Box(
modifier =
Modifier
.fillMaxSize()
.safeContentPadding(),
.windowInsetsPadding(
WindowInsets.systemBars.only(WindowInsetsSides.Vertical),
),
) {
AppNavHost()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ data class CommentUiModel(
val comment: TextFieldValue = TextFieldValue(""),
val isFocused: Boolean = false,
) {
val isEmpty: Boolean
get() = comment.text.isEmpty()

val hasMaxCommentLength: Boolean
get() = comment.text.length == COMMENT_COUNT

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.twix.network.model.request.photolog.mapper

import com.twix.domain.model.photo.PhotologParam
import com.twix.network.model.request.photolog.model.PhotologRequest

fun PhotologParam.toRequest(): PhotologRequest =
PhotologRequest(
goalId = goalId,
fileName = fileName,
comment = comment,
verificationDate = verificationDate.toString(),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.twix.network.model.request.photolog.model

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class PhotologRequest(
@SerialName("goalId") val goalId: Long,
@SerialName("fileName") val fileName: String,
@SerialName("comment") val comment: String,
@SerialName("verificationDate") val verificationDate: String,
)
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.twix.network.service

import com.twix.network.model.request.photolog.model.PhotologRequest
import com.twix.network.model.response.photo.model.PhotoLogUploadUrlResponse
import com.twix.network.model.response.photolog.PhotoLogResponse
import de.jensklingenberg.ktorfit.http.Body
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.POST
import de.jensklingenberg.ktorfit.http.Path
import de.jensklingenberg.ktorfit.http.Query

Expand All @@ -12,6 +15,11 @@ interface PhotoLogService {
@Query("goalId") goalId: Long,
): PhotoLogUploadUrlResponse

@POST("api/v1/photologs")
suspend fun uploadPhotoLog(
@Body request: PhotologRequest,
)

@GET("api/v1/photologs/goals/{goalId}")
suspend fun fetchPhotoLogs(
@Path("goalId") goalId: Long,
Expand Down
40 changes: 40 additions & 0 deletions core/ui/src/main/java/com/twix/ui/extension/Context.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.twix.ui.extension

import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import java.io.ByteArrayOutputStream

/**
* 주어진 [Uri]로부터 이미지를 읽어 JPEG 형식의 [ByteArray]로 변환한다.
*
* 내부 동작 과정:
* 1. [ContentResolver.openInputStream]으로 InputStream을 연다.
* 2. [BitmapFactory.decodeStream]으로 Bitmap 디코딩
* 3. JPEG(품질 90) 압축 후 ByteArray 반환
*
* 실패 케이스:
* - InputStream 열기 실패
* - 디코딩 실패 (손상 이미지 등)
* - 압축 실패
*
* @param imageUri 변환할 이미지 Uri (content:// 또는 file://)
* @return 변환 성공 시 JPEG 바이트 배열, 실패 시 null
*/
fun Context.uriToByteArray(imageUri: Uri): ByteArray? {
return try {
contentResolver.openInputStream(imageUri)?.use { inputStream ->
val bitmap = BitmapFactory.decodeStream(inputStream) ?: return null

ByteArrayOutputStream().use { outputStream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream)
bitmap.recycle()
outputStream.toByteArray()
}
}
} catch (e: Exception) {
e.printStackTrace()
null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.twix.util.bus

import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow

class TaskCertificationRefreshBus {
private val _events =
MutableSharedFlow<Unit>(
replay = 0,
extraBufferCapacity = 1,
)

val events: SharedFlow<Unit> = _events.asSharedFlow()

fun notifyChanged() = _events.tryEmit(Unit)
}
2 changes: 2 additions & 0 deletions core/util/src/main/java/com/twix/util/di/UtilModule.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.twix.util.di

import com.twix.util.bus.GoalRefreshBus
import com.twix.util.bus.TaskCertificationRefreshBus
import org.koin.dsl.module

val utilModule =
module {
single { GoalRefreshBus() }
single { TaskCertificationRefreshBus() }
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.twix.data.repository

import com.twix.domain.model.photo.PhotoLogUploadInfo
import com.twix.domain.model.photo.PhotologParam
import com.twix.domain.model.photolog.PhotoLogs
import com.twix.domain.repository.PhotoLogRepository
import com.twix.network.execute.safeApiCall
import com.twix.network.model.request.photolog.mapper.toRequest
import com.twix.network.model.response.photo.mapper.toDomain
import com.twix.network.model.response.photolog.mapper.toDomain
import com.twix.network.service.PhotoLogService
Expand All @@ -16,6 +18,9 @@ class DefaultPhotoLogRepository(
) : PhotoLogRepository {
override suspend fun getUploadUrl(goalId: Long): AppResult<PhotoLogUploadInfo> = safeApiCall { service.getUploadUrl(goalId).toDomain() }

override suspend fun uploadPhotoLog(photologParam: PhotologParam): AppResult<Unit> =
safeApiCall { service.uploadPhotoLog(photologParam.toRequest()) }

override suspend fun uploadPhotoLogImage(
goalId: Long,
bytes: ByteArray,
Expand Down
10 changes: 10 additions & 0 deletions domain/src/main/java/com/twix/domain/model/photo/PhotologParam.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.twix.domain.model.photo

import java.time.LocalDate

data class PhotologParam(
val goalId: Long,
val fileName: String,
val comment: String,
val verificationDate: LocalDate,
)
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package com.twix.domain.repository

import com.twix.domain.model.photo.PhotoLogUploadInfo
import com.twix.domain.model.photo.PhotologParam
import com.twix.domain.model.photolog.PhotoLogs
import com.twix.result.AppResult

interface PhotoLogRepository {
suspend fun getUploadUrl(goalId: Long): AppResult<PhotoLogUploadInfo>

suspend fun uploadPhotoLog(photologParam: PhotologParam): AppResult<Unit>

suspend fun uploadPhotoLogImage(
goalId: Long,
bytes: ByteArray,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
Expand All @@ -30,6 +31,7 @@ import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
Expand Down Expand Up @@ -62,29 +64,28 @@ import com.twix.task_certification.certification.model.TaskCertificationSideEffe
import com.twix.task_certification.certification.model.TaskCertificationUiState
import com.twix.ui.base.ObserveAsEvents
import com.twix.ui.extension.noRippleClickable
import com.twix.ui.extension.uriToByteArray
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.androidx.compose.koinViewModel
import org.koin.compose.koinInject
import com.twix.designsystem.R as DesR

@Composable
fun TaskCertificationRoute(
goalId: Long,
toastManager: ToastManager = koinInject(),
camera: Camera = koinInject(),
viewModel: TaskCertificationViewModel = koinViewModel(),
navigateToBack: () -> Unit,
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val cameraPreview by camera.surfaceRequests.collectAsStateWithLifecycle()

val context = LocalContext.current
val currentContext by rememberUpdatedState(context)
val lifecycleOwner = LocalLifecycleOwner.current
val coroutineScope = rememberCoroutineScope()

LaunchedEffect(goalId) {
viewModel.dispatch(TaskCertificationIntent.InitGoal(goalId))
}

val pickMedia =
rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->
viewModel.dispatch(TaskCertificationIntent.PickPicture(uri))
Expand Down Expand Up @@ -115,6 +116,35 @@ fun TaskCertificationRoute(
),
)
}

is TaskCertificationSideEffect.ShowToast -> {
toastManager.tryShow(
ToastData(
message = currentContext.getString(event.message),
type = event.type,
),
)
}

is TaskCertificationSideEffect.GetImageFromUri -> {
val bytes =
withContext(Dispatchers.IO) {
currentContext.uriToByteArray(event.uri)
}

if (bytes != null) {
viewModel.dispatch(TaskCertificationIntent.Upload(bytes))
} else {
toastManager.tryShow(
ToastData(
message = currentContext.getString(R.string.task_certification_image_translate_fail),
type = ToastType.ERROR,
),
)
}
}

TaskCertificationSideEffect.NavigateToDetail -> navigateToBack()
}
}

Expand Down Expand Up @@ -152,7 +182,7 @@ fun TaskCertificationRoute(
viewModel.dispatch(TaskCertificationIntent.CommentFocusChanged(it))
},
onClickUpload = {
viewModel.dispatch(TaskCertificationIntent.Upload)
viewModel.dispatch(TaskCertificationIntent.TryUpload)
},
)
}
Expand Down Expand Up @@ -194,7 +224,7 @@ private fun TaskCertificationScreen(
CommentErrorText()
} else {
AppText(
text = stringResource(R.string.task_certification_image_upload),
text = stringResource(R.string.task_certification_take_picture),
style = AppTextStyle.H2,
color = GrayColor.C100,
)
Expand Down
Loading