diff --git a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/OutLinedButton.kt b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/OutLinedButton.kt
new file mode 100644
index 00000000..7edce2fc
--- /dev/null
+++ b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/OutLinedButton.kt
@@ -0,0 +1,45 @@
+package com.unifest.android.core.designsystem.component
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import com.unifest.android.core.designsystem.ComponentPreview
+
+@Composable
+fun UnifestOutlinedButton(
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ borderColor: Color = Color(0xFFf5678E),
+ contentColor: Color = Color(0xFFf5678E),
+ enabled: Boolean = true,
+ content: @Composable RowScope.() -> Unit,
+) {
+ OutlinedButton(
+ onClick = onClick,
+ modifier = modifier,
+ enabled = enabled,
+ shape = RoundedCornerShape(10.dp),
+ colors = ButtonDefaults.outlinedButtonColors(
+ contentColor = contentColor,
+ ),
+ border = BorderStroke(1.dp, borderColor),
+ content = content,
+ )
+}
+
+@ComponentPreview
+@Composable
+fun UnifestOutlinedButtonPreview() {
+ UnifestOutlinedButton(
+ onClick = {},
+ ) {
+ Text("Outlined Button")
+ }
+}
diff --git a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/theme/Font.kt b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/theme/Font.kt
index 9cad6dda..24b2e456 100644
--- a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/theme/Font.kt
+++ b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/theme/Font.kt
@@ -50,6 +50,11 @@ val Title5 = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 13.sp,
)
+val BoothTitle0 = TextStyle(
+ fontFamily = pretendardFamily,
+ fontWeight = FontWeight.Bold,
+ fontSize = 24.sp,
+)
val BoothTitle1 = TextStyle(
fontFamily = pretendardFamily,
@@ -116,3 +121,15 @@ val Content4 = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 12.sp,
)
+
+val Content5 = TextStyle(
+ fontFamily = pretendardFamily,
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+)
+
+val Content6 = TextStyle(
+ fontFamily = pretendardFamily,
+ fontWeight = FontWeight.Medium,
+ fontSize = 12.sp,
+)
diff --git a/core/designsystem/src/main/res/drawable/calender_bottom.xml b/core/designsystem/src/main/res/drawable/calender_bottom.xml
new file mode 100644
index 00000000..50c22b02
--- /dev/null
+++ b/core/designsystem/src/main/res/drawable/calender_bottom.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/core/designsystem/src/main/res/drawable/ic_calender_down.xml b/core/designsystem/src/main/res/drawable/ic_calender_down.xml
new file mode 100644
index 00000000..611e4029
--- /dev/null
+++ b/core/designsystem/src/main/res/drawable/ic_calender_down.xml
@@ -0,0 +1,12 @@
+
+
+
diff --git a/core/designsystem/src/main/res/drawable/ic_calender_up.xml b/core/designsystem/src/main/res/drawable/ic_calender_up.xml
new file mode 100644
index 00000000..f1681b5d
--- /dev/null
+++ b/core/designsystem/src/main/res/drawable/ic_calender_up.xml
@@ -0,0 +1,12 @@
+
+
+
diff --git a/core/designsystem/src/main/res/drawable/ic_location.xml b/core/designsystem/src/main/res/drawable/ic_location.xml
new file mode 100644
index 00000000..102279a3
--- /dev/null
+++ b/core/designsystem/src/main/res/drawable/ic_location.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/core/designsystem/src/main/res/drawable/ic_schedule.xml b/core/designsystem/src/main/res/drawable/ic_schedule.xml
new file mode 100644
index 00000000..b50b8a03
--- /dev/null
+++ b/core/designsystem/src/main/res/drawable/ic_schedule.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml
index 5505387e..294b8cad 100644
--- a/core/designsystem/src/main/res/values/strings.xml
+++ b/core/designsystem/src/main/res/values/strings.xml
@@ -24,6 +24,16 @@
- 경상도
+
+ 오늘의 축제 일정
+ 오늘은 축제가 열리는 학교가 없어요
+ 다가오는 축제 일정
+ 관심 축제 추가하기
+ 관심 축제로 추가
+ 축제 일정 없음
+ 오늘은 축제가 열리는 학교가 없어요
+
+
Week mode
diff --git a/core/domain/src/main/kotlin/com/unifest/android/core/domain/entity/FestivalEventEntity.kt b/core/domain/src/main/kotlin/com/unifest/android/core/domain/entity/FestivalEventEntity.kt
new file mode 100644
index 00000000..592174e3
--- /dev/null
+++ b/core/domain/src/main/kotlin/com/unifest/android/core/domain/entity/FestivalEventEntity.kt
@@ -0,0 +1,12 @@
+package com.unifest.android.core.domain.entity
+
+import androidx.compose.runtime.Stable
+
+@Stable
+data class FestivalEventEntity(
+ val id: Int,
+ val date: String,
+ val name: String,
+ val location: String,
+ val celebrityImages: List,
+)
diff --git a/core/domain/src/main/kotlin/com/unifest/android/core/domain/entity/IncomingFestivalEventEntity.kt b/core/domain/src/main/kotlin/com/unifest/android/core/domain/entity/IncomingFestivalEventEntity.kt
new file mode 100644
index 00000000..bdd204d3
--- /dev/null
+++ b/core/domain/src/main/kotlin/com/unifest/android/core/domain/entity/IncomingFestivalEventEntity.kt
@@ -0,0 +1,11 @@
+package com.unifest.android.core.domain.entity
+
+import androidx.compose.runtime.Stable
+
+@Stable
+data class IncomingFestivalEventEntity(
+ val imageRes: Int,
+ val name: String,
+ val dates: String,
+ val location: String,
+)
diff --git a/feature/home/src/main/kotlin/com/unifest/android/feature/home/Calendar.kt b/feature/home/src/main/kotlin/com/unifest/android/feature/home/Calendar.kt
index fbeb2ca1..586d902a 100644
--- a/feature/home/src/main/kotlin/com/unifest/android/feature/home/Calendar.kt
+++ b/feature/home/src/main/kotlin/com/unifest/android/feature/home/Calendar.kt
@@ -2,27 +2,24 @@ package com.unifest.android.feature.home
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
+import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
-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.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material3.Checkbox
-import androidx.compose.material3.CheckboxDefaults
import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@@ -30,11 +27,12 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.paint
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
@@ -55,34 +53,31 @@ import com.kizitonwose.calendar.core.nextMonth
import com.kizitonwose.calendar.core.previousMonth
import com.kizitonwose.calendar.core.yearMonth
import com.unifest.android.core.designsystem.R
+import com.unifest.android.core.designsystem.theme.BoothTitle0
import com.unifest.android.core.designsystem.theme.UnifestTheme
import kotlinx.coroutines.launch
import java.time.DayOfWeek
import java.time.LocalDate
-import java.time.YearMonth
+import java.time.Month
@Composable
fun Calendar(adjacentMonths: Long = 500) {
val currentDate = remember { LocalDate.now() }
- val currentMonth = remember(currentDate) { currentDate.yearMonth }
- val startMonth = remember(currentDate) { currentMonth.minusMonths(adjacentMonths) }
- val endMonth = remember(currentDate) { currentMonth.plusMonths(adjacentMonths) }
- val selections = remember { mutableStateListOf() }
+ val currentYearMonth = remember(currentDate) { currentDate.yearMonth }
+ val startMonth = remember(currentDate) { currentYearMonth.minusMonths(adjacentMonths) }
+ val endMonth = remember(currentDate) { currentYearMonth.plusMonths(adjacentMonths) }
+ val selectedDate = remember { mutableStateOf(LocalDate.now()) }
val daysOfWeek = remember { daysOfWeek() }
-
var isWeekMode by remember { mutableStateOf(false) }
- var isAnimating by remember { mutableStateOf(false) }
- val coroutineScope = rememberCoroutineScope()
Column(
modifier = Modifier
- .fillMaxSize()
.background(Color.White),
) {
val monthState = rememberCalendarState(
startMonth = startMonth,
endMonth = endMonth,
- firstVisibleMonth = currentMonth,
+ firstVisibleMonth = currentYearMonth,
firstDayOfWeek = daysOfWeek.first(),
)
val weekState = rememberWeekCalendarState(
@@ -91,12 +86,13 @@ fun Calendar(adjacentMonths: Long = 500) {
firstVisibleWeekDate = currentDate,
firstDayOfWeek = daysOfWeek.first(),
)
- CalendarTitle(
+
+ MonthAndWeekCalendarTitle(
isWeekMode = isWeekMode,
monthState = monthState,
weekState = weekState,
- isAnimating = isAnimating,
)
+
CalendarHeader(daysOfWeek = daysOfWeek)
AnimatedVisibility(visible = !isWeekMode) {
HorizontalCalendar(
@@ -105,14 +101,10 @@ fun Calendar(adjacentMonths: Long = 500) {
val isSelectable = day.position == DayPosition.MonthDate
Day(
day.date,
- isSelected = isSelectable && selections.contains(day.date),
+ isSelected = isSelectable && selectedDate.value == day.date,
isSelectable = isSelectable,
) { clicked ->
- if (selections.contains(clicked)) {
- selections.remove(clicked)
- } else {
- selections.add(clicked)
- }
+ selectedDate.value = clicked
}
},
)
@@ -124,105 +116,49 @@ fun Calendar(adjacentMonths: Long = 500) {
val isSelectable = day.position == WeekDayPosition.RangeDate
Day(
day.date,
- isSelected = isSelectable && selections.contains(day.date),
+ isSelected = isSelectable && selectedDate.value == day.date,
isSelectable = isSelectable,
) { clicked ->
- if (selections.contains(clicked)) {
- selections.remove(clicked)
- } else {
- selections.add(clicked)
- }
+ selectedDate.value = clicked
}
},
)
}
- Spacer(modifier = Modifier.weight(1f))
- WeekModeToggle(
- modifier = Modifier.align(Alignment.CenterHorizontally),
+
+ ModeToggleButton(
isWeekMode = isWeekMode,
- ) { weekMode ->
- isAnimating = true
- isWeekMode = weekMode
- coroutineScope.launch {
- if (weekMode) {
- val targetDate = monthState.firstVisibleMonth.weekDays.last().last().date
- weekState.scrollToWeek(targetDate)
- weekState.animateScrollToWeek(targetDate) // Trigger a layout pass for title update
- } else {
- val targetMonth = weekState.firstVisibleWeek.days.first().date.yearMonth
- monthState.scrollToMonth(targetMonth)
- monthState.animateScrollToMonth(targetMonth) // Trigger a layout pass for title update
- }
- isAnimating = false
- }
- }
+ onModeChange = { isWeekMode = it },
+ )
}
}
@Composable
-fun MonthAndWeekCalendarTitle(
+fun ModeToggleButton(
+ modifier: Modifier = Modifier,
isWeekMode: Boolean,
- currentMonth: YearMonth,
- monthState: CalendarState,
- weekState: WeekCalendarState,
+ onModeChange: (Boolean) -> Unit,
) {
- val coroutineScope = rememberCoroutineScope()
- SimpleCalendarTitle(
- modifier = Modifier.padding(vertical = 10.dp, horizontal = 8.dp),
- currentMonth = currentMonth,
- goToPrevious = {
- coroutineScope.launch {
- if (isWeekMode) {
- val targetDate = weekState.firstVisibleWeek.days.first().date.minusDays(1)
- weekState.animateScrollToWeek(targetDate)
- } else {
- val targetMonth = monthState.firstVisibleMonth.yearMonth.previousMonth
- monthState.animateScrollToMonth(targetMonth)
- }
- }
- },
- goToNext = {
- coroutineScope.launch {
- if (isWeekMode) {
- val targetDate = weekState.firstVisibleWeek.days.last().date.plusDays(1)
- weekState.animateScrollToWeek(targetDate)
- } else {
- val targetMonth = monthState.firstVisibleMonth.yearMonth.nextMonth
- monthState.animateScrollToMonth(targetMonth)
- }
- }
- },
- )
-}
+ val icon = if (isWeekMode) painterResource(id = R.drawable.ic_calender_down) else painterResource(id = R.drawable.ic_calender_up)
+ val contentDescription = if (isWeekMode) "Month" else "Week"
+ val backgroundPainter = painterResource(id = R.drawable.calender_bottom)
-@Composable
-fun SimpleCalendarTitle(
- modifier: Modifier,
- currentMonth: YearMonth,
- goToPrevious: () -> Unit,
- goToNext: () -> Unit,
-) {
- Row(
- modifier = modifier.height(40.dp),
- verticalAlignment = Alignment.CenterVertically,
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .requiredHeight(40.dp)
+ .paint(painter = backgroundPainter, contentScale = ContentScale.FillBounds)
+ .then(modifier),
+ contentAlignment = Alignment.Center,
) {
- CalendarNavigationIcon(
- icon = painterResource(id = R.drawable.ic_chevron_left),
- contentDescription = "Previous",
- onClick = goToPrevious,
- )
- Text(
- modifier = Modifier.weight(1f),
- text = currentMonth.displayText(),
- fontSize = 22.sp,
- textAlign = TextAlign.Center,
- fontWeight = FontWeight.Medium,
- )
- CalendarNavigationIcon(
- icon = painterResource(id = R.drawable.ic_chevron_right),
- contentDescription = "Next",
- onClick = goToNext,
- )
+ IconButton(
+ onClick = { onModeChange(!isWeekMode) },
+ ) {
+ Icon(
+ painter = icon,
+ contentDescription = contentDescription,
+ tint = Color(0xFFD9D9D9),
+ )
+ }
}
}
@@ -245,32 +181,79 @@ private fun CalendarNavigationIcon(
.align(Alignment.Center),
painter = icon,
contentDescription = contentDescription,
+ tint = Color.Gray,
)
}
@Composable
-private fun CalendarTitle(
+fun MonthAndWeekCalendarTitle(
isWeekMode: Boolean,
monthState: CalendarState,
weekState: WeekCalendarState,
- isAnimating: Boolean,
) {
val visibleMonth = rememberFirstVisibleMonthAfterScroll(monthState)
- val visibleWeek = rememberFirstVisibleWeekAfterScroll(weekState)
- val visibleWeekMonth = visibleWeek.days.first().date.yearMonth
- // Track animation state to prevent updating the title too early before
- // the correct value is available (after the animation).
- val currentMonth = if (isWeekMode) {
- if (isAnimating) visibleMonth.yearMonth else visibleWeekMonth
- } else {
- if (isAnimating) visibleWeekMonth else visibleMonth.yearMonth
+ val currentMonth = visibleMonth.yearMonth.month
+
+ val coroutineScope = rememberCoroutineScope()
+ if (!isWeekMode) {
+ SimpleCalendarTitle(
+ modifier = Modifier.padding(20.dp),
+ currentMonth = currentMonth,
+ goToPrevious = {
+ coroutineScope.launch {
+ if (isWeekMode) {
+ val targetDate = weekState.firstVisibleWeek.days.first().date.minusDays(1)
+ weekState.animateScrollToWeek(targetDate)
+ } else {
+ val targetMonth = monthState.firstVisibleMonth.yearMonth.previousMonth
+ monthState.animateScrollToMonth(targetMonth)
+ }
+ }
+ },
+ goToNext = {
+ coroutineScope.launch {
+ if (isWeekMode) {
+ val targetDate = weekState.firstVisibleWeek.days.last().date.plusDays(1)
+ weekState.animateScrollToWeek(targetDate)
+ } else {
+ val targetMonth = monthState.firstVisibleMonth.yearMonth.nextMonth
+ monthState.animateScrollToMonth(targetMonth)
+ }
+ }
+ },
+ )
+ }
+}
+
+@Composable
+fun SimpleCalendarTitle(
+ // 실제로 달력의 상단에 현재 월을 표시하고, 이전/다음 월로 이동할 수 있는 화살표 아이콘을 제공하는 UI 컴포넌트
+ modifier: Modifier,
+ currentMonth: Month,
+ goToPrevious: () -> Unit,
+ goToNext: () -> Unit,
+) {
+ Row(
+ modifier = modifier.height(40.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(
+ modifier = Modifier.weight(1f),
+ text = currentMonth.displayText(),
+ style = BoothTitle0,
+ textAlign = TextAlign.Start,
+ )
+ CalendarNavigationIcon(
+ icon = painterResource(id = R.drawable.ic_chevron_left),
+ contentDescription = "Previous",
+ onClick = goToPrevious,
+ )
+ CalendarNavigationIcon(
+ icon = painterResource(id = R.drawable.ic_chevron_right),
+ contentDescription = "Next",
+ onClick = goToNext,
+ )
}
- MonthAndWeekCalendarTitle(
- isWeekMode = isWeekMode,
- currentMonth = currentMonth,
- monthState = monthState,
- weekState = weekState,
- )
}
@Composable
@@ -285,7 +268,8 @@ fun CalendarHeader(daysOfWeek: List) {
textAlign = TextAlign.Center,
fontSize = 15.sp,
text = dayOfWeek.displayText(),
- fontWeight = FontWeight.Medium,
+ fontWeight = FontWeight.Bold,
+ color = Color.Gray,
)
}
}
@@ -298,54 +282,41 @@ fun Day(
isSelectable: Boolean,
onClick: (LocalDate) -> Unit,
) {
- Box(
- modifier = Modifier
- .aspectRatio(1f) // This is important for square-sizing!
- .padding(6.dp)
- .clip(CircleShape)
- .background(color = if (isSelected) colorResource(R.color.example_1_selection_color) else Color.Transparent)
- .clickable(
- enabled = isSelectable,
- showRipple = !isSelected,
- onClick = { onClick(day) },
- ),
- contentAlignment = Alignment.Center,
- ) {
- val textColor = when {
- isSelected -> Color.White
- isSelectable -> Color.Unspecified
- else -> colorResource(R.color.inactive_text_color)
- }
- Text(
- text = day.dayOfMonth.toString(),
- color = textColor,
- fontSize = 14.sp,
- )
- }
-}
+ val currentDate = LocalDate.now()
+ val isToday = day == currentDate
-@Composable
-fun WeekModeToggle(
- modifier: Modifier,
- isWeekMode: Boolean,
- weekModeToggled: (isWeekMode: Boolean) -> Unit,
-) {
- // We want the entire content to be clickable, not just the checkbox.
- Row(
- modifier = modifier
- .padding(10.dp)
- .clip(MaterialTheme.shapes.small)
- .clickable { weekModeToggled(!isWeekMode) }
- .padding(10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterHorizontally),
- ) {
- Checkbox(
- checked = isWeekMode,
- onCheckedChange = null, // Check is handled by parent.
- colors = CheckboxDefaults.colors(checkedColor = colorResource(R.color.example_1_selection_color)),
- )
- Text(text = stringResource(R.string.week_mode))
+ Column {
+ Box(
+ modifier = Modifier
+ .aspectRatio(1f) // This is important for square-sizing!
+ .padding(12.dp)
+ .clip(CircleShape)
+ .background(color = if (isSelected) Color(0xFFF5687E) else Color.Transparent)
+ .then(
+ if (day == currentDate) {
+ Modifier.border(2.dp, Color(0xFFF5687E), CircleShape)
+ } else Modifier,
+ )
+ .clickable(
+ enabled = isSelectable,
+ showRipple = !isSelected,
+ onClick = { onClick(day) },
+ ),
+ contentAlignment = Alignment.Center,
+ ) {
+ val textColor = when {
+ isSelected -> Color.White
+ isToday -> Color(0xFFF5687E)
+ isSelectable -> Color.Unspecified
+ else -> colorResource(R.color.inactive_text_color)
+ }
+ Text(
+ text = day.dayOfMonth.toString(),
+ color = textColor,
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Bold,
+ )
+ }
}
}
diff --git a/feature/home/src/main/kotlin/com/unifest/android/feature/home/HomeScreen.kt b/feature/home/src/main/kotlin/com/unifest/android/feature/home/HomeScreen.kt
index 4d26026e..8c3686c8 100644
--- a/feature/home/src/main/kotlin/com/unifest/android/feature/home/HomeScreen.kt
+++ b/feature/home/src/main/kotlin/com/unifest/android/feature/home/HomeScreen.kt
@@ -1,26 +1,77 @@
package com.unifest.android.feature.home
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Circle
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
+import androidx.core.view.WindowInsetsCompat
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.unifest.android.core.designsystem.R
-import com.unifest.android.core.designsystem.component.TopAppBarNavigationType
-import com.unifest.android.core.designsystem.component.UnifestTopAppBar
+import com.unifest.android.core.designsystem.component.UnifestOutlinedButton
+import com.unifest.android.core.designsystem.theme.BoothLocation
+import com.unifest.android.core.designsystem.theme.Content4
+import com.unifest.android.core.designsystem.theme.Content5
+import com.unifest.android.core.designsystem.theme.Content6
+import com.unifest.android.core.designsystem.theme.Title2
+import com.unifest.android.core.designsystem.theme.Title3
import com.unifest.android.core.designsystem.theme.UnifestTheme
+import com.unifest.android.core.domain.entity.FestivalEventEntity
+import com.unifest.android.core.domain.entity.IncomingFestivalEventEntity
import com.unifest.android.core.ui.DevicePreview
+import com.unifest.android.feature.home.viewmodel.HomeUiState
+import com.unifest.android.feature.home.viewmodel.HomeViewModel
+import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun HomeRoute(
padding: PaddingValues,
onNavigateToIntro: () -> Unit,
+ viewModel: HomeViewModel = hiltViewModel(),
) {
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
HomeScreen(
padding = padding,
+ uiState = uiState,
onNavigateToIntro = onNavigateToIntro,
)
}
@@ -28,28 +79,302 @@ internal fun HomeRoute(
@Composable
internal fun HomeScreen(
padding: PaddingValues,
+ uiState: HomeUiState,
@Suppress("unused")
onNavigateToIntro: () -> Unit,
) {
- Column(
+ var selectedEventId by remember { mutableIntStateOf(-1) }
+ val view = LocalView.current
+ val insets = with(LocalDensity.current) {
+ WindowInsetsCompat.toWindowInsetsCompat(view.rootWindowInsets, view).getInsets(WindowInsetsCompat.Type.statusBars()).top.toDp()
+ }
+
+ Column(modifier = Modifier.padding(top = insets)) {
+ LazyColumn(
+ modifier = Modifier
+ .weight(1f)
+ .padding(bottom = padding.calculateBottomPadding()),
+ ) {
+ item { Calendar() }
+ item {
+ FestivalScheduleText()
+ }
+ if (uiState.festivalEvents.isEmpty()) {
+ item {
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(64.dp),
+ ) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Image(
+ imageVector = ImageVector.vectorResource(id = R.drawable.ic_schedule),
+ contentDescription = "축제 없음",
+ modifier = Modifier.size(23.dp),
+ )
+ Spacer(modifier = Modifier.height(10.dp))
+ Text(
+ text = stringResource(id = R.string.home_empty_festival_text),
+ style = Title2,
+ textAlign = TextAlign.Center,
+ )
+ Spacer(modifier = Modifier.height(9.dp))
+ Text(
+ text = stringResource(id = R.string.home_empty_festival_schedule_description_text),
+ style = Content6,
+ textAlign = TextAlign.Center,
+ color = Color(0xFF848484),
+ )
+ }
+ }
+ }
+ } else {
+ itemsIndexed(uiState.festivalEvents) { index, event ->
+ Column {
+ Spacer(modifier = Modifier.height(16.dp))
+ FestivalScheduleItem(event, selectedEventId) { eventId ->
+ selectedEventId = if (selectedEventId == eventId) -1 else eventId
+ }
+ }
+ if (index < uiState.festivalEvents.size - 1) {
+ Spacer(modifier = Modifier.height(16.dp))
+ HorizontalDivider(
+ color = Color(0xFFDFDFDF),
+ modifier = Modifier.padding(horizontal = 20.dp),
+ thickness = 1.dp,
+ )
+ }
+ }
+ }
+ item {
+ UnifestOutlinedButton(
+ onClick = { onNavigateToIntro() },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(20.dp),
+ contentColor = Color(0xFF585858),
+ borderColor = Color(0xFFD2D2D2),
+ ) {
+ Text(
+ text = stringResource(id = R.string.home_add_interest_festival_button),
+ style = BoothLocation,
+ )
+ }
+ }
+ item {
+ IncomingFestivalText()
+ }
+ items(uiState.incomingEvents) { event ->
+ IncomingFestivalCard(event)
+ Spacer(modifier = Modifier.height(8.dp))
+ }
+ }
+ }
+}
+
+@Composable
+fun FestivalScheduleText() {
+ Text(
+ text = stringResource(id = R.string.home_festival_schedule_text),
+ style = Title3,
+ modifier = Modifier.padding(start = 20.dp, top = 20.dp),
+ )
+}
+
+@Composable
+fun FestivalScheduleItem(
+ event: FestivalEventEntity,
+ selectedEventId: Int,
+ onEventClick: (Int) -> Unit,
+) {
+ Column {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable { onEventClick(event.id) }
+ .padding(start = 20.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Box(
+ modifier = Modifier
+ .width(3.dp)
+ .height(72.dp)
+ .background(Color(0xFF1FC0BA))
+ .align(Alignment.CenterVertically),
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Column {
+ Text(
+ text = event.date,
+ style = Content4,
+ color = Color(0xFFC0C0C0),
+ )
+ Spacer(modifier = Modifier.height(5.dp))
+ Text(
+ text = event.name,
+ style = Title2,
+ )
+ Spacer(modifier = Modifier.height(7.dp))
+ Row {
+ Icon(
+ imageVector = ImageVector.vectorResource(id = R.drawable.ic_location),
+ contentDescription = "Location Icon",
+ modifier = Modifier
+ .size(10.dp)
+ .align(Alignment.CenterVertically),
+ tint = Color.Unspecified,
+ )
+ Spacer(modifier = Modifier.width(5.dp))
+ Text(
+ text = event.location,
+ style = Content5,
+ color = Color(0xFF848484),
+ )
+ }
+ }
+ Spacer(modifier = Modifier.width(39.dp))
+ LazyRow {
+ items(event.celebrityImages) { _ ->
+ Icon(
+ imageVector = Icons.Default.Circle,
+ contentDescription = "Celebrity",
+ tint = Color(0xFFDFDFDF),
+ modifier = Modifier
+ .size(60.dp)
+ .padding(horizontal = 5.dp),
+ )
+ }
+ }
+ }
+ AnimatedVisibility(visible = selectedEventId == event.id) {
+ UnifestOutlinedButton(
+ onClick = { /*관심 축제 추가하기 버튼*/ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 16.dp, start = 20.dp, end = 20.dp),
+ ) {
+ Text(
+ text = stringResource(id = R.string.home_add_interest_festival_in_item_button),
+ style = BoothLocation,
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun IncomingFestivalText() {
+ Text(
+ text = stringResource(id = R.string.home_incoming_festival_text),
+ modifier = Modifier.padding(start = 20.dp, bottom = 16.dp),
+ style = Title3,
+ )
+}
+
+@Composable
+fun IncomingFestivalCard(event: IncomingFestivalEventEntity) {
+ Card(
modifier = Modifier
- .fillMaxSize()
- .padding(bottom = padding.calculateBottomPadding()),
- horizontalAlignment = Alignment.CenterHorizontally,
+ .padding(horizontal = 20.dp)
+ .fillMaxWidth(),
+ shape = RoundedCornerShape(10.dp),
+ colors = CardDefaults.cardColors(containerColor = Color.White),
+ border = BorderStroke(1.dp, Color(0xFFDEDEDE)),
) {
- UnifestTopAppBar(
- titleRes = R.string.intro_top_app_bar_title,
- navigationType = TopAppBarNavigationType.None,
- )
- Calendar()
+ Row(
+ modifier = Modifier
+ .padding(20.dp)
+ .fillMaxSize(),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Image(
+ imageVector = ImageVector.vectorResource(id = event.imageRes),
+ contentDescription = "${event.name} Logo",
+ modifier = Modifier.size(52.dp),
+ )
+ Spacer(modifier = Modifier.width(10.dp))
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ Text(
+ text = event.dates,
+ style = Content6,
+ color = Color(0xFF848484),
+ )
+ Spacer(modifier = Modifier.height(5.dp))
+ Text(
+ text = event.name,
+ style = Content4,
+ )
+ Spacer(modifier = Modifier.height(5.dp))
+ Row {
+ Icon(
+ imageVector = ImageVector.vectorResource(id = R.drawable.ic_location),
+ contentDescription = "Location Icon",
+ modifier = Modifier
+ .size(10.dp)
+ .align(Alignment.CenterVertically),
+ tint = Color.Unspecified,
+ )
+ Spacer(modifier = Modifier.width(5.dp))
+ Text(
+ text = event.location,
+ style = Content6,
+ color = Color(0xFF848484),
+ )
+ }
+ }
+ }
}
}
+@OptIn(ExperimentalFoundationApi::class)
@DevicePreview
@Composable
fun HomeScreenPreview() {
UnifestTheme {
HomeScreen(
+ uiState = HomeUiState(
+ festivalEvents = persistentListOf(
+ FestivalEventEntity(
+ id = 1,
+ date = "5/21(화)",
+ name = "녹색지대 DAY 1",
+ location = "건국대학교 서울캠퍼스",
+ celebrityImages = listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15),
+ ),
+ FestivalEventEntity(
+ id = 2,
+ date = "5/21(화)",
+ name = "녹색지대 DAY 1",
+ location = "건국대학교 서울캠퍼스",
+ celebrityImages = listOf(0, 1, 2),
+ ),
+ FestivalEventEntity(
+ id = 3,
+ date = "5/21(화)",
+ name = "녹색지대 DAY 1",
+ location = "건국대학교 서울캠퍼스",
+ celebrityImages = listOf(0, 1, 2),
+ ),
+ ),
+ incomingEvents = persistentListOf(
+ IncomingFestivalEventEntity(
+ imageRes = R.drawable.ic_waiting,
+ name = "녹색지대",
+ dates = "05/21(화) - 05/23(목)",
+ location = "건국대학교 서울캠퍼스",
+ ),
+ IncomingFestivalEventEntity(
+ imageRes = R.drawable.ic_waiting,
+ name = "녹색지대",
+ dates = "05/21(화) - 05/23(목)",
+ location = "건국대학교 서울캠퍼스",
+ ),
+ ),
+ ),
+
padding = PaddingValues(0.dp),
onNavigateToIntro = {},
)
diff --git a/feature/home/src/main/kotlin/com/unifest/android/feature/home/viewmodel/HomeUiState.kt b/feature/home/src/main/kotlin/com/unifest/android/feature/home/viewmodel/HomeUiState.kt
new file mode 100644
index 00000000..b57719f7
--- /dev/null
+++ b/feature/home/src/main/kotlin/com/unifest/android/feature/home/viewmodel/HomeUiState.kt
@@ -0,0 +1,11 @@
+package com.unifest.android.feature.home.viewmodel
+
+import com.unifest.android.core.domain.entity.FestivalEventEntity
+import com.unifest.android.core.domain.entity.IncomingFestivalEventEntity
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
+
+data class HomeUiState(
+ val incomingEvents: ImmutableList = persistentListOf(),
+ val festivalEvents: ImmutableList = persistentListOf(),
+)
diff --git a/feature/home/src/main/kotlin/com/unifest/android/feature/home/viewmodel/HomeViewModel.kt b/feature/home/src/main/kotlin/com/unifest/android/feature/home/viewmodel/HomeViewModel.kt
index be4b416f..676f1b79 100644
--- a/feature/home/src/main/kotlin/com/unifest/android/feature/home/viewmodel/HomeViewModel.kt
+++ b/feature/home/src/main/kotlin/com/unifest/android/feature/home/viewmodel/HomeViewModel.kt
@@ -1,8 +1,63 @@
package com.unifest.android.feature.home.viewmodel
import androidx.lifecycle.ViewModel
+import com.unifest.android.core.designsystem.R
+import com.unifest.android.core.domain.entity.FestivalEventEntity
+import com.unifest.android.core.domain.entity.IncomingFestivalEventEntity
import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
import javax.inject.Inject
@HiltViewModel
-class HomeViewModel @Inject constructor() : ViewModel()
+class HomeViewModel @Inject constructor() : ViewModel() {
+ private val _uiState = MutableStateFlow(HomeUiState())
+ val uiState: StateFlow = _uiState.asStateFlow()
+
+ init {
+ _uiState.update {
+ it.copy(
+ incomingEvents = persistentListOf(
+ IncomingFestivalEventEntity(
+ imageRes = R.drawable.ic_waiting,
+ name = "녹색지대",
+ dates = "05/21(화) - 05/23(목)",
+ location = "건국대학교 서울캠퍼스",
+ ),
+ IncomingFestivalEventEntity(
+ imageRes = R.drawable.ic_waiting,
+ name = "녹색지대",
+ dates = "05/21(화) - 05/23(목)",
+ location = "건국대학교 서울캠퍼스",
+ ),
+ ),
+ festivalEvents = persistentListOf(
+ FestivalEventEntity(
+ id = 1,
+ date = "5/21(화)",
+ name = "녹색지대 DAY 1",
+ location = "건국대학교 서울캠퍼스",
+ celebrityImages = listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15),
+ ),
+ FestivalEventEntity(
+ id = 2,
+ date = "5/21(화)",
+ name = "녹색지대 DAY 1",
+ location = "건국대학교 서울캠퍼스",
+ celebrityImages = listOf(0, 1, 2),
+ ),
+ FestivalEventEntity(
+ id = 3,
+ date = "5/21(화)",
+ name = "녹색지대 DAY 1",
+ location = "건국대학교 서울캠퍼스",
+ celebrityImages = listOf(0, 1, 2),
+ ),
+ ),
+ )
+ }
+ }
+}