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
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ dependencies {
implementation(projects.feature.login)
implementation(projects.feature.onboarding)
implementation(projects.feature.setting)
implementation(projects.feature.carddelete)
implementation(projects.feature.webview)

implementation(libs.compose.material.icons.core)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import com.gamss.android.core.designsystem.component.GamssPaperBackground
import com.gamss.android.core.designsystem.theme.GamssTheme
import com.gamss.android.feature.calendar.CalendarScreen
import com.gamss.android.feature.calendar.navigation.CalendarKey
import com.gamss.android.feature.carddelete.CardDeleteScreen
import com.gamss.android.feature.carddelete.navigation.CardDeleteKey
import com.gamss.android.feature.chat.ChatRoomScreen
import com.gamss.android.feature.chat.ChattingListScreen
import com.gamss.android.feature.chat.navigation.ChatKey
Expand Down Expand Up @@ -126,6 +128,12 @@ private fun mainEntryProvider(navigator: Navigator) = entryProvider {
onPrivacyPolicyClick = { navigator.navigate(WebViewKey(GamssWebPage.PrivacyPolicy)) },
)
}
entry<CardDeleteKey>(metadata = detailSlideTransition) {
CardDeleteScreen(
onBackClick = navigator::goBack,
onDeleteComplete = navigator::finishCurrentFlow,
)
}
entry<AccountInfoKey>(metadata = detailSlideTransition) {
AccountInfoScreen(
onBackClick = navigator::goBack,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ class Navigator(val state: NavigationState) {
}
}

/** 완료된 상세 흐름을 닫고 현재 탭의 첫 화면으로 돌아간다. */
fun finishCurrentFlow() {
clearSubStack()
}

/**
* 현재 탭의 상세 화면으로 이동한다.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import com.gamss.android.core.designsystem.theme.GamssTheme
* 버튼 너비는 [modifier]를 통해 화면에 맞게 조절할 수 있으며, 콘텐츠 주변에는 디자인 가이드의
* 최소 여백 16dp가 항상 적용됩니다. [enabled]가 `false`이면 [variant]와 관계없이 비활성 색상이
* 적용되고 클릭 이벤트도 전달되지 않습니다.
* [isProcessing]이 `true`이면 현재 색상은 유지하면서 중복 클릭만 막습니다.
*
* 누를 때 ripple을 그리지 않습니다. 디자인에 눌림 표현이 없어, 기본 indication을 두면 색이
* 겹쳐 보이는 잔상이 생깁니다.
Expand All @@ -38,15 +39,17 @@ fun GamssButton(
modifier: Modifier = Modifier,
variant: GamssButtonVariant = GamssButtonVariant.Primary,
enabled: Boolean = true,
isProcessing: Boolean = false,
) {
val colors = gamssButtonColors(variant = variant, enabled = enabled)
val isClickable = enabled && !isProcessing

Box(
modifier = modifier
.clip(RoundedCornerShape(GamssTheme.radius.radius200))
.background(colors.containerColor)
.clickable(
enabled = enabled,
enabled = isClickable,
role = Role.Button,
indication = null,
interactionSource = null,
Expand Down Expand Up @@ -84,6 +87,11 @@ private fun gamssButtonColors(
contentColor = GamssTheme.colors.gray025,
)

GamssButtonVariant.PrimaryDark -> GamssButtonColors(
containerColor = GamssTheme.colors.gray900,
contentColor = GamssTheme.colors.gray025,
)

GamssButtonVariant.Secondary -> GamssButtonColors(
containerColor = GamssTheme.colors.gray100,
contentColor = GamssTheme.colors.gray600,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.gamss.android.core.designsystem.button

enum class GamssButtonVariant {
Primary,
PrimaryDark,
Secondary,
Destructive,
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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.CardDeleteResponse
import com.gamss.android.data.remote.card.model.response.CardResponse
import com.gamss.android.data.remote.model.response.ApiResponse
import retrofit2.http.Body
Expand All @@ -19,7 +20,15 @@ internal interface CardService {
@POST("/api/cards")
suspend fun createCard(@Body request: CreateCardRequest): ApiResponse<CardResponse>

/** 모든 카드와 카드가 나온 채팅방을 함께 삭제한다. */
@DELETE("/api/cards")
suspend fun deleteAllCards(): ApiResponse<CardDeleteResponse>

/** 카드 한 장과 카드가 나온 채팅방을 함께 삭제한다. envelope 의 success 만 보고 data 는 쓰지 않는다. */
@DELETE("/api/cards/{cardId}")
suspend fun deleteCard(@Path("cardId") cardId: Long): ApiResponse<Unit>

/** 해당 감정인 카드와 카드가 나온 채팅방을 모두 함께 삭제한다. */
@DELETE("/api/cards/emotions/{emotion}")
suspend fun deleteCardsByEmotion(@Path("emotion") emotion: String): ApiResponse<CardDeleteResponse>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.gamss.android.data.remote.card.model.response

import kotlinx.serialization.Serializable

@Serializable
internal data class CardDeleteResponse(
val deletedCount: Int? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ internal class CardRepositoryImpl @Inject constructor(
}
}

override suspend fun deleteAllCards(): AppResult<Unit> = runCatchingApiCall {
val response = cardService.deleteAllCards()
response.throwIfFailed()
checkNotNull(response.data?.deletedCount) { "No deleted card count" }
Unit
}

override suspend fun deleteCard(cardId: Long): AppResult<Unit> {
val result = runCatchingApiCall {
cardService.deleteCard(cardId).throwIfFailed()
Expand All @@ -71,4 +78,11 @@ internal class CardRepositoryImpl @Inject constructor(
}
}
}

override suspend fun deleteCardsByEmotion(character: EmotionCharacter): AppResult<Unit> = runCatchingApiCall {
val response = cardService.deleteCardsByEmotion(character.toServerEmotionType())
response.throwIfFailed()
checkNotNull(response.data?.deletedCount) { "No deleted card count" }
Unit
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ 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.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.emotion.EmotionCharacter
import io.mockk.coEvery
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test

class CardRepositoryImplTest {
Expand All @@ -17,7 +21,53 @@ class CardRepositoryImplTest {
private val repository = CardRepositoryImpl(cardService)

@Test
fun `카드 삭제 요청을 전달한다`() = runTest {
fun `모든 카드 삭제 요청을 전달한다`() = runTest {
coEvery { cardService.deleteAllCards() } returns ApiResponse(
success = true,
data = CardDeleteResponse(deletedCount = 0),
)

assertEquals(AppResult.Success(Unit), repository.deleteAllCards())
}

@Test
fun `200이어도 실패 envelope면 실패로 전달한다`() = runTest {
coEvery { cardService.deleteAllCards() } returns ApiResponse<CardDeleteResponse>(
success = false,
error = ApiError(code = "CARD_DELETE_FAILED", message = "삭제 실패"),
)

val result = repository.deleteAllCards()

assertEquals(
"CARD_DELETE_FAILED",
((result as AppResult.Failure).throwable as ApiException).code,
)
}

@Test
fun `실패 envelope의 인증 만료는 세션 만료로 전달한다`() = runTest {
coEvery { cardService.deleteAllCards() } returns ApiResponse<CardDeleteResponse>(
success = false,
error = ApiError(code = "EXPIRED_TOKEN", message = "만료"),
)

val result = repository.deleteAllCards()

assertTrue((result as AppResult.Failure).throwable is SessionExpiredException)
}

@Test
fun `성공 응답에 삭제 수가 없으면 실패로 전달한다`() = runTest {
coEvery { cardService.deleteAllCards() } returns ApiResponse(success = true)

val result = repository.deleteAllCards()

assertTrue(result is AppResult.Failure)
}

@Test
fun `카드 한 장 삭제 요청을 전달한다`() = runTest {
coEvery { cardService.deleteCard(1L) } returns ApiResponse(success = true, data = Unit)

assertEquals(AppResult.Success(Unit), repository.deleteCard(1L))
Expand Down Expand Up @@ -47,4 +97,38 @@ class CardRepositoryImplTest {
((result as AppResult.Failure).throwable as ApiException).code,
)
}

@Test
fun `감정별 카드 삭제 요청을 전달한다`() = runTest {
coEvery { cardService.deleteCardsByEmotion("ANGER") } returns ApiResponse(
success = true,
data = CardDeleteResponse(deletedCount = 0),
)

assertEquals(AppResult.Success(Unit), repository.deleteCardsByEmotion(EmotionCharacter.ANGER))
}

@Test
fun `감정별 삭제 성공 응답에 삭제 수가 없으면 실패로 전달한다`() = runTest {
coEvery { cardService.deleteCardsByEmotion("ANGER") } returns ApiResponse(success = true)

val result = repository.deleteCardsByEmotion(EmotionCharacter.ANGER)

assertTrue(result is AppResult.Failure)
}

@Test
fun `감정별 삭제 실패 envelope는 실패로 전달한다`() = runTest {
coEvery { cardService.deleteCardsByEmotion("ANGER") } returns ApiResponse<CardDeleteResponse>(
success = false,
error = ApiError(code = "INVALID_INPUT", message = "지원하지 않는 감정"),
)

val result = repository.deleteCardsByEmotion(EmotionCharacter.ANGER)

assertEquals(
"INVALID_INPUT",
((result as AppResult.Failure).throwable as ApiException).code,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ interface CardRepository {
summary: String,
): AppResult<Card>

/** 모든 카드와 카드가 나온 채팅방을 함께 삭제한다. 되돌릴 수 없다. */
suspend fun deleteAllCards(): AppResult<Unit>

/** 카드 한 장과 카드가 나온 채팅방을 함께 삭제한다. 되돌릴 수 없다. */
suspend fun deleteCard(cardId: Long): AppResult<Unit>

/** 해당 감정인 카드와 카드가 나온 채팅방을 모두 함께 삭제한다. 되돌릴 수 없다. */
suspend fun deleteCardsByEmotion(character: EmotionCharacter): AppResult<Unit>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.gamss.android.domain.card

import com.gamss.android.core.common.AppResult
import com.gamss.android.domain.usecase.NoParamUseCase
import javax.inject.Inject

class DeleteAllCardsUseCase @Inject constructor(
private val cardRepository: CardRepository,
) : NoParamUseCase<AppResult<Unit>> {
override suspend fun invoke(): AppResult<Unit> = cardRepository.deleteAllCards()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.gamss.android.domain.card

import com.gamss.android.core.common.AppResult
import com.gamss.android.domain.emotion.EmotionCharacter
import com.gamss.android.domain.usecase.UseCase
import javax.inject.Inject

class DeleteCardsByEmotionUseCase @Inject constructor(
private val cardRepository: CardRepository,
) : UseCase<EmotionCharacter, AppResult<Unit>> {
override suspend fun invoke(params: EmotionCharacter): AppResult<Unit> =
cardRepository.deleteCardsByEmotion(params)
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ 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
@@ -0,0 +1,43 @@
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 {
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
fun 모든_카드_삭제를_리포지토리에_위임한다() = runBlocking {
val repository = RecordingRepository()

val result = DeleteAllCardsUseCase(repository)()

assertEquals(true, repository.deleteAllCalled)
assertEquals(AppResult.Success(Unit), result)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,15 @@ class DeleteCardUseCaseTest {
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
Loading
Loading