Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.gamss.android.data.remote.card

import com.gamss.android.data.remote.card.model.request.CreateCardRequest
import com.gamss.android.data.remote.card.model.response.CardCalendarResponse
import com.gamss.android.data.remote.card.model.response.CardDeleteResponse
import com.gamss.android.data.remote.card.model.response.CardResponse
import com.gamss.android.data.remote.model.response.ApiResponse
Expand All @@ -16,6 +17,12 @@ internal interface CardService {
@GET("/api/cards")
suspend fun getCardsByDate(@Query("date") date: String): ApiResponse<List<CardResponse>>

/** 그 달(KST)의 날짜별 대표 감정 목록만 준다. */
@GET("/api/cards/monthly")
suspend fun getCardsByMonth(
@Query("yearMonth") yearMonth: String,
): ApiResponse<List<CardCalendarResponse>>

/** 종료된 채팅방에만 만들 수 있다. 대사는 서버가 생성한다. */
@POST("/api/cards")
suspend fun createCard(@Body request: CreateCardRequest): ApiResponse<CardResponse>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.gamss.android.data.remote.card.model.response

import com.gamss.android.data.remote.emotion.toEmotionCharacter
import com.gamss.android.domain.card.CardEntry
import kotlinx.serialization.Serializable

@Serializable
internal data class CardCalendarResponse(
val date: String,
/** 카드 생성순이며 같은 감정이 여러 번 올 수 있다. */
val emotions: List<String> = emptyList(),
)

internal fun CardCalendarResponse.toDomain(): List<CardEntry> {
val createdDate = date.toLocalDateOrNull() ?: return emptyList()
return emotions.mapNotNull { it.toEmotionCharacter() }
.mapIndexed { index, character ->
CardEntry(date = createdDate, indexInDate = index, character = character)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ internal fun CardResponse.toDomainOrNull(): Card? = emotion.toEmotionCharacter()
}
}

private fun String.toLocalDateOrNull(): LocalDate? = try {
/** 날짜별·월별 응답이 같은 기준으로 날짜를 버려야 CardEntry.indexInDate 가 두 응답에서 같은 카드를 가리킨다. */
internal fun String.toLocalDateOrNull(): LocalDate? = try {
LocalDate.parse(this)
} catch (_: DateTimeParseException) {
null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import com.gamss.android.data.remote.emotion.toServerEmotionType
import com.gamss.android.data.remote.runCatchingApiCall
import com.gamss.android.data.remote.throwIfFailed
import com.gamss.android.domain.card.Card
import com.gamss.android.domain.card.CardEntry
import com.gamss.android.domain.card.CardNotRetryableException
import com.gamss.android.domain.card.CardRepository
import com.gamss.android.domain.emotion.EmotionCharacter
import java.time.LocalDate
import java.time.YearMonth
import javax.inject.Inject
import javax.inject.Singleton

Expand All @@ -27,6 +29,14 @@ internal class CardRepositoryImpl @Inject constructor(
checkNotNull(response.data) { "No available card data" }.mapNotNull { it.toDomainOrNull() }
}

// YearMonth.toString() 이 서버가 요구하는 yyyy-MM 그대로다.
override suspend fun getCardsByMonth(yearMonth: YearMonth): AppResult<List<CardEntry>> = runCatchingApiCall {
val response = cardService.getCardsByMonth(yearMonth.toString())
response.throwIfFailed()
checkNotNull(response.data) { "No available card data" }
.flatMap { it.toDomain() }
}

override suspend fun createCard(
conversationId: Long,
character: EmotionCharacter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,22 @@ package com.gamss.android.data.repository
import com.gamss.android.core.common.AppResult
import com.gamss.android.core.common.network.ApiException
import com.gamss.android.data.remote.card.CardService
import com.gamss.android.data.remote.card.model.response.CardCalendarResponse
import com.gamss.android.data.remote.card.model.response.CardDeleteResponse
import com.gamss.android.data.remote.model.response.ApiError
import com.gamss.android.data.remote.model.response.ApiResponse
import com.gamss.android.domain.auth.SessionExpiredException
import com.gamss.android.domain.card.CardEntry
import com.gamss.android.domain.emotion.EmotionCharacter
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import java.time.LocalDate
import java.time.YearMonth

class CardRepositoryImplTest {

Expand Down Expand Up @@ -131,4 +136,63 @@ class CardRepositoryImplTest {
((result as AppResult.Failure).throwable as ApiException).code,
)
}

@Test
fun `월을 API 형식으로 조회하고 하루치 감정 목록을 카드 한 건씩으로 펼친다`() = runTest {
coEvery { cardService.getCardsByMonth("2026-08") } returns ApiResponse(
success = true,
data = listOf(
CardCalendarResponse(date = "2026-08-15", emotions = listOf("ANGER", "JOY")),
CardCalendarResponse(date = "2026-08-16", emotions = listOf("GRUMPY")),
),
)

val result = repository.getCardsByMonth(YearMonth.of(2026, 8))

assertEquals(
listOf(
CardEntry(LocalDate.of(2026, 8, 15), 0, EmotionCharacter.ANGER),
CardEntry(LocalDate.of(2026, 8, 15), 1, EmotionCharacter.JOY),
CardEntry(LocalDate.of(2026, 8, 16), 0, EmotionCharacter.PRICKLY),
),
(result as AppResult.Success).data,
)
coVerify(exactly = 1) { cardService.getCardsByMonth("2026-08") }
}

/** 날짜별 조회도 알 수 없는 감정을 버리므로, 순번은 버린 뒤를 기준으로 세야 두 응답이 맞물린다. */
@Test
fun `날짜를 못 읽는 날은 그 날만 버리고 나머지 달은 살린다`() = runTest {
coEvery { cardService.getCardsByMonth(any()) } returns ApiResponse(
success = true,
data = listOf(
CardCalendarResponse(date = "2026-08-99", emotions = listOf("ANGER")),
CardCalendarResponse(date = "2026-08-16", emotions = listOf("JOY")),
),
)

val result = repository.getCardsByMonth(YearMonth.of(2026, 8))

assertEquals(
listOf(CardEntry(LocalDate.of(2026, 8, 16), 0, EmotionCharacter.JOY)),
(result as AppResult.Success).data,
)
}

@Test
fun `알 수 없는 감정을 버린 뒤를 기준으로 그날 순번을 센다`() = runTest {
coEvery { cardService.getCardsByMonth(any()) } returns ApiResponse(
success = true,
data = listOf(
CardCalendarResponse(date = "2026-08-15", emotions = listOf("UNKNOWN", "ANGER")),
),
)

val result = repository.getCardsByMonth(YearMonth.of(2026, 8))

assertEquals(
listOf(CardEntry(LocalDate.of(2026, 8, 15), 0, EmotionCharacter.ANGER)),
(result as AppResult.Success).data,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.gamss.android.domain.card

import com.gamss.android.domain.emotion.EmotionCharacter
import java.time.LocalDate

/** 월별 응답에는 카드 식별자가 없어 [date] 와 그날 순번 [indexInDate] 가 카드의 신원이다. */
data class CardEntry(
val date: LocalDate,
val indexInDate: Int,
val character: EmotionCharacter,
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ package com.gamss.android.domain.card
import com.gamss.android.core.common.AppResult
import com.gamss.android.domain.emotion.EmotionCharacter
import java.time.LocalDate
import java.time.YearMonth

interface CardRepository {

/** 대화 생성일(KST) 기준으로 해당 날짜의 카드를 가져온다. */
suspend fun getCardsByDate(date: LocalDate): AppResult<List<Card>>

/** 해당 달(KST)에 생성된 카드를 요약 없이 날짜·순번·감정만 가져온다. */
suspend fun getCardsByMonth(yearMonth: YearMonth): AppResult<List<CardEntry>>

/** 종료된 채팅방에만 만들 수 있고 방당 한 번만 성공한다. */
suspend fun createCard(
conversationId: Long,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.gamss.android.domain.card

import com.gamss.android.core.common.AppResult
import com.gamss.android.domain.usecase.UseCase
import java.time.YearMonth
import javax.inject.Inject

/** 선택한 달(KST)에 생성된 카드를 요약 없이 조회한다. */
class GetCardsByMonthUseCase @Inject constructor(
private val cardRepository: CardRepository,
) : UseCase<YearMonth, AppResult<List<CardEntry>>> {

override suspend fun invoke(params: YearMonth): AppResult<List<CardEntry>> =
cardRepository.getCardsByMonth(params)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import java.time.LocalDate

class CreateCardUseCaseTest {

private class RecordingRepository : CardRepository {
private class RecordingRepository : FakeCardRepository() {
var sentSummary: String? = null
private set

Expand All @@ -32,16 +32,6 @@ class CreateCardUseCaseTest {
),
)
}

override suspend fun getCardsByDate(date: LocalDate): AppResult<List<Card>> =
AppResult.Success(emptyList())

override suspend fun deleteAllCards(): AppResult<Unit> = error("사용하지 않음")

override suspend fun deleteCard(cardId: Long): AppResult<Unit> = error("사용하지 않음")

override suspend fun deleteCardsByEmotion(character: EmotionCharacter): AppResult<Unit> =
error("사용하지 않음")
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,19 @@
package com.gamss.android.domain.card

import com.gamss.android.core.common.AppResult
import com.gamss.android.domain.emotion.EmotionCharacter
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import java.time.LocalDate

class DeleteAllCardsUseCaseTest {

private class RecordingRepository : CardRepository {
private class RecordingRepository : FakeCardRepository() {
var deleteAllCalled = false

override suspend fun getCardsByDate(date: LocalDate): AppResult<List<Card>> = error("사용하지 않음")

override suspend fun createCard(
conversationId: Long,
character: EmotionCharacter,
summary: String,
): AppResult<Card> = error("사용하지 않음")

override suspend fun deleteAllCards(): AppResult<Unit> {
deleteAllCalled = true
return AppResult.Success(Unit)
}

override suspend fun deleteCard(cardId: Long): AppResult<Unit> = error("사용하지 않음")

override suspend fun deleteCardsByEmotion(character: EmotionCharacter): AppResult<Unit> =
error("사용하지 않음")
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,20 @@
package com.gamss.android.domain.card

import com.gamss.android.core.common.AppResult
import com.gamss.android.domain.emotion.EmotionCharacter
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import java.time.LocalDate

class DeleteCardUseCaseTest {

private class RecordingRepository : CardRepository {
private class RecordingRepository : FakeCardRepository() {
var deletedCardId: Long? = null
private set

override suspend fun getCardsByDate(date: LocalDate): AppResult<List<Card>> = error("사용하지 않음")

override suspend fun createCard(
conversationId: Long,
character: EmotionCharacter,
summary: String,
): AppResult<Card> = error("사용하지 않음")

override suspend fun deleteAllCards(): AppResult<Unit> = error("사용하지 않음")

override suspend fun deleteCard(cardId: Long): AppResult<Unit> {
deletedCardId = cardId
return AppResult.Success(Unit)
}

override suspend fun deleteCardsByEmotion(character: EmotionCharacter): AppResult<Unit> =
error("사용하지 않음")
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,12 @@ import com.gamss.android.domain.emotion.EmotionCharacter
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import java.time.LocalDate

class DeleteCardsByEmotionUseCaseTest {

private class RecordingRepository : CardRepository {
private class RecordingRepository : FakeCardRepository() {
var deletedCharacter: EmotionCharacter? = null

override suspend fun getCardsByDate(date: LocalDate): AppResult<List<Card>> = error("사용하지 않음")

override suspend fun createCard(
conversationId: Long,
character: EmotionCharacter,
summary: String,
): AppResult<Card> = error("사용하지 않음")

override suspend fun deleteAllCards(): AppResult<Unit> = error("사용하지 않음")

override suspend fun deleteCard(cardId: Long): AppResult<Unit> = error("사용하지 않음")

override suspend fun deleteCardsByEmotion(character: EmotionCharacter): AppResult<Unit> {
deletedCharacter = character
return AppResult.Success(Unit)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.gamss.android.domain.card

import com.gamss.android.core.common.AppResult
import com.gamss.android.domain.emotion.EmotionCharacter
import java.time.LocalDate
import java.time.YearMonth

/** 테스트가 실제로 쓰는 메서드만 override 한다. 나머지는 호출되면 바로 드러나야 한다. */
internal open class FakeCardRepository : CardRepository {

override suspend fun getCardsByDate(date: LocalDate): AppResult<List<Card>> =
error("이 테스트에서 쓰지 않는다")

override suspend fun getCardsByMonth(yearMonth: YearMonth): AppResult<List<CardEntry>> =
error("이 테스트에서 쓰지 않는다")

override suspend fun createCard(
conversationId: Long,
character: EmotionCharacter,
summary: String,
): AppResult<Card> = error("이 테스트에서 쓰지 않는다")

override suspend fun deleteAllCards(): AppResult<Unit> = error("이 테스트에서 쓰지 않는다")

override suspend fun deleteCard(cardId: Long): AppResult<Unit> = error("이 테스트에서 쓰지 않는다")

override suspend fun deleteCardsByEmotion(character: EmotionCharacter): AppResult<Unit> =
error("이 테스트에서 쓰지 않는다")
}
Loading
Loading