Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
0ceaf0e
✨ Feat: `feature:stats:detail` 모듈 추가
chanho0908 Feb 23, 2026
4a2eb33
✨ Feat: 통계 상세 화면 네비게이션 경로 추가
chanho0908 Feb 23, 2026
e92030b
✨ Feat: 통계 상세 화면으로 이동하는 기능 추가
chanho0908 Feb 23, 2026
03d9562
✨ Feat: 통계 상세 도메인 모델 추가
chanho0908 Feb 23, 2026
3b80b22
✨ Feat: 통계 상세 모델에 EMPTY 객체 추가
chanho0908 Feb 23, 2026
7b63a92
✨ Feat: 통계 캘린더 화면 구현
chanho0908 Feb 23, 2026
a022b51
✨ Feat: fake 통계 상세 정보 조회 기능 추가
chanho0908 Feb 23, 2026
b3f458b
✨ Feat: 통계 카드 클릭 시 상세 화면으로 이동 기능 추가
chanho0908 Feb 23, 2026
fb0ef73
✨ Feat: 통계 상세 MVI 설정
chanho0908 Feb 23, 2026
f66610c
♻️ Refactor: `dummyStatsCalendarUiModel` 제거
chanho0908 Feb 23, 2026
b72168d
♻️ Refactor: StatsCalendar 미리보기에서 불필요한 테스트 데이터 제거
chanho0908 Feb 23, 2026
e147fd2
✨ Feat: 캘린더 네비게이터 비활성화 상태 추가
chanho0908 Feb 24, 2026
436412c
✨ Feat: 통계 상세 화면 Topbar 구현
chanho0908 Feb 24, 2026
122ddc4
✨ Chore: StatsDetailIntent의 불필요한 상속 구문 제거
chanho0908 Feb 24, 2026
bc00104
✨ Feat: 통계 상세 화면 프리뷰용 `StatsDetailUiStatePreviewProvider` 추가
chanho0908 Feb 24, 2026
0ae1783
♻️ Refactor: 사용하지 않는 라이브러리 버전 정의 제거
chanho0908 Feb 24, 2026
dbac3fe
✨ Feat: 통계 상세 화면 라우트 추가
chanho0908 Feb 24, 2026
5a19e99
✨ Feat: 통계 상세 진입을 위한 NavGraphContributor 추가
chanho0908 Feb 24, 2026
c8c24f8
♻️ Refactor: 캘린더 네비게이터 버튼 활성화 로직 수정
chanho0908 Feb 24, 2026
0f83d9f
♻️ Refactor: 통계 상세 화면 패키지 구조 변경
chanho0908 Feb 24, 2026
8f4cc2a
♻️ Refactor: StatsCalendar의 날짜 셀 크기 조정 방식 변경
chanho0908 Feb 24, 2026
6602c24
♻️ Refactor: 통계 상세 화면 팝업 오프셋 계산 로직 수정
chanho0908 Feb 24, 2026
a2969d1
✨ Fix: StatsDetailGraph 임포트 경로 수정
chanho0908 Feb 24, 2026
7775d18
✨ Feat: 종료된 챌린지 상세 정보 데이터 추가
chanho0908 Feb 24, 2026
6972cf1
✨ Feat: 월별 통계 이동 가능 여부 확인 로직 추가
chanho0908 Feb 24, 2026
29dd0eb
♻️ Refactor: 반복 주기(repeat type) 문자열 리소스 공용화
chanho0908 Feb 24, 2026
a4e1685
✨ Feat: 통계 상세 요약 정보 컴포넌트 추가
chanho0908 Feb 24, 2026
7d82d7e
✨ Feat: 통계 상세 화면 구현
chanho0908 Feb 24, 2026
904b2e9
✨ Feat: StatsRefreshBus 추가
chanho0908 Feb 24, 2026
2fb9ad6
♻️ Refactor: 통계 화면 목록 이벤트 버스 적용
chanho0908 Feb 24, 2026
7ec42dd
Revert "♻️ Refactor: 통계 화면 목록 이벤트 버스 적용"
chanho0908 Feb 24, 2026
f2d4819
Revert "✨ Feat: StatsRefreshBus 추가"
chanho0908 Feb 24, 2026
a06a313
Revert "✨ Feat: 통계 상세 화면 구현"
chanho0908 Feb 24, 2026
09e8b7b
Revert "✨ Feat: 통계 상세 요약 정보 컴포넌트 추가"
chanho0908 Feb 24, 2026
6c575d3
Revert "♻️ Refactor: 반복 주기(repeat type) 문자열 리소스 공용화"
chanho0908 Feb 24, 2026
b569b14
Revert "✨ Feat: 월별 통계 이동 가능 여부 확인 로직 추가"
chanho0908 Feb 24, 2026
f1db709
✨ Feat: 월별 통계 이동 가능 여부 확인 로직 추가
chanho0908 Feb 24, 2026
a80ac45
Revert "✨ Feat: 월별 통계 이동 가능 여부 확인 로직 추가"
chanho0908 Feb 24, 2026
f165054
Merge branch 'feat/#88-stats-component' into feat/#88-stats-detail
chanho0908 Feb 24, 2026
3caa0e6
Merge branch 'feat/#88-stats-component' into feat/#88-stats-detail
chanho0908 Feb 24, 2026
cfd1d33
Merge branch 'develop' into feat/#88-stats-detail
chanho0908 Feb 25, 2026
336c69f
♻️ Refactor: `StatsCalendar`의 `horizontalArrangement` 속성 제거
chanho0908 Feb 25, 2026
235443b
✨ Refactor: `StatsCalendar` 람다 호출 방식 변경
chanho0908 Feb 25, 2026
f8bab45
♻️ Refactor: PictureDayCell 이미지 로딩 성능 최적화
chanho0908 Feb 25, 2026
cb4471d
♻️ Refactor: stats-detail 모듈 패키지 구조 변경
chanho0908 Feb 25, 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
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ dependencies {
implementation(projects.feature.goalEditor)
implementation(projects.feature.goalManage)
implementation(projects.feature.settings)
implementation(projects.feature.stats.detail)
implementation(projects.core.notification)
implementation(projects.core.navigationContract)

Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/com/yapp/twix/di/FeatureModules.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.twix.login.di.loginModule
import com.twix.main.di.mainModule
import com.twix.onboarding.di.onBoardingModule
import com.twix.settings.di.settingsModule
import com.twix.stats.detail.di.statsDetailModule
import com.twix.stats.di.statsModule
import com.twix.task_certification.di.taskCertificationModule
import org.koin.core.module.Module
Expand All @@ -22,4 +23,5 @@ val featureModules: List<Module> =
settingsModule,
onBoardingModule,
statsModule,
statsDetailModule,
)
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
Expand All @@ -26,6 +27,8 @@ fun CalendarNavigator(
onNextMonth: () -> Unit,
onPreviousMonth: () -> Unit,
modifier: Modifier = Modifier,
hasPrevious: Boolean = true,
hasNext: Boolean = true,
) {
Row(
modifier =
Expand All @@ -39,9 +42,10 @@ fun CalendarNavigator(
contentDescription = "previous month",
modifier =
Modifier
.noRippleClickable(onClick = onPreviousMonth)
.noRippleClickable(enabled = hasPrevious, onClick = onPreviousMonth)
.padding(6.dp)
.size(24.dp),
colorFilter = ColorFilter.tint(if (hasPrevious) GrayColor.C500 else GrayColor.C200),
)

AppText(
Expand All @@ -56,21 +60,24 @@ fun CalendarNavigator(
contentDescription = "next month",
modifier =
Modifier
.noRippleClickable(onClick = onNextMonth)
.noRippleClickable(enabled = hasNext, onClick = onNextMonth)
.padding(6.dp)
.size(24.dp),
colorFilter = ColorFilter.tint(if (hasNext) GrayColor.C500 else GrayColor.C200),
)
}
}

@Preview
@Preview(showBackground = true)
@Composable
fun CalendarNavigatorPreview() {
TwixTheme {
CalendarNavigator(
currentDate = LocalDate.now(),
onNextMonth = {},
onPreviousMonth = {},
hasPrevious = false,
hasNext = true,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package com.twix.designsystem.components.stats

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import coil3.request.crossfade
import com.twix.designsystem.components.text.AppText
import com.twix.designsystem.theme.CommonColor
import com.twix.designsystem.theme.DimmedColor
import com.twix.designsystem.theme.GrayColor
import com.twix.domain.model.enums.AppTextStyle
import com.twix.domain.model.stats.detail.CompletedDate
import com.twix.ui.extension.noRippleClickable
import java.time.LocalDate

@Composable
fun PictureDayCell(
date: LocalDate,
completed: CompletedDate?,
onDateSelected: (LocalDate) -> Unit,
modifier: Modifier = Modifier,
) {
val showBackgroundCard = completed?.myImageUrl != null && completed.partnerImageUrl != null
val hasImage = completed?.partnerImageUrl != null || completed?.myImageUrl != null

val textColor = if (hasImage) CommonColor.White else GrayColor.C500
val borderColor = if (showBackgroundCard) CommonColor.White else GrayColor.C400
val cornerShape = RoundedCornerShape(7.dp)
val context = LocalContext.current

Box(
modifier =
modifier
.aspectRatio(1f)
.noRippleClickable { onDateSelected(date) },
contentAlignment = Alignment.Center,
) {
if (showBackgroundCard) {
Box(
modifier =
Modifier
.size(36.dp)
.rotate(-16f)
.border(1.dp, GrayColor.C400, cornerShape)
.background(CommonColor.White, cornerShape),
)
}

Box(
modifier =
Modifier
.size(36.dp)
.clip(cornerShape)
.then(
if (showBackgroundCard) {
Modifier.border(1.dp, borderColor, cornerShape)
} else {
Modifier
},
),
contentAlignment = Alignment.Center,
) {
val displayImageUrl = completed?.partnerImageUrl ?: completed?.myImageUrl
if (displayImageUrl != null) {
val imageRequest =
remember(displayImageUrl, context) {
ImageRequest
.Builder(context)
.data(displayImageUrl)
.crossfade(true)
.build()
}

AsyncImage(
model = imageRequest,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize(),
)

Box(
modifier =
Modifier
.fillMaxSize()
.background(DimmedColor.D020),
)
}

AppText(
text = date.dayOfMonth.toString(),
style = AppTextStyle.B1,
color = textColor,
textAlign = TextAlign.Center,
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package com.twix.designsystem.components.stats

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.twix.designsystem.R
import com.twix.designsystem.components.stats.model.StatsCalendarUiModel
import com.twix.designsystem.components.text.AppText
import com.twix.designsystem.theme.GrayColor
import com.twix.designsystem.theme.TwixTheme
import com.twix.domain.model.enums.AppTextStyle
import com.twix.domain.model.stats.detail.CompletedDate
import java.time.LocalDate

@Composable
fun StatsCalendar(
uiModel: StatsCalendarUiModel,
onSelectedDate: (LocalDate) -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier.fillMaxWidth()) {
DayOfWeekHeader()

Spacer(modifier = Modifier.height(16.dp))

Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(20.dp),
) {
uiModel.weeks.forEach { week ->
Row(
modifier = Modifier.fillMaxWidth(),
) {
week.forEach { date ->
if (date == null) {
Box(modifier = Modifier.weight(1f))
} else {
PictureDayCell(
date = date,
completed = uiModel.completedDateMap[date],
onDateSelected = onSelectedDate,
modifier = Modifier.weight(1f),
Comment on lines +49 to +53
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

날짜 셀의 접근성 맥락(스크린리더 읽기 정보)이 부족합니다.

현재는 클릭 가능 셀이지만 TalkBack/VoiceOver에서 “날짜 + 완료 상태”를 충분히 전달하기 어렵습니다. 왜 문제가 되냐면, 숫자만 읽히면 사용자가 어떤 날짜를 선택하는지 맥락 파악이 어렵기 때문입니다.
PictureDayCell에 semantic label(예: “2월 3일, 완료됨/미완료”)을 전달하는 파라미터를 추가하는 방향으로 맞춰보실까요?

As per coding guidelines, core/design-system/**: [디자인 시스템 리뷰 가이드] - 접근성(Accessibility) 고려 여부.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/design-system/src/main/java/com/twix/designsystem/components/stats/StatsCalendar.kt`
around lines 49 - 53, The PictureDayCell lacks an accessibility/semantic label,
so update the component and its usage in StatsCalendar: add a new parameter
(e.g., semanticLabel: String) to PictureDayCell and, where StatsCalendar calls
PictureDayCell, pass a composed label that combines the date and completion
state (use uiModel.completedDateMap[date] to determine "완료됨"/"미완료" and format as
"M월 d일, 완료됨/미완료" or equivalent). Ensure PictureDayCell uses that parameter to
set semantics (contentDescription/semantics) so screen readers read the full
context when onDateSelected is focusable/clickable.

)
}
}

if (week.size < 7) {
repeat(7 - week.size) {
Box(modifier = Modifier.weight(1f))
}
}
}
}
}
}
}

@Composable
private fun DayOfWeekHeader() {
val days =
listOf(
stringResource(R.string.word_sunday),
stringResource(R.string.word_monday),
stringResource(R.string.word_tuesday),
stringResource(R.string.word_wednesday),
stringResource(R.string.word_thursday),
stringResource(R.string.word_friday),
stringResource(R.string.word_saturday),
)
Row(
modifier = Modifier.fillMaxWidth(),
) {
days.forEach { day ->
AppText(
text = day,
style = AppTextStyle.B2,
color = GrayColor.C300,
modifier =
Modifier
.weight(1f)
.height(24.dp),
textAlign = TextAlign.Center,
)
}
}
}

@Preview(showBackground = true)
@Composable
private fun StatsCalendarPreview() {
TwixTheme {
StatsCalendar(
uiModel =
StatsCalendarUiModel.create(
currentDate = LocalDate.of(2026, 2, 1),
completedDate =
listOf(
CompletedDate(
LocalDate.of(2026, 2, 1),
"https://picsum.photos/100",
"https://picsum.photos/100",
),
CompletedDate(
LocalDate.of(2026, 2, 3),
"https://picsum.photos/101",
null,
),
),
),
onSelectedDate = { },
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.twix.designsystem.components.stats.model

import androidx.compose.runtime.Immutable
import com.twix.domain.model.stats.detail.CompletedDate
import java.time.LocalDate

@Immutable
data class StatsCalendarUiModel(
val currentDate: LocalDate = LocalDate.now(),
val completedDateMap: Map<LocalDate, CompletedDate> = emptyMap(),
val weeks: List<List<LocalDate?>> = emptyList(),
) {
companion object {
private const val WEEK_LENGTH = 7

fun create(
currentDate: LocalDate,
completedDate: List<CompletedDate>,
): StatsCalendarUiModel {
val firstDayOfMonth = currentDate.withDayOfMonth(1)
val lastDay = currentDate.lengthOfMonth()
val emptyCellsBefore = firstDayOfMonth.dayOfWeek.value % WEEK_LENGTH

val calendarItems = mutableListOf<LocalDate?>()
repeat(emptyCellsBefore) { calendarItems.add(null) }
for (i in 1..lastDay) {
calendarItems.add(currentDate.withDayOfMonth(i))
}

return StatsCalendarUiModel(
currentDate = currentDate,
completedDateMap = completedDate.associateBy { it.date },
weeks = calendarItems.chunked(WEEK_LENGTH),
)
}
}
}
19 changes: 19 additions & 0 deletions core/navigation/src/main/java/com/twix/navigation/NavRoutes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,23 @@ sealed class NavRoutes(
object SettingsAccountRoute : NavRoutes("settings/account")

object SettingsAboutRoute : NavRoutes("settings/about")

/**
* StatsGraph
* */
object StatsDetailGraph : NavRoutes("stats_detail_graph")

object StatsDetailRoute : NavRoutes("stats_detail_graph/{goalId}?date={date}") {
const val ARG_GOAL_ID = "goalId"
const val ARG_DATE = "date"

fun createRoute(
goalId: Long,
date: LocalDate?,
): String {
val baseRoute = "stats_detail_graph/$goalId"
if (date != null) return "$baseRoute?date=$date"
return baseRoute
}
}
}
Loading