Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] 캘린더 컴포넌트를 구현합니다. #19

Merged
merged 11 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from 9 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
Expand Up @@ -3,18 +3,35 @@ package com.hongikyeolgong2.calendar.model
import java.time.LocalDate
import java.time.YearMonth
import java.time.format.DateTimeFormatter
import java.util.Locale

class Calendar(
initDate: LocalDate = LocalDate.now(),
private val dateTimeFormatter: DateTimeFormatter =
DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT_PATTERN),
DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT_PATTERN).withLocale(Locale.ENGLISH),
Copy link
Contributor

Choose a reason for hiding this comment

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

Locale.ENGLISH 를 적용하신 이유가 무엇인가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

명시해주지 않으면 의도와는 다르게 디폴트로 들어가는 인자가 1월 2024 이런식으로 디바이스의 언어설정 기준으로 들어가게 됩니다.
디자인상으로 의도된건 Jan 2024와 같은 영어약어의 표현이기 때문에 해당 Locale 설정을 추가하였습니다.

Copy link
Contributor

Choose a reason for hiding this comment

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

오 디바이스 언어를 인식하는줄은 몰랐습니다! 새로운 사실을 배워가네요

private val studyDays: List<StudyDay> = emptyList(),
) {
private var date: LocalDate = initDate

val now: String
get() = dateTimeFormatter.format(date)

fun getMonth(): List<StudyDay> {
val studyDaysWithMonth = getStudyDaysByMonth()
val existingDays = studyDaysWithMonth.associateBy { it.date.dayOfMonth }

return (1..getLastDayOfMonth()).map { day ->
existingDays[day] ?: StudyDay(
date = date.withDayOfMonth(day),
studyRoomUsage = StudyRoomUsage.NEVER_USED,
)
}.sortedBy { it.date.dayOfMonth }
}

fun getStudyDaysByMonth(): List<StudyDay> {
return studyDays.filter { it.date.month == date.month }
}

fun getLastDayOfMonth(): Int {
return YearMonth.from(date).atEndOfMonth().dayOfMonth
}
Expand All @@ -27,10 +44,6 @@ class Calendar(
date = date.plusMonths(1)
}

fun getStudyDaysByMonth(): List<StudyDay> {
return studyDays.filter { it.date.month == date.month }
}

companion object {
private const val DEFAULT_DATE_TIME_FORMAT_PATTERN = "MMM yyyy"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.hongikyeolgong2.calendar.model

enum class StudyRoomUsage {
NEVER_USED,
USED_ONCE,
USED_ONCE_EXTENDED_ONCE,
USED_ONCE_EXTENDED_TWICE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,44 @@ import java.time.format.DateTimeFormatter
import java.util.Locale

class CalendarTest : BehaviorSpec({
Given("현재 날짜가 포함된 달의 모든 날을 가져올 수 있다.") {
var date: LocalDate
var calendar: Calendar
var actual: List<StudyDay>

When("2024년 1월 5일이 주어지면") {
date = LocalDate.of(2024, 1, 5)
calendar = Calendar(date)

Then("31개의 날들이 반환된다") {
actual = calendar.getMonth()
actual.size shouldBe 31
}

Then("순서를 유지한체 1일부터 31일 까지 반환된다") {
actual = calendar.getMonth()
actual.first().date shouldBe LocalDate.of(2024, 1, 1)
actual.last().date shouldBe LocalDate.of(2024, 1, 31)
}
}

When("2024년 2월 15일이 주어지면") {
date = LocalDate.of(2024, 2, 15)
calendar = Calendar(date)

Then("29개의 날들이 반환된다") {
actual = calendar.getMonth()
actual.size shouldBe 29
}

Then("윤년이기 때문에 1부터 29일까지 반환된다.") {
actual = calendar.getMonth()
actual.first().date shouldBe LocalDate.of(2024, 2, 1)
actual.last().date shouldBe LocalDate.of(2024, 2, 29)
}
Comment on lines +40 to +44
Copy link
Contributor

Choose a reason for hiding this comment

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

윤년처리까지 치밀하게 테스트 해보셨군요..!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

워낙에 애초에 잘 짜여진 Java의 라이브러리다 보니 엣지케이스 위주로 테스트를 진행하였습니다!

}
}

Given("LocalDate가 주어지면, 해당 달의 끝날짜를 알 수 있다.") {
var date: LocalDate
var calendar: Calendar
Expand Down Expand Up @@ -178,8 +216,14 @@ class CalendarTest : BehaviorSpec({
actual shouldBe
listOf(
StudyDay(LocalDate.of(2024, 2, 1), StudyRoomUsage.USED_ONCE),
StudyDay(LocalDate.of(2024, 2, 3), StudyRoomUsage.USED_ONCE_EXTENDED_ONCE),
StudyDay(LocalDate.of(2024, 2, 4), StudyRoomUsage.USED_ONCE_EXTENDED_ONCE),
StudyDay(
LocalDate.of(2024, 2, 3),
StudyRoomUsage.USED_ONCE_EXTENDED_ONCE,
),
StudyDay(
LocalDate.of(2024, 2, 4),
StudyRoomUsage.USED_ONCE_EXTENDED_ONCE,
),
)
}
}
Expand Down
1 change: 1 addition & 0 deletions calendar-presentation/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
11 changes: 11 additions & 0 deletions calendar-presentation/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
plugins {
id("hongikyeolgong2.android.feature")
}

android {
namespace = "com.teamhy2.hongikyeolgong2.calendar.presentation"
}

dependencies {
implementation(projects.calendarDomain)
}
Empty file.
21 changes: 21 additions & 0 deletions calendar-presentation/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.hongikyeolgong2.calendar.presentation

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import junit.framework.TestCase.assertEquals
import org.junit.Test
import org.junit.runner.RunWith

/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.hongikyeolgong2.calendar.presentation.test", appContext.packageName)
}
}
2 changes: 2 additions & 0 deletions calendar-presentation/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
package com.hongikyeolgong2.calendar.presentation

import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
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.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
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.hongikyeolgong2.calendar.model.Calendar
import com.hongikyeolgong2.calendar.model.StudyDay
import com.hongikyeolgong2.calendar.model.StudyRoomUsage
import com.teamhy2.designsystem.R
import com.teamhy2.designsystem.ui.theme.Gray100
import com.teamhy2.designsystem.ui.theme.Gray300
import com.teamhy2.designsystem.ui.theme.HY2Theme
import com.teamhy2.designsystem.ui.theme.HY2Typography
import com.teamhy2.hongikyeolgong2.calendar.presentation.R.string.description_next_month
import com.teamhy2.hongikyeolgong2.calendar.presentation.R.string.description_previous_month
import java.time.LocalDate

private const val DAY_DEFAULT_MARGIN = 5

@Composable
fun Hy2Calendar(
title: String,
days: List<StudyDay>,
onPreviousMonthClick: () -> Unit,
onNextMonthClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
CalendarHeader(
title = title,
onPreviousMonthClick = onPreviousMonthClick,
onNextMonthClick = onNextMonthClick,
)
Spacer(modifier = Modifier.height(12.dp))
Row(modifier = Modifier.padding(bottom = 8.dp)) {
DayOfWeek.entries.forEach { dayOfWeek ->
DayOfWeek(
name = dayOfWeek.abbreviation,
modifier = Modifier.weight(1f),
)
}
}
LazyVerticalGrid(
columns = GridCells.Fixed(7),
verticalArrangement = Arrangement.spacedBy(DAY_DEFAULT_MARGIN.dp),
horizontalArrangement = Arrangement.spacedBy(DAY_DEFAULT_MARGIN.dp),
) {
items((days.first().date.dayOfWeek.ordinal + 1) % 7) {
Box(modifier = Modifier.weight(1f))
}

items(days) {
Day(studyDay = it, modifier = Modifier.weight(1f))
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

월을 표시하는 로직과 일을 표시하는 로직을 분리하면 가독성이더 좋아지지 않을까 제안드려봅니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

반영완료 하였습니다. 가독성을 위한 분리를 해보고 추후 장단점을 피부로 같이 느껴보는 시간을 가졌음 좋겠습니다!

변경 커밋

}
}

@Composable
fun CalendarHeader(
title: String,
onPreviousMonthClick: () -> Unit,
onNextMonthClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = title,
style = HY2Typography().title01,
color = Gray100,
)
Spacer(modifier = Modifier.weight(1f))
Icon(
painter = painterResource(id = R.drawable.ic_arrow_left),
modifier =
Modifier.clickable(
interactionSource = MutableInteractionSource(),
indication = null,
onClick = onPreviousMonthClick,
),
tint = Gray300,
contentDescription = stringResource(description_previous_month),
)
Icon(
painter = painterResource(id = R.drawable.ic_arrow_right),
modifier =
Modifier.clickable(
interactionSource = MutableInteractionSource(),
indication = null,
onClick = onNextMonthClick,
),
tint = Gray300,
contentDescription = stringResource(description_next_month),
)
}
}

@Composable
fun DayOfWeek(
name: String,
modifier: Modifier = Modifier,
) {
Text(
text = name,
modifier = modifier,
textAlign = TextAlign.Center,
style = HY2Typography().body04,
color = Gray300,
)
}

@Preview
@Composable
private fun Hy2CalendarPreview() {
HY2Theme {
val calendar by remember {
mutableStateOf(
Calendar(
studyDays =
listOf(
StudyDay(
date = LocalDate.now().withDayOfMonth(2),
studyRoomUsage = StudyRoomUsage.USED_ONCE_EXTENDED_ONCE,
),
StudyDay(
date = LocalDate.now().withDayOfMonth(5),
studyRoomUsage = StudyRoomUsage.USED_ONCE,
),
StudyDay(
date = LocalDate.now().withDayOfMonth(10),
studyRoomUsage = StudyRoomUsage.USED_ONCE_EXTENDED_TWICE,
),
StudyDay(
date = LocalDate.now().withDayOfMonth(20),
studyRoomUsage = StudyRoomUsage.NEVER_USED,
),
),
),
)
}
var title by remember {
mutableStateOf(calendar.now)
}

var month by remember {
mutableStateOf(calendar.getMonth())
}

Hy2Calendar(
title = title,
days = month,
onPreviousMonthClick = {
calendar.moveToPreviousMonth()
title = calendar.now
month = calendar.getMonth()
},
onNextMonthClick = {
calendar.moveToNextMonth()
title = calendar.now
month = calendar.getMonth()
},
)
}
}

@Preview
@Composable
private fun CalendarHeaderPreview() {
HY2Theme {
CalendarHeader(
title = "Jan 2024",
onPreviousMonthClick = { },
onNextMonthClick = { },
)
}
}

@Preview
@Composable
private fun DayOfWeekPreview() {
HY2Theme {
DayOfWeek(name = "Mon")
}
}
Loading
Loading