diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 801d9b2e7..adbb455aa 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -72,7 +72,7 @@ android:exported="false" android:screenOrientation="portrait" /> (R.layout.activity_notice_detail) { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - -// val noticeItem = intent.getSerializableExtra(NOTICE_DETAIL_KEY) as? NoticeModel -// binding.noticeItem = noticeItem - - onBackButtonClick() - handleBackPressed() -// setupHyperlink(noticeItem?.noticeContent) - } - - private fun onBackButtonClick() { - binding.ivNoticeDetailBack.setOnClickListener { - finish() - } - } - - private fun handleBackPressed() { - onBackPressedDispatcher.addCallback(this) { - finish() - } - } - - private fun setupHyperlink(noticeContent: String?) { - binding.tvNoticeDetailContent.apply { - movementMethod = LinkMovementMethod.getInstance() - text = noticeContent?.let { content -> - Html.fromHtml( - content, - Html.FROM_HTML_MODE_LEGACY, - ) - } - } - } - - companion object { - const val NOTICE_DETAIL_KEY = "NOTICE_DETAIL_KEY" - - fun getIntent( - context: Context, - noticeId: Long, - ): Intent = - Intent(context, NoticeDetailActivity::class.java).apply { - putExtra(NOTICE_DETAIL_KEY, noticeId) - } - } -} diff --git a/app/src/main/java/com/into/websoso/ui/notificationDetail/NotificationDetailActivity.kt b/app/src/main/java/com/into/websoso/ui/notificationDetail/NotificationDetailActivity.kt new file mode 100644 index 000000000..bf5851a03 --- /dev/null +++ b/app/src/main/java/com/into/websoso/ui/notificationDetail/NotificationDetailActivity.kt @@ -0,0 +1,48 @@ +package com.into.websoso.ui.notificationDetail + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.addCallback +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import com.into.websoso.core.designsystem.theme.WebsosoTheme +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class NotificationDetailActivity : ComponentActivity() { + private val notificationDetailViewModel: NotificationDetailViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + handleBackPressed() + + setContent { + WebsosoTheme { + NotificationDetailScreen( + viewModel = notificationDetailViewModel, + onBackButtonClick = { finish() }, + ) + } + } + } + + private fun handleBackPressed() { + onBackPressedDispatcher.addCallback(this) { + finish() + } + } + + companion object { + const val NOTIFICATION_DETAIL_KEY = "NOTIFICATION_DETAIL_KEY" + + fun getIntent( + context: Context, + notificationId: Long, + ): Intent = + Intent(context, NotificationDetailActivity::class.java).apply { + putExtra(NOTIFICATION_DETAIL_KEY, notificationId) + } + } +} diff --git a/app/src/main/java/com/into/websoso/ui/notificationDetail/NotificationDetailScreen.kt b/app/src/main/java/com/into/websoso/ui/notificationDetail/NotificationDetailScreen.kt new file mode 100644 index 000000000..30b47ce2c --- /dev/null +++ b/app/src/main/java/com/into/websoso/ui/notificationDetail/NotificationDetailScreen.kt @@ -0,0 +1,47 @@ +package com.into.websoso.ui.notificationDetail + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.into.websoso.core.designsystem.theme.WebsosoTheme +import com.into.websoso.data.model.NotificationDetailEntity +import com.into.websoso.ui.notice.component.NoticeAppBar +import com.into.websoso.ui.notificationDetail.component.NotificationDetailContent +import com.into.websoso.ui.notificationDetail.model.NotificationDetailUiState + +@Composable +fun NotificationDetailScreen( + viewModel: NotificationDetailViewModel, + onBackButtonClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val uiState by viewModel.notificationDetailUiState.collectAsStateWithLifecycle() + + Column(modifier = modifier.fillMaxSize()) { + NoticeAppBar(onBackButtonClick) + NotificationDetailContent( + uiState = uiState, + ) + } +} + +@Preview(showBackground = true) +@Composable +fun NotificationDetailScreenPreview() { + val uiState = NotificationDetailUiState( + noticeDetail = NotificationDetailEntity.DEFAULT, + ) + + WebsosoTheme { + Column(modifier = Modifier.fillMaxSize()) { + NoticeAppBar(onBackButtonClick = {}) + NotificationDetailContent( + uiState, + ) + } + } +} diff --git a/app/src/main/java/com/into/websoso/ui/notificationDetail/NotificationDetailViewModel.kt b/app/src/main/java/com/into/websoso/ui/notificationDetail/NotificationDetailViewModel.kt new file mode 100644 index 000000000..e5a756413 --- /dev/null +++ b/app/src/main/java/com/into/websoso/ui/notificationDetail/NotificationDetailViewModel.kt @@ -0,0 +1,68 @@ +package com.into.websoso.ui.notificationDetail + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.into.websoso.data.repository.NoticeRepository +import com.into.websoso.ui.notificationDetail.model.NotificationDetailUiState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class NotificationDetailViewModel + @Inject + constructor( + private val noticeRepository: NoticeRepository, + savedStateHandle: SavedStateHandle, + ) : ViewModel() { + private val _notificationDetailUiState: MutableStateFlow = + MutableStateFlow(NotificationDetailUiState()) + val notificationDetailUiState: StateFlow = _notificationDetailUiState.asStateFlow() + + // TODO: 에러처리 회의 종료 후 반영 필요 + private val _errorFlow = MutableSharedFlow() + val errorFlow = _errorFlow.asSharedFlow() + + init { + val notificationId = + savedStateHandle.get(NotificationDetailActivity.NOTIFICATION_DETAIL_KEY) ?: DEFAULT_NOTIFICATION_ID + if (notificationId != DEFAULT_NOTIFICATION_ID) { + getNotificationDetail(notificationId) + } else { + handleInvalidNotificationId() + } + } + + private fun getNotificationDetail(notificationId: Long) { + viewModelScope.launch { + runCatching { + noticeRepository.fetchNotificationDetail(notificationId) + }.onSuccess { noticeDetail -> + _notificationDetailUiState.update { uiState -> + uiState.copy( + noticeDetail = noticeDetail, + ) + } + }.onFailure { throwable -> + _errorFlow.emit(throwable) + } + } + } + + private fun handleInvalidNotificationId() { + viewModelScope.launch { + _errorFlow.emit(IllegalArgumentException("Invalid notification ID")) + } + } + + companion object { + private const val DEFAULT_NOTIFICATION_ID = -1L + } + } diff --git a/app/src/main/java/com/into/websoso/ui/notificationDetail/component/HyperlinkText.kt b/app/src/main/java/com/into/websoso/ui/notificationDetail/component/HyperlinkText.kt new file mode 100644 index 000000000..fd245d626 --- /dev/null +++ b/app/src/main/java/com/into/websoso/ui/notificationDetail/component/HyperlinkText.kt @@ -0,0 +1,88 @@ +package com.into.websoso.ui.notificationDetail.component + +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.platform.LocalUriHandler +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withLink +import androidx.compose.ui.tooling.preview.Preview +import com.into.websoso.core.designsystem.theme.Black +import com.into.websoso.core.designsystem.theme.HyperlinkBlue +import com.into.websoso.core.designsystem.theme.WebsosoTheme + +private const val URL_REGEX = "(https?://[\\w./?=&%#-]+|www\\.[\\w./?=&%#-]+)" + +@Composable +fun HyperlinkText( + text: String, + style: TextStyle = WebsosoTheme.typography.body2, + textColor: Color = Black, + hyperlinkColor: Color = HyperlinkBlue, + hyperlinkDecoration: TextDecoration = TextDecoration.Underline, + modifier: Modifier = Modifier, +) { + val uriHandler = LocalUriHandler.current + val urlRegex = Regex(URL_REGEX) + + val annotatedString = buildAnnotatedString { + var lastIndex = 0 + urlRegex.findAll(text).forEach { matchResult -> + var url = matchResult.value + val startIndex = matchResult.range.first + val endIndex = matchResult.range.last + 1 + + // `www.`로 시작하면 `https://` 추가 + if (url.startsWith("www.")) { + url = "https://$url" + } + + if (startIndex > lastIndex) { + append(text.substring(lastIndex, startIndex)) + } + + val linkUrl = LinkAnnotation.Url( + url, + TextLinkStyles( + SpanStyle( + color = hyperlinkColor, + textDecoration = hyperlinkDecoration, + ), + ), + ) { + uriHandler.openUri(url) + } + + withLink(linkUrl) { append(matchResult.value) } + lastIndex = endIndex + } + + if (lastIndex < text.length) { + append(text.substring(lastIndex)) + } + + addStyle( + style = SpanStyle(color = textColor), + start = 0, + end = text.length, + ) + } + + Text(text = annotatedString, modifier = modifier, style = style) +} + +@Preview(showBackground = true) +@Composable +fun HyperlinkTextPreview() { + WebsosoTheme { + HyperlinkText( + text = "구글은 www.google.com \n네이버는 https://www.naver.com 입니다.\n웹소소는 websoso.kr 입니다.", + ) + } +} diff --git a/app/src/main/java/com/into/websoso/ui/notificationDetail/component/NotificationDetailContent.kt b/app/src/main/java/com/into/websoso/ui/notificationDetail/component/NotificationDetailContent.kt new file mode 100644 index 000000000..a7a0ed52b --- /dev/null +++ b/app/src/main/java/com/into/websoso/ui/notificationDetail/component/NotificationDetailContent.kt @@ -0,0 +1,74 @@ +package com.into.websoso.ui.notificationDetail.component + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.into.websoso.core.designsystem.theme.Black +import com.into.websoso.core.designsystem.theme.Gray200 +import com.into.websoso.core.designsystem.theme.Gray50 +import com.into.websoso.core.designsystem.theme.WebsosoTheme +import com.into.websoso.data.model.NotificationDetailEntity +import com.into.websoso.ui.notificationDetail.model.NotificationDetailUiState + +@Composable +fun NotificationDetailContent( + uiState: NotificationDetailUiState, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxSize(), + ) { + Column( + modifier = modifier + .padding(20.dp), + ) { + Text( + text = uiState.noticeDetail.notificationTitle, + style = WebsosoTheme.typography.headline1, + color = Black, + textAlign = TextAlign.Start, + ) + Spacer(modifier = Modifier.height(10.dp)) + Text( + text = uiState.noticeDetail.notificationCreatedDate, + style = WebsosoTheme.typography.body5, + color = Gray200, + textAlign = TextAlign.Start, + ) + } + HorizontalDivider(thickness = 1.dp, color = Gray50) + HyperlinkText( + uiState.noticeDetail.notificationDetail, + style = + WebsosoTheme.typography.body2, + textColor = Black, + modifier = Modifier + .padding(top = 24.dp, start = 20.dp, end = 20.dp) + .fillMaxSize(), + ) + } +} + +@Preview(showBackground = true) +@Composable +fun NotificationDetailBodyPreview() { + val uiState = NotificationDetailUiState( + noticeDetail = NotificationDetailEntity.DEFAULT, + ) + + WebsosoTheme { + NotificationDetailContent( + uiState, + ) + } +} diff --git a/app/src/main/java/com/into/websoso/ui/notificationDetail/model/NotificationDetailUiState.kt b/app/src/main/java/com/into/websoso/ui/notificationDetail/model/NotificationDetailUiState.kt new file mode 100644 index 000000000..9a158d52b --- /dev/null +++ b/app/src/main/java/com/into/websoso/ui/notificationDetail/model/NotificationDetailUiState.kt @@ -0,0 +1,7 @@ +package com.into.websoso.ui.notificationDetail.model + +import com.into.websoso.data.model.NotificationDetailEntity + +data class NotificationDetailUiState( + val noticeDetail: NotificationDetailEntity = NotificationDetailEntity.DEFAULT, +) diff --git a/app/src/main/res/layout/activity_notice_detail.xml b/app/src/main/res/layout/activity_notice_detail.xml deleted file mode 100644 index 0e8659e72..000000000 --- a/app/src/main/res/layout/activity_notice_detail.xml +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -