Skip to content

Commit

Permalink
Merge pull request #60 from YAPP-Github/feature/TNT-189
Browse files Browse the repository at this point in the history
[TNT-189] 트레이니 알림 화면 UI 구현
  • Loading branch information
SeonJeongk authored Feb 3, 2025
2 parents d671417 + f759288 commit e318c1f
Show file tree
Hide file tree
Showing 21 changed files with 518 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ fun TnTCheckToggle(
@Composable
fun TnTSwitch(
checked: Boolean,
onCheckedChange: () -> Unit,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val trackColor = if (checked) {
Expand All @@ -75,7 +75,7 @@ fun TnTSwitch(
.height(24.dp)
.clip(RoundedCornerShape(12.dp))
.background(trackColor)
.clickable(onClick = onCheckedChange)
.clickable(onClick = onClick)
.padding(horizontal = 2.dp),
) {
Box(
Expand Down Expand Up @@ -109,7 +109,7 @@ private fun TnTSwitchPreview() {

TnTSwitch(
checked = checked,
onCheckedChange = { checked = !checked },
onClick = { checked = !checked },
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package co.kr.tnt.designsystem.component.notification

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import co.kr.tnt.designsystem.component.notification.model.NotificationIcon
import co.kr.tnt.designsystem.theme.TnTTheme

@Composable
fun TnTNotification(
type: NotificationIcon,
title: String,
contents: String,
time: String,
isChecked: Boolean,
) {
val backgroundColor = if (isChecked) {
TnTTheme.colors.commonColors.Common0
} else {
TnTTheme.colors.neutralColors.Neutral100
}

Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.background(backgroundColor)
.padding(20.dp),
) {
Image(
painter = painterResource(type.icon),
contentDescription = null,
)
Column(
modifier = Modifier.fillMaxWidth(),
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 6.dp),
) {
Text(
text = title,
color = TnTTheme.colors.neutralColors.Neutral400,
style = TnTTheme.typography.label1Bold,
)
Text(
text = time,
color = TnTTheme.colors.neutralColors.Neutral400,
style = TnTTheme.typography.label1Medium,
)
}
Text(
text = contents,
color = TnTTheme.colors.neutralColors.Neutral900,
style = TnTTheme.typography.body2Medium,
modifier = Modifier.fillMaxWidth(),
)
}
}
}

@Preview
@Composable
private fun TnTNotificationPreview() {
TnTTheme {
TnTNotification(
type = NotificationIcon.LINK,
title = "알림 문구",
contents = "알림 상세 문구",
time = "2분전",
isChecked = false,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package co.kr.tnt.designsystem.component.notification.model

import co.kr.tnt.core.designsystem.R

enum class NotificationIcon(val icon: Int) {
LINK(R.drawable.ic_link_notif),
SCHEDULE(R.drawable.ic_schedule_notif),
}
25 changes: 25 additions & 0 deletions core/designsystem/src/main/res/drawable/ic_link_notif.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M8,0L24,0A8,8 0,0 1,32 8L32,24A8,8 0,0 1,24 32L8,32A8,8 0,0 1,0 24L0,8A8,8 0,0 1,8 0z"
android:fillColor="#D478FF"/>
<path
android:pathData="M15.396,17.187m-6.928,-4a8,8 48.563,1 1,13.856 8a8,8 48.563,1 1,-13.856 -8"
android:fillColor="#FEFBFF"/>
<path
android:pathData="M18.597,8.643C18.873,8.165 19.484,8.001 19.963,8.277L21.262,9.027C21.74,9.303 21.904,9.915 21.628,10.393L20.128,12.991L17.097,11.241L18.597,8.643Z"
android:fillColor="#FEFBFF"/>
<path
android:pathData="M19.664,10.19L19.914,9.757C20.466,8.8 21.689,8.473 22.646,9.025L23.079,9.275"
android:strokeLineJoin="round"
android:strokeWidth="0.5"
android:fillColor="#00000000"
android:strokeColor="#FEFBFF"
android:strokeLineCap="round"/>
<path
android:pathData="M26.553,7.866C26.698,7.714 26.949,7.859 26.89,8.061L26.513,9.347C26.475,9.477 26.574,9.606 26.709,9.603L28.049,9.571C28.259,9.567 28.334,9.846 28.15,9.947L26.974,10.59C26.855,10.655 26.833,10.816 26.931,10.91L27.902,11.835C28.053,11.98 27.908,12.231 27.707,12.172L26.421,11.794C26.291,11.756 26.161,11.856 26.164,11.991L26.196,13.331C26.201,13.541 25.921,13.616 25.821,13.432L25.178,12.255C25.113,12.136 24.951,12.115 24.858,12.213L23.933,13.183C23.788,13.335 23.537,13.19 23.596,12.989L23.973,11.703C24.011,11.573 23.912,11.443 23.777,11.446L22.437,11.478C22.227,11.483 22.152,11.203 22.336,11.102L23.512,10.46C23.631,10.395 23.653,10.233 23.555,10.139L22.584,9.214C22.433,9.069 22.578,8.819 22.779,8.878L24.065,9.255C24.195,9.293 24.325,9.194 24.321,9.058L24.29,7.718C24.285,7.509 24.565,7.434 24.665,7.618L25.308,8.794C25.373,8.913 25.535,8.934 25.628,8.836L26.553,7.866Z"
android:fillColor="#F2D6FF"/>
</vector>
33 changes: 33 additions & 0 deletions core/designsystem/src/main/res/drawable/ic_schedule_notif.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M0,8C0,3.582 3.582,0 8,0H24C28.418,0 32,3.582 32,8V24C32,28.418 28.418,32 24,32H8C3.582,32 0,28.418 0,24V8Z"
android:fillColor="#FFA938"/>
<path
android:pathData="M10.578,8.784L21.423,8.784A2.946,2.946 0,0 1,24.368 11.729L24.368,21.477A2.946,2.946 0,0 1,21.423 24.423L10.578,24.423A2.946,2.946 0,0 1,7.633 21.477L7.633,11.729A2.946,2.946 0,0 1,10.578 8.784z"
android:fillColor="#FAFAFA"/>
<path
android:pathData="M7.633,11.729C7.633,10.102 8.952,8.784 10.578,8.784H21.423C23.049,8.784 24.368,10.102 24.368,11.729V14.315H7.633V11.729Z"
android:fillColor="#FF9200"/>
<path
android:pathData="M18.802,16.574V21.216H17.839V17.492H17.812L16.747,18.16V17.304L17.896,16.574H18.802Z"
android:fillColor="#FF9200"/>
<path
android:pathData="M14.425,21.316C13.262,21.313 12.557,20.425 12.56,18.899C12.562,17.378 13.264,16.51 14.425,16.51C15.581,16.51 16.291,17.383 16.288,18.899C16.286,20.43 15.586,21.316 14.425,21.316ZM13.543,18.899C13.541,19.974 13.886,20.513 14.425,20.513C14.962,20.513 15.307,19.974 15.305,18.899C15.305,17.839 14.96,17.304 14.425,17.304C13.888,17.304 13.546,17.839 13.543,18.899Z"
android:fillColor="#FF9200"/>
<path
android:pathData="M11.241,7.578L11.241,7.578A0.359,0.359 0,0 1,11.6 7.936L11.6,9.54A0.359,0.359 0,0 1,11.241 9.899L11.241,9.899A0.359,0.359 0,0 1,10.883 9.54L10.883,7.936A0.359,0.359 0,0 1,11.241 7.578z"
android:fillColor="#FAFAFA"/>
<path
android:pathData="M14.414,7.578L14.414,7.578A0.359,0.359 0,0 1,14.772 7.936L14.772,9.54A0.359,0.359 0,0 1,14.414 9.899L14.414,9.899A0.359,0.359 0,0 1,14.055 9.54L14.055,7.936A0.359,0.359 0,0 1,14.414 7.578z"
android:fillColor="#FAFAFA"/>
<path
android:pathData="M17.586,7.578L17.586,7.578A0.359,0.359 0,0 1,17.944 7.936L17.944,9.54A0.359,0.359 0,0 1,17.586 9.899L17.586,9.899A0.359,0.359 0,0 1,17.227 9.54L17.227,7.936A0.359,0.359 0,0 1,17.586 7.578z"
android:fillColor="#FAFAFA"/>
<path
android:pathData="M20.757,7.578L20.757,7.578A0.359,0.359 0,0 1,21.115 7.936L21.115,9.54A0.359,0.359 0,0 1,20.757 9.899L20.757,9.899A0.359,0.359 0,0 1,20.398 9.54L20.398,7.936A0.359,0.359 0,0 1,20.757 7.578z"
android:fillColor="#FAFAFA"/>
</vector>
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ sealed interface Route {
@Serializable
data object TraineeMyPage : Route

@Serializable
data object TraineeNotification : Route

@Serializable
data class WebView(val url: String) : Route
}
34 changes: 34 additions & 0 deletions core/ui/src/main/java/co/kr/tnt/ui/model/NotificationState.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package co.kr.tnt.ui.model

import co.kr.tnt.designsystem.component.notification.model.NotificationIcon
import co.kr.tnt.domain.model.NotificationInfo
import co.kr.tnt.domain.model.NotificationType
import co.kr.tnt.ui.util.NotificationTimeFormatter

data class NotificationState(
val type: NotificationIcon,
val title: String,
val contents: String,
val time: String,
val isChecked: Boolean = true,
) {
companion object {
fun fromDomain(domain: NotificationInfo): NotificationState {
val timeFormatter = NotificationTimeFormatter()
return NotificationState(
type = when (domain.type) {
NotificationType.LINK -> NotificationIcon.LINK
NotificationType.SCHEDULE -> NotificationIcon.SCHEDULE
},
title = domain.title,
contents = domain.contents,
time = timeFormatter.formatTime(domain.time),
isChecked = domain.isChecked,
)
}
}
}

fun List<NotificationInfo>.toUiStateList(): List<NotificationState> {
return this.map { NotificationState.fromDomain(it) }
}
25 changes: 25 additions & 0 deletions core/ui/src/main/java/co/kr/tnt/ui/util/NotificationTimeUtil.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package co.kr.tnt.ui.util

import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
import java.util.Locale

class NotificationTimeFormatter {
fun formatTime(notificationTime: LocalDateTime): String {
val now = LocalDateTime.now()

val minutesDiff = ChronoUnit.MINUTES.between(notificationTime, now)
val hoursDiff = ChronoUnit.HOURS.between(notificationTime, now)

return when {
minutesDiff < 1 -> "방금"
minutesDiff < 60 -> "${minutesDiff}분 전"
hoursDiff < 24 -> "${hoursDiff}시간 전"
else -> {
val dateFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd", Locale.getDefault())
notificationTime.format(dateFormatter)
}
}
}
}
3 changes: 3 additions & 0 deletions core/ui/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
<string name="height_unit">cm</string>
<string name="weight_unit">kg</string>

<string name="notification">알림</string>
<string name="no_recent_notifications">최근 받은 알림이 없어요</string>

<!-- Meal -->
<string name="meal_breakfast">아침</string>
<string name="meal_lunch">점심</string>
Expand Down
28 changes: 28 additions & 0 deletions domain/src/main/java/co/kr/tnt/domain/model/NotificationInfo.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package co.kr.tnt.domain.model

import java.time.LocalDateTime

data class NotificationInfo(
val type: NotificationType,
val title: String,
val contents: String,
val time: LocalDateTime,
val isChecked: Boolean,
)

// TODO API 나오면 수정 필요
enum class NotificationType {
LINK,
SCHEDULE,
;

companion object {
fun from(type: String): NotificationType {
return when (type) {
"LINK" -> LINK
"SCHEDULE" -> SCHEDULE
else -> throw IllegalArgumentException("지원하지 않는 $type 입니다.")
}
}
}
}
1 change: 1 addition & 0 deletions feature/main/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ dependencies {
implementation(projects.feature.trainer.connect)
implementation(projects.feature.trainee.connect)
implementation(projects.feature.trainee.mypage)
implementation(projects.feature.trainee.notification)

implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
Expand Down
4 changes: 4 additions & 0 deletions feature/main/src/main/java/co/kr/tnt/main/ui/TnTNavHost.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import co.kr.tnt.roleselect.roleSelectionScreen
import co.kr.tnt.trainee.connect.navigation.navigateToTraineeConnect
import co.kr.tnt.trainee.connect.navigation.traineeConnectScreen
import co.kr.tnt.trainee.mypage.navigation.traineeMyPageScreen
import co.kr.tnt.trainee.notification.navigation.traineeNotification
import co.kr.tnt.trainee.signup.navigation.navigateToTraineeSignUp
import co.kr.tnt.trainee.signup.navigation.traineeSignUpScreen
import co.kr.tnt.trainer.connect.navigation.navigateToTrainerConnect
Expand Down Expand Up @@ -100,6 +101,9 @@ fun TnTNavHost(
navController.navigateToWebView(url = url)
},
)
traineeNotification(
navigateToPrevious = { navController.popBackStack() },
)
webViewScreen(
navigateToPrevious = { navController.popBackStack() },
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ private fun TraineeMyPageScreen(
)
TnTSwitch(
checked = state.isPushEnabled,
onCheckedChange = onPushNotificationToggle,
onClick = onPushNotificationToggle,
)
}
Column(
Expand Down
1 change: 1 addition & 0 deletions feature/trainee/notification/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
13 changes: 13 additions & 0 deletions feature/trainee/notification/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import co.kr.tnt.setNamespace

plugins {
id("tnt.android.feature")
}

android {
setNamespace("feature.trainee.notification")
}

dependencies {
implementation(libs.kotlinx.immutable)
}
4 changes: 4 additions & 0 deletions feature/trainee/notification/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package co.kr.tnt.trainee.notification

import co.kr.tnt.ui.base.UiEvent
import co.kr.tnt.ui.base.UiSideEffect
import co.kr.tnt.ui.base.UiState
import co.kr.tnt.ui.model.NotificationState

internal class TraineeNotificationContract {
data class TraineeNotificationUiState(
val notifications: List<NotificationState> = emptyList(),
) : UiState

sealed interface TraineeNotificationUiEvent : UiEvent {
data object OnBackClick : TraineeNotificationUiEvent
}

sealed interface TraineeNotificationEffect : UiSideEffect {
data class ShowToast(val message: String) : TraineeNotificationEffect
data object NavigateToPrevious : TraineeNotificationEffect
}
}
Loading

0 comments on commit e318c1f

Please sign in to comment.