Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3b05efd
[ADD/#265] Kotest 의존성 추가 및 단위 테스트를 위한 JUnit 플랫폼 설정
MoonsuKang Jun 4, 2025
8309c17
[ADD/#265] DraftDiaryContents 및 CreatedDraftDiaryInfo 데이터 클래스 추가
MoonsuKang Jun 4, 2025
ba09f76
[ADD/#265] 임시저장을 위한 Response/Request DTO 추가
MoonsuKang Jun 4, 2025
bf282eb
[FEAT/#265] 임시저장 Diary API 메서드 추가
MoonsuKang Jun 4, 2025
ffdab1b
[FEAT/#265] 임시저장 Diary 관련 API 메서드 추가(dataSource)
MoonsuKang Jun 4, 2025
54ab943
[FEAT/#265] 임시저장을 위한 다이어리 API 메서드 추가(Repository)
MoonsuKang Jun 4, 2025
761fa55
[FEAT/#265] 임시저장 기능을 위한 UseCase 추가
MoonsuKang Jun 4, 2025
9e9a87c
[FEAT/#265] ViewModel에 임시 일기 fetch/save 로직 추가 및 상태 처리 구현
MoonsuKang Jun 4, 2025
c80e418
[ADD/#265] Lifecycle STARTED 기준 LaunchedEffect 유틸 함수 추가
MoonsuKang Jun 4, 2025
d822f93
[ADD/#265] route단에 임시저장 fetch 추가 및 뒤로가기 분기 추가
MoonsuKang Jun 4, 2025
6a1ebec
[TEST/#265] FakeDiaryRepository 추가
MoonsuKang Jun 4, 2025
64ff044
[TEST/#265] FakeDiaryRemoteDataSource 클래스 추가 및 임시저장 기능 구현
MoonsuKang Jun 4, 2025
62c351c
[TEST/#265] 임시저장 다이어리 조회 및 저장 기능에 대한 테스트 추가
MoonsuKang Jun 4, 2025
d674b06
[TEST/#265] WriteDiaryViewModel 단위 테스트 구현 (임시저장 한정)
MoonsuKang Jun 4, 2025
29d1f2d
[REFACTOR/#265] onDismiss, onDismissExitDialog 분리 및 BackHandler 처리
MoonsuKang Jun 5, 2025
1863b26
[REFACTOR/#265] "보내기" 클릭 시 에러메시지가 나타나지 않는 문제 해결
MoonsuKang Jun 5, 2025
2e10906
[ADD/#265] 임시저장 Fetch실패 시 에러메시지 처리 추가
MoonsuKang Jun 5, 2025
808001c
[MOD/#265] 프린트문 제거
MoonsuKang Jun 5, 2025
794e940
[REFACTOR/#265] API 명세 변경에 따른 비즈니스 로직 수정
MoonsuKang Jun 6, 2025
b6c76f4
[REFACTOR/#265] 임시저장 처리 개선 및 기본 항목 보장 추가
MoonsuKang Jun 6, 2025
ca66f7a
[REFACTOR/#265] 임시저장 처리 개선 및 기본 항목 보장 추가
MoonsuKang Jun 6, 2025
84ccb64
[REFACTOR/#265] 임시저장 API 응답 타입 변경(->Unit) 및 관련 코드 수정
MoonsuKang Jun 6, 2025
05b2f97
[REFACTOR/#265] 임시저장 API 응답 타입 변경(->Unit) 및 관련 코드 수정
MoonsuKang Jun 6, 2025
a606664
[REFACTOR/#265] Repository의 임시저장 일기 조회 실패 메시지 처리 책임을 ViewModel로 이동
MoonsuKang Jun 6, 2025
58c726d
[ADD/#265] trailing comma
MoonsuKang Jun 6, 2025
14fc909
Merge branch 'develop' into feat/#265-draft-save-api
MoonsuKang Jun 7, 2025
9fda999
[ADD/#265] close blacket 추가
MoonsuKang Jun 7, 2025
bd24e15
[ADD/#265] import 순서 변경
MoonsuKang Jun 7, 2025
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: 11 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ android {
buildTypes {
debug {
isMinifyEnabled = false
buildConfigField("String", "CLODY_BASE_URL", properties["clody.base.url"].toString())
buildConfigField("String", "CLODY_BASE_URL", properties["clody.test.url"].toString())
}

release {
Expand All @@ -77,6 +77,11 @@ android {
buildConfig = true
compose = true
}
testOptions {
unitTests.all {
it.useJUnitPlatform()
}
}
}

dependencies {
Expand Down Expand Up @@ -123,6 +128,11 @@ dependencies {
// Mavericks
implementation(libs.bundles.mavericks)

// Kotest
testImplementation(libs.bundles.kotest)
testImplementation(libs.mockk)
testImplementation(libs.coroutines.test)

// Play Store
implementation(libs.bundles.plays)

Expand Down
14 changes: 14 additions & 0 deletions app/src/main/java/com/sopt/clody/data/remote/api/DiaryService.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.sopt.clody.data.remote.api

import com.sopt.clody.data.remote.dto.base.ApiResponse
import com.sopt.clody.data.remote.dto.request.SaveDraftDiaryRequestDto
import com.sopt.clody.data.remote.dto.request.WriteDiaryRequestDto
import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto
import com.sopt.clody.data.remote.dto.response.DiaryTimeResponseDto
import com.sopt.clody.data.remote.dto.response.DraftDiariesResponseDto
import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto
import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto
import com.sopt.clody.data.remote.dto.response.ReplyDiaryResponseDto
Expand Down Expand Up @@ -59,4 +61,16 @@ interface DiaryService {
@Query("month") month: Int,
@Query("date") date: Int,
): ApiResponse<ReplyDiaryResponseDto>

@GET("api/v1/draft")
suspend fun fetchDraftDiary(
@Query("year") year: Int,
@Query("month") month: Int,
@Query("date") date: Int,
): ApiResponse<DraftDiariesResponseDto>

@POST("api/v1/draft")
suspend fun saveDraftDiary(
@Body request: SaveDraftDiaryRequestDto,
): ApiResponse<Unit>
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.sopt.clody.data.remote.datasource

import com.sopt.clody.data.remote.dto.base.ApiResponse
import com.sopt.clody.data.remote.dto.request.SaveDraftDiaryRequestDto
import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto
import com.sopt.clody.data.remote.dto.response.DiaryTimeResponseDto
import com.sopt.clody.data.remote.dto.response.DraftDiariesResponseDto
import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto
import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto
import com.sopt.clody.data.remote.dto.response.ReplyDiaryResponseDto
Expand All @@ -16,4 +18,6 @@ interface DiaryRemoteDataSource {
suspend fun getMonthlyCalendarData(year: Int, month: Int): ApiResponse<MonthlyCalendarResponseDto>
suspend fun getMonthlyDiary(year: Int, month: Int): ApiResponse<MonthlyDiaryResponseDto>
suspend fun getReplyDiary(year: Int, month: Int, date: Int): ApiResponse<ReplyDiaryResponseDto>
suspend fun fetchDraftDiary(year: Int, month: Int, date: Int): ApiResponse<DraftDiariesResponseDto>
suspend fun saveDraftDiary(request: SaveDraftDiaryRequestDto): ApiResponse<Unit>
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package com.sopt.clody.data.remote.datasourceimpl
import com.sopt.clody.data.remote.api.DiaryService
import com.sopt.clody.data.remote.datasource.DiaryRemoteDataSource
import com.sopt.clody.data.remote.dto.base.ApiResponse
import com.sopt.clody.data.remote.dto.request.SaveDraftDiaryRequestDto
import com.sopt.clody.data.remote.dto.request.WriteDiaryRequestDto
import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto
import com.sopt.clody.data.remote.dto.response.DiaryTimeResponseDto
import com.sopt.clody.data.remote.dto.response.DraftDiariesResponseDto
import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto
import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto
import com.sopt.clody.data.remote.dto.response.ReplyDiaryResponseDto
Expand Down Expand Up @@ -35,4 +37,10 @@ class DiaryRemoteDataSourceImpl @Inject constructor(

override suspend fun getReplyDiary(year: Int, month: Int, date: Int): ApiResponse<ReplyDiaryResponseDto> =
diaryService.getReplyDiary(year = year, month = month, date = date)

override suspend fun fetchDraftDiary(year: Int, month: Int, date: Int): ApiResponse<DraftDiariesResponseDto> =
diaryService.fetchDraftDiary(year = year, month = month, date = date)

override suspend fun saveDraftDiary(request: SaveDraftDiaryRequestDto): ApiResponse<Unit> =
diaryService.saveDraftDiary(request)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.sopt.clody.data.remote.dto.request

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

@Serializable
data class SaveDraftDiaryRequestDto(
@SerialName("date") val date: String,
@SerialName("draftDiaries") val draftDiaries: List<String>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.sopt.clody.data.remote.dto.response

import com.sopt.clody.domain.model.DraftDiaryContents
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class DraftDiariesResponseDto(
@SerialName("draftDiaries") val draftDiaries: List<String>,
) {
fun toDomain() = DraftDiaryContents(
draftDiaries = draftDiaries ?: emptyList(),
)
}
Comment on lines +8 to +14
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

Fix inconsistency between property nullability and null check.

There's a logical inconsistency: draftDiaries is declared as non-null List<String> on line 9, but line 12 performs a null check draftDiaries ?: emptyList(). This null check is unnecessary since the property cannot be null.

Option 1: If the API can return null, make the property nullable:

-@SerialName("draftDiaries") val draftDiaries: List<String>,
+@SerialName("draftDiaries") val draftDiaries: List<String>?,

Option 2: If the API always returns a list, remove the null check:

 fun toDomain() = DraftDiaryContents(
-    draftDiaries = draftDiaries ?: emptyList(),
+    draftDiaries = draftDiaries,
 )

Please verify the API contract to determine which approach is correct.

📝 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
data class DraftDiariesResponseDto(
@SerialName("draftDiaries") val draftDiaries: List<String>,
) {
fun toDomain() = DraftDiaryContents(
draftDiaries = draftDiaries ?: emptyList(),
)
}
data class DraftDiariesResponseDto(
@SerialName("draftDiaries") val draftDiaries: List<String>?, // now nullable
) {
fun toDomain() = DraftDiaryContents(
draftDiaries = draftDiaries ?: emptyList(), // safe fallback
)
}
Suggested change
data class DraftDiariesResponseDto(
@SerialName("draftDiaries") val draftDiaries: List<String>,
) {
fun toDomain() = DraftDiaryContents(
draftDiaries = draftDiaries ?: emptyList(),
)
}
data class DraftDiariesResponseDto(
@SerialName("draftDiaries") val draftDiaries: List<String>,
) {
fun toDomain() = DraftDiaryContents(
draftDiaries = draftDiaries, // no need for `?:`
)
}
🤖 Prompt for AI Agents
In
app/src/main/java/com/sopt/clody/data/remote/dto/response/DraftDiariesResponseDto.kt
between lines 8 and 14, the property draftDiaries is declared as a non-null
List<String> but the toDomain function performs a redundant null check on it. To
fix this, either make draftDiaries nullable if the API can return null, or if
the API guarantees a non-null list, remove the null check in toDomain and
directly use draftDiaries. Confirm the API contract to decide which fix to
apply.

Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package com.sopt.clody.data.repositoryimpl

import com.sopt.clody.data.remote.datasource.DiaryRemoteDataSource
import com.sopt.clody.data.remote.dto.request.SaveDraftDiaryRequestDto
import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto
import com.sopt.clody.data.remote.dto.response.DiaryTimeResponseDto
import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto
import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto
import com.sopt.clody.data.remote.dto.response.ReplyDiaryResponseDto
import com.sopt.clody.data.remote.dto.response.WriteDiaryResponseDto
import com.sopt.clody.data.remote.util.handleApiResponse
import com.sopt.clody.domain.model.DraftDiaryContents
import com.sopt.clody.domain.repository.DiaryRepository
import com.sopt.clody.presentation.utils.network.ErrorMessages
import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE
Expand Down Expand Up @@ -80,4 +82,21 @@ class DiaryRepositoryImpl @Inject constructor(
}
response
}

override suspend fun fetchDraftDiary(year: Int, month: Int, date: Int): Result<DraftDiaryContents> =
runCatching {
diaryRemoteDataSource
.fetchDraftDiary(year, month, date)
.handleApiResponse()
.getOrThrow()
.toDomain()
}

override suspend fun saveDraftDiary(date: String, contents: List<String>): Result<Unit> =
runCatching {
diaryRemoteDataSource
.saveDraftDiary(SaveDraftDiaryRequestDto(date = date, draftDiaries = contents))
.handleApiResponse()
.getOrThrow()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.sopt.clody.domain.model

data class DraftDiaryContents(
val draftDiaries: List<String>,
)
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto
import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto
import com.sopt.clody.data.remote.dto.response.ReplyDiaryResponseDto
import com.sopt.clody.data.remote.dto.response.WriteDiaryResponseDto
import com.sopt.clody.domain.model.DraftDiaryContents

interface DiaryRepository {
suspend fun writeDiary(date: String, content: List<String>): Result<WriteDiaryResponseDto>
Expand All @@ -15,4 +16,6 @@ interface DiaryRepository {
suspend fun getMonthlyCalendarData(year: Int, month: Int): Result<MonthlyCalendarResponseDto>
suspend fun getMonthlyDiary(year: Int, month: Int): Result<MonthlyDiaryResponseDto>
suspend fun getReplyDiary(year: Int, month: Int, date: Int): Result<ReplyDiaryResponseDto>
suspend fun fetchDraftDiary(year: Int, month: Int, date: Int): Result<DraftDiaryContents>
suspend fun saveDraftDiary(date: String, contents: List<String>): Result<Unit>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.sopt.clody.domain.usecase

import com.sopt.clody.domain.model.DraftDiaryContents
import com.sopt.clody.domain.repository.DiaryRepository
import javax.inject.Inject

class FetchDraftDiaryUseCase @Inject constructor(
private val repository: DiaryRepository,
) {
suspend operator fun invoke(year: Int, month: Int, day: Int): Result<DraftDiaryContents> {
return repository.fetchDraftDiary(year, month, day)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.sopt.clody.domain.usecase

import com.sopt.clody.domain.repository.DiaryRepository
import javax.inject.Inject

class SaveDraftDiaryUseCase @Inject constructor(
private val diaryRepository: DiaryRepository,
) {
suspend operator fun invoke(date: String, contents: List<String>): Result<Unit> {
return diaryRepository.saveDraftDiary(date, contents)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ fun ClodyDialog(
confirmButtonColor: Color,
confirmButtonTextColor: Color,
onDismiss: () -> Unit,
onDismissButtonClick: (() -> Unit)? = null,
) {
var isButtonClicked by remember { mutableStateOf(false) }

Expand All @@ -68,8 +69,7 @@ fun ClodyDialog(
.wrapContentHeight(),
) {
Column(
modifier = Modifier
.padding(28.dp),
modifier = Modifier.padding(28.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Expand All @@ -92,21 +92,21 @@ fun ClodyDialog(
Spacer(modifier = Modifier.height(32.dp))

Row(
modifier = Modifier
.fillMaxWidth(),
modifier = Modifier.fillMaxWidth(),
) {
Button(
onClick = {
if (!isButtonClicked) {
isButtonClicked = true
onDismiss()
// dismiss 버튼 클릭 시: 우선순위는 onDismissButtonClick
onDismissButtonClick?.invoke() ?: onDismiss()
}
},
modifier = Modifier
.weight(1f)
.background(
color = ClodyTheme.colors.gray07,
shape = RoundedCornerShape(size = 8.dp),
shape = RoundedCornerShape(8.dp),
),
colors = ButtonDefaults.buttonColors(ClodyTheme.colors.gray07),
) {
Expand All @@ -130,7 +130,7 @@ fun ClodyDialog(
.weight(1f)
.background(
color = confirmButtonColor,
shape = RoundedCornerShape(size = 8.dp),
shape = RoundedCornerShape(8.dp),
),
colors = ButtonDefaults.buttonColors(confirmButtonColor),
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ fun WriteDiaryTextField(
.fillMaxWidth()
.padding(start = 8.dp, top = 6.dp),
) {
if ((showWarning && !isTextValid && text.isNotEmpty()) || isTextTooLong) {
if ((showWarning && !isTextValid) || isTextTooLong) {
Text(
text = "2~50자 까지 입력할 수 있어요.",
color = ClodyTheme.colors.red,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.sopt.clody.presentation.ui.writediary.screen

import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
Expand Down Expand Up @@ -48,6 +49,7 @@ import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints
import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils
import com.sopt.clody.presentation.utils.base.BasePreview
import com.sopt.clody.presentation.utils.base.ClodyPreview
import com.sopt.clody.presentation.utils.extension.LaunchedEffectWhenStarted
import com.sopt.clody.presentation.utils.extension.getDayOfWeek
import com.sopt.clody.presentation.utils.extension.heightForScreenPercentage
import com.sopt.clody.ui.theme.ClodyTheme
Expand All @@ -74,6 +76,10 @@ fun WriteDiaryRoute(
val showDialog by viewModel::showDialog
val showExitDialog by viewModel::showExitDialog

LaunchedEffectWhenStarted {
viewModel.fetchDraftDiary(year, month, date)
}

LaunchedEffect(writeDiaryState) {
when (writeDiaryState) {
is WriteDiaryState.Success -> navigateToReplyLoading(year, month, date)
Expand All @@ -83,6 +89,19 @@ fun WriteDiaryRoute(
}
}

BackHandler {
if (showExitDialog) {
viewModel.updateShowExitDialog(false)
} else {
if (viewModel.hasChangedFromInitial()) {
AmplitudeUtils.trackEvent(AmplitudeConstraints.WRITING_DIARY_BACK)
viewModel.updateShowExitDialog(true)
} else {
navigateToPrevious()
}
}
}

WriteDiaryScreen(
isLoading = writeDiaryState is WriteDiaryState.Loading,
entries = entries,
Expand All @@ -95,7 +114,7 @@ fun WriteDiaryRoute(
failureMessage = failureMessage,
showExitDialog = showExitDialog,
onClickBack = {
if (entries.any { it.isNotBlank() }) {
if (viewModel.hasChangedFromInitial()) {
AmplitudeUtils.trackEvent(AmplitudeConstraints.WRITING_DIARY_BACK)
viewModel.updateShowExitDialog(true)
} else {
Expand Down Expand Up @@ -138,9 +157,11 @@ fun WriteDiaryRoute(
onDismissLimitMessage = { viewModel.updateShowLimitMessage(false) },
onDismissEmptyFieldsMessage = { viewModel.updateShowEmptyFieldsMessage(false) },
onDismissFailureDialog = { viewModel.resetFailureDialog() },
onDismiss = { viewModel.updateShowExitDialog(false) },
onDismissExitDialog = {
viewModel.updateDraftUsage()
viewModel.updateShowExitDialog(false)
viewModel.saveDraftDiary(year, month, date)
navigateToHome(year, month)
},
onConfirmExitDialog = {
Expand Down Expand Up @@ -177,6 +198,7 @@ fun WriteDiaryScreen(
failureMessage: String,
showExitDialog: Boolean,
onDismissFailureDialog: () -> Unit,
onDismiss: () -> Unit,
onDismissExitDialog: () -> Unit,
onConfirmExitDialog: () -> Unit,
year: Int,
Expand Down Expand Up @@ -285,7 +307,8 @@ fun WriteDiaryScreen(
confirmAction = onConfirmExitDialog,
confirmButtonColor = ClodyTheme.colors.red,
confirmButtonTextColor = ClodyTheme.colors.white,
onDismiss = onDismissExitDialog,
onDismiss = onDismiss,
onDismissButtonClick = onDismissExitDialog,
)
}
}
Expand Down Expand Up @@ -382,6 +405,7 @@ private fun WriteDiaryScreenPreview() {
failureMessage = "",
showExitDialog = false,
onDismissFailureDialog = {},
onDismiss = {},
onDismissExitDialog = {},
onConfirmExitDialog = {},
year = 2023,
Expand Down
Loading