Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
b70c7b0
feat: 보관함 화면 추가 및 archive 모듈 정리
soyeonLee126 Aug 12, 2026
be1ff99
chore: feature/home 모듈의 미사용 worry_bin 중복 에셋 제거
soyeonLee126 Aug 15, 2026
c502347
feat: 보관함 화면을 Figma 디자인에 맞춰 재구성
soyeonLee126 Aug 15, 2026
c687447
fix: 리베이스로 남은 CalendarKey 참조 및 보관함 탭 가드 누락 수정
soyeonLee126 Aug 15, 2026
bb64566
refactor: CardRepository를 조회/쓰기 책임으로 분리하고 날짜별 카드 조회 API 추가
soyeonLee126 Aug 15, 2026
86c9ba4
feat: 보관함 상세 화면과 카드 물리 낙하 애니메이션 추가
soyeonLee126 Aug 15, 2026
4bcd542
feat: 메인 네비게이션에 보관함 상세 화면 연결
soyeonLee126 Aug 15, 2026
64c39b1
chore: 보관함 탭 원격설정 게이트 제거
soyeonLee126 Aug 15, 2026
b374cd3
fix: detekt 규칙 위반 수정
soyeonLee126 Aug 15, 2026
c3140d0
fix: 하위 화면 이동 시 탭바가 애니메이션 없이 사라지던 문제 수정
soyeonLee126 Aug 17, 2026
b090b11
fix: 보관함 그리드 클릭 리플 제거 및 세로 중앙 정렬
soyeonLee126 Aug 17, 2026
ad4bbee
fix: 보관함 그리드가 실제로 중앙 정렬되지 않던 문제 수정
soyeonLee126 Aug 17, 2026
a455740
fix: GamssTopBar가 상태바 영역과 겹쳐 그려지던 문제 수정
soyeonLee126 Aug 17, 2026
9f44a6f
refactor: 상태바 회피 처리를 MainScreen의 NavDisplay 한 곳으로 통합
soyeonLee126 Aug 17, 2026
4fa55c0
chore: 원격설정으로 보관함 탭을 가리던 경로 제거
soyeonLee126 Aug 17, 2026
78e9975
docs: UseChatEndFeature 주석에서 보관함 탭 관련 설명 제거
soyeonLee126 Aug 17, 2026
572f4d9
feat: 카드 월별 조회 API 추가
soyeonLee126 Aug 17, 2026
62bed2c
feat: 보관함 상세에 연·월 선택 바텀시트 추가
soyeonLee126 Aug 17, 2026
0e605bb
fix: 월 선택 시트 버튼이 3버튼 내비바에 붙던 문제 수정
soyeonLee126 Aug 17, 2026
4c3dca2
chore: 시트 닫기 아이콘 파일명을 ic_close_line 으로 변경
soyeonLee126 Aug 17, 2026
ee7b9a3
feat: 월 선택 박스를 손그림 테두리로 바꾸고 셰브론 정렬·크기 보정
soyeonLee126 Aug 17, 2026
cf98389
Merge branch 'develop' into feature/archive-room
soyeonLee126 Aug 17, 2026
7b6ed8f
refactor: 보관함 상세 화면을 조립·월 박스·종이 더미로 분리
soyeonLee126 Aug 17, 2026
c3397a7
Merge branch 'develop' into feature/archive-room
soyeonLee126 Aug 17, 2026
b8bf6b0
feat: 비우기에 확인 다이얼로그 연결
soyeonLee126 Aug 17, 2026
c3f0732
revert: 탭바 접힘 애니메이션 되돌리기
soyeonLee126 Aug 17, 2026
b950284
refactor: CardQueryRepository/CardWriteRepository 를 CardRepository 하나…
soyeonLee126 Aug 17, 2026
82f049f
fix: 보관함 그리드가 오른쪽으로 2dp 치우치던 문제 수정
soyeonLee126 Aug 17, 2026
374ef80
Merge branch 'develop' into feature/archive-room
soyeonLee126 Aug 17, 2026
61034a9
feat: 보관함 종이를 누르면 카드 상세를 띄운다
soyeonLee126 Aug 17, 2026
41dd9a0
refactor: calendar 모듈 잔재 제거 및 archive 디렉터리 정리
soyeonLee126 Aug 18, 2026
483af18
Merge branch 'feature/card-month-query' into feature/archive-room
soyeonLee126 Aug 18, 2026
e876b2e
Merge branch 'feature/archive-list' into feature/archive-room
soyeonLee126 Aug 18, 2026
fb84bb3
refactor: 동작 설명 주석 정리
soyeonLee126 Aug 18, 2026
2e5551e
Merge branch 'feature/archive-list' into feature/archive-room
soyeonLee126 Aug 18, 2026
1697bef
Merge branch 'feature/archive-list' into feature/archive-room
soyeonLee126 Aug 18, 2026
bd1f2ca
Merge remote-tracking branch 'origin/develop' into feature/archive-room
soyeonLee126 Aug 18, 2026
6dfd166
refactor: 종이 낙하 구동부를 PaperFall 로 분리
soyeonLee126 Aug 18, 2026
4f3a62f
refactor: 보관함 상세 상태를 sealed 로 묶고 calendar 잔재 정리
soyeonLee126 Aug 18, 2026
0353e09
fix: 늦게 온 월별 응답이 지금 보고 있는 달을 덮던 문제 수정
soyeonLee126 Aug 18, 2026
c67b3ca
fix: 종이 더미가 넓은 화면에서 쏠리고 월 셀렉터를 가리던 문제 수정
soyeonLee126 Aug 18, 2026
0284ce6
fix: 보관함 비우기를 파쇄 화면에 연결
soyeonLee126 Aug 18, 2026
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: 12 additions & 0 deletions app/src/main/kotlin/com/gamss/android/app/main/MainScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ import com.gamss.android.app.navigation.topLevelDestinations
import com.gamss.android.core.designsystem.component.GamssBottomBar
import com.gamss.android.core.designsystem.component.GamssPaperBackground
import com.gamss.android.core.designsystem.theme.GamssTheme
import com.gamss.android.feature.archive.ArchiveDetailScreen
import com.gamss.android.feature.archive.ArchiveScreen
import com.gamss.android.feature.archive.navigation.ArchiveDetailKey
import com.gamss.android.feature.archive.navigation.ArchiveKey
import com.gamss.android.feature.carddelete.CardDeleteScreen
import com.gamss.android.feature.carddelete.navigation.CardDeleteKey
Expand Down Expand Up @@ -125,6 +127,16 @@ private fun mainEntryProvider(navigator: Navigator) = entryProvider {
entry<ArchiveKey> {
ArchiveScreen(
onNavigateToSetting = { navigator.navigate(SettingKey) },
onArchiveClick = { navigator.navigate(ArchiveDetailKey(it)) },
)
}
// 보관함 상세도 탭 안쪽의 상세 화면이라 다른 상세들과 같은 슬라이드를 쓴다.
entry<ArchiveDetailKey>(metadata = detailSlideTransition) { key ->
ArchiveDetailScreen(
emotion = key.emotion,
onBackClick = navigator::goBack,
onOpenConversation = { conversationId -> navigator.navigate(ChatRoomKey(conversationId)) },
onNavigateToCardDelete = { navigator.navigate(CardDeleteKey) },
)
}
entry<SettingKey>(metadata = detailSlideTransition) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
package com.gamss.android.feature.archive

import android.content.Context
import android.content.Intent
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.gamss.android.core.designsystem.button.GamssButtonVariant
import com.gamss.android.core.designsystem.component.GamssIconButton
import com.gamss.android.core.designsystem.component.GamssTopBar
import com.gamss.android.core.designsystem.dialog.GamssDialog
import com.gamss.android.core.designsystem.dialog.GamssDialogAction
import com.gamss.android.core.designsystem.theme.GamssTheme
import com.gamss.android.domain.card.Card
import com.gamss.android.domain.card.CardEntry
import com.gamss.android.domain.emotion.EmotionCharacter
import com.gamss.android.feature.archive.component.CardDetailDialog
import com.gamss.android.feature.archive.component.MonthSelector
import com.gamss.android.feature.archive.component.PaperPile
import com.gamss.android.feature.archive.component.YearMonthPickerSheet
import org.orbitmvi.orbit.compose.collectAsState
import org.orbitmvi.orbit.compose.collectSideEffect
import java.time.LocalDate
import java.time.YearMonth
import com.gamss.android.core.designsystem.R as DesignSystemR

/**
* 시스템 뒤로가기는 여기서 BackHandler 로 받지 않는다. 먼저 가로채면 NavDisplay 의 onBack 에 닿지
* 않아, predictive back 미리보기와 pop 트랜지션이 이 화면에서만 빠진다.
*/
@Composable
fun ArchiveDetailScreen(
emotion: EmotionCharacter,
onBackClick: () -> Unit,
onOpenConversation: (Long) -> Unit,
onNavigateToCardDelete: () -> Unit,
viewModel: ArchiveDetailViewModel = hiltViewModel(),
) {
val state by viewModel.collectAsState()
val context = LocalContext.current
val shareChooserTitle = stringResource(R.string.archive_card_share_chooser_title)
val cardLoadFailedMessage = stringResource(R.string.archive_card_load_error)
val discardFailedMessage = stringResource(R.string.archive_card_discard_failure)

LaunchedEffect(emotion) { viewModel.load(emotion) }

viewModel.collectSideEffect { sideEffect ->
when (sideEffect) {
is ArchiveDetailSideEffect.OpenChatRoom -> onOpenConversation(sideEffect.conversationId)
ArchiveDetailSideEffect.OpenCardDelete -> onNavigateToCardDelete()

ArchiveDetailSideEffect.CardLoadFailed ->
Toast.makeText(context, cardLoadFailedMessage, Toast.LENGTH_SHORT).show()

ArchiveDetailSideEffect.CardDiscardFailed ->
Toast.makeText(context, discardFailedMessage, Toast.LENGTH_SHORT).show()
}
}

ArchiveDetailFrame(
emotion = emotion,
state = state,
onBackClick = onBackClick,
onPaperClick = viewModel::selectCard,
onMonthClick = viewModel::showMonthPicker,
onClearClick = viewModel::showClearDialog,
)

ArchiveDetailOverlays(
state = state,
onMonthSelect = viewModel::selectMonth,
onMonthPickerDismiss = viewModel::dismissMonthPicker,
onClearConfirm = viewModel::confirmClear,
onClearDismiss = viewModel::dismissClearDialog,
onCardDismiss = viewModel::dismissCard,
onCardDiscard = viewModel::discardSelectedCard,
onCardConversationClick = viewModel::viewSelectedConversation,
onCardShare = { card -> shareCard(context, card, shareChooserTitle) },
)
}

/** 항상 보이는 부분. 위에 겹쳐 뜨는 시트·다이얼로그는 [ArchiveDetailOverlays] 가 맡는다. */
@Composable
private fun ArchiveDetailFrame(
emotion: EmotionCharacter,
state: ArchiveDetailState,
onBackClick: () -> Unit,
onPaperClick: (CardEntry) -> Unit,
onMonthClick: () -> Unit,
onClearClick: () -> Unit,
) {
Scaffold(
containerColor = GamssTheme.colors.white,
topBar = {
ArchiveDetailTopBar(
emotion = emotion,
onBackClick = onBackClick,
onClearClick = onClearClick,
)
},
) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding).fillMaxSize()) {
ArchiveDetailCards(cards = state.cards, onPaperClick = onPaperClick)
// 종이가 쌓일 자리보다 나중에 둔다. 카드가 많아 더미가 위로 넘치면 종이가 셀렉터를
// 가리고 탭까지 먹어 달을 못 바꾸게 된다.
MonthSelector(
yearMonth = state.yearMonth,
onClick = onMonthClick,
modifier = Modifier.align(Alignment.TopCenter),
)
}
}
}

/** 셋 다 아무것도 안 뜬 상태가 기본이라, 프레임과 떼어 여기서만 켜고 끈다. */
@Composable
private fun ArchiveDetailOverlays(
state: ArchiveDetailState,
onMonthSelect: (YearMonth) -> Unit,
onMonthPickerDismiss: () -> Unit,
onClearConfirm: () -> Unit,
onClearDismiss: () -> Unit,
onCardDismiss: () -> Unit,
onCardDiscard: () -> Unit,
onCardConversationClick: () -> Unit,
onCardShare: (Card) -> Unit,
) {
if (state.isMonthPickerVisible) {
YearMonthPickerSheet(
selected = state.yearMonth,
onSelect = onMonthSelect,
onDismiss = onMonthPickerDismiss,
)
}

if (state.isClearDialogVisible) {
ClearConfirmDialog(onConfirm = onClearConfirm, onDismiss = onClearDismiss)
}

state.selectedCard?.let { card ->
CardDetailDialog(
card = card,
onDismiss = onCardDismiss,
onDiscardClick = onCardDiscard,
onViewConversationClick = onCardConversationClick,
onShareClick = { onCardShare(card) },
)
}
}

private fun shareCard(context: Context, card: Card, chooserTitle: String) {
val sendIntent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, "${card.summary}\n\n${card.message}")
}
context.startActivity(Intent.createChooser(sendIntent, chooserTitle))
}

/** 되돌릴 수 없는 삭제라 파쇄 화면으로 넘기기 전에 확인을 한 번 받는다. */
@Composable
private fun ClearConfirmDialog(
onConfirm: () -> Unit,
onDismiss: () -> Unit,
) {
GamssDialog(
title = stringResource(R.string.archive_clear_dialog_title),
subtitle = stringResource(R.string.archive_clear_dialog_description),
primaryAction = GamssDialogAction(
label = stringResource(R.string.archive_clear),
onClick = onConfirm,
variant = GamssButtonVariant.Destructive,
),
secondaryAction = GamssDialogAction(
label = stringResource(R.string.archive_clear_dialog_cancel),
onClick = onDismiss,
variant = GamssButtonVariant.Secondary,
),
onDismissRequest = onDismiss,
)
}

@Composable
private fun ArchiveDetailTopBar(
emotion: EmotionCharacter,
onBackClick: () -> Unit,
onClearClick: () -> Unit,
) {
GamssTopBar(
// 비우기는 터치 영역만큼 안쪽 여백을 물고 있어, 오른쪽은 그만큼 뺀다.
contentPadding = PaddingValues(
start = TopBarHorizontalPadding,
end = TopBarHorizontalPadding - ClearHitPadding,
),
leading = {
Row(verticalAlignment = Alignment.CenterVertically) {
GamssIconButton(
iconRes = DesignSystemR.drawable.ic_left_chevron,
contentDescription = stringResource(R.string.archive_back_description),
onClick = onBackClick,
hitPadding = BackHitPadding,
)
Text(
text = emotion.displayName,
style = GamssTheme.typography.title4,
color = GamssTheme.colors.gray900,
modifier = Modifier.padding(start = TitleStartPadding),
)
}
},
trailing = {
Text(
text = stringResource(R.string.archive_clear),
style = GamssTheme.typography.body4Medium,
color = GamssTheme.colors.gray900,
modifier = Modifier
.clip(RoundedCornerShape(ClearRippleRadius))
.clickable(role = Role.Button, onClick = onClearClick)
.padding(ClearHitPadding),
)
},
)
}

@Composable
private fun ArchiveDetailCards(
cards: ArchiveCards,
onPaperClick: (CardEntry) -> Unit,
) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
when (cards) {
ArchiveCards.Loading -> CircularProgressIndicator(color = GamssTheme.colors.gray700)
ArchiveCards.LoadFailed -> EmptyMessage(textRes = R.string.archive_cards_load_failed)
is ArchiveCards.Loaded -> if (cards.entries.isEmpty()) {
EmptyMessage(textRes = R.string.archive_cards_empty)
} else {
PaperPile(cards = cards.entries, onPaperClick = onPaperClick)
}
}
}
}

@Composable
private fun EmptyMessage(@StringRes textRes: Int) {
Text(
text = stringResource(textRes),
style = GamssTheme.typography.body4Medium,
color = GamssTheme.colors.gray500,
)
}

private val TopBarHorizontalPadding = 18.dp
private val BackHitPadding = 0.dp
private val TitleStartPadding = 12.dp
private val ClearHitPadding = 8.dp
private val ClearRippleRadius = 8.dp

@Preview(showBackground = true, widthDp = 402, heightDp = 874)
@Composable
@Suppress("UnusedPrivateMember")
private fun ArchiveDetailPaperPilePreview() {
GamssTheme(darkTheme = false) {
ArchiveDetailFrame(
emotion = EmotionCharacter.QUIRKY,
state = ArchiveDetailState(
emotion = EmotionCharacter.QUIRKY,
yearMonth = YearMonth.of(2026, 7),
cards = ArchiveCards.Loaded(List(24) { index -> PreviewCard.copy(indexInDate = index) }),
),
onBackClick = {},
onPaperClick = {},
onMonthClick = {},
onClearClick = {},
)
}
}

private val PreviewCard = CardEntry(
date = LocalDate.of(2026, 7, 23),
indexInDate = 0,
character = EmotionCharacter.QUIRKY,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.gamss.android.feature.archive

sealed interface ArchiveDetailSideEffect {
data class OpenChatRoom(val conversationId: Long) : ArchiveDetailSideEffect
data object CardLoadFailed : ArchiveDetailSideEffect
data object CardDiscardFailed : ArchiveDetailSideEffect

/** 실제 삭제는 파쇄 화면이 맡는다. 여기서는 그 화면으로 넘기기만 한다. */
data object OpenCardDelete : ArchiveDetailSideEffect
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.gamss.android.feature.archive

import com.gamss.android.domain.card.Card
import com.gamss.android.domain.card.CardEntry
import com.gamss.android.domain.emotion.EmotionCharacter
import java.time.YearMonth

data class ArchiveDetailState(
val emotion: EmotionCharacter? = null,
val yearMonth: YearMonth,
val cards: ArchiveCards = ArchiveCards.Loading,
val isMonthPickerVisible: Boolean = false,
val isClearDialogVisible: Boolean = false,
val selectedCard: Card? = null,
val isCardLoading: Boolean = false,
)

/** 종이 더미 자리가 가질 수 있는 상태. 셋이 겹칠 수 없어 플래그 조합 대신 하나로 든다. */
sealed interface ArchiveCards {
data object Loading : ArchiveCards
data object LoadFailed : ArchiveCards
data class Loaded(val entries: List<CardEntry>) : ArchiveCards
}
Loading
Loading