Skip to content

[Feat/#265] 임시저장 API연결을 진행합니다.#271

Merged
MoonsuKang merged 28 commits intodevelopfrom
feat/#265-draft-save-api
Jun 7, 2025
Merged

[Feat/#265] 임시저장 API연결을 진행합니다.#271
MoonsuKang merged 28 commits intodevelopfrom
feat/#265-draft-save-api

Conversation

@MoonsuKang
Copy link
Copy Markdown
Contributor

@MoonsuKang MoonsuKang commented Jun 4, 2025

📌 ISSUE

closed #265

📄 Work Description

임시저장 기능 구현을 위해 다음과 같은 작업을 진행했습니다.

1. 임시저장 API 연결

  • fetchDraftDiary, saveDraftDiary 메서드 추가
  • Service → DataSource → Repository 계층을 통해 도메인까지 연결

2. 전체 구조 적용

  • 흐름: Service → DataSource → Repository → UseCase → ViewModel → UI
  • WriteDiaryViewModel에 임시저장 fetch/save 로직 구현
  • initialEntries 값을 기준으로 변경 여부 판단 로직 작성
  • 뒤로가기 시 변경 감지 → 임시저장 or 다이얼로그 분기 처리

3. UI 연동 및 생명주기 처리

  • WriteDiaryScreen 진입 시 fetchDraftDiary 자동 호출
  • repeatOnLifecycle(Lifecycle.State.STARTED) 기반의 유틸 함수 LaunchedEffectWhenStarted 활용

4. 간단한 테스트 코드 작성

  • FakeDiaryRepository, FakeDiaryRemoteDataSource 구현
  • Kotest 기반의 단위 테스트로 fetch/save 동작 검증

✨ PR Point

이론상 제대로 동작할 겁니다? ㅋㅋㅋㅋㅋ.....

📸 ScreenShot/Video

Summary by CodeRabbit

  • New Features

    • Added the ability to fetch and save draft diary entries, allowing users to temporarily store and retrieve diary content.
    • Enhanced the diary writing screen with draft auto-saving and restoration when navigating away or returning.
    • Improved back button handling to prompt users to save drafts before exiting if changes are detected.
  • Bug Fixes

    • Adjusted warning message display in diary text fields to show for all invalid input cases.
  • Improvements

    • Updated dialog components to allow custom actions on dismiss.
    • Added clearer error messages for failed draft diary fetch attempts.
  • Tests

    • Introduced comprehensive tests for draft diary features, including repository and ViewModel behavior.

@MoonsuKang MoonsuKang requested a review from SYAAINN June 4, 2025 08:01
@MoonsuKang MoonsuKang self-assigned this Jun 4, 2025
@MoonsuKang MoonsuKang added ✨ FEAT 기능 개발 🌊 문수 문수 labels Jun 4, 2025
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jun 4, 2025

Walkthrough

This update integrates the draft diary (임시저장) API into the application. It introduces new DTOs, repository methods, use cases, and ViewModel logic for fetching and saving draft diaries. The UI is updated to handle draft diary state and user interactions, while new tests and test doubles are added to verify the draft diary functionality. Build and test configurations are also updated.

Changes

File(s) / Group Change Summary
app/build.gradle.kts, gradle/libs.versions.toml Added Kotest, MockK, and coroutines-test dependencies; enabled JUnit Platform for unit tests.
.../api/DiaryService.kt, .../datasource/DiaryRemoteDataSource.kt, .../datasourceimpl/DiaryRemoteDataSourceImpl.kt Added fetch/save draft diary API methods and DTO imports.
.../dto/request/SaveDraftDiaryRequestDto.kt, .../dto/response/DraftDiariesResponseDto.kt Introduced new DTOs for draft diary request/response.
.../repositoryimpl/DiaryRepositoryImpl.kt, .../repository/DiaryRepository.kt Added repository methods for fetching and saving draft diaries.
.../model/DraftDiaryContents.kt Added domain model for draft diary contents.
.../usecase/FetchDraftDiaryUseCase.kt, .../usecase/SaveDraftDiaryUseCase.kt Added use cases for fetching and saving draft diaries.
.../ui/writediary/screen/WriteDiaryScreen.kt, .../ui/writediary/screen/WriteDiaryViewModel.kt Integrated draft diary fetch/save logic and state into ViewModel and UI, updated dialog logic.
.../ui/component/dialog/ClodyDialog.kt Added optional dismiss button callback to dialog component.
.../ui/writediary/component/textfield/WriteDiaryTextField.kt Adjusted warning display logic for text field validation.
.../utils/extension/LifeCycle.kt Added lifecycle-aware LaunchedEffect utility for Compose.
.../utils/network/ErrorMessages.kt Added error message for draft diary fetch failure.
app/src/test/java/com/sopt/clody/FakeDiaryRepository.kt, .../datasource/FakeDiaryRemoteDataSource.kt Added test doubles for repository and remote data source with draft diary support.
.../WriteDiaryViewModelTest.kt, .../repositoryimpl/DiaryRepositoryImplTest.kt Added Kotest-based tests for ViewModel and repository draft diary logic.

Sequence Diagram(s)

sequenceDiagram
    participant UI (WriteDiaryScreen)
    participant VM (WriteDiaryViewModel)
    participant UseCaseFetch
    participant UseCaseSave
    participant Repo
    participant RemoteDS
    participant API

    UI->>VM: On screen start, fetchDraftDiary(year, month, day)
    VM->>UseCaseFetch: invoke(year, month, day)
    UseCaseFetch->>Repo: fetchDraftDiary(year, month, day)
    Repo->>RemoteDS: fetchDraftDiary(year, month, day)
    RemoteDS->>API: GET /draft-diary
    API-->>RemoteDS: DraftDiariesResponseDto
    RemoteDS-->>Repo: ApiResponse<DraftDiariesResponseDto>
    Repo-->>UseCaseFetch: Result<DraftDiaryContents>
    UseCaseFetch-->>VM: Result<DraftDiaryContents>
    VM-->>UI: Update entries with fetched drafts

    UI->>VM: On exit/dismiss, saveDraftDiary(year, month, day)
    VM->>UseCaseSave: invoke(date, contents)
    UseCaseSave->>Repo: saveDraftDiary(date, contents)
    Repo->>RemoteDS: saveDraftDiary(request)
    RemoteDS->>API: POST /draft-diary
    API-->>RemoteDS: ApiResponse<Unit>
    RemoteDS-->>Repo: ApiResponse<Unit>
    Repo-->>UseCaseSave: Result<Unit>
    UseCaseSave-->>VM: Result<Unit>
    VM-->>UI: Handle save result (success/failure)
Loading

Assessment against linked issues

Objective Addressed Explanation
Integrate draft diary (임시저장) API: fetch and save endpoints (#265)
Add repository, use case, and ViewModel logic for draft diary (#265)
Update UI to handle draft diary state and user interactions (#265)
Add tests and test doubles for draft diary functionality (#265)

Suggested labels

🔥 민재

Poem

In fields of code, a diary’s draft,
Now saved and fetched, no longer daft.
With tests that hop and logic neat,
The rabbits cheer this feature’s feat!
Compose and Kotest join the fun,
🐇 Clody’s journey has begun!


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 14fc909 and bd24e15.

📒 Files selected for processing (2)
  • app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt (4 hunks)
  • gradle/libs.versions.toml (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • gradle/libs.versions.toml
🧰 Additional context used
🪛 detekt (1.23.8)
app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt

[warning] 209-209: String.format("%04d-%02d-%02d", year, month, day) uses implicitly default locale for string formatting.

(detekt.potential-bugs.ImplicitDefaultLocale)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build
🔇 Additional comments (4)
app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt (4)

28-29: LGTM: Clean dependency injection for draft functionality.

The addition of the two new use cases follows proper dependency injection patterns and integrates well with the existing constructor.


67-67: LGTM: Appropriate state tracking for change detection.

The initialEntries property provides a clean way to track the original draft state for comparison purposes.


181-183: LGTM: Simple and effective change detection.

The hasChangedFromInitial() method provides a straightforward way to detect if the user has made changes to the draft.


221-228: LGTM: Well-designed utility method for state reset.

The ensureDefaultEntry() method properly resets all related state (entries, warnings, and UI flags) to maintain consistency.

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (6)
app/src/main/java/com/sopt/clody/domain/model/CreatedDraftDiaryInfo.kt (1)

3-5: Consider using a more type-safe timestamp representation.

While using String for createdAt works functionally, it lacks type safety and validation. Consider using LocalDateTime, Instant, or a custom timestamp type to prevent parsing errors and ensure consistent formatting.

+import java.time.LocalDateTime
+
 data class CreatedDraftDiaryInfo(
-    val createdAt: String,
+    val createdAt: LocalDateTime,
 )
app/src/test/java/com/sopt/clody/repositoryimpl/DiaryRepositoryImplTest.kt (2)

26-27: Remove debugging println statements.

These debugging statements should be removed before merging to production.

-                println("fetchDraftDiary result: $result")
-                println("fetchDraftDiary error: ${result.exceptionOrNull()?.message}")

51-52: Remove debugging println statements.

These debugging statements should be removed before merging to production.

-                println("saveDraftDiary result: $result")
-                println("saveDraftDiary error: ${result.exceptionOrNull()?.message}")
app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt (1)

202-214: Remove debug println statement.

The implementation is solid, but the println statement should be removed for production code.

Apply this diff to remove the debug statement:

            result.onSuccess {
                _failureMessage.value = ""
                _showFailureDialog.value = false
-                println("성공함")
            }.onFailure { e ->
app/src/test/java/com/sopt/clody/datasource/FakeDiaryRemoteDataSource.kt (2)

48-63: Consider more descriptive error messages.

The implementation logic is correct, but the error messages could be more helpful for debugging test failures.

-            ?: throw IllegalStateException("draftDiariesResponse not set")
+            ?: throw IllegalStateException("draftDiariesResponse not set. Call setDraftDiariesResponse() before testing fetchDraftDiary.")
-            ?: throw IllegalStateException("saveDraftResponse not set")
+            ?: throw IllegalStateException("saveDraftResponse not set. Call setSaveDraftDiaryResponse() before testing saveDraftDiary.")

64-79: Consider consistency for hardcoded messages.

The helper methods work well for test setup, but the hardcoded Korean message "성공" might be inconsistent with other parts of the codebase.

Consider extracting the success message to a constant or making it configurable:

+    companion object {
+        private const val SUCCESS_MESSAGE = "성공"
+    }
+
     fun setDraftDiariesResponse(list: List<String>) {
         draftDiariesResponse = ApiResponse(
             status = 200,
-            message = "성공",
+            message = SUCCESS_MESSAGE,
             data = DraftDiariesResponseDto(draftDiaries = list),
         )
     }

     fun setSaveDraftDiaryResponse(createdAt: String) {
         saveDraftResponse = ApiResponse(
             status = 201,
-            message = "성공",
+            message = SUCCESS_MESSAGE,
             data = DraftDiaryCreatedResponseDto(createdAt),
         )
     }

Alternatively, you could reference a shared constant if one exists elsewhere in the codebase for API success messages.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 91e35d0 and d674b06.

📒 Files selected for processing (21)
  • app/build.gradle.kts (2 hunks)
  • app/src/main/java/com/sopt/clody/data/remote/api/DiaryService.kt (2 hunks)
  • app/src/main/java/com/sopt/clody/data/remote/datasource/DiaryRemoteDataSource.kt (2 hunks)
  • app/src/main/java/com/sopt/clody/data/remote/datasourceimpl/DiaryRemoteDataSourceImpl.kt (2 hunks)
  • app/src/main/java/com/sopt/clody/data/remote/dto/request/SaveDraftDiaryRequestDto.kt (1 hunks)
  • app/src/main/java/com/sopt/clody/data/remote/dto/response/DraftDiariesResponseDto.kt (1 hunks)
  • app/src/main/java/com/sopt/clody/data/remote/dto/response/DraftDiaryCreatedResponseDto.kt (1 hunks)
  • app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt (2 hunks)
  • app/src/main/java/com/sopt/clody/domain/model/CreatedDraftDiaryInfo.kt (1 hunks)
  • app/src/main/java/com/sopt/clody/domain/model/DraftDiaryContents.kt (1 hunks)
  • app/src/main/java/com/sopt/clody/domain/repository/DiaryRepository.kt (2 hunks)
  • app/src/main/java/com/sopt/clody/domain/usecase/FetchDraftDiaryUseCase.kt (1 hunks)
  • app/src/main/java/com/sopt/clody/domain/usecase/SaveDraftDiaryUseCase.kt (1 hunks)
  • app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt (4 hunks)
  • app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt (4 hunks)
  • app/src/main/java/com/sopt/clody/presentation/utils/extension/LifeCycle.kt (2 hunks)
  • app/src/test/java/com/sopt/clody/FakeDiaryRepository.kt (1 hunks)
  • app/src/test/java/com/sopt/clody/WriteDiaryViewModelTest.kt (1 hunks)
  • app/src/test/java/com/sopt/clody/datasource/FakeDiaryRemoteDataSource.kt (1 hunks)
  • app/src/test/java/com/sopt/clody/repositoryimpl/DiaryRepositoryImplTest.kt (1 hunks)
  • gradle/libs.versions.toml (3 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt (1)
app/src/main/java/com/sopt/clody/presentation/utils/extension/LifeCycle.kt (1)
  • LaunchedEffectWhenStarted (42-55)
app/src/test/java/com/sopt/clody/WriteDiaryViewModelTest.kt (1)
app/src/test/java/com/sopt/clody/repositoryimpl/DiaryRepositoryImplTest.kt (1)
  • Given (8-59)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build
🔇 Additional comments (33)
app/src/main/java/com/sopt/clody/presentation/utils/extension/LifeCycle.kt (2)

3-4: LGTM: Clean import additions for Compose integration.

The new imports are properly organized and necessary for the Composable function implementation.

Also applies to: 7-7


32-55: Excellent lifecycle-aware Composable utility function.

The implementation follows Compose best practices:

  • Proper use of LocalLifecycleOwner.current
  • Correct LaunchedEffect pattern with lifecycle dependency
  • Flexible parameters with sensible defaults
  • Comprehensive Korean documentation
  • Clean separation of concerns

This utility will be valuable for lifecycle-aware side effects throughout the app.

gradle/libs.versions.toml (2)

126-131: Well-structured test dependency definitions.

The library entries follow the existing conventions and properly reference the version variables.


204-208: Clean bundle organization for Kotest modules.

The bundle groups the related Kotest modules logically, following the existing pattern in the file.

app/build.gradle.kts (2)

80-84: Correct JUnit Platform configuration for Kotest.

The useJUnitPlatform() configuration is required for Kotest tests to run properly. Well implemented.


131-134: Proper test dependency additions.

The test dependencies are correctly placed and reference the appropriate library aliases from the version catalog.

app/src/main/java/com/sopt/clody/domain/model/DraftDiaryContents.kt (1)

1-5: Clean and well-designed domain model.

The data class follows Kotlin conventions with:

  • Proper package structure
  • Descriptive property naming
  • Appropriate type for the use case
  • Simple, focused design

Perfect for representing draft diary contents in the domain layer.

app/src/main/java/com/sopt/clody/data/remote/dto/request/SaveDraftDiaryRequestDto.kt (1)

6-9: LGTM! Clean DTO implementation.

The DTO follows good practices with proper serialization annotations and clear field mapping. The structure is appropriate for handling draft diary requests.

app/src/main/java/com/sopt/clody/domain/usecase/FetchDraftDiaryUseCase.kt (1)

7-13: LGTM! Clean use case implementation following clean architecture.

The use case correctly implements the single responsibility principle with proper dependency injection and error handling through Result type. The delegation pattern to the repository is appropriate.

app/src/main/java/com/sopt/clody/data/remote/dto/response/DraftDiaryCreatedResponseDto.kt (1)

8-16: LGTM: Clean DTO implementation following established patterns.

The DTO correctly handles the API response structure with proper serialization annotations and domain conversion. The createdAt field as String is acceptable if it matches the API contract, though consider using a more specific timestamp type if the API allows.

app/src/main/java/com/sopt/clody/domain/usecase/SaveDraftDiaryUseCase.kt (1)

7-13: LGTM: Use case follows clean architecture principles.

The implementation correctly encapsulates the repository call with proper dependency injection and error handling via Result type. While it's a thin abstraction layer, it maintains architectural consistency and provides a clean interface for the presentation layer.

app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt (4)

51-51: Good import addition for lifecycle-aware functionality.

The import of LaunchedEffectWhenStarted supports the lifecycle-aware draft fetching implementation.


78-80: Excellent use of lifecycle-aware effect for draft fetching.

The LaunchedEffectWhenStarted utility correctly ensures that draft diary fetching occurs when the screen starts, following proper lifecycle management. This will automatically fetch any existing draft when the user enters the write diary screen.


103-103: Smart change detection for improved UX.

Using hasChangedFromInitial() to determine whether to show the exit dialog is a good UX improvement. Users will only be prompted about unsaved changes when they actually have changes to save.


146-150: Excellent UX: Auto-save on exit dialog dismiss.

The implementation to save the draft diary when dismissing the exit dialog provides excellent user experience. Users can safely dismiss the dialog knowing their work will be preserved, while still having the option to discard changes via the confirm action.

app/src/main/java/com/sopt/clody/domain/repository/DiaryRepository.kt (2)

9-11: Good domain model imports for draft functionality.

The imports correctly bring in the necessary domain models for the new draft diary methods.


20-21: Well-designed repository interface extensions.

The new methods follow established patterns with appropriate parameter types and Result return types for consistent error handling. The fetchDraftDiary method correctly takes date parameters, while saveDraftDiary accepts content for persistence.

app/src/test/java/com/sopt/clody/repositoryimpl/DiaryRepositoryImplTest.kt (1)

8-59: Well-structured BDD tests with good coverage.

The test structure follows good BDD practices with clear Given/When/Then scenarios. The use of FakeDiaryRemoteDataSource for dependency injection is appropriate for unit testing, and the assertions properly validate both success states and data integrity.

app/src/main/java/com/sopt/clody/data/remote/datasource/DiaryRemoteDataSource.kt (1)

4-4: Clean interface extensions following existing patterns.

The new method signatures are well-designed and consistent with the existing interface conventions. The parameter naming and return types appropriately match the draft diary functionality requirements.

Also applies to: 7-8, 22-23

app/src/main/java/com/sopt/clody/data/remote/api/DiaryService.kt (1)

4-4: Well-designed API endpoints following REST conventions.

The new endpoints correctly use appropriate HTTP methods (GET for fetching, POST for saving) and follow the existing parameter naming conventions. The Retrofit annotations are properly applied.

Also applies to: 8-9, 66-76

app/src/main/java/com/sopt/clody/data/remote/datasourceimpl/DiaryRemoteDataSourceImpl.kt (1)

6-6: Appropriate data source implementations maintaining clean architecture.

The implementations correctly delegate to the service layer without adding business logic, maintaining proper separation of concerns. The parameter passing follows the established patterns in the class.

Also applies to: 10-11, 42-46

app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt (2)

87-94: LGTM! Consistent implementation pattern.

The fetchDraftDiary method follows the established pattern used by other methods in this repository implementation, properly handling exceptions and API responses.


96-103: LGTM! Proper request DTO usage and error handling.

The saveDraftDiary method correctly wraps the contents in a SaveDraftDiaryRequestDto and follows the same error handling pattern as other methods.

app/src/test/java/com/sopt/clody/FakeDiaryRepository.kt (1)

18-44: LGTM! Appropriate stub implementation for focused testing.

The NotImplementedError approach for unused methods is appropriate for a focused fake repository that only needs to support draft diary operations.

app/src/test/java/com/sopt/clody/WriteDiaryViewModelTest.kt (3)

20-32: LGTM! Proper coroutine test setup.

The test dispatcher setup and teardown correctly handles the main dispatcher for coroutine testing.


34-56: Test depends on FakeDiaryRepository fix.

This test sets draftDiaryResult but currently the FakeDiaryRepository.fetchDraftDiary method doesn't use this property. The test will work after the FakeDiaryRepository is fixed to use the result properties.


59-84: LGTM! Comprehensive save operation test.

The test properly sets up the ViewModel state, performs operations, and verifies the expected outcome using async verification.

app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt (4)

12-13: LGTM! Proper dependency injection of use cases.

The new use case dependencies are correctly injected and follow the established pattern in the ViewModel.

Also applies to: 26-27


64-64: LGTM! Good approach for change tracking.

The initialEntries property provides a clean way to track the original state for change detection.


178-180: LGTM! Simple and effective change detection.

The comparison logic correctly identifies when the current entries differ from the initial state.


182-200: LGTM! Comprehensive fetch implementation with proper state management.

The method properly clears existing state, populates new data, updates initial entries for change tracking, and handles both success and failure cases appropriately.

app/src/test/java/com/sopt/clody/datasource/FakeDiaryRemoteDataSource.kt (2)

15-19: Well-structured test double implementation.

The fake data source follows good testing practices by focusing on specific functionality (draft diaries) and using a fail-fast approach with stored responses.


20-47: Appropriate approach for focused test double.

Using NotImplementedError for unused methods clearly indicates the scope of this fake implementation and helps catch any unintended usage during testing.

Comment on lines +51 to +52
kotestVersion = "5.9.0"
mockk = "1.13.10"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify dependency versions for latest stable releases.

The dependency versions should be checked to ensure you're using the latest stable releases for optimal security and features.


🌐 Web query:

What are the latest stable versions of Kotest and MockK for Android development?

💡 Result:

As of June 4, 2025, the latest stable versions of Kotest and MockK for Android development are:

  • Kotest: Version 5.7.0, released on May 15, 2025.

  • MockK: Version 1.13.16, released on January 10, 2025.

These versions are compatible with Android development and can be integrated into your project using the following dependencies:

For Kotest:

testImplementation 'io.kotest:kotest-runner-junit5:5.7.0'
testImplementation 'io.kotest:kotest-assertions-core:5.7.0'

For MockK:

testImplementation 'io.mockk:mockk:1.13.16'

Ensure that your project is configured to use JUnit 5, as Kotest integrates with it for running tests.

For more details on integrating these libraries into your Android project, refer to their official documentation:

Citations:


Update Kotest and MockK to latest stable releases

The PR bumps Kotest to 5.9.0 and MockK to 1.13.10, but the latest stable versions (as of June 4, 2025) are:

  • Kotest 5.7.0
  • MockK 1.13.16

Please update gradle/libs.versions.toml accordingly:

 kotestVersion = "5.9.0"
-mockk = "1.13.10"
+kotestVersion = "5.7.0"
+mockk        = "1.13.16"

And update any Kotest module entries or bundles to use 5.7.0 instead of 5.9.0.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In gradle/libs.versions.toml at lines 51 to 52, the Kotest and MockK versions
are set to 5.9.0 and 1.13.10 respectively, but the latest stable versions are
Kotest 5.7.0 and MockK 1.13.16. Update the kotestVersion to "5.7.0" and mockk to
"1.13.16". Also, ensure any other Kotest module entries or bundles in the file
use version 5.7.0 to maintain consistency.

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

Comment on lines +14 to +16
var draftDiaryResult: Result<DraftDiaryContents>? = null
var saveDraftResult: Result<CreatedDraftDiaryInfo>? = null
private var draftDiaryContents: DraftDiaryContents? = null
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 inconsistent property usage in test fake.

The draftDiaryResult property is declared but never used in the fetchDraftDiary implementation. This creates confusion as tests may set draftDiaryResult expecting it to control the behavior.

Apply this diff to fix the inconsistency:

    override suspend fun fetchDraftDiary(year: Int, month: Int, date: Int): Result<DraftDiaryContents> {
-        return draftDiaryContents?.let { Result.success(it) }
-            ?: Result.failure(IllegalStateException("No draft found"))
+        return draftDiaryResult ?: Result.failure(IllegalStateException("No draft found"))
    }

    override suspend fun saveDraftDiary(contents: List<String>): Result<CreatedDraftDiaryInfo> {
        draftDiaryContents = DraftDiaryContents(contents)
-        return Result.success(CreatedDraftDiaryInfo(createdAt = "2024-06-01T00:00:00"))
+        return saveDraftResult ?: Result.success(CreatedDraftDiaryInfo(createdAt = "2024-06-01T00:00:00"))
    }
📝 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
var draftDiaryResult: Result<DraftDiaryContents>? = null
var saveDraftResult: Result<CreatedDraftDiaryInfo>? = null
private var draftDiaryContents: DraftDiaryContents? = null
override suspend fun fetchDraftDiary(year: Int, month: Int, date: Int): Result<DraftDiaryContents> {
return draftDiaryResult ?: Result.failure(IllegalStateException("No draft found"))
}
override suspend fun saveDraftDiary(contents: List<String>): Result<CreatedDraftDiaryInfo> {
draftDiaryContents = DraftDiaryContents(contents)
return saveDraftResult ?: Result.success(CreatedDraftDiaryInfo(createdAt = "2024-06-01T00:00:00"))
}
🤖 Prompt for AI Agents
In app/src/test/java/com/sopt/clody/FakeDiaryRepository.kt around lines 14 to
16, the property draftDiaryResult is declared but not used in the
fetchDraftDiary method, causing confusion in tests. Update the fetchDraftDiary
implementation to return draftDiaryResult instead of draftDiaryContents,
ensuring that the property controls the method's behavior as expected in tests.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt (1)

208-211: Reconsider success state management for save operations.

Clearing failure states on successful save might not provide appropriate user feedback. Users typically expect confirmation that their draft was saved successfully.

Consider adding a success state or toast message instead of just clearing error states:

 result.onSuccess {
-    _failureMessage.value = ""
-    _showFailureDialog.value = false
+    // Consider showing success feedback to user
+    // e.g., a success toast or updating a save status
 }.onFailure { e ->
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 808001c and ca66f7a.

📒 Files selected for processing (6)
  • app/src/main/java/com/sopt/clody/data/remote/dto/request/SaveDraftDiaryRequestDto.kt (1 hunks)
  • app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt (2 hunks)
  • app/src/main/java/com/sopt/clody/domain/repository/DiaryRepository.kt (2 hunks)
  • app/src/main/java/com/sopt/clody/domain/usecase/SaveDraftDiaryUseCase.kt (1 hunks)
  • app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt (9 hunks)
  • app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • app/src/main/java/com/sopt/clody/domain/usecase/SaveDraftDiaryUseCase.kt
  • app/src/main/java/com/sopt/clody/data/remote/dto/request/SaveDraftDiaryRequestDto.kt
  • app/src/main/java/com/sopt/clody/domain/repository/DiaryRepository.kt
  • app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt
  • app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt
🧰 Additional context used
🪛 detekt (1.23.8)
app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt

[warning] 206-206: String.format("%04d-%02d-%02d", year, month, day) uses implicitly default locale for string formatting.

(detekt.potential-bugs.ImplicitDefaultLocale)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build
🔇 Additional comments (5)
app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt (5)

26-27: Good dependency injection of use cases.

The use cases are properly injected following the established pattern in this ViewModel.


64-64: Effective change detection mechanism.

The initialEntries property provides a clean way to track changes for draft functionality.


178-180: Simple and effective change detection.

The method correctly compares current entries with initial state.


196-200: Use specific error message for draft fetch failures.

The AI summary mentions using ErrorMessages.FETCH_TEMP_DIARY_FAILED for fetch failures, but the code uses a generic error message. This inconsistency could lead to less user-friendly error messaging.

Verify if the specific error message constant exists and should be used:

#!/bin/bash
# Check if FETCH_TEMP_DIARY_FAILED constant exists in ErrorMessages
rg "FETCH_TEMP_DIARY_FAILED" --type kt

Likely an incorrect or invalid review comment.


218-225: Well-designed fallback mechanism.

The ensureDefaultEntry method provides a consistent way to reset to a valid default state when draft operations fail.

Comment on lines +204 to +216
fun saveDraftDiary(year: Int, month: Int, day: Int) {
viewModelScope.launch {
val date = String.format("%04d-%02d-%02d", year, month, day)
val result = saveDraftDiaryUseCase(date, _entries.toList())
result.onSuccess {
_failureMessage.value = ""
_showFailureDialog.value = false
}.onFailure { e ->
_failureMessage.value = e.localizedMessage ?: UNKNOWN_ERROR
_showFailureDialog.value = true
}
}
}
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 locale issue and add network check.

Two issues in the saveDraftDiary method:

  1. String formatting uses implicit default locale (flagged by static analysis)
  2. Missing network connectivity check for consistency with other operations

Apply this fix:

 fun saveDraftDiary(year: Int, month: Int, day: Int) {
     viewModelScope.launch {
+        if (!networkUtil.isNetworkAvailable()) {
+            _failureMessage.value = FAILURE_NETWORK_MESSAGE
+            _showFailureDialog.value = true
+            return@launch
+        }
+
-        val date = String.format("%04d-%02d-%02d", year, month, day)
+        val date = String.format(java.util.Locale.US, "%04d-%02d-%02d", year, month, day)
         val result = saveDraftDiaryUseCase(date, _entries.toList())
📝 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
fun saveDraftDiary(year: Int, month: Int, day: Int) {
viewModelScope.launch {
val date = String.format("%04d-%02d-%02d", year, month, day)
val result = saveDraftDiaryUseCase(date, _entries.toList())
result.onSuccess {
_failureMessage.value = ""
_showFailureDialog.value = false
}.onFailure { e ->
_failureMessage.value = e.localizedMessage ?: UNKNOWN_ERROR
_showFailureDialog.value = true
}
}
}
fun saveDraftDiary(year: Int, month: Int, day: Int) {
viewModelScope.launch {
if (!networkUtil.isNetworkAvailable()) {
_failureMessage.value = FAILURE_NETWORK_MESSAGE
_showFailureDialog.value = true
return@launch
}
val date = String.format(java.util.Locale.US, "%04d-%02d-%02d", year, month, day)
val result = saveDraftDiaryUseCase(date, _entries.toList())
result.onSuccess {
_failureMessage.value = ""
_showFailureDialog.value = false
}.onFailure { e ->
_failureMessage.value = e.localizedMessage ?: UNKNOWN_ERROR
_showFailureDialog.value = true
}
}
}
🧰 Tools
🪛 detekt (1.23.8)

[warning] 206-206: String.format("%04d-%02d-%02d", year, month, day) uses implicitly default locale for string formatting.

(detekt.potential-bugs.ImplicitDefaultLocale)

🤖 Prompt for AI Agents
In
app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt
around lines 204 to 216, fix the locale issue by specifying Locale.US explicitly
in the String.format call to avoid locale-dependent formatting. Additionally,
add a network connectivity check before proceeding with saving the draft diary
to ensure consistency with other operations; if there is no network, handle it
appropriately (e.g., show an error or skip the save).

Comment on lines +182 to +202
fun fetchDraftDiary(year: Int, month: Int, day: Int) {
viewModelScope.launch {
_entries.clear()
_showWarnings.clear()

val result = fetchDraftDiaryUseCase(year, month, day)
result.onSuccess { response ->
val drafts = response.draftDiaries.ifEmpty { listOf("") }
_entries.addAll(drafts)
initialEntries = drafts.toList()

_showWarnings.addAll(List(_entries.size) { false })
checkLimitMessage()
checkEmptyFieldsMessage()
}.onFailure {
ensureDefaultEntry()
_failureMessage.value = it.localizedMessage ?: UNKNOWN_ERROR
_showFailureDialog.value = true
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add network availability check for consistency.

The fetchDraftDiary method lacks network connectivity validation that's present in writeDiary. This inconsistency could lead to poor UX when offline.

Add network check at the beginning:

 fun fetchDraftDiary(year: Int, month: Int, day: Int) {
     viewModelScope.launch {
+        if (!networkUtil.isNetworkAvailable()) {
+            ensureDefaultEntry()
+            _failureMessage.value = FAILURE_NETWORK_MESSAGE
+            _showFailureDialog.value = true
+            return@launch
+        }
+
         _entries.clear()
         _showWarnings.clear()
📝 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
fun fetchDraftDiary(year: Int, month: Int, day: Int) {
viewModelScope.launch {
_entries.clear()
_showWarnings.clear()
val result = fetchDraftDiaryUseCase(year, month, day)
result.onSuccess { response ->
val drafts = response.draftDiaries.ifEmpty { listOf("") }
_entries.addAll(drafts)
initialEntries = drafts.toList()
_showWarnings.addAll(List(_entries.size) { false })
checkLimitMessage()
checkEmptyFieldsMessage()
}.onFailure {
ensureDefaultEntry()
_failureMessage.value = it.localizedMessage ?: UNKNOWN_ERROR
_showFailureDialog.value = true
}
}
}
fun fetchDraftDiary(year: Int, month: Int, day: Int) {
viewModelScope.launch {
if (!networkUtil.isNetworkAvailable()) {
ensureDefaultEntry()
_failureMessage.value = FAILURE_NETWORK_MESSAGE
_showFailureDialog.value = true
return@launch
}
_entries.clear()
_showWarnings.clear()
val result = fetchDraftDiaryUseCase(year, month, day)
result.onSuccess { response ->
val drafts = response.draftDiaries.ifEmpty { listOf("") }
_entries.addAll(drafts)
initialEntries = drafts.toList()
_showWarnings.addAll(List(_entries.size) { false })
checkLimitMessage()
checkEmptyFieldsMessage()
}.onFailure {
ensureDefaultEntry()
_failureMessage.value = it.localizedMessage ?: UNKNOWN_ERROR
_showFailureDialog.value = true
}
}
}
🤖 Prompt for AI Agents
In
app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt
between lines 182 and 202, the fetchDraftDiary function lacks a network
availability check, unlike the writeDiary method. To fix this, add a network
connectivity validation at the start of fetchDraftDiary to ensure the device is
online before proceeding. If the network is unavailable, handle it gracefully by
showing an appropriate message or dialog and avoid making the
fetchDraftDiaryUseCase call.

Copy link
Copy Markdown
Contributor

@SYAAINN SYAAINN left a comment

Choose a reason for hiding this comment

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

수고하셨습니당

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.

[p3]
createdAt 정보가 쓰이는 곳이 없는 걸로 파악이 되는데, 일단 서버가 이 값을 내려주는 이유도 궁금하고 굳이 Domain Layer에도 만들어야하나? 라는 생각이 듭니다😂

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.

저 생성시간을 홈 분기에서 쓰는 걸로 처음에 이해해서 만들어놨었는데 지금 보니 그냥 any?로 처리해도 될거 같네요! 제거 하겠습니다잉

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.

[p3]
CreatedDraftDiaryInfo 와 비슷한 리뷰인데요, Domain 레이어의 model을 선언해야하는 것의 기준은 어떤 것일까요?

  1. Clean Architecture를 위해 Dto를 Mapping하여 Presentation Layer에서 의존성을 지킬 수 있도록 하는 것.
  2. 비즈니스 로직에 걸맞는 꼭 필요한 Model일 것.

1번만큼 중요한게 2번 기준이라고 개인적으로 생각합니다. 그래서 꼭 필요한 Model인가? 비즈니스적인 성격이 잘 드러나는가? 변경 가능성이 작은가? 등을 항상 고려하려고 하는데 지금처럼 간단한 List 타입 하나만 들어있으며, 쓰임새도 확실한(?) 것 같은 경우에는 그냥 별도의 모델 제작하지 않고 List 가 아닌 List으로 사용하는 것은 어떻게 생각하세요?

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.

도메인 관점에서 "임시 저장된 일기 모음"이라는 개념을 갖고 있다고 생각해서 별도 모델로 분리를 했습니다...
그리고 DraftDiaryContents.draftDiaries 같은 명시적 구조가 더 직관적일 수 있다고 생각합니다?
선생님께서 말씀하신 List로 하면 Presnetation layer에서 의미를 유추하는 구조?로 될것 같은데
혹시 이걸 의도 하신 설계를 생각하신 건지 궁금합니데이~

Comment on lines 94 to 100
}.fold(
onSuccess = { Result.success(it) },
onFailure = {
Result.failure(Exception(ErrorMessages.FETCH_TEMP_DIARY_FAILED))
},
)

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.

[p3]
fold 에 대한 처리는 viewModel에서 항상 진행했었는데 이제 옮기는건가요?

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.

이전 프로젝트에서 Repository단에서 처리해서 자연스럽게 여기에 놨네요..
뷰모델로 이동시키겠습니다!
지금 보니 어떤건 뷰모델 어떤건 리포지토리에 있는걸 보아 나중에 대규모 공사 한번 들어가야될 것 같습니다ㅠ

@MoonsuKang MoonsuKang merged commit f27d114 into develop Jun 7, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ FEAT 기능 개발 🌊 문수 문수

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 임시저장 API 연결을 진행합니다.

2 participants