diff --git a/feature/home/presentation/src/test/kotlin/com/afternote/feature/home/presentation/HomeTabViewModelTest.kt b/feature/home/presentation/src/test/kotlin/com/afternote/feature/home/presentation/HomeTabViewModelTest.kt index 8a0953f04f..5b48241f43 100644 --- a/feature/home/presentation/src/test/kotlin/com/afternote/feature/home/presentation/HomeTabViewModelTest.kt +++ b/feature/home/presentation/src/test/kotlin/com/afternote/feature/home/presentation/HomeTabViewModelTest.kt @@ -461,12 +461,12 @@ private class FakeDailyQuestionRepository : DailyQuestionRepository { draftOnly: Boolean?, ): Result> = unexpected("DailyQuestionRepository.getList") - override suspend fun create(payload: DailyQuestionCreatePayload): Result = unexpected("DailyQuestionRepository.create") + override suspend fun create(payload: DailyQuestionCreatePayload): Result = unexpected("DailyQuestionRepository.create") override suspend fun update( id: Long, payload: DailyQuestionUpdatePayload, - ): Result = unexpected("DailyQuestionRepository.update") + ): Result = unexpected("DailyQuestionRepository.update") override suspend fun delete(id: Long): Result = unexpected("DailyQuestionRepository.delete") } diff --git a/feature/mindrecord/data/src/main/kotlin/com/afternote/feature/mindrecord/data/api/DailyQuestionApiService.kt b/feature/mindrecord/data/src/main/kotlin/com/afternote/feature/mindrecord/data/api/DailyQuestionApiService.kt index ad9b549a48..a2ac76ea39 100644 --- a/feature/mindrecord/data/src/main/kotlin/com/afternote/feature/mindrecord/data/api/DailyQuestionApiService.kt +++ b/feature/mindrecord/data/src/main/kotlin/com/afternote/feature/mindrecord/data/api/DailyQuestionApiService.kt @@ -1,6 +1,7 @@ package com.afternote.feature.mindrecord.data.api import com.afternote.core.network.model.BaseResponse +import com.afternote.feature.mindrecord.data.dto.DailyQuestionAnswerResponseDto import com.afternote.feature.mindrecord.data.dto.DailyQuestionCreateRequestDto import com.afternote.feature.mindrecord.data.dto.DailyQuestionListItemDto import com.afternote.feature.mindrecord.data.dto.DailyQuestionUpdateRequestDto @@ -27,13 +28,13 @@ interface DailyQuestionApiService { @POST("daily-questions") suspend fun createDailyQuestion( @Body request: DailyQuestionCreateRequestDto, - ): BaseResponse + ): BaseResponse @PATCH("daily-questions/{userDailyQuestionId}") suspend fun updateDailyQuestion( @Path("userDailyQuestionId") userDailyQuestionId: Long, @Body request: DailyQuestionUpdateRequestDto, - ): BaseResponse + ): BaseResponse @DELETE("daily-questions/{userDailyQuestionId}") suspend fun deleteDailyQuestion( diff --git a/feature/mindrecord/data/src/main/kotlin/com/afternote/feature/mindrecord/data/dto/DailyQuestionDto.kt b/feature/mindrecord/data/src/main/kotlin/com/afternote/feature/mindrecord/data/dto/DailyQuestionDto.kt index 846982506d..b379db34d8 100644 --- a/feature/mindrecord/data/src/main/kotlin/com/afternote/feature/mindrecord/data/dto/DailyQuestionDto.kt +++ b/feature/mindrecord/data/src/main/kotlin/com/afternote/feature/mindrecord/data/dto/DailyQuestionDto.kt @@ -77,3 +77,19 @@ data class TodayDailyQuestionDto( @JsonNames("draft") val isDraft: Boolean, ) + +/** + * 생성·수정 응답 (`DailyQuestionAnswerResponse` 실측, 2026-08-23). + * + * 종전에는 `BaseResponse` 으로 받아 방금 만든 답변의 식별자를 버렸다. 그러면 저장 + * 직후 그 레코드를 가리키려고 목록을 다시 조회해 추측으로 찾아야 한다 (#573). + * + * `userDailyQuestionId`("내 답변")와 요청의 `questionId`("질문")는 의미가 다르다. + * 관찰된 값이 같더라도 섞어 쓰면 안 된다. + */ +@Serializable +data class DailyQuestionAnswerResponseDto( + @SerialName("userDailyQuestionId") val userDailyQuestionId: Long, + @SerialName("content") val content: String = "", + @SerialName("isDraft") val isDraft: Boolean = false, +) diff --git a/feature/mindrecord/data/src/main/kotlin/com/afternote/feature/mindrecord/data/repositoryimpl/DailyQuestionRepositoryImpl.kt b/feature/mindrecord/data/src/main/kotlin/com/afternote/feature/mindrecord/data/repositoryimpl/DailyQuestionRepositoryImpl.kt index 99cf6cf2ba..b0bee02a84 100644 --- a/feature/mindrecord/data/src/main/kotlin/com/afternote/feature/mindrecord/data/repositoryimpl/DailyQuestionRepositoryImpl.kt +++ b/feature/mindrecord/data/src/main/kotlin/com/afternote/feature/mindrecord/data/repositoryimpl/DailyQuestionRepositoryImpl.kt @@ -34,19 +34,23 @@ class DailyQuestionRepositoryImpl api.getTodayDailyQuestion().requireData().toDomain() } - override suspend fun create(payload: DailyQuestionCreatePayload): Result = + override suspend fun create(payload: DailyQuestionCreatePayload): Result = runCatchingCancellable { - api.createDailyQuestion(payload.toRequest()).requireStatus() + api + .createDailyQuestion(payload.toRequest()) + .requireData() + .userDailyQuestionId } override suspend fun update( id: Long, payload: DailyQuestionUpdatePayload, - ): Result = + ): Result = runCatchingCancellable { api .updateDailyQuestion(userDailyQuestionId = id, request = payload.toRequest()) - .requireStatus() + .requireData() + .userDailyQuestionId } override suspend fun delete(id: Long): Result = diff --git a/feature/mindrecord/data/src/test/java/com/afternote/feature/mindrecord/data/dto/DailyQuestionAnswerResponseContractTest.kt b/feature/mindrecord/data/src/test/java/com/afternote/feature/mindrecord/data/dto/DailyQuestionAnswerResponseContractTest.kt new file mode 100644 index 0000000000..7671fb49c5 --- /dev/null +++ b/feature/mindrecord/data/src/test/java/com/afternote/feature/mindrecord/data/dto/DailyQuestionAnswerResponseContractTest.kt @@ -0,0 +1,83 @@ +package com.afternote.feature.mindrecord.data.dto + +import com.afternote.core.network.model.BaseResponse +import kotlinx.serialization.json.Json +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * 생성·수정 응답의 `userDailyQuestionId` 계약을 고정한다 (#573). + * + * 종전에는 `BaseResponse` 으로 받아 이 값을 버렸다. 그러면 저장 직후 그 레코드를 + * 가리키려고 목록을 다시 조회해 추측으로 골라야 한다. + * + * 아래 본문은 2026-08-23 `afternote.kro.kr` 실측 응답 그대로다. + */ +class DailyQuestionAnswerResponseContractTest { + private val json = Json { ignoreUnknownKeys = true } + + @Test + fun `생성 응답에서 내 답변 식별자를 읽는다`() { + val body = + """ + { + "status": 200, "code": 200, "message": "성공", + "data": { "userDailyQuestionId": 19, "content": "응답 계약 실측", + "isDraft": true, "receivers": [] } + } + """.trimIndent() + + val decoded = + json.decodeFromString( + BaseResponse.serializer(DailyQuestionAnswerResponseDto.serializer()), + body, + ) + + assertEquals(19L, decoded.data!!.userDailyQuestionId) + assertEquals("응답 계약 실측", decoded.data!!.content) + assertEquals(true, decoded.data!!.isDraft) + } + + @Test + fun `수정 응답도 같은 스키마다`() { + val body = + """ + { + "status": 200, "code": 200, "message": "성공", + "data": { "userDailyQuestionId": 19, "content": "응답 계약 실측 수정", + "isDraft": true, "receivers": [] } + } + """.trimIndent() + + val decoded = + json.decodeFromString( + BaseResponse.serializer(DailyQuestionAnswerResponseDto.serializer()), + body, + ) + + assertEquals(19L, decoded.data!!.userDailyQuestionId) + } + + @Test + fun `식별자가 없으면 실패로 드러난다`() { + // 기본값을 두지 않는다 — 이 값이 없으면 저장 직후 흐름이 성립하지 않으므로 + // 조용히 0 으로 메우지 않고 파싱에서 막는다. + val body = + """ + { + "status": 200, "code": 200, "message": "성공", + "data": { "content": "식별자 없음", "isDraft": true } + } + """.trimIndent() + + val failed = + runCatching { + json.decodeFromString( + BaseResponse.serializer(DailyQuestionAnswerResponseDto.serializer()), + body, + ) + }.isFailure + + assertEquals(true, failed) + } +} diff --git a/feature/mindrecord/data/src/test/kotlin/com/afternote/feature/mindrecord/data/repositoryimpl/MindRecordCancellationTest.kt b/feature/mindrecord/data/src/test/kotlin/com/afternote/feature/mindrecord/data/repositoryimpl/MindRecordCancellationTest.kt index a390f74a77..bae7e3fe8e 100644 --- a/feature/mindrecord/data/src/test/kotlin/com/afternote/feature/mindrecord/data/repositoryimpl/MindRecordCancellationTest.kt +++ b/feature/mindrecord/data/src/test/kotlin/com/afternote/feature/mindrecord/data/repositoryimpl/MindRecordCancellationTest.kt @@ -3,6 +3,7 @@ package com.afternote.feature.mindrecord.data.repositoryimpl import com.afternote.core.network.model.BaseResponse import com.afternote.feature.mindrecord.data.api.DailyQuestionApiService import com.afternote.feature.mindrecord.data.api.DiaryApiService +import com.afternote.feature.mindrecord.data.dto.DailyQuestionAnswerResponseDto import com.afternote.feature.mindrecord.data.dto.DailyQuestionCreateRequestDto import com.afternote.feature.mindrecord.data.dto.DailyQuestionListItemDto import com.afternote.feature.mindrecord.data.dto.DailyQuestionUpdateRequestDto @@ -88,12 +89,13 @@ private class NeverReturningDailyQuestionApi : DailyQuestionApiService { override suspend fun getTodayDailyQuestion(): BaseResponse = awaitCancellation() - override suspend fun createDailyQuestion(request: DailyQuestionCreateRequestDto): BaseResponse = awaitCancellation() + override suspend fun createDailyQuestion(request: DailyQuestionCreateRequestDto): BaseResponse = + awaitCancellation() override suspend fun updateDailyQuestion( userDailyQuestionId: Long, request: DailyQuestionUpdateRequestDto, - ): BaseResponse = awaitCancellation() + ): BaseResponse = awaitCancellation() override suspend fun deleteDailyQuestion(userDailyQuestionId: Long): BaseResponse = awaitCancellation() } diff --git a/feature/mindrecord/domain/src/main/kotlin/com/afternote/feature/mindrecord/domain/repository/DailyQuestionRepository.kt b/feature/mindrecord/domain/src/main/kotlin/com/afternote/feature/mindrecord/domain/repository/DailyQuestionRepository.kt index 51671c88d5..b001a7a158 100644 --- a/feature/mindrecord/domain/src/main/kotlin/com/afternote/feature/mindrecord/domain/repository/DailyQuestionRepository.kt +++ b/feature/mindrecord/domain/src/main/kotlin/com/afternote/feature/mindrecord/domain/repository/DailyQuestionRepository.kt @@ -19,12 +19,19 @@ interface DailyQuestionRepository { suspend fun getToday(): Result - suspend fun create(payload: DailyQuestionCreatePayload): Result + /** + * 답변 생성. + * + * 서버가 돌려주는 `userDailyQuestionId`("내 답변" 식별자)를 그대로 올린다 — 저장 직후 + * 그 레코드를 가리켜야 하는 흐름(이어쓰기·수정·삭제)이 목록을 다시 뒤지지 않게 한다 (#573). + */ + suspend fun create(payload: DailyQuestionCreatePayload): Result + /** 답변 수정. 생성과 같은 스키마로 응답하므로 식별자를 그대로 올린다 (#573). */ suspend fun update( id: Long, payload: DailyQuestionUpdatePayload, - ): Result + ): Result suspend fun delete(id: Long): Result } diff --git a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DailyQuestionWriteViewModel.kt b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DailyQuestionWriteViewModel.kt index 4587c894f1..f893d48f9d 100644 --- a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DailyQuestionWriteViewModel.kt +++ b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DailyQuestionWriteViewModel.kt @@ -173,8 +173,17 @@ class DailyQuestionWriteViewModel ) } result - .onSuccess { - _uiState.update { it.copy(submitState = SubmitState.Succeeded) } + .onSuccess { savedId -> + _uiState.update { + it.copy( + submitState = SubmitState.Succeeded, + // 서버가 돌려준 "내 답변" 식별자를 그대로 든다 (#573). + // 임시저장 뒤 이어서 저장하면 목록을 다시 뒤지지 않고 이 값으로 PATCH 한다 — + // 종전에는 응답을 버려 `resumeDraft()` 가 목록을 재조회해 첫 draft 를 + // 추측으로 골랐다. + draftId = if (isDraft) savedId else null, + ) + } // 임시저장이 하나 늘었으니 툴바 숫자도 따라가야 한다 (#769). if (isDraft) loadDraftCount() }.onFailure { e -> diff --git a/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DailyQuestionWriteViewModelTest.kt b/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DailyQuestionWriteViewModelTest.kt index eae73c3e34..c77902d7ad 100644 --- a/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DailyQuestionWriteViewModelTest.kt +++ b/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DailyQuestionWriteViewModelTest.kt @@ -181,15 +181,16 @@ private class FakeDailyQuestionRepository( override suspend fun getToday(): Result = onGetToday() - override suspend fun create(payload: DailyQuestionCreatePayload): Result { + override suspend fun create(payload: DailyQuestionCreatePayload): Result { createCallCount += 1 - return Result.success(Unit) + // 서버가 돌려주는 "내 답변" 식별자 (#573). + return Result.success(CREATED_ANSWER_ID) } override suspend fun update( id: Long, payload: DailyQuestionUpdatePayload, - ): Result = error("update 는 이 시나리오에서 호출되면 안 됨") + ): Result = error("update 는 이 시나리오에서 호출되면 안 됨") override suspend fun delete(id: Long): Result = error("delete 는 이 시나리오에서 호출되면 안 됨") } @@ -201,6 +202,8 @@ private object NoopPhotoUploadRepository : PhotoUploadRepository { ): Result = error("upload 는 이 시나리오에서 호출되면 안 됨") } +private const val CREATED_ANSWER_ID = 19L + /** 툴바 카운트는 이 테스트의 관심사가 아니다 — 0건으로 고정한다 (#769). */ private fun noopDraftLoader() = MindRecordDraftLoader( @@ -232,12 +235,12 @@ private object EmptyDraftDailyQuestionRepository : DailyQuestionRepository { override suspend fun getToday(): Result = error("호출되면 안 됨") - override suspend fun create(payload: DailyQuestionCreatePayload): Result = error("호출되면 안 됨") + override suspend fun create(payload: DailyQuestionCreatePayload): Result = error("호출되면 안 됨") override suspend fun update( id: Long, payload: DailyQuestionUpdatePayload, - ): Result = error("호출되면 안 됨") + ): Result = error("호출되면 안 됨") override suspend fun delete(id: Long): Result = error("호출되면 안 됨") } diff --git a/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DraftListDeleteTest.kt b/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DraftListDeleteTest.kt index ee21fee940..7f66c2bb0e 100644 --- a/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DraftListDeleteTest.kt +++ b/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DraftListDeleteTest.kt @@ -136,12 +136,12 @@ private class FakeDailyQuestionDraftRepository( override suspend fun getToday(): Result = error("호출되면 안 됨") - override suspend fun create(payload: DailyQuestionCreatePayload): Result = error("호출되면 안 됨") + override suspend fun create(payload: DailyQuestionCreatePayload): Result = error("호출되면 안 됨") override suspend fun update( id: Long, payload: DailyQuestionUpdatePayload, - ): Result = error("호출되면 안 됨") + ): Result = error("호출되면 안 됨") override suspend fun delete(id: Long): Result { if (removesOnDelete) remaining.removeAll { it.dailyQuestionId == id }