diff --git a/app/build.gradle b/app/build.gradle index 58d84760..4709fad5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,8 +29,8 @@ android { applicationId "com.mashup" minSdkVersion minVersion targetSdkVersion targetVersion - versionCode 31 - versionName "1.6.2" + versionCode 34 + versionName "1.7.2" testInstrumentationRunner "com.mashup.core.testing.MashUpTestRunner" vectorDrawables { @@ -107,6 +107,8 @@ dependencies { implementation project(":feature:setting") implementation project(":feature:danggn") implementation project(":feature:myPage") + implementation project(":feature:moreMenu") + implementation project(':feature:moreMenu:notice') // ml Kit implementation "com.google.mlkit:barcode-scanning:$barcodeSacnnerVersion" @@ -164,4 +166,7 @@ dependencies { // Naver Map implementation "io.github.fornewid:naver-map-compose:1.4.0" + + // Date Time + implementation 'com.jakewharton.threetenabp:threetenabp:1.3.1' } \ No newline at end of file diff --git a/app/src/debug/java/com/mashup/data/network/NetworkConstanst.kt b/app/src/debug/java/com/mashup/data/network/NetworkConstanst.kt index c941150a..ce43f3f0 100644 --- a/app/src/debug/java/com/mashup/data/network/NetworkConstanst.kt +++ b/app/src/debug/java/com/mashup/data/network/NetworkConstanst.kt @@ -1,3 +1,4 @@ package com.mashup.data.network const val API_HOST = "https://api.dev-member.mash-up.kr/" +const val WEB_HOST = "https://dev-app.mash-up.kr/" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b4c7f6b2..16a58fa4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,7 +6,8 @@ - + + @@ -15,19 +16,21 @@ - - + + @@ -62,8 +65,9 @@ android:name=".ui.login.LoginActivity" android:exported="true" android:screenOrientation="portrait" - tools:ignore="LockedOrientationActivity"/> - + + tools:ignore="LockedOrientationActivity" /> + tools:ignore="LockedOrientationActivity" /> + + + + + + + diff --git a/app/src/main/java/com/mashup/constant/ExtraConstant.kt b/app/src/main/java/com/mashup/constant/ExtraConstant.kt index 092e1e24..1494c71f 100644 --- a/app/src/main/java/com/mashup/constant/ExtraConstant.kt +++ b/app/src/main/java/com/mashup/constant/ExtraConstant.kt @@ -6,6 +6,7 @@ const val EXTRA_LOGIN_TYPE = "EXTRA_MAIN_TYPE" const val EXTRA_TITLE_KEY = "EXTRA_TITLE_KEY" const val EXTRA_URL_KEY = "EXTRA_URL_KEY" const val EXTRA_SCHEDULE_ID = "EXTRA_SCHEDULE_ID" +const val EXTRA_SCHEDULE_TYPE = "EXTRA_SCHEDULE_TYPE" const val EXTRA_LOGOUT = "EXTRA_LOGOUT" const val EXTRA_WITH_DRAWL = "EXTRA_WITH_DRAWL" diff --git a/app/src/main/java/com/mashup/constant/log/UserActionLogs.kt b/app/src/main/java/com/mashup/constant/log/UserActionLogs.kt index 61194d3f..0388170d 100644 --- a/app/src/main/java/com/mashup/constant/log/UserActionLogs.kt +++ b/app/src/main/java/com/mashup/constant/log/UserActionLogs.kt @@ -1,42 +1,96 @@ package com.mashup.constant.log -const val LOG_BACK = "back" -const val LOG_CLOSE = "close" +/** + * Category Common + */ +const val LOG_COMMON_BACK = "back" +const val LOG_COMMON_CLOSE = "close" +const val LOG_COMMON_POPUP_CONFIRM = "popup_new_confirm" +const val LOG_COMMON_POPUP_CANCEL = "popup_new_cancel" +/** + * Category SignUp + */ const val LOG_LOGIN = "login" const val LOG_SIGN_UP = "signup" - -const val LOG_LOGOUT = "logout" -const val LOG_DELETE_USER = "delete_user" -const val LOG_SNS_FACEBOOK = "facebook" -const val LOG_SNS_INSTAGRAM = "instagram" -const val LOG_SNS_TISTORY = "tistory" -const val LOG_SNS_YOUTUBE = "youtube" -const val LOG_SNS_MASHUP_HOME = "mashup_home" -const val LOG_SNS_MASHUP_RECRUIT = "mashup_recruit" -const val LOG_PLACE_SIGN_CODE = "signup_code" -const val LOG_PLACE_SIGN_MEMBER_INFO = "signup_info" -const val LOG_PLACE_SIGN_PLATFORM = "signup_platform" const val LOG_POPUP_SIGNUP_CONFIRM = "popup_signup_confirm" const val LOG_POPUP_SIGNUP_CANCEL = "popup_signup_cancel" - const val LOG_PLACE_CHANGE_PASSWORD = "change_password" -const val LOG_PLACE_ENTER_ID = "enter_id" -const val LOG_SCHEDULE_LIST_REFRESH = "refresh" -const val LOG_SCHEDULE_STATUS_CONFIRM = "status_confirm" -const val LOG_SCHEDULE_EVENT_DETAIL = "event_detail" +/** + * Category Event List + */ +const val LOG_EVENT_LIST_REFRESH = "refresh" +const val LOG_EVENT_LIST_STATUS_CONFIRM = "status_confirm" +const val LOG_EVENT_LIST_EVENT_DETAIL = "event_detail" +const val LOG_EVENT_LIST_WEEK_MASHONG = "week_mashong" +const val LOG_EVENT_LIST_WEEK = "week" +const val LOG_EVENT_LIST_ALL = "all" +const val LOG_EVENT_LIST_DETAIL_COPY = "detail_copy" + +/** + * Category More + */ +const val LOG_MORE_BIRTH = "more_birth" +const val LOG_MORE_MASHONG = "more_mashong" +const val LOG_MORE_CARROT = "more_carrot" +const val LOG_MORE_ALARM = "more_alarm" +const val LOG_MORE_SETTING = "more_setting" + +/** + * Category Alarm + */ +const val LOG_ALARM_LIST = "alarm_list" +/** + * Category Logout + */ +const val LOG_POPUP_LOGOUT_CONFIRM = "popup_logout_confirm" +const val LOG_POPUP_LOGOUT_CANCEL = "popup_logout_cancel" + +/** + * Category QR + */ const val LOG_QR = "qr" const val LOG_QR_SUCCESS = "qr_success" const val LOG_QR_DONE = "qr_done" const val LOG_QR_TIME_FAIL = "qr_time_fail" const val LOG_QR_WRONG = "qr_wrong" +const val LOG_QR_LOCATION = "qr_location" +const val LOG_QR_DISTANCE_OUT_OF_RANGE = "qr_distance_out_of_range" -const val LOG_DELETE_SUCCESS_USER = "delete_user_success" +/** + * Category MyPage + */ +const val LOG_MYPAGE_HELP = "help" -const val LOG_COMMON_POPUP_CONFIRM = "popup_new_confirm" -const val LOG_COMMON_POPUP_CANCEL = "popup_new_cancel" +/** + * Category Setting + */ +const val LOG_SETTING_LOGOUT = "logout" +const val LOG_SETTING_DELETE_USER = "delete_user" +const val LOG_SETTING_SNS_FACEBOOK = "facebook" +const val LOG_SETTING_SNS_INSTAGRAM = "instagram" +const val LOG_SETTING_SNS_TISTORY = "tistory" +const val LOG_SETTING_SNS_YOUTUBE = "youtube" +const val LOG_SETTING_SNS_MASHUP_HOME = "mashup_home" +const val LOG_SETTING_SNS_MASHUP_RECRUIT = "mashup_recruit" +/** + * Category Delete User + */ +const val LOG_DELETE_USER_SUCCESS = "delete_user_success" + +/** + * Category Danggn + */ const val LOG_DANGGN = "danggn" const val LOG_DANGGN_HELP = "danggn_help" + +/** + * UnCategorization + */ +const val LOG_PLACE_SIGN_CODE = "signup_code" +const val LOG_PLACE_SIGN_MEMBER_INFO = "signup_info" +const val LOG_PLACE_SIGN_PLATFORM = "signup_platform" +const val LOG_PLACE_ENTER_ID = "enter_id" diff --git a/app/src/main/java/com/mashup/data/dto/ScheduleResponse.kt b/app/src/main/java/com/mashup/data/dto/ScheduleResponse.kt index 8d1a97e5..80003897 100644 --- a/app/src/main/java/com/mashup/data/dto/ScheduleResponse.kt +++ b/app/src/main/java/com/mashup/data/dto/ScheduleResponse.kt @@ -9,6 +9,10 @@ import java.util.Locale @JsonClass(generateAdapter = true) data class ScheduleResponse( + @field:Json(name = "scheduleType") + val scheduleType: String, + @field:Json(name = "notice") + val notice: String?, @field:Json(name = "scheduleId") val scheduleId: Int, @field:Json(name = "dateCount") @@ -54,9 +58,11 @@ data class ScheduleResponse( dateCount == 0 -> { "D-Day" } + dateCount > 0 -> { "D-$dateCount" } + else -> { "D+${-dateCount}" } diff --git a/app/src/main/java/com/mashup/di/NetworkModule.kt b/app/src/main/java/com/mashup/di/NetworkModule.kt index bda8c4a2..047d8941 100644 --- a/app/src/main/java/com/mashup/di/NetworkModule.kt +++ b/app/src/main/java/com/mashup/di/NetworkModule.kt @@ -5,7 +5,9 @@ import com.facebook.flipper.plugins.network.NetworkFlipperPlugin import com.mashup.BuildConfig.DEBUG_MODE import com.mashup.core.model.Platform import com.mashup.core.network.adapter.PlatformJsonAdapter +import com.mashup.core.network.dao.MetaDao import com.mashup.core.network.dao.PopupDao +import com.mashup.core.network.dao.PushHistoryDao import com.mashup.core.network.dao.StorageDao import com.mashup.data.network.API_HOST import com.mashup.network.CustomDateAdapter @@ -145,4 +147,20 @@ class NetworkModule { ): MemberProfileDao { return retrofit.create() } + + @Provides + @Singleton + fun provideMetaDao( + retrofit: Retrofit + ): MetaDao { + return retrofit.create() + } + + @Provides + @Singleton + fun providePushHistoryDao( + retrofit: Retrofit + ): PushHistoryDao { + return retrofit.create() + } } diff --git a/app/src/main/java/com/mashup/service/MashUpFirebaseMessagingService.kt b/app/src/main/java/com/mashup/service/MashUpFirebaseMessagingService.kt index 90587a55..3210b1b8 100644 --- a/app/src/main/java/com/mashup/service/MashUpFirebaseMessagingService.kt +++ b/app/src/main/java/com/mashup/service/MashUpFirebaseMessagingService.kt @@ -13,12 +13,15 @@ import android.os.Build import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat +import androidx.core.os.bundleOf import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage import com.mashup.BuildConfig import com.mashup.R import com.mashup.constant.EXTRA_LINK +import com.mashup.constant.log.LOG_ALARM_LIST import com.mashup.ui.splash.SplashActivity +import com.mashup.util.AnalyticsManager import dagger.hilt.android.AndroidEntryPoint import java.net.URL @@ -58,6 +61,13 @@ class MashUpFirebaseMessagingService : FirebaseMessagingService() { imageUrl: Uri?, data: Map ) { + AnalyticsManager.addEvent( + eventName = LOG_ALARM_LIST, + params = bundleOf( + "place" to "PUSH", + "type" to PushLinkType.getPushLinkType(data[EXTRA_LINK].orEmpty()).name + ) + ) val splashIntent = Intent(this, SplashActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP putExtra(EXTRA_LINK, data[EXTRA_LINK]) diff --git a/app/src/main/java/com/mashup/service/PushLinkType.kt b/app/src/main/java/com/mashup/service/PushLinkType.kt index ae3e1982..b015cd70 100644 --- a/app/src/main/java/com/mashup/service/PushLinkType.kt +++ b/app/src/main/java/com/mashup/service/PushLinkType.kt @@ -5,6 +5,8 @@ enum class PushLinkType { QR, // QR 페이지 DANGGN, // 당근 페이지 DANGGN_REWARD, + BIRTHDAY, // 생일 축하 + MASHONG, // 매숑이 키우기 MYPAGE, UNKNOWN ; diff --git a/app/src/main/java/com/mashup/ui/login/LoginActivity.kt b/app/src/main/java/com/mashup/ui/login/LoginActivity.kt index 97ae5ad4..a0fd12d3 100644 --- a/app/src/main/java/com/mashup/ui/login/LoginActivity.kt +++ b/app/src/main/java/com/mashup/ui/login/LoginActivity.kt @@ -26,6 +26,8 @@ import com.mashup.ui.main.model.MainTab import com.mashup.ui.password.PasswordActivity import com.mashup.ui.qrscan.QRScanActivity import com.mashup.ui.signup.SignUpActivity +import com.mashup.ui.webview.birthday.BirthdayActivity +import com.mashup.ui.webview.mashong.MashongActivity import com.mashup.util.AnalyticsManager import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collectLatest @@ -160,6 +162,14 @@ class LoginActivity : BaseActivity() { ) } + PushLinkType.BIRTHDAY -> { + buildTaskStack(baseIntent, BirthdayActivity.newIntent(this)) + } + + PushLinkType.MASHONG -> { + buildTaskStack(baseIntent, MashongActivity.newIntent(this)) + } + PushLinkType.QR -> { buildTaskStack(baseIntent, QRScanActivity.newIntent(this)) } diff --git a/app/src/main/java/com/mashup/ui/main/MainActivity.kt b/app/src/main/java/com/mashup/ui/main/MainActivity.kt index 4c230395..231c8ef0 100644 --- a/app/src/main/java/com/mashup/ui/main/MainActivity.kt +++ b/app/src/main/java/com/mashup/ui/main/MainActivity.kt @@ -5,11 +5,14 @@ import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.os.Build +import android.os.Bundle import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.navOptions +import com.jakewharton.threetenabp.AndroidThreeTen import com.mashup.R import com.mashup.base.BaseActivity import com.mashup.constant.EXTRA_ANIMATION @@ -30,6 +33,7 @@ import com.mashup.ui.main.model.MainTab import com.mashup.ui.main.popup.MainBottomPopup import com.mashup.ui.qrscan.CongratsAttendanceScreen import com.mashup.ui.qrscan.QRScanActivity +import com.mashup.ui.webview.birthday.BirthdayActivity import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -58,12 +62,18 @@ class MainActivity : BaseActivity() { viewModel.confirmAttendance() viewModel.successAttendance() } + QRScanActivity.RESULT_CONFIRM_QR -> { viewModel.confirmAttendance() } } } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + AndroidThreeTen.init(this) + } + override fun initViews() { super.initViews() @@ -131,6 +141,17 @@ class MainActivity : BaseActivity() { ) ) } + + MainPopupType.BIRTHDAY_CELEBRATION -> { + viewModel.disablePopup(popupType) + startActivity( + BirthdayActivity.newIntent( + context = this@MainActivity, + urlKey = "birthday/event" + ) + ) + } + else -> { } } @@ -140,18 +161,22 @@ class MainActivity : BaseActivity() { } private fun navigationTab(toDestination: MainTab) { - val currentNavigationId = navController.currentDestination?.id val newNavigationId = when (toDestination) { MainTab.EVENT -> { R.id.eventFragment } + MainTab.MY_PAGE -> { R.id.myPageFragment } } - if (currentNavigationId != newNavigationId) { - navController.navigate(newNavigationId) + val navOptions = navOptions { + popUpTo(newNavigationId) { + saveState = true + } + launchSingleTop = true } + navController.navigate(newNavigationId, null, navOptions) } private fun setUIOfTab(tab: MainTab) = with(viewBinding.layoutMainTab) { @@ -176,6 +201,7 @@ class MainActivity : BaseActivity() { tvMyPage.setTextColor(unSelectedColor) imgMyPage.imageTintList = unSelectedColorList } + MainTab.MY_PAGE -> { tvEvent.setTextColor(unSelectedColor) imgEvent.imageTintList = unSelectedColorList diff --git a/app/src/main/java/com/mashup/ui/main/model/MainPopupType.kt b/app/src/main/java/com/mashup/ui/main/model/MainPopupType.kt index 8c0c21cd..41d142a2 100644 --- a/app/src/main/java/com/mashup/ui/main/model/MainPopupType.kt +++ b/app/src/main/java/com/mashup/ui/main/model/MainPopupType.kt @@ -1,7 +1,7 @@ package com.mashup.ui.main.model enum class MainPopupType { - DANGGN, DANGGN_UPDATE, UNKNOWN; + DANGGN, DANGGN_UPDATE, BIRTHDAY_CELEBRATION, UNKNOWN; companion object { fun getMainPopupType(type: String): MainPopupType { diff --git a/app/src/main/java/com/mashup/ui/main/popup/MainBottomPopup.kt b/app/src/main/java/com/mashup/ui/main/popup/MainBottomPopup.kt index 3e2b3d14..45868f87 100644 --- a/app/src/main/java/com/mashup/ui/main/popup/MainBottomPopup.kt +++ b/app/src/main/java/com/mashup/ui/main/popup/MainBottomPopup.kt @@ -96,12 +96,18 @@ class MainBottomPopup : BottomSheetDialogFragment() { MainBottomPopupScreen( viewModel = viewModel, onClickLeftButton = { - AnalyticsManager.addEvent(LOG_COMMON_POPUP_CANCEL, bundleOf("key" to viewModel.popupKey)) + AnalyticsManager.addEvent( + LOG_COMMON_POPUP_CANCEL, + bundleOf("key" to viewModel.popupKey) + ) dismiss() }, onClickRightButton = { mainViewModel.onClickPopup(viewModel.popupKey.orEmpty()) - AnalyticsManager.addEvent(LOG_COMMON_POPUP_CONFIRM, bundleOf("key" to viewModel.popupKey)) + AnalyticsManager.addEvent( + LOG_COMMON_POPUP_CONFIRM, + bundleOf("key" to viewModel.popupKey) + ) dismiss() } ) @@ -215,19 +221,23 @@ fun MainBottomPopupContent( .padding(horizontal = 20.dp), horizontalArrangement = Arrangement.spacedBy(12.dp) ) { - MashUpButton( - modifier = Modifier.wrapContentWidth(), - text = mainPopupEntity.leftButtonText, - buttonStyle = ButtonStyle.INVERSE, - onClick = onClickLeftButton - ) + if (mainPopupEntity.leftButtonText.isNotEmpty()) { + MashUpButton( + modifier = Modifier.wrapContentWidth(), + text = mainPopupEntity.leftButtonText, + buttonStyle = ButtonStyle.INVERSE, + onClick = onClickLeftButton + ) + } - MashUpButton( - modifier = Modifier.weight(1f), - text = mainPopupEntity.rightButtonText, - buttonStyle = ButtonStyle.PRIMARY, - onClick = onClickRightButton - ) + if (mainPopupEntity.rightButtonText.isNotEmpty()) { + MashUpButton( + modifier = Modifier.weight(1f), + text = mainPopupEntity.rightButtonText, + buttonStyle = ButtonStyle.PRIMARY, + onClick = onClickRightButton + ) + } } Spacer(modifier = Modifier.height(24.dp)) diff --git a/app/src/main/java/com/mashup/ui/main/popup/MainBottomPopupViewModel.kt b/app/src/main/java/com/mashup/ui/main/popup/MainBottomPopupViewModel.kt index fce4e18e..6248ed89 100644 --- a/app/src/main/java/com/mashup/ui/main/popup/MainBottomPopupViewModel.kt +++ b/app/src/main/java/com/mashup/ui/main/popup/MainBottomPopupViewModel.kt @@ -7,14 +7,17 @@ import com.mashup.constant.EXTRA_POPUP_KEY import com.mashup.core.common.base.BaseViewModel import com.mashup.core.data.repository.PopUpRepository import com.mashup.core.data.repository.StorageRepository +import com.mashup.datastore.data.repository.UserPreferenceRepository import com.mashup.ui.main.model.MainPopupEntity import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.first import javax.inject.Inject @HiltViewModel class MainBottomPopupViewModel @Inject constructor( private val storageRepository: StorageRepository, private val popUpRepository: PopUpRepository, + private val userPreferenceRepository: UserPreferenceRepository, savedStateHandle: SavedStateHandle ) : BaseViewModel() { val popupKey = savedStateHandle.get(EXTRA_POPUP_KEY) @@ -30,10 +33,13 @@ class MainBottomPopupViewModel @Inject constructor( if (popupKey.isNullOrBlank()) return@mashUpScope val result = storageRepository.getStorage(popupKey) + val userName = userPreferenceRepository.getUserPreference().first().name + val title = result.data?.valueMap?.get("title").orEmpty().replace("\${name}", userName) + if (result.isSuccess()) { _uiState.value = MainBottomPopupUiState.Success( MainPopupEntity( - title = result.data?.valueMap?.get("title").orEmpty(), + title = title, description = result.data?.valueMap?.get("subtitle").orEmpty(), imageResName = result.data?.valueMap?.get("imageName").orEmpty(), leftButtonText = result.data?.valueMap?.get("leftButtonTitle").orEmpty(), diff --git a/app/src/main/java/com/mashup/ui/moremenu/MoreMenuActivity.kt b/app/src/main/java/com/mashup/ui/moremenu/MoreMenuActivity.kt new file mode 100644 index 00000000..c949fdc0 --- /dev/null +++ b/app/src/main/java/com/mashup/ui/moremenu/MoreMenuActivity.kt @@ -0,0 +1,79 @@ +package com.mashup.ui.moremenu + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.example.moremenu.MoreMenuRoute +import com.example.moremenu.model.Menu +import com.example.moremenu.model.MenuType +import com.example.moremenu.model.MoreMenuSideEffect +import com.mashup.core.ui.theme.MashUpTheme +import com.mashup.ui.danggn.ShakeDanggnActivity +import com.mashup.ui.notice.NoticeActivity +import com.mashup.ui.setting.SettingActivity +import com.mashup.ui.webview.birthday.BirthdayActivity +import com.mashup.ui.webview.mashong.MashongActivity +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class MoreMenuActivity : ComponentActivity() { + + private val moreMenuViewModel: MoreMenuViewModel by viewModels() + + override fun onResume() { + super.onResume() + moreMenuViewModel.getMoreMenuState() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + moreMenuViewModel.moreMenuEvent.collect { sideEffect -> + when (sideEffect) { + is MoreMenuSideEffect.NavigateMenu -> onNavigateMenu(sideEffect.menu) + is MoreMenuSideEffect.NavigateBackStack -> finish() + } + } + } + } + setContent { + MashUpTheme { + val moreMenuState by moreMenuViewModel.moreMenuState.collectAsState() + Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + MoreMenuRoute( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + moreMenuState = moreMenuState, + onBackPressed = moreMenuViewModel::onClickBackButton, + onClickMenu = moreMenuViewModel::onClickMenuButton + ) + } + } + } + } + + private fun onNavigateMenu(menu: Menu) { + val intent = when (menu.type) { + MenuType.NOTI -> NoticeActivity.newIntent(this) + MenuType.DANGGN -> ShakeDanggnActivity.newIntent(this) + MenuType.MASHONG -> MashongActivity.newIntent(this) + MenuType.SETTING -> SettingActivity.newIntent(this) + MenuType.BIRTHDAY -> BirthdayActivity.newIntent(this) + } + startActivity(intent) + } +} diff --git a/app/src/main/java/com/mashup/ui/moremenu/MoreMenuViewModel.kt b/app/src/main/java/com/mashup/ui/moremenu/MoreMenuViewModel.kt new file mode 100644 index 00000000..2bce389e --- /dev/null +++ b/app/src/main/java/com/mashup/ui/moremenu/MoreMenuViewModel.kt @@ -0,0 +1,134 @@ +package com.mashup.ui.moremenu + +import androidx.core.os.bundleOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.moremenu.model.Menu +import com.example.moremenu.model.Menu.Companion.toMenu +import com.example.moremenu.model.MoreMenuSideEffect +import com.example.moremenu.model.MoreMenuState +import com.mashup.constant.log.LOG_MORE_ALARM +import com.mashup.constant.log.LOG_MORE_BIRTH +import com.mashup.constant.log.LOG_MORE_CARROT +import com.mashup.constant.log.LOG_MORE_MASHONG +import com.mashup.constant.log.LOG_MORE_SETTING +import com.mashup.core.data.repository.MetaRepository +import com.mashup.core.data.repository.PushHistoryRepository +import com.mashup.core.network.Response +import com.mashup.core.network.dto.PushHistoryResponse +import com.mashup.core.network.dto.RnbResponse +import com.mashup.util.AnalyticsManager +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class MoreMenuViewModel @Inject constructor( + private val metaRepository: MetaRepository, + private val pushHistoryRepository: PushHistoryRepository +) : ViewModel() { + + private val _moreMenuState = MutableStateFlow(MoreMenuState()) + val moreMenuState = _moreMenuState.asStateFlow() + + private val _moreMenuEvent = MutableSharedFlow() + val moreMenuEvent = _moreMenuEvent.asSharedFlow() + + fun getMoreMenuState() { + viewModelScope.launch { + val rnb: Flow> = flow { emit(metaRepository.getRnb()) } + val notice: Flow> = + flow { emit(pushHistoryRepository.getPushHistory(page = 0, size = 1)) } + + combine(rnb, notice) { rnbFlow, noticeFlow -> + rnbFlow to noticeFlow + }.collect { (rnb, notice) -> + + when { + rnb.isSuccess() && notice.isSuccess() -> { + val rnbResponse = rnb.data?.toMenu() ?: emptyList() + val isHasNewNotice = notice.data?.unread?.isNotEmpty() ?: false + _moreMenuState.value = MoreMenuState( + menus = rnbResponse, + isShowNewIcon = isHasNewNotice + ) + } + + rnb.isSuccess() && notice.isSuccess().not() -> { + val rnbResponse = rnb.data?.toMenu() ?: emptyList() + _moreMenuState.value = MoreMenuState( + menus = rnbResponse, + isShowNewIcon = false + ) + } + + rnb.isSuccess().not() -> { + _moreMenuState.value = MoreMenuState( + menus = emptyList(), + isShowNewIcon = false + ) + } + } + } + } + } + + fun onClickBackButton() { + viewModelScope.launch { + _moreMenuEvent.emit(MoreMenuSideEffect.NavigateBackStack) + } + } + + fun onClickMenuButton(menu: Menu) { + val bundle = bundleOf( + "place" to "LIST", + "type" to menu.menuName + ) + when (menu) { + is Menu.Noti -> { + AnalyticsManager.addEvent( + eventName = LOG_MORE_ALARM, + params = bundle + ) + } + + is Menu.Setting -> { + AnalyticsManager.addEvent( + eventName = LOG_MORE_SETTING, + params = bundle + ) + } + + is Menu.Mashong -> { + AnalyticsManager.addEvent( + eventName = LOG_MORE_MASHONG, + params = bundle + ) + } + + is Menu.Danggn -> { + AnalyticsManager.addEvent( + eventName = LOG_MORE_CARROT, + params = bundle + ) + } + + is Menu.BirthDay -> { + AnalyticsManager.addEvent( + eventName = LOG_MORE_BIRTH, + params = bundle + ) + } + } + viewModelScope.launch { + _moreMenuEvent.emit(MoreMenuSideEffect.NavigateMenu(menu)) + } + } +} diff --git a/app/src/main/java/com/mashup/ui/mypage/AttendanceType.kt b/app/src/main/java/com/mashup/ui/mypage/AttendanceType.kt index e70239e9..1fdd755d 100644 --- a/app/src/main/java/com/mashup/ui/mypage/AttendanceType.kt +++ b/app/src/main/java/com/mashup/ui/mypage/AttendanceType.kt @@ -19,6 +19,8 @@ enum class AttendanceType(@DrawableRes val resourceId: Int) { MASHUP_SUBLEADER(R.drawable.img_mashupsubleader), TECH_BLOG_WRITE(R.drawable.img_techblogwrite), MASHUP_CONTENTS_WRITE(R.drawable.img_mashupcontentswrite), + ADD_SCORE_DURING_SEMINAR_ACTIVITY_0_5(R.drawable.img_presentation), + ADD_SCORE_DURING_SEMINAR_ACTIVITY_1(R.drawable.img_presentation), DEFAULT(R.drawable.img_default_score); companion object { diff --git a/app/src/main/java/com/mashup/ui/mypage/MyPageFragment.kt b/app/src/main/java/com/mashup/ui/mypage/MyPageFragment.kt index a4310bba..5cb2878a 100644 --- a/app/src/main/java/com/mashup/ui/mypage/MyPageFragment.kt +++ b/app/src/main/java/com/mashup/ui/mypage/MyPageFragment.kt @@ -9,11 +9,13 @@ import androidx.core.content.ContextCompat import androidx.fragment.app.viewModels import com.mashup.R import com.mashup.base.BaseFragment +import com.mashup.core.common.extensions.setStatusBarColorRes import com.mashup.databinding.FragmentMyPageBinding import com.mashup.feature.mypage.profile.model.ProfileCardData import com.mashup.ui.setting.SettingActivity import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collectLatest +import com.mashup.core.common.R as CR @AndroidEntryPoint class MyPageFragment : BaseFragment() { @@ -62,10 +64,15 @@ class MyPageFragment : BaseFragment() { override fun initViews() { super.initViews() + initStatusBar() initSwipeRefresh() initRecyclerView() } + private fun initStatusBar() { + requireActivity().setStatusBarColorRes(CR.color.gray50) + } + private fun initSwipeRefresh() { viewBinding.layoutSwipe.apply { setOnRefreshListener { viewModel.getMyPageData() } diff --git a/app/src/main/java/com/mashup/ui/notice/NoticeActivity.kt b/app/src/main/java/com/mashup/ui/notice/NoticeActivity.kt new file mode 100644 index 00000000..a68d2440 --- /dev/null +++ b/app/src/main/java/com/mashup/ui/notice/NoticeActivity.kt @@ -0,0 +1,90 @@ +package com.mashup.ui.notice + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.example.notice.NoticeRoute +import com.example.notice.model.NoticeSideEffect +import com.mashup.core.ui.theme.MashUpTheme +import com.mashup.ui.danggn.ShakeDanggnActivity +import com.mashup.ui.login.LoginType +import com.mashup.ui.main.MainActivity +import com.mashup.ui.qrscan.QRScanActivity +import com.mashup.ui.webview.birthday.BirthdayActivity +import com.mashup.ui.webview.mashong.MashongActivity +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class NoticeActivity : ComponentActivity() { + private val noticeViewModel: NoticeViewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + MashUpTheme { + val noticeState by noticeViewModel.noticeState.collectAsState() + + LaunchedEffect(Unit) { + noticeViewModel.noticeEvent.collect { + when (it) { + is NoticeSideEffect.OnBackPressed -> finish() + is NoticeSideEffect.OnNavigateMenu -> { + val intent = when (it.notice.linkType) { + "QR" -> { + QRScanActivity.newIntent(this@NoticeActivity) + } + + "DANGGN" -> { + ShakeDanggnActivity.newIntent(this@NoticeActivity) + } + + "BIRTHDAY" -> { + BirthdayActivity.newIntent(this@NoticeActivity) + } + + "MASHONG" -> { + MashongActivity.newIntent(this@NoticeActivity) + } + + "SEMINAR" -> { + MainActivity.newIntent( + this@NoticeActivity, + loginType = LoginType.AUTO + ) + } + + else -> { + MainActivity.newIntent( + this@NoticeActivity, + loginType = LoginType.AUTO + ) + } + } + startActivity(intent) + } + } + } + } + + NoticeRoute( + modifier = Modifier.fillMaxSize(), + noticeState = noticeState, + onBackPressed = noticeViewModel::onBackPressed, + onClickNoticeItem = noticeViewModel::onClickNoticeItem, + onLoadNextNotice = noticeViewModel::onLoadNextNotice + ) + } + } + } + + companion object { + fun newIntent(context: Context) = Intent(context, NoticeActivity::class.java) + } +} diff --git a/app/src/main/java/com/mashup/ui/notice/NoticeViewModel.kt b/app/src/main/java/com/mashup/ui/notice/NoticeViewModel.kt new file mode 100644 index 00000000..f6f7539e --- /dev/null +++ b/app/src/main/java/com/mashup/ui/notice/NoticeViewModel.kt @@ -0,0 +1,109 @@ +package com.mashup.ui.notice + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.notice.model.NoticeSideEffect +import com.example.notice.model.NoticeState +import com.mashup.core.data.repository.PushHistoryRepository +import com.mashup.core.network.Response +import com.mashup.core.network.dto.PushHistoryResponse +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class NoticeViewModel @Inject constructor( + private val pushHistoryRepository: PushHistoryRepository +) : ViewModel() { + + private val _noticeState = MutableStateFlow(NoticeState()) + val noticeState = _noticeState.asStateFlow() + + private val _noticeEvent = MutableSharedFlow() + val noticeEvent = _noticeEvent.asSharedFlow() + + private var _currentPage = MutableStateFlow(0) + + init { + viewModelScope.launch { + val noticeResponse: Response = + pushHistoryRepository.getPushHistory( + page = _currentPage.value, + size = PAGE_SIZE, + sort = DESC + ) + + if (noticeResponse.isSuccess()) { + val pushHistoryResponse = noticeResponse.data + onReadNewNoticeList() + _noticeState.value = NoticeState( + oldNoticeList = pushHistoryResponse?.read ?: emptyList(), + newNoticeList = pushHistoryResponse?.unread ?: emptyList(), + isError = false + ) + } else { + _noticeState.value = NoticeState( + isError = true + ) + } + } + } + + fun onLoadNextNotice() { + viewModelScope.launch { + _currentPage.value += 1 + val noticeResponse: Response = + pushHistoryRepository.getPushHistory( + page = _currentPage.value, + size = PAGE_SIZE, + sort = DESC + ) + + if (noticeResponse.isSuccess()) { + val pushHistoryResponse = noticeResponse.data + onReadNewNoticeList() + _noticeState.value = NoticeState( + oldNoticeList = _noticeState.value.oldNoticeList + pushHistoryResponse?.read.orEmpty(), + newNoticeList = _noticeState.value.newNoticeList + pushHistoryResponse?.unread.orEmpty(), + isError = false + ) + } else { + _noticeState.value = NoticeState( + isError = true + ) + } + } + } + + fun onBackPressed() { + viewModelScope.launch { + _noticeEvent.emit(NoticeSideEffect.OnBackPressed) + } + } + + fun onClickNoticeItem(notice: PushHistoryResponse.Notice) { + viewModelScope.launch { + _noticeEvent.emit(NoticeSideEffect.OnNavigateMenu(notice)) + } + } + + private fun onReadNewNoticeList() { + viewModelScope.launch { + val currentPage = _currentPage.value + pushHistoryRepository.postPushHistoryCheck( + page = currentPage, + size = PAGE_SIZE, + sort = DESC + ) + } + } + + companion object { + const val PAGE_SIZE = 100 + const val DESC = "createdAt,desc" + } +} diff --git a/app/src/main/java/com/mashup/ui/password/PasswordActivity.kt b/app/src/main/java/com/mashup/ui/password/PasswordActivity.kt index fc2df900..4c4bc5ab 100644 --- a/app/src/main/java/com/mashup/ui/password/PasswordActivity.kt +++ b/app/src/main/java/com/mashup/ui/password/PasswordActivity.kt @@ -10,7 +10,7 @@ import com.mashup.R import com.mashup.base.BaseActivity import com.mashup.constant.EXTRA_ANIMATION import com.mashup.constant.log.KEY_PLACE -import com.mashup.constant.log.LOG_BACK +import com.mashup.constant.log.LOG_COMMON_BACK import com.mashup.constant.log.LOG_PLACE_CHANGE_PASSWORD import com.mashup.constant.log.LOG_PLACE_ENTER_ID import com.mashup.core.common.model.NavigationAnimationType @@ -41,7 +41,7 @@ class PasswordActivity : BaseActivity() { override fun onBackPressed() { getPlaceGALog()?.run { AnalyticsManager.addEvent( - LOG_BACK, + LOG_COMMON_BACK, bundleOf(KEY_PLACE to this) ) } diff --git a/app/src/main/java/com/mashup/ui/schedule/ScheduleFragment.kt b/app/src/main/java/com/mashup/ui/schedule/ScheduleFragment.kt index 1af862c7..fc4e15cf 100644 --- a/app/src/main/java/com/mashup/ui/schedule/ScheduleFragment.kt +++ b/app/src/main/java/com/mashup/ui/schedule/ScheduleFragment.kt @@ -1,5 +1,6 @@ package com.mashup.ui.schedule +import android.content.Intent import android.os.Bundle import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.ui.Modifier @@ -7,10 +8,14 @@ import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import com.mashup.R import com.mashup.base.BaseFragment +import com.mashup.core.common.extensions.setStatusBarColorRes +import com.mashup.core.common.utils.ToastUtil import com.mashup.core.ui.theme.MashUpTheme import com.mashup.databinding.FragmentScheduleBinding import com.mashup.ui.main.MainViewModel +import com.mashup.ui.moremenu.MoreMenuActivity import dagger.hilt.android.AndroidEntryPoint +import com.mashup.core.common.R as CR @AndroidEntryPoint class ScheduleFragment : BaseFragment() { @@ -27,12 +32,20 @@ class ScheduleFragment : BaseFragment() { override fun initViews() { super.initViews() + requireActivity().setStatusBarColorRes(CR.color.white) viewBinding.cvSchedule.setContent { MashUpTheme { ScheduleRoute( modifier = Modifier.fillMaxSize(), mainViewModel = mainViewModel, - viewModel = viewModel + viewModel = viewModel, + onClickMoreMenuIcon = { + val intent = Intent(requireActivity(), MoreMenuActivity::class.java) + requireActivity().startActivity(intent) + }, + makeToast = { message -> + ToastUtil.showToast(requireContext(), message) + } ) } } diff --git a/app/src/main/java/com/mashup/ui/schedule/ScheduleRoute.kt b/app/src/main/java/com/mashup/ui/schedule/ScheduleRoute.kt new file mode 100644 index 00000000..67494bb6 --- /dev/null +++ b/app/src/main/java/com/mashup/ui/schedule/ScheduleRoute.kt @@ -0,0 +1,267 @@ +package com.mashup.ui.schedule + +import android.content.Context +import androidx.compose.foundation.Image +import androidx.compose.foundation.LocalOverscrollConfiguration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +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.rememberLazyListState +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +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.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.mashup.R +import com.mashup.constant.log.LOG_EVENT_LIST_ALL +import com.mashup.constant.log.LOG_EVENT_LIST_STATUS_CONFIRM +import com.mashup.constant.log.LOG_EVENT_LIST_WEEK +import com.mashup.constant.log.LOG_EVENT_LIST_WEEK_MASHONG +import com.mashup.core.common.extensions.fromHtml +import com.mashup.core.ui.colors.Brand500 +import com.mashup.core.ui.colors.Gray50 +import com.mashup.core.ui.colors.White +import com.mashup.core.ui.theme.MashUpTheme +import com.mashup.core.ui.widget.MashUpHtmlText +import com.mashup.ui.attendance.platform.PlatformAttendanceActivity +import com.mashup.ui.main.MainViewModel +import com.mashup.ui.schedule.component.ScheduleTabRow +import com.mashup.ui.schedule.detail.ScheduleDetailActivity +import com.mashup.ui.schedule.model.ScheduleType +import com.mashup.ui.webview.mashong.MashongActivity +import com.mashup.util.AnalyticsManager +import com.mashup.core.common.R as CR + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun ScheduleRoute( + mainViewModel: MainViewModel, + viewModel: ScheduleViewModel, + modifier: Modifier = Modifier, + onClickMoreMenuIcon: () -> Unit = {}, + makeToast: (String) -> Unit = {} +) { + val context = LocalContext.current + + var title by remember { mutableStateOf("") } + + var isRefreshing by remember { mutableStateOf(false) } + val pullRefreshState = rememberPullRefreshState( + refreshing = isRefreshing, + onRefresh = { + isRefreshing = true + viewModel.getScheduleList() // refresh api + } + ) + + val lifecycle = LocalLifecycleOwner.current + LaunchedEffect(Unit) { + lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + mainViewModel.onAttendance.collect { + viewModel.getScheduleList() + } + } + } + + val scheduleState by viewModel.scheduleState.collectAsState() + LaunchedEffect(scheduleState) { + when (val state = scheduleState) { + is ScheduleState.Loading -> {} + is ScheduleState.Init -> { + isRefreshing = false + } + + is ScheduleState.Success -> { + isRefreshing = false + title = context.setUiOfScheduleTitle(state.scheduleTitleState) + } + + is ScheduleState.Error -> { + isRefreshing = false + } + } + } + + val weeklyListState = rememberLazyListState() + val dailyListState = rememberLazyListState() + + var selectedTabIndex by remember { mutableIntStateOf(0) } + CompositionLocalProvider( + LocalOverscrollConfiguration provides null + ) { + Box( + modifier = modifier + .pullRefresh(pullRefreshState) + .background(color = if (ScheduleType.values()[selectedTabIndex] == ScheduleType.WEEK) Color.White else Gray50) + ) { + LazyColumn( + modifier = modifier, + state = if (selectedTabIndex == 0) weeklyListState else dailyListState + ) { + item { + ScheduleTopbar( + title, + onClickMoreMenuIcon = onClickMoreMenuIcon + ) + Spacer( + modifier = Modifier + .height(26.dp) + .fillMaxWidth() + .background(Color.White) + ) + } + + stickyHeader { + ScheduleTabRow( + modifier = Modifier.background(White), + selectedTabIndex = selectedTabIndex, + updateSelectedTabIndex = { index -> + if (index == 0) { + AnalyticsManager.addEvent(eventName = LOG_EVENT_LIST_WEEK) + } else { + AnalyticsManager.addEvent(eventName = LOG_EVENT_LIST_ALL) + } + selectedTabIndex = index + } + ) + } + + item { + when (scheduleState) { + is ScheduleState.Error -> {} + is ScheduleState.Init -> {} + else -> { + ScheduleScreen( + modifier = Modifier.fillMaxSize(), + scheduleState = scheduleState, + dailyListState = dailyListState, + onClickScheduleInformation = { id, type -> context.moveToScheduleInformation(id, type) }, + onClickMashongButton = { + AnalyticsManager.addEvent(eventName = LOG_EVENT_LIST_WEEK_MASHONG) + context.moveToMashong() + }, + makeToast = makeToast, + refreshState = isRefreshing, + scheduleType = ScheduleType.values()[selectedTabIndex] + ) + } + } + } + } + + PullRefreshIndicator( + modifier = Modifier.align(Alignment.TopCenter), + scale = true, + contentColor = Brand500, + refreshing = isRefreshing, + state = pullRefreshState + ) + } + } +} + +fun Context.setUiOfScheduleTitle(scheduleTitleState: ScheduleTitleState): String { + return when (scheduleTitleState) { + ScheduleTitleState.Empty -> { + getString(R.string.empty_schedule) + } + + is ScheduleTitleState.End -> { + getString(R.string.end_schedule, scheduleTitleState.generatedNumber) + } + + is ScheduleTitleState.DateCount -> { + getString(R.string.event_list_title, scheduleTitleState.dataCount) + } + + is ScheduleTitleState.SchedulePreparing -> { + getString(R.string.preparing_attendance) + } + } +} + +fun Context.moveToScheduleInformation(scheduleId: Int, scheduleType: String) { + startActivity( + ScheduleDetailActivity.newIntent(this, scheduleId, scheduleType) + ) +} + +fun Context.moveToAttendance(scheduleId: Int) { + AnalyticsManager.addEvent(eventName = LOG_EVENT_LIST_STATUS_CONFIRM) + startActivity( + PlatformAttendanceActivity.newIntent(this, scheduleId) + ) +} + +fun Context.moveToMashong() { + startActivity( + MashongActivity.newIntent(this) + ) +} + +@Composable +fun ScheduleTopbar( + title: String, + onClickMoreMenuIcon: () -> Unit = {} +) { + Row( + modifier = Modifier + .background(White) + .padding(horizontal = 20.dp) + .padding(top = 24.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + MashUpHtmlText( + content = title.fromHtml(), + modifier = Modifier.weight(1f, false), + textAppearance = CR.style.TextAppearance_Mashup_Header1_24_B + ) + Spacer(modifier = Modifier.width(8.dp)) + Image( + painter = painterResource(id = CR.drawable.ic_more), + contentDescription = null, + modifier = Modifier + .size(20.dp) + .clickable { + onClickMoreMenuIcon() + } + ) + } +} + +@Preview +@Composable +fun PreviewScheduleTopbar() { + MashUpTheme { + ScheduleTopbar(title = "다음 세미나 준비 중이에요.\n조금만 기다려주세요.") + } +} diff --git a/app/src/main/java/com/mashup/ui/schedule/ScheduleScreen.kt b/app/src/main/java/com/mashup/ui/schedule/ScheduleScreen.kt index 8aa9b5e2..faeb21c2 100644 --- a/app/src/main/java/com/mashup/ui/schedule/ScheduleScreen.kt +++ b/app/src/main/java/com/mashup/ui/schedule/ScheduleScreen.kt @@ -1,308 +1,65 @@ package com.mashup.ui.schedule -import android.content.Context -import androidx.appcompat.widget.AppCompatTextView -import androidx.compose.foundation.Image -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.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.PageSize -import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.pullrefresh.PullRefreshIndicator -import androidx.compose.material.pullrefresh.pullRefresh -import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.viewinterop.AndroidView -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.repeatOnLifecycle -import com.mashup.R -import com.mashup.constant.log.LOG_SCHEDULE_EVENT_DETAIL -import com.mashup.constant.log.LOG_SCHEDULE_STATUS_CONFIRM -import com.mashup.core.common.extensions.fromHtml -import com.mashup.core.ui.colors.Brand500 -import com.mashup.ui.attendance.platform.PlatformAttendanceActivity -import com.mashup.ui.danggn.ShakeDanggnActivity -import com.mashup.ui.main.MainViewModel -import com.mashup.ui.main.model.MainPopupType -import com.mashup.ui.schedule.detail.ScheduleDetailActivity -import com.mashup.ui.schedule.item.ScheduleViewPagerEmptyItem -import com.mashup.ui.schedule.item.ScheduleViewPagerInProgressItem -import com.mashup.ui.schedule.item.ScheduleViewPagerSuccessItem -import com.mashup.ui.schedule.model.ScheduleCard +import androidx.core.os.bundleOf +import com.mashup.constant.log.LOG_EVENT_LIST_EVENT_DETAIL +import com.mashup.ui.schedule.component.DailySchedule +import com.mashup.ui.schedule.component.WeeklySchedule +import com.mashup.ui.schedule.model.ScheduleType import com.mashup.util.AnalyticsManager -import com.mashup.util.debounce -import kotlin.math.abs -import kotlin.math.absoluteValue - -@OptIn(ExperimentalMaterialApi::class) -@Composable -fun ScheduleRoute( - mainViewModel: MainViewModel, - viewModel: ScheduleViewModel, - modifier: Modifier = Modifier -) { - var isRefreshing by remember { mutableStateOf(false) } - - val pullRefreshState = rememberPullRefreshState( - refreshing = isRefreshing, - onRefresh = { - isRefreshing = true - // refresh api - viewModel.getScheduleList() - } - ) - val context = LocalContext.current - - val state by viewModel.scheduleState.collectAsState() - - var title by remember { mutableStateOf("") } - - val lifecycle = LocalLifecycleOwner.current - - LaunchedEffect(Unit) { - lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { - mainViewModel.onAttendance.collect { - viewModel.getScheduleList() - } - } - } - - LaunchedEffect(state) { - when (state) { - is ScheduleState.Loading -> {} - is ScheduleState.Empty -> { isRefreshing = false } - is ScheduleState.Success -> { - isRefreshing = false - title = context.setUiOfScheduleTitle( - (state as ScheduleState.Success).scheduleTitleState - ) - } - is ScheduleState.Error -> { isRefreshing = false } - } - } - - val scrollState = rememberScrollState() - - Box( - modifier = modifier.pullRefresh(pullRefreshState) - ) { - Column( - modifier = modifier - ) { - Row( - modifier = Modifier - .padding(horizontal = 20.dp) - .padding(top = 24.dp) - .fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - AndroidView( - factory = { context -> - AppCompatTextView( - context - ).apply { - setTextAppearance( - com.mashup.core.common.R.style.TextAppearance_Mashup_Header1_24_B - ) - text = title - } - }, - update = { view -> - view.text = title - } - ) - if (title.isNotEmpty()) { - val coroutineScope = rememberCoroutineScope() - Image( - modifier = Modifier.clickable { - debounce(500L, scope = coroutineScope, destinationFunction = { - mainViewModel.disablePopup(MainPopupType.DANGGN) - }) - context.startActivity( - ShakeDanggnActivity.newIntent(context) - ) - }, - painter = painterResource( - id = com.mashup.core.common.R.drawable.img_carrot_button - ), - contentDescription = null - ) - } - } - when (state) { - is ScheduleState.Error -> {} - is ScheduleState.Empty -> {} - else -> { - ScheduleScreen( - modifier = Modifier.fillMaxSize().verticalScroll(scrollState), - scheduleState = state, - onClickScheduleInformation = { scheduleId: Int -> - AnalyticsManager.addEvent(eventName = LOG_SCHEDULE_EVENT_DETAIL) - context.startActivity( - ScheduleDetailActivity.newIntent(context, scheduleId) - ) - }, - onClickAttendance = { scheduleId: Int -> - AnalyticsManager.addEvent(eventName = LOG_SCHEDULE_STATUS_CONFIRM) - context.startActivity( - PlatformAttendanceActivity.newIntent(context, scheduleId) - ) - }, - refreshState = isRefreshing - - ) - } - } - } - - PullRefreshIndicator( - modifier = Modifier.align(Alignment.TopCenter), - scale = true, - contentColor = Brand500, - refreshing = isRefreshing, - state = pullRefreshState - ) - } -} @Composable fun ScheduleScreen( scheduleState: ScheduleState, + dailyListState: LazyListState, modifier: Modifier = Modifier, - onClickScheduleInformation: (Int) -> Unit = {}, - onClickAttendance: (Int) -> Unit = {}, + scheduleType: ScheduleType = ScheduleType.WEEK, + onClickScheduleInformation: (Int, String) -> Unit = { _, _ -> }, + onClickMashongButton: () -> Unit = {}, + makeToast: (String) -> Unit = {}, refreshState: Boolean = false ) { - var cacheScheduleState by remember { - mutableStateOf(scheduleState) - } - LaunchedEffect(scheduleState) { if (scheduleState is ScheduleState.Success) { - cacheScheduleState = scheduleState + if (scheduleState.schedulePosition < scheduleState.scheduleList.size) { + dailyListState.animateScrollToItem(scheduleState.schedulePosition) + } } } - when (cacheScheduleState) { - is ScheduleState.Success -> { - val castingState = cacheScheduleState as ScheduleState.Success - val horizontalPagerState = rememberPagerState( - initialPage = if (castingState.scheduleList.size < 6) 1 else castingState.scheduleList.size - 4, - pageCount = { castingState.scheduleList.size } + when (scheduleType) { + ScheduleType.WEEK -> { + WeeklySchedule( + scheduleState = scheduleState, + modifier = modifier, + onClickScheduleInformation = { scheduleId: Int, type: String -> + AnalyticsManager.addEvent( + eventName = LOG_EVENT_LIST_EVENT_DETAIL, + params = bundleOf("place" to "이번주일정") + ) + onClickScheduleInformation(scheduleId, type) + }, + onClickMashongButton = onClickMashongButton, + makeToast = makeToast, + refreshState = refreshState ) - LaunchedEffect(refreshState) { - if (refreshState.not()) { // refresh 가 끝났을 경우 - horizontalPagerState.animateScrollToPage(castingState.schedulePosition) - } - } + } - HorizontalPager( + ScheduleType.TOTAL -> { + DailySchedule( + scheduleState = scheduleState, modifier = modifier, - state = horizontalPagerState, - pageSpacing = 12.dp, - pageSize = PageSize.Fill, - contentPadding = PaddingValues(33.dp), - verticalAlignment = Alignment.Top - ) { index -> - when (val data = castingState.scheduleList[index]) { - is ScheduleCard.EmptySchedule -> { - Box( - modifier = Modifier.fillMaxSize() - ) { - ScheduleViewPagerEmptyItem( - modifier = Modifier - .graphicsLayer { - val pageOffset = ( - (horizontalPagerState.currentPage - index) + horizontalPagerState - .currentPageOffsetFraction - ).absoluteValue - scaleY = 1 - 0.1f * abs(pageOffset) - }, - data = data - ) - } - } - is ScheduleCard.EndSchedule -> { - Box( - modifier = Modifier.fillMaxSize() - ) { - ScheduleViewPagerSuccessItem( - modifier = Modifier - .graphicsLayer { - val pageOffset = ( - (horizontalPagerState.currentPage - index) + horizontalPagerState - .currentPageOffsetFraction - ).absoluteValue - scaleY = 1 - 0.1f * abs(pageOffset) - }, - data = data, - onClickScheduleInformation = onClickScheduleInformation, - onClickAttendance = onClickAttendance - ) - } - } - is ScheduleCard.InProgressSchedule -> { - Box( - modifier = Modifier.fillMaxSize() - ) { - ScheduleViewPagerInProgressItem( - modifier = Modifier - .graphicsLayer { - val pageOffset = ( - (horizontalPagerState.currentPage - index) + horizontalPagerState - .currentPageOffsetFraction - ).absoluteValue - scaleY = 1 - 0.1f * abs(pageOffset) - }, - data = data, - onClickScheduleInformation = onClickScheduleInformation, - onClickAttendance = onClickAttendance - ) - } - } + onClickScheduleInformation = { scheduleId: Int, type: String -> + AnalyticsManager.addEvent( + eventName = LOG_EVENT_LIST_EVENT_DETAIL, + params = bundleOf("place" to "전체일정") + ) + onClickScheduleInformation(scheduleId, type) } - } - } - else -> {} - } -} -fun Context.setUiOfScheduleTitle(scheduleTitleState: ScheduleTitleState): String { - return when (scheduleTitleState) { - ScheduleTitleState.Empty -> { - getString(R.string.empty_schedule) - } - is ScheduleTitleState.End -> { - getString(R.string.end_schedule, scheduleTitleState.generatedNumber) - } - is ScheduleTitleState.DateCount -> { - getString(R.string.event_list_title, scheduleTitleState.dataCount).fromHtml().toString() - } - is ScheduleTitleState.SchedulePreparing -> { - getString(R.string.preparing_attendance) + ) } } } diff --git a/app/src/main/java/com/mashup/ui/schedule/ScheduleViewModel.kt b/app/src/main/java/com/mashup/ui/schedule/ScheduleViewModel.kt index 71fcb289..cc5bea37 100644 --- a/app/src/main/java/com/mashup/ui/schedule/ScheduleViewModel.kt +++ b/app/src/main/java/com/mashup/ui/schedule/ScheduleViewModel.kt @@ -1,6 +1,9 @@ package com.mashup.ui.schedule import com.mashup.core.common.base.BaseViewModel +import com.mashup.core.common.extensions.month +import com.mashup.core.common.extensions.year +import com.mashup.core.ui.widget.PlatformType import com.mashup.data.dto.ScheduleResponse import com.mashup.data.dto.SchedulesProgress import com.mashup.data.repository.AttendanceRepository @@ -8,12 +11,19 @@ import com.mashup.data.repository.ScheduleRepository import com.mashup.datastore.data.repository.AppPreferenceRepository import com.mashup.datastore.data.repository.UserPreferenceRepository import com.mashup.ui.schedule.model.ScheduleCard +import com.mashup.ui.schedule.util.convertCamelCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first +import org.threeten.bp.DayOfWeek +import org.threeten.bp.Instant +import org.threeten.bp.LocalDateTime +import org.threeten.bp.ZoneId +import org.threeten.bp.temporal.TemporalAdjusters +import java.util.Date import javax.inject.Inject @HiltViewModel @@ -23,7 +33,7 @@ class ScheduleViewModel @Inject constructor( private val scheduleRepository: ScheduleRepository, private val attendanceRepository: AttendanceRepository ) : BaseViewModel() { - private val _scheduleState = MutableStateFlow(ScheduleState.Empty) + private val _scheduleState = MutableStateFlow(ScheduleState.Init) val scheduleState: StateFlow = _scheduleState private val _showCoachMark = MutableSharedFlow() @@ -37,19 +47,27 @@ class ScheduleViewModel @Inject constructor( scheduleRepository.getScheduleList(generateNumber) .onSuccess { response -> + + val weeklySchedule = if (response.scheduleList.isEmpty()) { + listOf() + } else { + response.scheduleList.filterSchedulesForCurrentWeek() + } _scheduleState.emit( ScheduleState.Success( scheduleTitleState = when { response.progress == SchedulesProgress.DONE -> { ScheduleTitleState.End(generateNumber) } + response.progress == SchedulesProgress.NOT_REGISTERED -> { ScheduleTitleState.Empty } - response.progress == SchedulesProgress.ON_GOING && - response.dateCount != null -> { + + response.progress == SchedulesProgress.ON_GOING && response.dateCount != null -> { ScheduleTitleState.DateCount(response.dateCount) } + else -> { ScheduleTitleState.SchedulePreparing } @@ -59,7 +77,16 @@ class ScheduleViewModel @Inject constructor( } else { response.scheduleList.map { mapperToScheduleCard(it) } }, - schedulePosition = getSchedulePosition(response.scheduleList) + monthlyScheduleList = getMonthlyScheduleList(response.scheduleList), + schedulePosition = getSchedulePosition(response.scheduleList), + weeklySchedule = weeklySchedule.map { mapperToScheduleCard(it) }, + weeklySchedulePosition = if (weeklySchedule.isEmpty()) { + 0 + } else { + getSchedulePosition( + weeklySchedule + ) + } ) ) showCoachMark(response.scheduleList) @@ -69,13 +96,33 @@ class ScheduleViewModel @Inject constructor( ScheduleState.Success( scheduleTitleState = ScheduleTitleState.Empty, scheduleList = listOf(ScheduleCard.EmptySchedule()), - schedulePosition = 0 + monthlyScheduleList = emptyList(), + schedulePosition = 0, + weeklySchedule = listOf(ScheduleCard.EmptySchedule()), + weeklySchedulePosition = 0 ) ) } } } + private fun List.filterSchedulesForCurrentWeek(): List { + val koreaZone = ZoneId.of("Asia/Seoul") + val now = LocalDateTime.now(koreaZone) + val startOfWeek = now.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)).toLocalDate().atTime(0, 0, 0, 0).atZone(koreaZone).toLocalDateTime() + val endOfWeek = now.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)).toLocalDate().atTime(23, 59, 59, 999999999).atZone(koreaZone).toLocalDateTime() + val result = this.filter { + val scheduleStart = it.startedAt.toLocalDateTime(koreaZone) + scheduleStart.isAfter(startOfWeek) && scheduleStart.isBefore(endOfWeek) + } + return result + } + + private fun Date.toLocalDateTime(zone: ZoneId = ZoneId.systemDefault()): LocalDateTime { + val instant = Instant.ofEpochMilli(this.time) + return LocalDateTime.ofInstant(instant, zone) + } + override fun handleErrorCode(code: String) { mashUpScope { _scheduleState.emit(ScheduleState.Error(code)) @@ -89,7 +136,7 @@ class ScheduleViewModel @Inject constructor( attendanceRepository.getScheduleAttendanceInfo(scheduleResponse.scheduleId) .onSuccess { response -> - return if (response.attendanceInfos.isEmpty()) { + return if (response.attendanceInfos.isEmpty() && scheduleResponse.scheduleType.convertCamelCase() == PlatformType.Seminar) { ScheduleCard.InProgressSchedule( scheduleResponse = scheduleResponse, attendanceInfo = response @@ -108,6 +155,14 @@ class ScheduleViewModel @Inject constructor( return ScheduleCard.EmptySchedule(scheduleResponse) } + private fun getMonthlyScheduleList(scheduleList: List): List>> { + return scheduleList.groupBy { + val year = it.startedAt.year() + val month = it.startedAt.month() + "${year}년 ${month}월" + }.toList() + } + private fun getSchedulePosition(schedules: List): Int { return schedules.size - schedules.filter { it.dateCount >= 0 }.size } @@ -133,12 +188,15 @@ sealed interface ScheduleTitleState { } sealed interface ScheduleState { - object Empty : ScheduleState + object Init : ScheduleState object Loading : ScheduleState data class Success( val scheduleTitleState: ScheduleTitleState, val scheduleList: List, - val schedulePosition: Int + val monthlyScheduleList: List>>, + val weeklySchedule: List, + val schedulePosition: Int, + val weeklySchedulePosition: Int ) : ScheduleState data class Error(val code: String) : ScheduleState diff --git a/app/src/main/java/com/mashup/ui/schedule/ViewEventTimeline.kt b/app/src/main/java/com/mashup/ui/schedule/ViewEventTimeline.kt index 0632363e..40d2c435 100644 --- a/app/src/main/java/com/mashup/ui/schedule/ViewEventTimeline.kt +++ b/app/src/main/java/com/mashup/ui/schedule/ViewEventTimeline.kt @@ -3,10 +3,13 @@ package com.mashup.ui.schedule import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size import androidx.compose.material3.Divider 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.res.stringResource @@ -21,6 +24,7 @@ import com.mashup.core.ui.colors.Gray500 import com.mashup.core.ui.colors.Gray600 import com.mashup.core.ui.colors.Gray800 import com.mashup.core.ui.theme.MashUpTheme +import com.mashup.core.ui.typography.Body5 import com.mashup.core.ui.typography.Caption2 import com.mashup.core.ui.typography.SubTitle3 @@ -39,29 +43,33 @@ fun ViewEventTimeline( val (divider, attendanceImage, attendanceCaption, attendanceTime, attendanceStatus) = createRefs() Image( - modifier = Modifier.size(20.dp).constrainAs(attendanceImage) { - start.linkTo(parent.start) - top.linkTo(parent.top) - bottom.linkTo(attendanceStatus.bottom) - }, + modifier = Modifier + .size(20.dp) + .constrainAs(attendanceImage) { + start.linkTo(parent.start) + top.linkTo(parent.top) + bottom.linkTo(attendanceStatus.bottom) + }, painter = painterResource(id = image), contentDescription = null ) Text( modifier = Modifier.constrainAs(attendanceCaption) { - top.linkTo(parent.top) + top.linkTo(attendanceImage.top) + bottom.linkTo(attendanceImage.bottom, margin = 3.dp) start.linkTo(attendanceImage.end, 8.dp) }, text = caption, - style = Caption2, + style = Body5, color = Gray600 ) Text( modifier = Modifier.constrainAs(attendanceStatus) { - top.linkTo(attendanceCaption.bottom, 2.dp) - start.linkTo(attendanceCaption.start) + top.linkTo(attendanceCaption.top) + bottom.linkTo(attendanceCaption.bottom) + start.linkTo(attendanceCaption.end, 7.dp) }, text = stringResource(status), style = SubTitle3.copy( @@ -79,18 +87,22 @@ fun ViewEventTimeline( color = Gray500 ) if (isFinal.not()) { - Divider( + Row( modifier = Modifier.constrainAs(divider) { start.linkTo(attendanceImage.start) end.linkTo(attendanceImage.end) - top.linkTo(attendanceStatus.bottom, 2.dp) + top.linkTo(attendanceStatus.bottom, 6.dp) height = Dimension.value(16.dp) width = Dimension.value(1.dp) }, - thickness = 1.dp, - color = Gray200 - - ) + verticalAlignment = Alignment.CenterVertically + ) { + Divider( + modifier = Modifier.height(12.dp), + thickness = 1.dp, + color = Gray200 + ) + } } } } diff --git a/app/src/main/java/com/mashup/ui/schedule/component/DailySchedule.kt b/app/src/main/java/com/mashup/ui/schedule/component/DailySchedule.kt new file mode 100644 index 00000000..464104ff --- /dev/null +++ b/app/src/main/java/com/mashup/ui/schedule/component/DailySchedule.kt @@ -0,0 +1,83 @@ +package com.mashup.ui.schedule.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +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.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.mashup.core.ui.colors.Gray50 +import com.mashup.core.ui.colors.Gray600 +import com.mashup.core.ui.typography.Body5 +import com.mashup.ui.schedule.ScheduleState +import com.mashup.ui.schedule.daily.DailyScheduleByMonth +import com.mashup.core.common.R as CR + +@Composable +fun DailySchedule( + scheduleState: ScheduleState, + modifier: Modifier = Modifier, + onClickScheduleInformation: (Int, String) -> Unit = { _, _ -> } +) { + var cacheScheduleState by remember { + mutableStateOf(scheduleState) + } + + LaunchedEffect(scheduleState) { + if (scheduleState is ScheduleState.Success) { + cacheScheduleState = scheduleState + } + } + + (cacheScheduleState as? ScheduleState.Success)?.let { state -> + if (state.monthlyScheduleList.isEmpty()) { + Column( + modifier = Modifier + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(200.dp)) + Image( + painter = painterResource(id = CR.drawable.img_placeholder_sleeping), + contentDescription = null, + modifier = Modifier.size(88.dp) + ) + Spacer(modifier = Modifier.height(12.dp)) + Text( + text = "열심히 일정을 준비하고 있어요\n조금만 기다려 주세요!", + style = Body5.copy(color = Gray600), + textAlign = TextAlign.Center + ) + } + } else { + Spacer(modifier = Modifier.height(22.dp)) + state.monthlyScheduleList.forEach { (title, scheduleList) -> + DailyScheduleByMonth( + title = title, + scheduleList = scheduleList, + modifier = Modifier + .background(Gray50) + .padding(horizontal = 20.dp), + isWeeklySchedule = { scheduleId -> + state.weeklySchedule.any { scheduleId == it.getScheduleId() } + }, + onClickScheduleInformation = onClickScheduleInformation + ) + } + } + } +} diff --git a/app/src/main/java/com/mashup/ui/schedule/component/ScheduleTabRow.kt b/app/src/main/java/com/mashup/ui/schedule/component/ScheduleTabRow.kt new file mode 100644 index 00000000..195774f9 --- /dev/null +++ b/app/src/main/java/com/mashup/ui/schedule/component/ScheduleTabRow.kt @@ -0,0 +1,165 @@ +package com.mashup.ui.schedule.component + +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Tab +import androidx.compose.material3.TabPosition +import androidx.compose.material3.TabRow +import androidx.compose.material3.TabRowDefaults +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.composed +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.debugInspectorInfo +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.mashup.core.ui.colors.Gray950 +import com.mashup.core.ui.theme.MashUpTheme +import com.mashup.core.ui.typography.SubTitle1 + +/** + * 일정 탭을 표시하는 컴포저블입니다. + * @param modifier Modifier + * @param tabMenu List 탭 메뉴 리스트 + * @param selectedTabIndex Int 선택된 탭 인덱스 + * @param updateSelectedTabIndex (Int) -> Unit 선택된 탭 인덱스 업데이트 함수 + * + * 사용 예시 + * ``` + * val selectedTabIndex by remember { mutableIntStateOf(0) } + * + * ScheduleTabRow( + * modifier = Modifier.fillMaxWidth(), + * selectedTabIndex = selectedTabIndex, + * updateSelectedTabIndex = { selectedTabIndex = it } + * ) + * + * when(selectedTabIndex) { + * 0 -> { // 이번 주 일정 Contents } + * 1 -> { // 전체 일정 Contents } + * } + * ``` + */ +@Composable +fun ScheduleTabRow( + modifier: Modifier = Modifier, + tabMenu: List = listOf("이번 주 일정", "전체 일정"), + selectedTabIndex: Int = 0, + updateSelectedTabIndex: (Int) -> Unit = {} +) { + Column( + modifier = modifier + ) { + TabRow( + modifier = Modifier.fillMaxWidth(), + selectedTabIndex = selectedTabIndex, + containerColor = Color.Transparent, + indicator = { tabPositions -> + TabRowDefaults.Indicator( + color = Gray950, + modifier = Modifier + .customTabIndicatorOffset( + tabPositions[selectedTabIndex], + tabWidth = 150.dp + ) + .clip( + RoundedCornerShape(20.dp) + ) + ) + }, + tabs = { + tabMenu.forEachIndexed { index, a -> + Tab( + selected = index == selectedTabIndex, + onClick = { + updateSelectedTabIndex(index) + } + ) { + Spacer( + modifier = Modifier.height(2.5.dp) + ) + Text( + text = a, + style = SubTitle1.copy( + fontWeight = if (index == selectedTabIndex) { + FontWeight.W700 + } else { + FontWeight.W400 + } + + ), + color = Gray950 + ) + Spacer( + modifier = Modifier.height(14.5.dp) + ) + } + } + } + ) + } +} + +fun Modifier.customTabIndicatorOffset( + currentTabPosition: TabPosition, + tabWidth: Dp +): Modifier = composed( + inspectorInfo = debugInspectorInfo { + name = "customTabIndicatorOffset" + value = currentTabPosition + } +) { + val currentTabWidth by animateDpAsState( + targetValue = tabWidth, + animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing), + label = "" + ) + + val indicatorOffset by animateDpAsState( + targetValue = ((currentTabPosition.left + currentTabPosition.right - tabWidth) / 2), + animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing), + label = "" + ) + + fillMaxWidth() + .wrapContentSize(Alignment.BottomStart) // indicator 표시 위치 + .offset(x = indicatorOffset) + .width(currentTabWidth) +} + +@Preview +@Composable +private fun PreviewTabRow() { + MashUpTheme { + var selectedTabIndex by remember { mutableIntStateOf(0) } + + ScheduleTabRow( + modifier = Modifier + .fillMaxWidth() + .background(color = Color.White), + selectedTabIndex = selectedTabIndex, + updateSelectedTabIndex = { + selectedTabIndex = it + } + ) + } +} diff --git a/app/src/main/java/com/mashup/ui/schedule/component/WeeklySchedule.kt b/app/src/main/java/com/mashup/ui/schedule/component/WeeklySchedule.kt new file mode 100644 index 00000000..6535d2e7 --- /dev/null +++ b/app/src/main/java/com/mashup/ui/schedule/component/WeeklySchedule.kt @@ -0,0 +1,133 @@ +package com.mashup.ui.schedule.component + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PageSize +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.graphics.graphicsLayer +import androidx.compose.ui.unit.dp +import com.mashup.ui.schedule.ScheduleState +import com.mashup.ui.schedule.item.EmptyScheduleItem +import com.mashup.ui.schedule.item.ScheduleViewPagerEmptyItem +import com.mashup.ui.schedule.item.ScheduleViewPagerInProgressItem +import com.mashup.ui.schedule.item.ScheduleViewPagerSuccessItem +import com.mashup.ui.schedule.model.ScheduleCard +import kotlin.math.abs +import kotlin.math.absoluteValue + +@Composable +fun WeeklySchedule( + scheduleState: ScheduleState, + modifier: Modifier = Modifier, + onClickScheduleInformation: (Int, String) -> Unit = { _, _ -> }, + onClickMashongButton: () -> Unit = {}, + makeToast: (String) -> Unit = {}, + refreshState: Boolean = false +) { + var cacheScheduleState by remember { + mutableStateOf(scheduleState) + } + + LaunchedEffect(scheduleState) { + if (scheduleState is ScheduleState.Success) { + cacheScheduleState = scheduleState + } + } + + when (cacheScheduleState) { + is ScheduleState.Success -> { + val castingState = cacheScheduleState as ScheduleState.Success + val horizontalPagerState = rememberPagerState( + initialPage = castingState.weeklySchedulePosition, + pageCount = { castingState.weeklySchedule.size } + ) + LaunchedEffect(refreshState) { + if (refreshState.not()) { // refresh 가 끝났을 경우 + horizontalPagerState.animateScrollToPage( + castingState.weeklySchedulePosition + ) + } + } + + if (castingState.weeklySchedule.isEmpty()) { + EmptyScheduleItem( + modifier = modifier, + onClickMashongButton = onClickMashongButton + ) + } else { + HorizontalPager( + modifier = modifier, + state = horizontalPagerState, + pageSpacing = 12.dp, + pageSize = PageSize.Fill, + contentPadding = PaddingValues(33.dp), + verticalAlignment = Alignment.Top + ) { index -> + when (val data = castingState.weeklySchedule[index]) { + is ScheduleCard.EmptySchedule -> { + Box( + modifier = Modifier.fillMaxSize() + ) { + ScheduleViewPagerEmptyItem( + modifier = Modifier + .fillMaxSize() + .graphicsLayer { + val pageOffset = + ((horizontalPagerState.currentPage - index) + horizontalPagerState.currentPageOffsetFraction).absoluteValue + scaleY = 1 - 0.1f * abs(pageOffset) + }, + data = data + ) + } + } + + is ScheduleCard.EndSchedule -> { + Box( + modifier = Modifier.fillMaxSize() + ) { + ScheduleViewPagerSuccessItem( + modifier = Modifier.graphicsLayer { + val pageOffset = + ((horizontalPagerState.currentPage - index) + horizontalPagerState.currentPageOffsetFraction).absoluteValue + scaleY = 1 - 0.1f * abs(pageOffset) + }, + data = data, + onClickScheduleInformation = onClickScheduleInformation, + makeToast = makeToast + ) + } + } + + is ScheduleCard.InProgressSchedule -> { + Box( + modifier = Modifier.fillMaxSize() + ) { + ScheduleViewPagerInProgressItem( + modifier = Modifier.graphicsLayer { + val pageOffset = + ((horizontalPagerState.currentPage - index) + horizontalPagerState.currentPageOffsetFraction).absoluteValue + scaleY = 1 - 0.1f * abs(pageOffset) + }, + data = data, + onClickScheduleInformation = onClickScheduleInformation + ) + } + } + } + } + } + } + + else -> {} + } +} diff --git a/app/src/main/java/com/mashup/ui/schedule/daily/DailyScheduleByMonth.kt b/app/src/main/java/com/mashup/ui/schedule/daily/DailyScheduleByMonth.kt new file mode 100644 index 00000000..dd795089 --- /dev/null +++ b/app/src/main/java/com/mashup/ui/schedule/daily/DailyScheduleByMonth.kt @@ -0,0 +1,86 @@ +package com.mashup.ui.schedule.daily + +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.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.mashup.core.common.extensions.day +import com.mashup.core.common.extensions.week +import com.mashup.core.ui.colors.Gray500 +import com.mashup.core.ui.theme.MashUpTheme +import com.mashup.core.ui.typography.Body3 +import com.mashup.core.ui.typography.Caption2 +import com.mashup.core.ui.typography.Title3 +import com.mashup.data.dto.ScheduleResponse + +/** + * DailyScheduleByMonth.kt + * + * Created by Minji Jeong on 2024/06/29 + * Copyright © 2024 MashUp All rights reserved. + */ + +@Composable +fun DailyScheduleByMonth( + title: String, + scheduleList: List, + modifier: Modifier = Modifier, + isWeeklySchedule: (Int) -> Boolean = { false }, + onClickScheduleInformation: (Int, String) -> Unit = { _, _ -> } +) { + Column(modifier = modifier) { + Text(title, style = Title3) + Spacer(modifier = Modifier.height(18.dp)) + scheduleList + .groupBy { it.startedAt.day() } + .forEach { (_, dailySchedule) -> + DailyScheduleByDay(dailySchedule, isWeeklySchedule, onClickScheduleInformation) + } + } +} + +@Composable +fun DailyScheduleByDay( + schedule: List, + isWeeklySchedule: (Int) -> Boolean, + onClickScheduleInformation: (Int, String) -> Unit +) { + Row(modifier = Modifier.padding(bottom = 32.dp)) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(top = 14.dp, end = 14.dp) + ) { + Text(text = "${schedule[0].startedAt.day()}", style = Body3) + Text(text = schedule[0].startedAt.week(), style = Caption2.copy(color = Gray500)) + } + + Column { + schedule.forEach { + val highlight = isWeeklySchedule.invoke(it.scheduleId) + DailyScheduleItem( + title = it.name, + time = it.getTimeLine(), + place = it.location?.detailAddress ?: "-", + highlight = highlight, + modifier = Modifier.padding(bottom = 12.dp), + onClickScheduleInformation = { onClickScheduleInformation.invoke(it.scheduleId, it.scheduleType) } + ) + } + } + } +} + +@Preview +@Composable +fun PreviewDailySchedule() { + MashUpTheme { + DailyScheduleByMonth("2024년 8월", emptyList()) + } +} diff --git a/app/src/main/java/com/mashup/ui/schedule/daily/DailyScheduleItem.kt b/app/src/main/java/com/mashup/ui/schedule/daily/DailyScheduleItem.kt new file mode 100644 index 00000000..7345b01f --- /dev/null +++ b/app/src/main/java/com/mashup/ui/schedule/daily/DailyScheduleItem.kt @@ -0,0 +1,95 @@ +package com.mashup.ui.schedule.daily + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.mashup.core.ui.colors.Brand500 +import com.mashup.core.ui.colors.Gray100 +import com.mashup.core.ui.colors.White +import com.mashup.core.ui.theme.MashUpTheme +import com.mashup.core.ui.typography.SubTitle1 +import com.mashup.ui.schedule.detail.composable.ScheduleInfoText +import com.mashup.core.common.R as CR + +/** + * TotalScheduleItem.kt + * + * Created by Minji Jeong on 2024/06/29 + * Copyright © 2024 MashUp All rights reserved. + */ + +@Composable +fun DailyScheduleItem( + title: String, + time: String, + place: String, + highlight: Boolean, + modifier: Modifier = Modifier, + onClickScheduleInformation: () -> Unit = {} +) { + val borderColor = if (highlight) { + listOf(Brand500, Color(0xFF31C1FF)) + } else { + listOf(Gray100, Gray100) + } + + Column( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .clickable { onClickScheduleInformation() } + .background(White) + .border( + width = if (highlight) (1.5).dp else 1.dp, + brush = Brush.horizontalGradient(borderColor), + shape = RoundedCornerShape(16.dp) + ) + .padding(horizontal = 20.dp, vertical = 16.dp) + ) { + Text(text = title, style = SubTitle1) + Spacer(modifier = Modifier.height(4.dp)) + ScheduleInfoText(iconRes = CR.drawable.ic_clock, info = time) + Spacer(modifier = Modifier.height(4.dp)) + ScheduleInfoText(iconRes = CR.drawable.ic_mappin, info = place) + } +} + +@Preview +@Composable +fun PreviewDailyScheduleItemHighlight() { + MashUpTheme { + DailyScheduleItem( + "안드로이드 팀 세미나", + "오후 3:00 - 오전 7:00", + "디스코드", + true + ) + } +} + +@Preview +@Composable +fun PreviewDailyScheduleItem() { + MashUpTheme { + DailyScheduleItem( + "안드로이드 팀 세미나", + "오후 3:00 - 오전 7:00", + "디스코드", + false + ) + } +} diff --git a/app/src/main/java/com/mashup/ui/schedule/detail/ScheduleDetailActivity.kt b/app/src/main/java/com/mashup/ui/schedule/detail/ScheduleDetailActivity.kt index 11ffa21b..26f1427a 100644 --- a/app/src/main/java/com/mashup/ui/schedule/detail/ScheduleDetailActivity.kt +++ b/app/src/main/java/com/mashup/ui/schedule/detail/ScheduleDetailActivity.kt @@ -5,13 +5,22 @@ import android.content.ClipboardManager import android.content.Context import android.content.Intent import androidx.activity.viewModels +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import com.mashup.R import com.mashup.base.BaseActivity import com.mashup.constant.EXTRA_SCHEDULE_ID +import com.mashup.constant.EXTRA_SCHEDULE_TYPE +import com.mashup.constant.log.LOG_EVENT_LIST_DETAIL_COPY import com.mashup.core.common.constant.SCHEDULE_NOT_FOUND +import com.mashup.core.common.extensions.setStatusBarColorRes +import com.mashup.core.ui.theme.MashUpTheme import com.mashup.databinding.ActivityScheduleDetailBinding +import com.mashup.ui.attendance.platform.PlatformAttendanceActivity +import com.mashup.util.AnalyticsManager import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collectLatest +import com.mashup.core.common.R as CR @AndroidEntryPoint class ScheduleDetailActivity : BaseActivity() { @@ -19,14 +28,22 @@ class ScheduleDetailActivity : BaseActivity() { private val viewModel: ScheduleDetailViewModel by viewModels() - private val eventDetailAdapter by lazy { - EventDetailAdapter(copyToClipboard = ::copyToClipboard) - } - override fun initViews() { - initButton() - viewBinding.rvEvent.apply { - adapter = eventDetailAdapter + super.initViews() + + setStatusBarColorRes(CR.color.white) + viewBinding.composeView.setContent { + val state by viewModel.scheduleState.collectAsState() + + MashUpTheme { + ScheduleDetailScreen( + state = state, + isPlatformSeminar = viewModel.scheduleType != "ALL", + copyToClipboard = ::copyToClipboard, + moveToPlatformAttendance = ::moveToPlatformAttendance, + onBackPressed = { finish() } + ) + } } } @@ -38,15 +55,17 @@ class ScheduleDetailActivity : BaseActivity() { ScheduleState.Loading -> { showLoading() } + is ScheduleState.Success -> { hideLoading() - eventDetailAdapter.submitList(state.eventDetailList) } + is ScheduleState.Error -> { hideLoading() handleCommonError(state.code) handleScheduleDetailErrorCode(state) } + else -> { hideLoading() } @@ -67,23 +86,24 @@ class ScheduleDetailActivity : BaseActivity() { codeMessage?.run { showToast(this) } } - private fun initButton() { - viewBinding.btnReturn.setOnClickListener { - onBackPressed() - } - } - private fun copyToClipboard(text: String) { + AnalyticsManager.addEvent(eventName = LOG_EVENT_LIST_DETAIL_COPY) (getSystemService(CLIPBOARD_SERVICE) as? ClipboardManager)?.let { clipboardManager -> val clip = ClipData.newPlainText("location", text) clipboardManager.setPrimaryClip(clip) } } + private fun moveToPlatformAttendance() { + val intent = PlatformAttendanceActivity.newIntent(this, viewModel.scheduleId) + startActivity(intent) + } + companion object { - fun newIntent(context: Context, scheduleId: Int) = + fun newIntent(context: Context, scheduleId: Int, scheduleType: String) = Intent(context, ScheduleDetailActivity::class.java).apply { putExtra(EXTRA_SCHEDULE_ID, scheduleId) + putExtra(EXTRA_SCHEDULE_TYPE, scheduleType) } } } diff --git a/app/src/main/java/com/mashup/ui/schedule/detail/ScheduleDetailAdapter.kt b/app/src/main/java/com/mashup/ui/schedule/detail/ScheduleDetailAdapter.kt deleted file mode 100644 index 2bdf73f5..00000000 --- a/app/src/main/java/com/mashup/ui/schedule/detail/ScheduleDetailAdapter.kt +++ /dev/null @@ -1,169 +0,0 @@ -package com.mashup.ui.schedule.detail - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.foundation.layout.padding -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.compose.ui.unit.dp -import androidx.databinding.DataBindingUtil -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import com.mashup.core.ui.theme.MashUpTheme -import com.mashup.databinding.ItemEventTimelineContentBinding -import com.mashup.databinding.ItemEventTimelineHeaderBinding -import com.mashup.ui.schedule.detail.composable.ScheduleDetailInfoContent -import com.mashup.ui.schedule.detail.composable.ScheduleDetailLocationContent -import com.mashup.ui.schedule.model.EventDetail -import com.mashup.ui.schedule.model.EventDetailType - -class EventDetailAdapter( - private val copyToClipboard: (String) -> Unit -) : - ListAdapter(EventComparator) { - - override fun getItemViewType(position: Int): Int { - return getItem(position).type.num - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return when (viewType) { - EventDetailType.HEADER.num -> { - HeaderViewHolder(parent) - } - EventDetailType.CONTENT.num -> { - ContentViewHolder(parent) - } - EventDetailType.LOCATION.num -> { - LocationViewHolder(ComposeView(parent.context)) - } - EventDetailType.INFO.num -> { - InfoViewHolder(ComposeView(parent.context)) - } - else -> { - InfoViewHolder(ComposeView(parent.context)) - } - } - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (holder) { - is HeaderViewHolder -> { - holder.bind(getItem(position)) - } - is ContentViewHolder -> { - holder.bind(getItem(position)) - } - is LocationViewHolder -> { - holder.bind(getItem(position), copyToClipboard) - } - is InfoViewHolder -> { - holder.bind(getItem(position)) - } - } - } - - class HeaderViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder( - ItemEventTimelineHeaderBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ).root - ) { - private val binding: ItemEventTimelineHeaderBinding? = DataBindingUtil.bind(itemView) - - fun bind(item: EventDetail) { - binding?.model = item - if (item.header?.eventId == 1) { - binding?.line?.visibility = View.GONE - } - } - } - - class ContentViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder( - ItemEventTimelineContentBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ).root - ) { - private val binding: ItemEventTimelineContentBinding? = - DataBindingUtil.bind(itemView) - - fun bind(item: EventDetail) { - binding?.model = item - } - } - - class LocationViewHolder(private val composeView: ComposeView) : - RecyclerView.ViewHolder(composeView) { - init { - composeView.setViewCompositionStrategy( - ViewCompositionStrategy.DisposeOnDetachedFromWindowOrReleasedFromPool // (Default) - ) - } - - fun bind(item: EventDetail, copyToClipboard: (String) -> Unit) { - item.location?.let { location -> - composeView.setContent { - MashUpTheme { - ScheduleDetailLocationContent( - detailAddress = location.detailAddress.orEmpty(), - roadAddress = location.roadAddress.orEmpty(), - latitude = location.latitude, - longitude = location.longitude, - copyToClipboard = copyToClipboard - ) - } - } - } - } - } - - class InfoViewHolder(private val composeView: ComposeView) : - RecyclerView.ViewHolder(composeView) { - init { - composeView.setViewCompositionStrategy( - ViewCompositionStrategy.DisposeOnDetachedFromWindowOrReleasedFromPool // (Default) - ) - } - - fun bind(item: EventDetail) { - item.info?.let { info -> - composeView.setContent { - MashUpTheme { - ScheduleDetailInfoContent( - title = info.title, - date = info.date, - time = info.time, - modifier = Modifier.padding(top = 24.dp) - ) - } - } - } - } - } - - interface OnItemEventListener { - fun onExitEventClick() - } -} - -object EventComparator : DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: EventDetail, - newItem: EventDetail - ): Boolean { - return oldItem.id == newItem.id - } - - override fun areContentsTheSame( - oldItem: EventDetail, - newItem: EventDetail - ): Boolean { - return oldItem == newItem - } -} diff --git a/app/src/main/java/com/mashup/ui/schedule/detail/ScheduleDetailScreen.kt b/app/src/main/java/com/mashup/ui/schedule/detail/ScheduleDetailScreen.kt new file mode 100644 index 00000000..2d8e58d4 --- /dev/null +++ b/app/src/main/java/com/mashup/ui/schedule/detail/ScheduleDetailScreen.kt @@ -0,0 +1,155 @@ +package com.mashup.ui.schedule.detail + +import androidx.compose.foundation.LocalOverscrollConfiguration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.mashup.core.ui.theme.MashUpTheme +import com.mashup.core.ui.widget.ButtonStyle +import com.mashup.core.ui.widget.MashUpButton +import com.mashup.core.ui.widget.MashUpToolbar +import com.mashup.ui.schedule.detail.composable.ScheduleDetailContentItem +import com.mashup.ui.schedule.detail.composable.ScheduleDetailHeaderItem +import com.mashup.ui.schedule.detail.composable.ScheduleDetailInfoItem +import com.mashup.ui.schedule.detail.composable.ScheduleDetailLocationItem +import com.mashup.ui.schedule.model.EventDetail + +@Composable +fun ScheduleDetailScreen( + state: ScheduleState, + isPlatformSeminar: Boolean, + copyToClipboard: (String) -> Unit, + moveToPlatformAttendance: () -> Unit, + onBackPressed: () -> Unit +) { + Box(modifier = Modifier.fillMaxSize()) { + Column { + MashUpToolbar( + title = "상세 스케쥴", + showBackButton = true, + onClickBackButton = onBackPressed + ) + + when (state) { + is ScheduleState.Empty -> {} + is ScheduleState.Success -> EventDetailList(state.eventDetailList, copyToClipboard) + else -> {} + } + } + + Box( + modifier = Modifier + .fillMaxWidth() + .height(140.dp) + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color.Transparent, + Color.White + ) + ) + ) + .align(Alignment.BottomCenter) + ) + + if (isPlatformSeminar.not()) { + MashUpButton( + text = "플랫폼 출석현황 보러가기", + onClick = moveToPlatformAttendance, + modifier = Modifier + .fillMaxWidth() + .padding(start = 20.dp, bottom = 28.dp, end = 20.dp) + .align(Alignment.BottomCenter), + buttonStyle = ButtonStyle.INVERSE + ) + } + } +} + +@Composable +fun EventDetailList(eventDetailList: List, copyToClipboard: (String) -> Unit) { + CompositionLocalProvider( + LocalOverscrollConfiguration provides null + ) { + LazyColumn( + modifier = Modifier + .padding(start = 20.dp, bottom = 60.dp, end = 20.dp) + ) { + itemsIndexed(eventDetailList) { _, item -> + when (item) { + is EventDetail.Info -> { + ScheduleDetailInfoItem( + title = item.title, + date = item.date, + time = item.formattedTime + ) + } + + is EventDetail.Location -> { + ScheduleDetailLocationItem( + detailAddress = item.detailAddress, + roadAddress = item.roadAddress, + latitude = item.latitude, + longitude = item.longitude, + copyToClipboard = copyToClipboard + ) + } + + is EventDetail.Notice -> { + ScheduleDetailLocationItem(item.content) + } + + is EventDetail.Header -> { + ScheduleDetailHeaderItem( + isFirstEvent = item.eventId == 1, + title = item.title, + time = item.formattedTime + ) + } + + is EventDetail.Content -> { + ScheduleDetailContentItem( + contentId = item.contentId, + title = item.title, + content = item.content, + time = item.formattedTime + ) + } + } + } + + item { + Spacer(modifier = Modifier.height(28.dp)) + } + } + } +} + +@Preview +@Composable +fun PreviewScheduleDetailScreen() { + MashUpTheme { + ScheduleDetailScreen( + state = ScheduleState.Empty, + isPlatformSeminar = false, + copyToClipboard = {}, + moveToPlatformAttendance = {}, + onBackPressed = {} + ) + } +} diff --git a/app/src/main/java/com/mashup/ui/schedule/detail/ScheduleDetailViewModel.kt b/app/src/main/java/com/mashup/ui/schedule/detail/ScheduleDetailViewModel.kt index 91927b61..5d3dae40 100644 --- a/app/src/main/java/com/mashup/ui/schedule/detail/ScheduleDetailViewModel.kt +++ b/app/src/main/java/com/mashup/ui/schedule/detail/ScheduleDetailViewModel.kt @@ -2,18 +2,11 @@ package com.mashup.ui.schedule.detail import androidx.lifecycle.SavedStateHandle import com.mashup.constant.EXTRA_SCHEDULE_ID +import com.mashup.constant.EXTRA_SCHEDULE_TYPE import com.mashup.core.common.base.BaseViewModel -import com.mashup.core.common.extensions.getTimeFormat -import com.mashup.data.dto.ContentResponse -import com.mashup.data.dto.EventResponse -import com.mashup.data.dto.ScheduleResponse import com.mashup.data.repository.ScheduleRepository -import com.mashup.ui.schedule.model.Body import com.mashup.ui.schedule.model.EventDetail -import com.mashup.ui.schedule.model.EventDetailType -import com.mashup.ui.schedule.model.Header -import com.mashup.ui.schedule.model.Info -import com.mashup.ui.schedule.model.Location +import com.mashup.ui.schedule.model.EventDetailMapper import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -22,12 +15,11 @@ import javax.inject.Inject @HiltViewModel class ScheduleDetailViewModel @Inject constructor( private val scheduleRepository: ScheduleRepository, + private val eventMapper: EventDetailMapper, savedStateHandle: SavedStateHandle ) : BaseViewModel() { - private val scheduleId = - checkNotNull( - savedStateHandle.get(EXTRA_SCHEDULE_ID) - ) + val scheduleId = checkNotNull(savedStateHandle.get(EXTRA_SCHEDULE_ID)) + val scheduleType = checkNotNull(savedStateHandle.get(EXTRA_SCHEDULE_TYPE)) private val _scheduleState = MutableStateFlow(ScheduleState.Empty) val scheduleState: StateFlow = _scheduleState @@ -41,16 +33,15 @@ class ScheduleDetailViewModel @Inject constructor( _scheduleState.emit(ScheduleState.Loading) scheduleRepository.getSchedule(scheduleId) .onSuccess { response -> - _scheduleState.emit( - ScheduleState.Success( - getEventDetailList( - title = response.name, - date = response.getDate(), - eventList = response.eventList, - location = response.location - ) - ) + val eventList = eventMapper.getEventDetailList( + title = response.name, + date = response.getDate(), + eventList = response.eventList, + location = response.location, + notice = response.notice ) + + _scheduleState.emit(ScheduleState.Success(eventList)) } .onFailure { code -> handleErrorCode(code) @@ -63,93 +54,6 @@ class ScheduleDetailViewModel @Inject constructor( _scheduleState.emit(ScheduleState.Error(code)) } } - - private fun getEventDetailList( - title: String, - date: String, - eventList: List, - location: ScheduleResponse.Location? - ): List { - var itemId = 0 - val eventDetailList = mutableListOf() - - val startAt = eventList.first().startedAt.getTimeFormat() - val endedAt = eventList.last().endedAt.getTimeFormat() - eventDetailList.add(mapToInfoModel(itemId++, title, date, "$startAt - $endedAt")) - - if (location?.detailAddress != null) { // 위치 정보가 있는 경우(온라인이면 placeName이 Zoom으로 내려옴) - eventDetailList.add(mapToLocationModel(itemId++, location)) - } - - eventList.forEachIndexed { eventIndex, event -> - eventDetailList.add(mapToHeaderModel(itemId++, eventIndex, event)) - - event.contentList.forEachIndexed { contentIndex, content -> - eventDetailList.add(mapToContentModel(itemId++, contentIndex, content)) - } - } - - return eventDetailList - } - - private fun mapToHeaderModel(itemId: Int, eventIndex: Int, event: EventResponse): EventDetail { - return EventDetail( - id = itemId, - type = EventDetailType.HEADER, - header = Header( - eventId = eventIndex + 1, - startedAt = event.startedAt, - endedAt = event.endedAt - ) - ) - } - - private fun mapToContentModel( - itemId: Int, - contentIndex: Int, - content: ContentResponse - ): EventDetail { - return EventDetail( - id = itemId, - type = EventDetailType.CONTENT, - body = Body( - contentId = "${contentIndex + 1}", - title = content.title, - content = content.content.orEmpty(), - startedAt = content.startedAt - ) - ) - } - - private fun mapToLocationModel(itemId: Int, location: ScheduleResponse.Location): EventDetail { - return EventDetail( - id = itemId, - type = EventDetailType.LOCATION, - location = Location( - detailAddress = location.detailAddress.orEmpty(), - roadAddress = location.roadAddress.orEmpty(), - latitude = location.latitude, - longitude = location.longitude - ) - ) - } - - private fun mapToInfoModel( - itemId: Int, - title: String, - date: String, - time: String - ): EventDetail { - return EventDetail( - id = itemId, - type = EventDetailType.INFO, - info = Info( - title = title, - date = date, - time = time - ) - ) - } } sealed interface ScheduleState { diff --git a/app/src/main/java/com/mashup/ui/schedule/detail/composable/ScheduleDetailContentItem.kt b/app/src/main/java/com/mashup/ui/schedule/detail/composable/ScheduleDetailContentItem.kt new file mode 100644 index 00000000..99223626 --- /dev/null +++ b/app/src/main/java/com/mashup/ui/schedule/detail/composable/ScheduleDetailContentItem.kt @@ -0,0 +1,89 @@ +package com.mashup.ui.schedule.detail.composable + +import androidx.compose.foundation.background +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.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.mashup.core.ui.colors.Gray100 +import com.mashup.core.ui.colors.Gray400 +import com.mashup.core.ui.colors.Gray600 +import com.mashup.core.ui.theme.MashUpTheme +import com.mashup.core.ui.typography.Body4 +import com.mashup.core.ui.typography.Caption1 +import com.mashup.core.ui.typography.Caption2 +import com.mashup.core.ui.typography.SubTitle2 + +@Composable +fun ScheduleDetailContentItem( + contentId: String, + title: String, + content: String, + time: String +) { + Column(modifier = Modifier.padding(vertical = 8.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = Modifier + .size(20.dp) + .clip(CircleShape) + .background(Gray100), + contentAlignment = Alignment.Center + ) { + Text( + text = contentId, + style = Caption2, + color = Gray600 + ) + } + + Text( + text = title, + style = SubTitle2, + modifier = Modifier + .weight(1f) + .padding(horizontal = 8.dp) + ) + + Text( + text = time, + style = Caption1, + color = Gray400 + ) + } + + if (content.isNotEmpty()) { + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = content, + style = Body4, + color = Gray600, + modifier = Modifier.padding(start = 28.dp) + ) + } + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewScheduleDetailContentItem() { + MashUpTheme { + ScheduleDetailContentItem( + contentId = "1", + title = "안드로이드 팀 세미나", + content = "Android Crew", + time = "AM 11:00" + ) + } +} diff --git a/app/src/main/java/com/mashup/ui/schedule/detail/composable/ScheduleDetailHeaderItem.kt b/app/src/main/java/com/mashup/ui/schedule/detail/composable/ScheduleDetailHeaderItem.kt new file mode 100644 index 00000000..503dcd45 --- /dev/null +++ b/app/src/main/java/com/mashup/ui/schedule/detail/composable/ScheduleDetailHeaderItem.kt @@ -0,0 +1,87 @@ +package com.mashup.ui.schedule.detail.composable + +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.Spacer +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.shape.CircleShape +import androidx.compose.material.Divider +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.mashup.core.common.R +import com.mashup.core.ui.colors.Brand100 +import com.mashup.core.ui.colors.Brand500 +import com.mashup.core.ui.colors.Gray100 +import com.mashup.core.ui.theme.MashUpTheme +import com.mashup.core.ui.typography.Caption1 +import com.mashup.core.ui.typography.Header2 + +@Composable +fun ScheduleDetailHeaderItem( + isFirstEvent: Boolean, + title: String, + time: String +) { + Column { + if (isFirstEvent.not()) { + Spacer(modifier = Modifier.height(20.dp)) + Divider(color = Gray100, thickness = 1.dp) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 20.dp, bottom = 12.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(text = title, style = Header2) + + Row( + modifier = Modifier + .clip(CircleShape) + .background(Brand100) + .padding(horizontal = 8.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.ic_clock), + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + + Spacer(modifier = Modifier.width(4.dp)) + + Text( + text = time, + style = Caption1, + color = Brand500 + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewScheduleDetailHeaderItem() { + MashUpTheme { + ScheduleDetailHeaderItem( + isFirstEvent = false, + title = "1부", + time = "10:00 - 11:00" + ) + } +} diff --git a/app/src/main/java/com/mashup/ui/schedule/detail/composable/ScheduleDetailInfoContent.kt b/app/src/main/java/com/mashup/ui/schedule/detail/composable/ScheduleDetailInfoItem.kt similarity index 91% rename from app/src/main/java/com/mashup/ui/schedule/detail/composable/ScheduleDetailInfoContent.kt rename to app/src/main/java/com/mashup/ui/schedule/detail/composable/ScheduleDetailInfoItem.kt index 728f8df8..7e7ddaa1 100644 --- a/app/src/main/java/com/mashup/ui/schedule/detail/composable/ScheduleDetailInfoContent.kt +++ b/app/src/main/java/com/mashup/ui/schedule/detail/composable/ScheduleDetailInfoItem.kt @@ -13,7 +13,7 @@ import com.mashup.core.ui.typography.Header1 import com.mashup.core.common.R as CR @Composable -fun ScheduleDetailInfoContent( +fun ScheduleDetailInfoItem( title: String, date: String, time: String, @@ -33,9 +33,9 @@ fun ScheduleDetailInfoContent( @Preview @Composable -fun PreviewScheduleDetailInfoContent() { +fun PreviewScheduleDetailInfoItem() { MashUpTheme { - ScheduleDetailInfoContent( + ScheduleDetailInfoItem( title = "1차 정기 세미나", date = "3월 27일", time = "오후 3:00 - 오전 7:00" diff --git a/app/src/main/java/com/mashup/ui/schedule/detail/composable/ScheduleDetailLocationContent.kt b/app/src/main/java/com/mashup/ui/schedule/detail/composable/ScheduleDetailLocationItem.kt similarity index 97% rename from app/src/main/java/com/mashup/ui/schedule/detail/composable/ScheduleDetailLocationContent.kt rename to app/src/main/java/com/mashup/ui/schedule/detail/composable/ScheduleDetailLocationItem.kt index 84b38ebf..dedb249c 100644 --- a/app/src/main/java/com/mashup/ui/schedule/detail/composable/ScheduleDetailLocationContent.kt +++ b/app/src/main/java/com/mashup/ui/schedule/detail/composable/ScheduleDetailLocationItem.kt @@ -41,7 +41,7 @@ import com.mashup.core.common.R as CR @OptIn(ExperimentalNaverMapApi::class) @Composable -fun ScheduleDetailLocationContent( +fun ScheduleDetailLocationItem( detailAddress: String, roadAddress: String, latitude: Double?, @@ -140,9 +140,9 @@ fun ScheduleDetailLocationContent( @Preview @Composable -fun PreviewScheduleDetailLocationContent() { +fun PreviewScheduleDetailLocationItem() { MashUpTheme { - ScheduleDetailLocationContent( + ScheduleDetailLocationItem( detailAddress = "알파돔타워", roadAddress = "경기도 성남시 분당구 판교역로 152", latitude = 37.532600, diff --git a/app/src/main/java/com/mashup/ui/schedule/detail/composable/ScheduleDetailNoticeItem.kt b/app/src/main/java/com/mashup/ui/schedule/detail/composable/ScheduleDetailNoticeItem.kt new file mode 100644 index 00000000..0596b592 --- /dev/null +++ b/app/src/main/java/com/mashup/ui/schedule/detail/composable/ScheduleDetailNoticeItem.kt @@ -0,0 +1,23 @@ +package com.mashup.ui.schedule.detail.composable + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.mashup.core.ui.colors.Gray600 +import com.mashup.core.ui.colors.Gray700 +import com.mashup.core.ui.typography.Body5 +import com.mashup.core.ui.typography.SubTitle2 + +@Composable +fun ScheduleDetailLocationItem(content: String) { + Column { + Spacer(modifier = Modifier.height(16.dp)) + Text(text = "공지", style = SubTitle2, color = Gray700) + Spacer(modifier = Modifier.height(4.dp)) + Text(text = content, style = Body5, color = Gray600) + } +} diff --git a/app/src/main/java/com/mashup/ui/schedule/detail/composable/ScheduleInfoText.kt b/app/src/main/java/com/mashup/ui/schedule/detail/composable/ScheduleInfoText.kt index 033bb9e2..f5dda445 100644 --- a/app/src/main/java/com/mashup/ui/schedule/detail/composable/ScheduleInfoText.kt +++ b/app/src/main/java/com/mashup/ui/schedule/detail/composable/ScheduleInfoText.kt @@ -17,7 +17,7 @@ import androidx.compose.ui.unit.dp import com.mashup.core.ui.colors.Gray300 import com.mashup.core.ui.colors.Gray700 import com.mashup.core.ui.theme.MashUpTheme -import com.mashup.core.ui.typography.Body3 +import com.mashup.core.ui.typography.Body4 import com.mashup.core.common.R as CR @Composable @@ -39,7 +39,7 @@ fun ScheduleInfoText( Spacer(modifier = Modifier.width(4.dp)) - Text(text = info, style = Body3, color = Gray700) + Text(text = info, style = Body4, color = Gray700) } } diff --git a/app/src/main/java/com/mashup/ui/schedule/item/CardInfoItem.kt b/app/src/main/java/com/mashup/ui/schedule/item/CardInfoItem.kt index d0d0eb4e..8b4ebfe7 100644 --- a/app/src/main/java/com/mashup/ui/schedule/item/CardInfoItem.kt +++ b/app/src/main/java/com/mashup/ui/schedule/item/CardInfoItem.kt @@ -3,14 +3,11 @@ package com.mashup.ui.schedule.item import androidx.compose.foundation.Image import androidx.compose.foundation.background 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.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -22,19 +19,18 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.mashup.core.common.R -import com.mashup.core.ui.colors.Gray100 import com.mashup.core.ui.colors.Gray300 import com.mashup.core.ui.colors.Gray400 -import com.mashup.core.ui.colors.Gray600 -import com.mashup.core.ui.colors.Gray700 +import com.mashup.core.ui.colors.Gray800 import com.mashup.core.ui.colors.Gray900 import com.mashup.core.ui.theme.MashUpTheme -import com.mashup.core.ui.typography.Body3 +import com.mashup.core.ui.typography.Body4 import com.mashup.core.ui.typography.Header1 +import com.mashup.core.ui.widget.MashupPlatformBadge @Composable fun CardInfoItem( - dDay: String, + platform: String, title: String, calendar: String, timeLine: String, @@ -42,19 +38,7 @@ fun CardInfoItem( modifier: Modifier = Modifier ) { Column(modifier = modifier) { - Box( - modifier = Modifier.background( - color = Gray100, - shape = RoundedCornerShape(100.dp) - ).padding(horizontal = 10.dp, vertical = 3.5.dp), - contentAlignment = Alignment.Center - ) { - Text( - style = Body3, - text = dDay, - color = Gray600 - ) - } + MashupPlatformBadge(platform = platform) Spacer( modifier = Modifier.height(10.dp) ) @@ -82,9 +66,9 @@ fun CardInfoItem( ) Text( - style = Body3, - text = calendar, - color = Gray700 + style = Body4, + text = calendar.ifEmpty { "-" }, + color = Gray800 ) } @@ -100,32 +84,30 @@ fun CardInfoItem( colorFilter = ColorFilter.tint(color = Gray300) ) Text( - text = timeLine, - style = Body3, - color = Gray700 + text = timeLine.ifEmpty { "-" }, + style = Body4, + color = Gray800 ) } - if (location.isNotEmpty()) { - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Image( - modifier = Modifier.size(20.dp), - painter = painterResource(id = R.drawable.ic_location), - contentDescription = null, - contentScale = ContentScale.Fit, - colorFilter = ColorFilter.tint(color = Gray300) - ) + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + modifier = Modifier.size(20.dp), + painter = painterResource(id = R.drawable.ic_location), + contentDescription = null, + contentScale = ContentScale.Fit, + colorFilter = ColorFilter.tint(color = Gray300) + ) - Text( - style = Body3, - text = location, - color = Gray700 - ) - } + Text( + style = Body4, + text = location.ifEmpty { "-" }, + color = Gray800 + ) } } } @@ -137,7 +119,7 @@ private fun PreviewCardInfoItem() { MashUpTheme { CardInfoItem( modifier = Modifier.background(color = Color.White), - dDay = "D+13", + platform = "ALL", title = "스케쥴 테스트", calendar = "02월 05일", timeLine = "오후 01:00 - 오후 01:10", diff --git a/app/src/main/java/com/mashup/ui/schedule/item/EmptyScheduleItem.kt b/app/src/main/java/com/mashup/ui/schedule/item/EmptyScheduleItem.kt new file mode 100644 index 00000000..efe58e7e --- /dev/null +++ b/app/src/main/java/com/mashup/ui/schedule/item/EmptyScheduleItem.kt @@ -0,0 +1,79 @@ +package com.mashup.ui.schedule.item + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.mashup.core.ui.theme.MashUpTheme +import com.mashup.core.ui.typography.Header2 +import com.mashup.core.ui.widget.MashUpGradientButton +import com.mashup.core.common.R as CommonR + +@Composable +fun EmptyScheduleItem( + modifier: Modifier = Modifier, + onClickMashongButton: () -> Unit = {} +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Spacer( + modifier = Modifier.height(34.dp) + ) + Image( + painter = painterResource(id = CommonR.drawable.img_empty_schdule), + contentDescription = null, + modifier = Modifier + .width(284.dp) + .height(256.dp) + ) + Spacer( + modifier = Modifier.height(17.dp) + ) + Text( + text = "이번주는 자유다..!", + style = Header2, + color = Color(0xFF412491) + ) + Spacer( + modifier = Modifier.height(17.dp) + ) + MashUpGradientButton( + modifier = Modifier + .width(256.dp) + .height(48.dp), + text = "매숑이 밥주러 가기", + onClick = onClickMashongButton, + gradientColors = listOf( + Color(0xFFB398FE), + Color(0xFF47BBF1) + ) + ) + } +} + +@Preview +@Composable +private fun PreviewEmptyScheduleItem() { + MashUpTheme { + Box( + modifier = Modifier.background(color = Color.White) + ) { + EmptyScheduleItem() + } + } +} diff --git a/app/src/main/java/com/mashup/ui/schedule/item/SchedeuleViewPagerInProgressItem.kt b/app/src/main/java/com/mashup/ui/schedule/item/SchedeuleViewPagerInProgressItem.kt index cd5bbcae..279c4580 100644 --- a/app/src/main/java/com/mashup/ui/schedule/item/SchedeuleViewPagerInProgressItem.kt +++ b/app/src/main/java/com/mashup/ui/schedule/item/SchedeuleViewPagerInProgressItem.kt @@ -1,8 +1,6 @@ package com.mashup.ui.schedule.item -import android.text.SpannableStringBuilder import android.view.Gravity -import android.view.View import androidx.appcompat.widget.AppCompatTextView import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -15,73 +13,89 @@ import androidx.compose.foundation.layout.Spacer 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.layout.wrapContentHeight import androidx.compose.foundation.shape.RoundedCornerShape 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.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.ParagraphStyle +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView -import androidx.compose.ui.viewinterop.AndroidViewBinding import androidx.constraintlayout.compose.ConstraintLayout -import androidx.core.content.res.ResourcesCompat -import androidx.core.text.HtmlCompat import com.mashup.R -import com.mashup.core.ui.colors.Brand100 -import com.mashup.core.ui.colors.Gray100 -import com.mashup.core.ui.colors.Gray50 -import com.mashup.core.ui.colors.Gray700 import com.mashup.core.ui.theme.MashUpTheme -import com.mashup.core.ui.typography.Body1 import com.mashup.data.dto.ScheduleResponse -import com.mashup.databinding.LayoutAttendanceCoachMarkBindingImpl import com.mashup.ui.schedule.model.ScheduleCard +import com.mashup.ui.schedule.util.getBackgroundColor +import com.mashup.ui.schedule.util.getBorderColor +import com.mashup.ui.schedule.util.getButtonTextColor import java.util.Date @Composable fun ScheduleViewPagerInProgressItem( data: ScheduleCard.InProgressSchedule, modifier: Modifier = Modifier, - onClickScheduleInformation: (Int) -> Unit = {}, - onClickAttendance: (Int) -> Unit = {} + onClickScheduleInformation: (Int, String) -> Unit = { _, _ -> } ) { + val textColor by remember { mutableStateOf(data.scheduleResponse.scheduleType.getButtonTextColor()) } Column( - modifier = modifier.fillMaxWidth().wrapContentHeight().background( - color = Color.White, - shape = RoundedCornerShape(20.dp) - ).border( - width = 1.dp, - color = Gray100, - shape = RoundedCornerShape(20.dp) - ).clip(RoundedCornerShape(20.dp)) + modifier = modifier + .fillMaxWidth() + .wrapContentHeight() + .background( + color = data.scheduleResponse.scheduleType.getBackgroundColor(), + shape = RoundedCornerShape(20.dp) + ) + .border( + width = 1.dp, + color = data.scheduleResponse.scheduleType.getBorderColor(), + shape = RoundedCornerShape(20.dp) + ) + .clip(RoundedCornerShape(20.dp)) .clickable { - onClickScheduleInformation(data.scheduleResponse.scheduleId) - }.padding(20.dp), + onClickScheduleInformation(data.scheduleResponse.scheduleId, data.scheduleResponse.scheduleType) + } + .padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally ) { CardInfoItem( - modifier = Modifier.fillMaxWidth().wrapContentHeight(), - dDay = data.scheduleResponse.getDDay(), + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + platform = data.scheduleResponse.scheduleType, title = data.scheduleResponse.name, calendar = data.scheduleResponse.getDate(), timeLine = data.scheduleResponse.getTimeLine(), - location = "" + location = data.scheduleResponse.location?.detailAddress ?: "" ) Spacer(modifier = Modifier.height(16.dp)) ConstraintLayout { - val (coachMark, button, schedule) = createRefs() + val (button, schedule) = createRefs() Column( - modifier = Modifier.background(color = Gray50, shape = RoundedCornerShape(16.dp)) - .height(220.dp).padding(top = 16.dp, bottom = 20.dp, start = 20.dp, end = 20.dp).fillMaxWidth() + modifier = Modifier + .background(color = Color(0xFFE1F2FA), shape = RoundedCornerShape(16.dp)) + .height(176.dp) + .padding(top = 12.dp, bottom = 12.dp, start = 20.dp, end = 20.dp) + .fillMaxWidth() .constrainAs(schedule) { top.linkTo(parent.top) start.linkTo(parent.start) @@ -91,50 +105,57 @@ fun ScheduleViewPagerInProgressItem( verticalArrangement = Arrangement.Center ) { Image( - painterResource(id = com.mashup.core.common.R.drawable.img_standby), + modifier = Modifier.size(100.dp), + painter = painterResource(id = com.mashup.core.common.R.drawable.img_standby), contentDescription = null ) Spacer( modifier = Modifier.height(10.dp) ) - val text = stringResource(id = R.string.description_standby_schedule) - val spannableString = SpannableStringBuilder( - String.format( - text, - data.attendanceInfo?.memberName ?: "알 수 없음" - ) - ).toString() + Text( - text = HtmlCompat.fromHtml( - spannableString, - HtmlCompat.FROM_HTML_MODE_COMPACT - ).toAnnotatedString(), - color = Gray700, - style = Body1 + text = buildAnnotatedString { + withStyle( + ParagraphStyle(lineHeight = 19.09.sp) + ) { + withStyle( + SpanStyle( + color = Color(0xFF4D535E), + fontSize = 16.sp, + fontWeight = FontWeight.W500 + ) + ) { + append(data.attendanceInfo?.memberName ?: "알 수 없음") + } + withStyle( + SpanStyle( + color = Color(0xFFABB2C1), + fontSize = 16.sp, + fontWeight = FontWeight.W400 + ) + ) { + append("님의\n참석을 기다리고 있어요.") + } + } + }, + textAlign = TextAlign.Center ) } - AndroidViewBinding( - modifier = Modifier.constrainAs(coachMark) { - bottom.linkTo(button.top, 4.dp) - start.linkTo(button.start) - end.linkTo(button.end) - }, - factory = LayoutAttendanceCoachMarkBindingImpl::inflate, - update = { - this.root.visibility = View.VISIBLE - } - ) AndroidView( - modifier = Modifier.fillMaxWidth().height(48.dp).background( - color = Brand100, - shape = RoundedCornerShape(16.dp) - ).constrainAs(button) { - bottom.linkTo(parent.bottom) - start.linkTo(parent.start) - end.linkTo(parent.end) - top.linkTo(schedule.bottom, 12.dp) - }, + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + .background( + color = Color(0xFFC2EBFF), + shape = RoundedCornerShape(16.dp) + ) + .constrainAs(button) { + bottom.linkTo(parent.bottom) + start.linkTo(parent.start) + end.linkTo(parent.end) + top.linkTo(schedule.bottom, 14.dp) + }, factory = { context -> AppCompatTextView(context).apply { text = context.getString(R.string.click_attendance_list) @@ -143,15 +164,11 @@ fun ScheduleViewPagerInProgressItem( ) gravity = Gravity.CENTER setTextColor( - ResourcesCompat.getColor( - resources, - com.mashup.core.common.R.color.brand500, - null - ) + textColor.toArgb() ) setPadding(12, 0, 0, 0) setOnClickListener { - onClickAttendance(data.scheduleResponse.scheduleId) + onClickScheduleInformation(data.scheduleResponse.scheduleId, data.scheduleResponse.scheduleType) } } } @@ -165,7 +182,9 @@ fun ScheduleViewPagerInProgressItem( private fun PreviewScheduleViewPagerEmptySchedule() { MashUpTheme { Box( - modifier = Modifier.width(294.dp).height(479.dp) + modifier = Modifier + .width(294.dp) + .height(479.dp) ) { ScheduleViewPagerInProgressItem( data = ScheduleCard.InProgressSchedule( @@ -182,7 +201,9 @@ private fun PreviewScheduleViewPagerEmptySchedule() { longitude = 0.0, roadAddress = null, detailAddress = null - ) + ), + scheduleType = "ALL", + notice = null ), attendanceInfo = null ) diff --git a/app/src/main/java/com/mashup/ui/schedule/item/ScheduleViewPagerEmptySchedule.kt b/app/src/main/java/com/mashup/ui/schedule/item/ScheduleViewPagerEmptySchedule.kt index a4b32ce6..5c8560c6 100644 --- a/app/src/main/java/com/mashup/ui/schedule/item/ScheduleViewPagerEmptySchedule.kt +++ b/app/src/main/java/com/mashup/ui/schedule/item/ScheduleViewPagerEmptySchedule.kt @@ -22,12 +22,12 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.mashup.R -import com.mashup.core.ui.colors.Gray100 import com.mashup.core.ui.colors.Gray400 -import com.mashup.core.ui.colors.Gray50 import com.mashup.core.ui.theme.MashUpTheme import com.mashup.core.ui.typography.Body1 import com.mashup.ui.schedule.model.ScheduleCard +import com.mashup.ui.schedule.util.getBackgroundColor +import com.mashup.ui.schedule.util.getBorderColor @Composable fun ScheduleViewPagerEmptyItem( @@ -36,28 +36,29 @@ fun ScheduleViewPagerEmptyItem( ) { Column( modifier = modifier.fillMaxWidth().wrapContentHeight().background( - color = Color.White, + color = data.scheduleResponse?.scheduleType?.getBackgroundColor() ?: Color(0xFFECF9FF), shape = RoundedCornerShape(20.dp) - ).border( - width = 1.dp, - color = Gray100, - shape = RoundedCornerShape(20.dp) - ).padding(20.dp), + ) + .border( + width = 1.dp, + color = data.scheduleResponse?.scheduleType?.getBorderColor() ?: Color(0xFFE1F2FA), + shape = RoundedCornerShape(20.dp) + ).padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally ) { CardInfoItem( modifier = Modifier.fillMaxWidth().wrapContentHeight(), - dDay = data.scheduleResponse?.getDDay() ?: "?", + platform = data.scheduleResponse?.scheduleType ?: "ALL", title = data.scheduleResponse?.name ?: "", calendar = data.scheduleResponse?.getDate() ?: "-", timeLine = data.scheduleResponse?.getTimeLine() ?: "-", - location = "" + location = "-" ) Spacer(modifier = Modifier.height(16.dp)) Box( modifier = Modifier.fillMaxWidth().background( - color = Gray50, + color = Color(0xFFE1F2FA), shape = RoundedCornerShape(16.dp) ).padding( vertical = 22.dp, diff --git a/app/src/main/java/com/mashup/ui/schedule/item/ScheduleViewPagerSuccessItem.kt b/app/src/main/java/com/mashup/ui/schedule/item/ScheduleViewPagerSuccessItem.kt index 07c783a2..281329df 100644 --- a/app/src/main/java/com/mashup/ui/schedule/item/ScheduleViewPagerSuccessItem.kt +++ b/app/src/main/java/com/mashup/ui/schedule/item/ScheduleViewPagerSuccessItem.kt @@ -8,46 +8,68 @@ import android.text.style.StyleSpan import android.text.style.UnderlineSpan import android.view.Gravity import androidx.appcompat.widget.AppCompatTextView +import androidx.compose.foundation.Image 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.Spacer 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.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Divider 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.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView -import androidx.core.content.res.ResourcesCompat import androidx.core.text.HtmlCompat import com.mashup.R -import com.mashup.core.ui.colors.Brand100 -import com.mashup.core.ui.colors.Gray100 -import com.mashup.core.ui.colors.Gray50 +import com.mashup.core.ui.colors.Brand200 +import com.mashup.core.ui.colors.Gray600 import com.mashup.core.ui.colors.Gray700 +import com.mashup.core.ui.colors.Gray900 import com.mashup.core.ui.typography.Body1 +import com.mashup.core.ui.typography.Body5 +import com.mashup.core.ui.typography.Caption1 +import com.mashup.core.ui.typography.SubTitle2 +import com.mashup.core.ui.widget.PlatformType import com.mashup.data.dto.EventResponse import com.mashup.ui.schedule.ViewEventTimeline import com.mashup.ui.schedule.model.ScheduleCard +import com.mashup.ui.schedule.util.convertCamelCase +import com.mashup.ui.schedule.util.getBackgroundColor +import com.mashup.ui.schedule.util.getBorderColor +import com.mashup.ui.schedule.util.getButtonBackgroundColor +import com.mashup.ui.schedule.util.getButtonTextColor +import com.mashup.ui.schedule.util.getEventTimelineBackgroundColor import com.mashup.ui.schedule.util.onBindAttendanceImage import com.mashup.ui.schedule.util.onBindAttendanceStatus import com.mashup.ui.schedule.util.onBindAttendanceTime @@ -56,27 +78,36 @@ import com.mashup.ui.schedule.util.onBindAttendanceTime fun ScheduleViewPagerSuccessItem( data: ScheduleCard.EndSchedule, modifier: Modifier = Modifier, - onClickScheduleInformation: (Int) -> Unit = {}, - onClickAttendance: (Int) -> Unit = {} + onClickScheduleInformation: (Int, String) -> Unit = { _, _ -> }, + makeToast: (String) -> Unit = {} ) { val context = LocalContext.current val listState = rememberLazyListState() + val textColor by remember { mutableStateOf(data.scheduleResponse.scheduleType.getButtonTextColor()) } Column( modifier = modifier .fillMaxWidth() .wrapContentHeight() .background( - color = Color.White, + color = data.scheduleResponse.scheduleType.getBackgroundColor(), shape = RoundedCornerShape(20.dp) ) .border( width = 1.dp, - color = Gray100, + color = data.scheduleResponse.scheduleType.getBorderColor(), shape = RoundedCornerShape(20.dp) - ).clip(RoundedCornerShape(20.dp)) + ) + .clip(RoundedCornerShape(20.dp)) .clickable { - onClickScheduleInformation(data.scheduleResponse.scheduleId) + if (data.scheduleResponse.notice.isNullOrEmpty() && data.scheduleResponse.eventList.isEmpty()) { + makeToast("볼 수 있는 일정이 없어요..!") + } else { + onClickScheduleInformation( + data.scheduleResponse.scheduleId, + data.scheduleResponse.scheduleType + ) + } } .padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally @@ -85,111 +116,215 @@ fun ScheduleViewPagerSuccessItem( modifier = Modifier .fillMaxWidth() .wrapContentHeight(), - dDay = data.scheduleResponse.getDDay(), + platform = data.scheduleResponse.scheduleType, title = data.scheduleResponse.name, calendar = data.scheduleResponse.getDate(), timeLine = data.scheduleResponse.getTimeLine(), location = data.scheduleResponse.location?.detailAddress ?: "" ) - Spacer(modifier = Modifier.height(16.dp)) - LazyColumn( - state = listState, - modifier = Modifier - .background(color = Gray50, shape = RoundedCornerShape(16.dp)) - .height(220.dp) - .padding(top = 16.dp, start = 20.dp, end = 20.dp) - ) { - itemsIndexed(data.scheduleResponse.eventList, key = { _: Int, item: EventResponse -> - item.eventId - }) { index: Int, _: EventResponse -> - Column { - if (index == 0) { - val spannableString = SpannableStringBuilder( - String.format( - context.resources.getString( - R.string.event_list_card_title - ), - data.attendanceInfo.memberName - ) - ).toString() + if (data.scheduleResponse.scheduleType.convertCamelCase() == PlatformType.Seminar) { + Spacer(modifier = Modifier.height(12.dp)) + LazyColumn( + state = listState, + modifier = Modifier + .background( + color = data.scheduleResponse.scheduleType.getEventTimelineBackgroundColor(), + shape = RoundedCornerShape(16.dp) + ) + .height(176.dp) + .padding(top = 16.dp, start = 20.dp, end = 20.dp) + ) { + itemsIndexed(data.scheduleResponse.eventList, key = { _: Int, item: EventResponse -> + item.eventId + }) { index: Int, _: EventResponse -> + Column { + if (index == 0) { + val spannableString = SpannableStringBuilder( + String.format( + context.resources.getString( + R.string.event_list_card_title + ), + data.attendanceInfo.memberName + ) + ).toString() - Text( - text = HtmlCompat.fromHtml( - spannableString, - HtmlCompat.FROM_HTML_MODE_COMPACT - ).toAnnotatedString(), - color = Gray700, - style = Body1 - ) + Text( + text = HtmlCompat.fromHtml( + spannableString, + HtmlCompat.FROM_HTML_MODE_COMPACT + ).toAnnotatedString(), + color = Gray700, + style = Body1 + ) - Spacer( - modifier = Modifier.height(16.dp) + Spacer( + modifier = Modifier.height(16.dp) + ) + } + ViewEventTimeline( + modifier = Modifier.fillMaxWidth(), + caption = stringResource(id = R.string.attendance_caption, index + 1), + time = onBindAttendanceTime(data.attendanceInfo.getAttendanceAt(index)), + status = onBindAttendanceStatus( + data.attendanceInfo.getAttendanceStatus(index) + ), + image = onBindAttendanceImage( + data.attendanceInfo.getAttendanceStatus(index) + ) ) } + Spacer( + modifier = Modifier.height(6.dp) + ) + } + item { + val status = data.attendanceInfo.getFinalAttendance() ViewEventTimeline( modifier = Modifier.fillMaxWidth(), - caption = stringResource(id = R.string.attendance_caption, index + 1), - time = onBindAttendanceTime(data.attendanceInfo.getAttendanceAt(index)), - status = onBindAttendanceStatus( - data.attendanceInfo.getAttendanceStatus(index) - ), - image = onBindAttendanceImage( - data.attendanceInfo.getAttendanceStatus(index) - ) + caption = stringResource(id = R.string.attendance_final), + status = onBindAttendanceStatus(status, isFinal = true), + image = onBindAttendanceImage(status, isFinal = true), + isFinal = true ) } - } - item { - val status = data.attendanceInfo.getFinalAttendance() - ViewEventTimeline( - modifier = Modifier.fillMaxWidth(), - caption = stringResource(id = R.string.attendance_final), - status = onBindAttendanceStatus(status, isFinal = true), - image = onBindAttendanceImage(status, isFinal = true), - isFinal = true - ) - } - item { - Spacer(modifier = Modifier.height(20.dp)) + item { + Spacer(modifier = Modifier.height(20.dp)) + } } - } - - Spacer( - modifier = Modifier.height(12.dp) - ) - AndroidView( - modifier = Modifier - .fillMaxWidth() - .height(48.dp) - .background( - color = Brand100, - shape = RoundedCornerShape(16.dp) + Spacer( + modifier = Modifier.height(12.dp) + ) + AndroidView( + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + .background( + color = data.scheduleResponse.scheduleType.getButtonBackgroundColor(), + shape = RoundedCornerShape(16.dp) + ), + factory = { context -> + AppCompatTextView(context).apply { + text = context.getString(R.string.click_attendance_list) + setTextAppearance( + com.mashup.core.common.R.style.TextAppearance_Mashup_Body3_14_M + ) + gravity = Gravity.CENTER + setTextColor( + textColor.toArgb() + ) + setPadding(12, 0, 0, 0) + setOnClickListener { + onClickScheduleInformation( + data.scheduleResponse.scheduleId, + data.scheduleResponse.scheduleType + ) + } + } + } + ) + } else { + Spacer(modifier = Modifier.height(18.dp)) + Divider( + modifier = Modifier.fillMaxWidth(), + color = Brand200 + ) + Spacer( + modifier = Modifier.height(18.dp) + ) + Text( + modifier = Modifier.fillMaxWidth(), + text = "공지", + textAlign = TextAlign.Left, + style = SubTitle2.copy( + lineHeight = 19.09.sp ), - factory = { context -> - AppCompatTextView(context).apply { - text = context.getString(R.string.click_attendance_list) - setTextAppearance( - com.mashup.core.common.R.style.TextAppearance_Mashup_Body3_14_M - ) - gravity = Gravity.CENTER - setTextColor( - ResourcesCompat.getColor( - resources, - com.mashup.core.common.R.color.brand500, - null + color = Gray900 + ) + Spacer( + modifier = Modifier.height(6.dp) + ) + + if (data.scheduleResponse.notice.isNullOrEmpty()) { + Box( + modifier = Modifier + .fillMaxWidth() + .background( + color = Color(0xFFEFE9F8), + shape = RoundedCornerShape(16.dp) + ) + .padding( + vertical = 22.dp, + horizontal = 20.dp + ), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + modifier = Modifier.size(88.dp), + painter = painterResource(id = com.mashup.core.common.R.drawable.img_placeholder_sleeping), + contentDescription = null + ) + Text( + text = "공지가 없어요!", + style = Caption1, + color = Gray600 ) - ) - setPadding(12, 0, 0, 0) - setOnClickListener { - onClickAttendance(data.scheduleResponse.scheduleId) } } + } else { + Text( + text = data.scheduleResponse.notice, + maxLines = 5, + style = Body5.copy( + lineHeight = 20.sp + ), + color = Gray700, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Left, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 20.dp) + ) + + AndroidView( + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + .background( + color = data.scheduleResponse.scheduleType.getButtonBackgroundColor(), + shape = RoundedCornerShape(16.dp) + ), + factory = { context -> + AppCompatTextView(context).apply { + text = context.getString(R.string.click_attendance_list) + setTextAppearance( + com.mashup.core.common.R.style.TextAppearance_Mashup_Body3_14_M + ) + gravity = Gravity.CENTER + setTextColor( + textColor.toArgb() + ) + setPadding(12, 0, 0, 0) + setOnClickListener { + onClickScheduleInformation( + data.scheduleResponse.scheduleId, + data.scheduleResponse.scheduleType + ) + } + } + } + ) } - ) + } } } + fun Spanned.toAnnotatedString(): AnnotatedString = buildAnnotatedString { val spanned = this@toAnnotatedString append(spanned.toString()) @@ -206,11 +341,13 @@ fun Spanned.toAnnotatedString(): AnnotatedString = buildAnnotatedString { end ) } + is UnderlineSpan -> addStyle( SpanStyle(textDecoration = TextDecoration.Underline), start, end ) + is ForegroundColorSpan -> addStyle( SpanStyle(color = Color(span.foregroundColor)), start, diff --git a/app/src/main/java/com/mashup/ui/schedule/model/EventDetail.kt b/app/src/main/java/com/mashup/ui/schedule/model/EventDetail.kt index a5a29344..c4243580 100644 --- a/app/src/main/java/com/mashup/ui/schedule/model/EventDetail.kt +++ b/app/src/main/java/com/mashup/ui/schedule/model/EventDetail.kt @@ -1,59 +1,52 @@ package com.mashup.ui.schedule.model -import java.text.SimpleDateFormat +import com.mashup.core.common.extensions.getTimeFormat import java.util.Date -import java.util.Locale -data class EventDetail( - val id: Int, - val type: EventDetailType, - val header: Header? = null, - val body: Body? = null, - val location: Location? = null, - val info: Info? = null -) - -data class Header( - val eventId: Int, - val startedAt: Date, - val endedAt: Date +sealed class EventDetail( + open val index: Int, + val type: EventDetailType ) { - fun getHeader() = "${eventId}부" - fun getTimeStampStr(): String { - return try { - val timeLineFormat = SimpleDateFormat("a hh:mm", Locale.ENGLISH) - "${timeLineFormat.format(startedAt)} - ${timeLineFormat.format(endedAt)}" - } catch (ignore: Exception) { - "??:?? - ??:??" - } + data class Header( + override val index: Int, + val eventId: Int, + val startedAt: Date, + val endedAt: Date + ) : EventDetail(index, EventDetailType.HEADER) { + val title = "${eventId}부" + val formattedTime = "${startedAt.getTimeFormat()} - ${endedAt.getTimeFormat()}" } -} -data class Body( - val contentId: String, - val title: String, - val content: String, - val startedAt: Date -) { - fun getTimeStampStr(): String { - return try { - val timeLineFormat = SimpleDateFormat("a hh:mm", Locale.ENGLISH) - timeLineFormat.format(startedAt) - } catch (ignore: Exception) { - "??:??" - } + data class Content( + override val index: Int, + val contentId: String, + val title: String, + val content: String, + val startedAt: Date + ) : EventDetail(index, EventDetailType.CONTENT) { + val formattedTime = startedAt.getTimeFormat() } -} -data class Location( - val detailAddress: String?, - val roadAddress: String?, - val latitude: Double?, - val longitude: Double? -) + data class Location( + override val index: Int, + val detailAddress: String, + val roadAddress: String, + val latitude: Double?, + val longitude: Double? + ) : EventDetail(index, EventDetailType.LOCATION) -data class Info( - val title: String, - val date: String, - val time: String -) + data class Info( + override val index: Int, + val title: String, + val date: String, + val startedAt: Date, + val endedAt: Date + ) : EventDetail(index, EventDetailType.INFO) { + val formattedTime = "${startedAt.getTimeFormat()} - ${endedAt.getTimeFormat()}" + } + + data class Notice( + override val index: Int, + val content: String + ) : EventDetail(index, EventDetailType.NOTICE) +} diff --git a/app/src/main/java/com/mashup/ui/schedule/model/EventDetailMapper.kt b/app/src/main/java/com/mashup/ui/schedule/model/EventDetailMapper.kt new file mode 100644 index 00000000..3a2bf34a --- /dev/null +++ b/app/src/main/java/com/mashup/ui/schedule/model/EventDetailMapper.kt @@ -0,0 +1,107 @@ +package com.mashup.ui.schedule.model + +import com.mashup.data.dto.ContentResponse +import com.mashup.data.dto.EventResponse +import com.mashup.data.dto.ScheduleResponse +import java.util.Date +import javax.inject.Inject + +class EventDetailMapper @Inject constructor() { + fun getEventDetailList( + title: String, + date: String, + eventList: List, + location: ScheduleResponse.Location?, + notice: String? + ): List { + val eventDetailList = mutableListOf() + var index = 0 + + val infoModel = mapToInfoModel( + index++, + title, + date, + eventList.first().startedAt, + eventList.last().endedAt + ) + eventDetailList.add(infoModel) + + if (location?.detailAddress != null) { // 위치 정보가 있는 경우(온라인이면 placeName이 Zoom으로 내려옴) + val locationModel = mapToLocationModel(index++, location) + eventDetailList.add(locationModel) + } + + if (!notice.isNullOrEmpty()) { + val noticeModel = mapToNoticeModel(index++, notice) + eventDetailList.add(noticeModel) + } + + eventList.forEachIndexed { eventIndex, event -> + val headerModel = mapToHeaderModel(index++, eventIndex, event) + eventDetailList.add(headerModel) + + event.contentList.forEachIndexed { contentIndex, content -> + val contentModel = mapToContentModel(index++, contentIndex, content) + eventDetailList.add(contentModel) + } + } + + return eventDetailList + } + + private fun mapToHeaderModel(index: Int, eventIndex: Int, event: EventResponse): EventDetail { + return EventDetail.Header( + index = index, + eventId = eventIndex + 1, + startedAt = event.startedAt, + endedAt = event.endedAt + ) + } + + private fun mapToContentModel( + index: Int, + contentIndex: Int, + content: ContentResponse + ): EventDetail { + return EventDetail.Content( + index = index, + contentId = "${contentIndex + 1}", + title = content.title, + content = content.content.orEmpty(), + startedAt = content.startedAt + ) + } + + private fun mapToLocationModel(index: Int, location: ScheduleResponse.Location): EventDetail { + return EventDetail.Location( + index = index, + detailAddress = location.detailAddress.orEmpty(), + roadAddress = location.roadAddress.orEmpty(), + latitude = location.latitude, + longitude = location.longitude + ) + } + + private fun mapToNoticeModel(index: Int, content: String): EventDetail { + return EventDetail.Notice( + index = index, + content = content + ) + } + + private fun mapToInfoModel( + index: Int, + title: String, + date: String, + startedAt: Date, + endedAt: Date + ): EventDetail { + return EventDetail.Info( + index = index, + title = title, + date = date, + startedAt = startedAt, + endedAt = endedAt + ) + } +} diff --git a/app/src/main/java/com/mashup/ui/schedule/model/EventDetailType.kt b/app/src/main/java/com/mashup/ui/schedule/model/EventDetailType.kt index 8d052a3f..e9baf2bb 100644 --- a/app/src/main/java/com/mashup/ui/schedule/model/EventDetailType.kt +++ b/app/src/main/java/com/mashup/ui/schedule/model/EventDetailType.kt @@ -1,3 +1,3 @@ package com.mashup.ui.schedule.model -enum class EventDetailType(val num: Int) { HEADER(1), CONTENT(2), LOCATION(3), INFO(4) } +enum class EventDetailType(val num: Int) { HEADER(1), CONTENT(2), LOCATION(3), INFO(4), NOTICE(5) } diff --git a/app/src/main/java/com/mashup/ui/schedule/model/ScheduleType.kt b/app/src/main/java/com/mashup/ui/schedule/model/ScheduleType.kt new file mode 100644 index 00000000..16d17a31 --- /dev/null +++ b/app/src/main/java/com/mashup/ui/schedule/model/ScheduleType.kt @@ -0,0 +1,5 @@ +package com.mashup.ui.schedule.model + +enum class ScheduleType { + WEEK, TOTAL +} diff --git a/app/src/main/java/com/mashup/ui/schedule/util/ScheduleColor.kt b/app/src/main/java/com/mashup/ui/schedule/util/ScheduleColor.kt new file mode 100644 index 00000000..eae3d3b0 --- /dev/null +++ b/app/src/main/java/com/mashup/ui/schedule/util/ScheduleColor.kt @@ -0,0 +1,54 @@ +package com.mashup.ui.schedule.util + +import androidx.compose.ui.graphics.Color +import com.mashup.core.ui.widget.PlatformType + +fun String.getEventTimelineBackgroundColor(): Color { + return when (this) { + "ALL" -> Color(0xFFE1F2FA) + else -> Color(0xFFF5F1FF) + } +} + +fun String.getButtonBackgroundColor(): Color { + return when (this) { + "ALL" -> Color(0xFFC2EBFF) + else -> Color(0xFFE7DEFF) + } +} + +fun String.getBackgroundColor(): Color { + return when (this) { + "ALL" -> Color(0xFFECF9FF) + else -> Color(0xFFF5F1FF) + } +} + +fun String.getBorderColor(): Color { + return when (this) { + "ALL" -> Color(0xFFE1F2FA) + else -> Color(0xFFE7DEFF).copy( + alpha = 0.3f + ) + } +} + +fun String.getButtonTextColor(): Color { + return when (this) { + "ALL" -> Color(0xFF358CB6) + else -> Color(0xFF6A36FF) + } +} + +fun String.convertCamelCase(): PlatformType { + return when (this) { + "ALL" -> PlatformType.Seminar + "DESIGN" -> PlatformType.Design + "SPRING" -> PlatformType.Spring + "IOS" -> PlatformType.Ios + "ANDROID" -> PlatformType.Android + "WEB" -> PlatformType.Web + "NODE" -> PlatformType.Node + else -> PlatformType.Seminar + } +} diff --git a/app/src/main/java/com/mashup/ui/setting/SettingActivity.kt b/app/src/main/java/com/mashup/ui/setting/SettingActivity.kt index def650cf..08896b99 100644 --- a/app/src/main/java/com/mashup/ui/setting/SettingActivity.kt +++ b/app/src/main/java/com/mashup/ui/setting/SettingActivity.kt @@ -10,14 +10,14 @@ import com.mashup.R import com.mashup.URL import com.mashup.base.BaseActivity import com.mashup.constant.EXTRA_ANIMATION -import com.mashup.constant.log.LOG_DELETE_USER -import com.mashup.constant.log.LOG_LOGOUT -import com.mashup.constant.log.LOG_SNS_FACEBOOK -import com.mashup.constant.log.LOG_SNS_INSTAGRAM -import com.mashup.constant.log.LOG_SNS_MASHUP_HOME -import com.mashup.constant.log.LOG_SNS_MASHUP_RECRUIT -import com.mashup.constant.log.LOG_SNS_TISTORY -import com.mashup.constant.log.LOG_SNS_YOUTUBE +import com.mashup.constant.log.LOG_SETTING_DELETE_USER +import com.mashup.constant.log.LOG_SETTING_LOGOUT +import com.mashup.constant.log.LOG_SETTING_SNS_FACEBOOK +import com.mashup.constant.log.LOG_SETTING_SNS_INSTAGRAM +import com.mashup.constant.log.LOG_SETTING_SNS_MASHUP_HOME +import com.mashup.constant.log.LOG_SETTING_SNS_MASHUP_RECRUIT +import com.mashup.constant.log.LOG_SETTING_SNS_TISTORY +import com.mashup.constant.log.LOG_SETTING_SNS_YOUTUBE import com.mashup.core.common.model.NavigationAnimationType import com.mashup.core.common.widget.CommonDialog import com.mashup.core.ui.theme.MashUpTheme @@ -61,7 +61,7 @@ class SettingActivity : BaseActivity() { } private fun onClickLogoutButton() { - AnalyticsManager.addEvent(LOG_LOGOUT) + AnalyticsManager.addEvent(LOG_SETTING_LOGOUT) showLogoutDialog() } @@ -95,7 +95,7 @@ class SettingActivity : BaseActivity() { } private fun moveToDeleteAccount() { - AnalyticsManager.addEvent(LOG_DELETE_USER) + AnalyticsManager.addEvent(LOG_SETTING_DELETE_USER) startActivity( WithdrawalActivity.newInstance(this) ) @@ -103,12 +103,12 @@ class SettingActivity : BaseActivity() { private fun onClickSNS(link: String) { val eventLog = when (link) { - URL.FACEBOOK -> LOG_SNS_FACEBOOK - URL.INSTAGRAM -> LOG_SNS_INSTAGRAM - URL.TISTORY -> LOG_SNS_TISTORY - URL.YOUTUBE -> LOG_SNS_YOUTUBE - URL.MASHUP_UP_HOME -> LOG_SNS_MASHUP_HOME - URL.MASHUP_UP_RECRUIT -> LOG_SNS_MASHUP_RECRUIT + URL.FACEBOOK -> LOG_SETTING_SNS_FACEBOOK + URL.INSTAGRAM -> LOG_SETTING_SNS_INSTAGRAM + URL.TISTORY -> LOG_SETTING_SNS_TISTORY + URL.YOUTUBE -> LOG_SETTING_SNS_YOUTUBE + URL.MASHUP_UP_HOME -> LOG_SETTING_SNS_MASHUP_HOME + URL.MASHUP_UP_RECRUIT -> LOG_SETTING_SNS_MASHUP_RECRUIT else -> null } eventLog?.run { AnalyticsManager.addEvent(this) } diff --git a/app/src/main/java/com/mashup/ui/signup/SignUpActivity.kt b/app/src/main/java/com/mashup/ui/signup/SignUpActivity.kt index dc3658f0..4f1cd520 100644 --- a/app/src/main/java/com/mashup/ui/signup/SignUpActivity.kt +++ b/app/src/main/java/com/mashup/ui/signup/SignUpActivity.kt @@ -9,8 +9,8 @@ import com.mashup.R import com.mashup.base.BaseActivity import com.mashup.constant.EXTRA_ANIMATION import com.mashup.constant.log.KEY_PLACE -import com.mashup.constant.log.LOG_BACK -import com.mashup.constant.log.LOG_CLOSE +import com.mashup.constant.log.LOG_COMMON_BACK +import com.mashup.constant.log.LOG_COMMON_CLOSE import com.mashup.constant.log.LOG_PLACE_SIGN_CODE import com.mashup.constant.log.LOG_PLACE_SIGN_MEMBER_INFO import com.mashup.constant.log.LOG_PLACE_SIGN_PLATFORM @@ -48,7 +48,7 @@ class SignUpActivity : BaseActivity() { viewBinding.toolbar.setOnCloseButtonClickListener { getPlaceGALog()?.run { AnalyticsManager.addEvent( - LOG_CLOSE, + LOG_COMMON_CLOSE, bundleOf(KEY_PLACE to this) ) } @@ -85,7 +85,7 @@ class SignUpActivity : BaseActivity() { override fun onBackPressed() { getPlaceGALog()?.run { AnalyticsManager.addEvent( - LOG_BACK, + LOG_COMMON_BACK, bundleOf(KEY_PLACE to this) ) } diff --git a/app/src/main/java/com/mashup/ui/webview/WebViewActivity.kt b/app/src/main/java/com/mashup/ui/webview/WebViewActivity.kt index f01be4a4..45ddab37 100644 --- a/app/src/main/java/com/mashup/ui/webview/WebViewActivity.kt +++ b/app/src/main/java/com/mashup/ui/webview/WebViewActivity.kt @@ -7,11 +7,13 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import com.mashup.R import com.mashup.base.BaseActivity import com.mashup.constant.EXTRA_ANIMATION import com.mashup.constant.EXTRA_TITLE_KEY import com.mashup.constant.EXTRA_URL_KEY +import com.mashup.core.common.bridge.MashupBridge import com.mashup.core.common.extensions.setStatusBarColorRes import com.mashup.core.common.model.NavigationAnimationType import com.mashup.databinding.ActivityWebViewBinding @@ -26,13 +28,13 @@ class WebViewActivity : BaseActivity() { super.initViews() setStatusBarColorRes(com.mashup.core.common.R.color.white) initWindowInset() - initCompose() } private fun initCompose() { viewBinding.webView.setContent { val webViewUiState by viewModel.webViewUiState.collectAsState(WebViewUiState.Loading) + val context = LocalContext.current WebViewScreen( modifier = Modifier.fillMaxSize(), @@ -40,7 +42,8 @@ class WebViewActivity : BaseActivity() { onBackPressed = { finish() }, isScrollTop = { viewModel.onWebViewScroll(it) - } + }, + mashupBridge = MashupBridge(context) ) } } diff --git a/app/src/main/java/com/mashup/ui/webview/WebViewScreen.kt b/app/src/main/java/com/mashup/ui/webview/WebViewScreen.kt index 17698811..a38aae98 100644 --- a/app/src/main/java/com/mashup/ui/webview/WebViewScreen.kt +++ b/app/src/main/java/com/mashup/ui/webview/WebViewScreen.kt @@ -8,29 +8,36 @@ import androidx.compose.ui.Modifier import com.google.accompanist.web.WebView import com.google.accompanist.web.rememberWebViewNavigator import com.google.accompanist.web.rememberWebViewState +import com.mashup.core.common.bridge.MashupBridge import com.mashup.core.ui.widget.MashUpToolbar @Composable fun WebViewScreen( + mashupBridge: MashupBridge, modifier: Modifier = Modifier, webViewUiState: WebViewUiState, isScrollTop: (Boolean) -> Unit = {}, - onBackPressed: () -> Unit + onBackPressed: () -> Unit = {}, + isShowMashUpToolbar: Boolean = true ) { Column(modifier = modifier) { - MashUpToolbar( - modifier = Modifier.fillMaxWidth(), - title = (webViewUiState as? WebViewUiState.Success)?.title.orEmpty(), - showBackButton = true, - showBottomDivider = (webViewUiState as? WebViewUiState.Success)?.showToolbarDivider - ?: false, - onClickBackButton = onBackPressed - ) + if (isShowMashUpToolbar) { + MashUpToolbar( + modifier = Modifier.fillMaxWidth(), + title = (webViewUiState as? WebViewUiState.Success)?.title.orEmpty(), + showBackButton = true, + showBottomDivider = (webViewUiState as? WebViewUiState.Success)?.showToolbarDivider + ?: false, + onClickBackButton = onBackPressed + ) + } if (webViewUiState is WebViewUiState.Success) { MashUpWebView( webViewUrl = webViewUiState.webViewUrl, isScrollTop = isScrollTop, - onBackPressed = onBackPressed + onBackPressed = onBackPressed, + mashupBridge = mashupBridge, + additionalHttpHeaders = webViewUiState.additionalHttpHeaders ) } } @@ -39,10 +46,15 @@ fun WebViewScreen( @Composable private fun MashUpWebView( webViewUrl: String?, + mashupBridge: MashupBridge, isScrollTop: (Boolean) -> Unit = {}, + additionalHttpHeaders: Map = emptyMap(), onBackPressed: () -> Unit ) { - val webViewState = rememberWebViewState(url = webViewUrl.orEmpty()) + val webViewState = rememberWebViewState( + url = webViewUrl.orEmpty(), + additionalHttpHeaders = additionalHttpHeaders + ) val webViewNavigator = rememberWebViewNavigator() WebView( @@ -51,9 +63,11 @@ private fun MashUpWebView( onCreated = { webView -> with(webView) { settings.run { + javaScriptEnabled = true domStorageEnabled = true loadWithOverviewMode = true defaultTextEncodingName = "UTF-8" + addJavascriptInterface(mashupBridge, MashupBridge.name) } setOnScrollChangeListener { view, _, _, _, _ -> isScrollTop(!view.canScrollVertically(-1)) diff --git a/app/src/main/java/com/mashup/ui/webview/WebViewViewModel.kt b/app/src/main/java/com/mashup/ui/webview/WebViewViewModel.kt index a782098d..830945aa 100644 --- a/app/src/main/java/com/mashup/ui/webview/WebViewViewModel.kt +++ b/app/src/main/java/com/mashup/ui/webview/WebViewViewModel.kt @@ -5,6 +5,8 @@ import androidx.lifecycle.viewModelScope import com.mashup.constant.EXTRA_TITLE_KEY import com.mashup.constant.EXTRA_URL_KEY import com.mashup.core.common.base.BaseViewModel +import com.mashup.data.network.WEB_HOST +import com.mashup.datastore.data.repository.UserPreferenceRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -14,7 +16,8 @@ import javax.inject.Inject @HiltViewModel class WebViewViewModel @Inject constructor( - savedStateHandle: SavedStateHandle + savedStateHandle: SavedStateHandle, + userPreferenceRepository: UserPreferenceRepository ) : BaseViewModel() { private val showDividerFlow = MutableStateFlow(false) @@ -22,12 +25,18 @@ class WebViewViewModel @Inject constructor( val webViewUiState = combine( savedStateHandle.getStateFlow(EXTRA_TITLE_KEY, ""), savedStateHandle.getStateFlow(EXTRA_URL_KEY, ""), - showDividerFlow - ) { title, webViewUrl, showDivider -> + showDividerFlow, + userPreferenceRepository.getUserPreference() + ) { title, webViewUrl, showDivider, prefs -> + var convertWebViewUrl = WEB_HOST + webViewUrl + if (title == "mashong") { + convertWebViewUrl += prefs.platform + } WebViewUiState.Success( title = title, - webViewUrl = webViewUrl, - showToolbarDivider = showDivider + webViewUrl = convertWebViewUrl, + showToolbarDivider = showDivider, + additionalHttpHeaders = mapOf(Pair("authorization", prefs.token)) ) }.stateIn( viewModelScope, @@ -49,6 +58,7 @@ sealed interface WebViewUiState { data class Success( val title: String, val webViewUrl: String, - val showToolbarDivider: Boolean + val showToolbarDivider: Boolean, + val additionalHttpHeaders: Map ) : WebViewUiState } diff --git a/app/src/main/java/com/mashup/ui/webview/birthday/BirthdayActivity.kt b/app/src/main/java/com/mashup/ui/webview/birthday/BirthdayActivity.kt new file mode 100644 index 00000000..d893ed2d --- /dev/null +++ b/app/src/main/java/com/mashup/ui/webview/birthday/BirthdayActivity.kt @@ -0,0 +1,66 @@ +package com.mashup.ui.webview.birthday + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.KeyEvent +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.mashup.constant.EXTRA_TITLE_KEY +import com.mashup.constant.EXTRA_URL_KEY +import com.mashup.core.common.bridge.MashupBridge +import com.mashup.core.ui.theme.MashUpTheme +import com.mashup.ui.webview.WebViewScreen +import com.mashup.ui.webview.WebViewUiState +import com.mashup.ui.webview.WebViewViewModel +import com.mashup.util.setFullScreen +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class BirthdayActivity : ComponentActivity() { + + private val webViewViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + MashUpTheme { + val webViewUiState by webViewViewModel.webViewUiState.collectAsState(WebViewUiState.Loading) + WebViewScreen( + modifier = Modifier.fillMaxSize().imePadding(), + webViewUiState = webViewUiState, + mashupBridge = MashupBridge( + this, + onBackPressed = ::finish + ), + isShowMashUpToolbar = false + ) + } + } + setFullScreen() + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + finish() + return true + } + return super.onKeyDown(keyCode, event) + } + + companion object { + fun newIntent(context: Context, urlKey: String = "birthday/crew-list"): Intent = + Intent(context, BirthdayActivity::class.java).apply { + putExtra(EXTRA_TITLE_KEY, "birthday") + putExtra(EXTRA_URL_KEY, urlKey) + } + } +} diff --git a/app/src/main/java/com/mashup/ui/webview/mashong/MashongActivity.kt b/app/src/main/java/com/mashup/ui/webview/mashong/MashongActivity.kt new file mode 100644 index 00000000..a45c0ee6 --- /dev/null +++ b/app/src/main/java/com/mashup/ui/webview/mashong/MashongActivity.kt @@ -0,0 +1,69 @@ +package com.mashup.ui.webview.mashong + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.KeyEvent +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.mashup.constant.EXTRA_TITLE_KEY +import com.mashup.constant.EXTRA_URL_KEY +import com.mashup.core.common.bridge.MashupBridge +import com.mashup.core.ui.theme.MashUpTheme +import com.mashup.ui.danggn.ShakeDanggnActivity +import com.mashup.ui.webview.WebViewScreen +import com.mashup.ui.webview.WebViewUiState +import com.mashup.ui.webview.WebViewViewModel +import com.mashup.util.setFullScreen +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class MashongActivity : ComponentActivity() { + + private val webViewViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + MashUpTheme { + val webViewUiState by webViewViewModel.webViewUiState.collectAsState(WebViewUiState.Loading) + WebViewScreen( + modifier = Modifier.fillMaxSize(), + webViewUiState = webViewUiState, + mashupBridge = MashupBridge( + this, + onBackPressed = ::finish, + onNavigateDanggn = { + startActivity(ShakeDanggnActivity.newIntent(this)) + } + ), + isShowMashUpToolbar = false + ) + } + } + setFullScreen() + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + finish() + return true + } + return super.onKeyDown(keyCode, event) + } + + companion object { + fun newIntent(context: Context): Intent = + Intent(context, MashongActivity::class.java).apply { + putExtra(EXTRA_TITLE_KEY, "mashong") + putExtra(EXTRA_URL_KEY, "mashong/") + } + } +} diff --git a/app/src/main/java/com/mashup/ui/withdrawl/WithdrawalActivity.kt b/app/src/main/java/com/mashup/ui/withdrawl/WithdrawalActivity.kt index 8dcb38aa..d119889b 100644 --- a/app/src/main/java/com/mashup/ui/withdrawl/WithdrawalActivity.kt +++ b/app/src/main/java/com/mashup/ui/withdrawl/WithdrawalActivity.kt @@ -8,7 +8,7 @@ import androidx.core.view.WindowInsetsCompat import com.mashup.R import com.mashup.base.BaseActivity import com.mashup.constant.EXTRA_ANIMATION -import com.mashup.constant.log.LOG_DELETE_SUCCESS_USER +import com.mashup.constant.log.LOG_DELETE_USER_SUCCESS import com.mashup.core.common.extensions.setEmptyUIOfTextField import com.mashup.core.common.extensions.setFailedUiOfTextField import com.mashup.core.common.extensions.setSuccessUiOfTextField @@ -86,7 +86,7 @@ class WithdrawalActivity : BaseActivity() { } is WithdrawalState.Success -> { hideLoading() - AnalyticsManager.addEvent(LOG_DELETE_SUCCESS_USER) + AnalyticsManager.addEvent(LOG_DELETE_USER_SUCCESS) finish() startActivity( LoginActivity.newIntent( diff --git a/app/src/main/java/com/mashup/util/FullScreen.kt b/app/src/main/java/com/mashup/util/FullScreen.kt new file mode 100644 index 00000000..8567a3a3 --- /dev/null +++ b/app/src/main/java/com/mashup/util/FullScreen.kt @@ -0,0 +1,19 @@ +package com.mashup.util + +import android.os.Build +import android.view.View +import android.view.WindowInsets +import android.view.WindowInsetsController +import androidx.activity.ComponentActivity + +fun ComponentActivity.setFullScreen() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + window.insetsController?.apply { + hide(WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars()) + systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + } else { + window.decorView.systemUiVisibility = + (View.SYSTEM_UI_FLAG_IMMERSIVE or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_FULLSCREEN) + } +} diff --git a/app/src/main/res/layout/activity_schedule_detail.xml b/app/src/main/res/layout/activity_schedule_detail.xml index a72893a3..3ae97818 100644 --- a/app/src/main/res/layout/activity_schedule_detail.xml +++ b/app/src/main/res/layout/activity_schedule_detail.xml @@ -1,35 +1,9 @@ - + - - - - - - + android:layout_height="match_parent" + android:background="@color/white" /> \ No newline at end of file diff --git a/app/src/main/res/layout/item_event_timeline_content.xml b/app/src/main/res/layout/item_event_timeline_content.xml deleted file mode 100644 index 7d936230..00000000 --- a/app/src/main/res/layout/item_event_timeline_content.xml +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_event_timeline_header.xml b/app/src/main/res/layout/item_event_timeline_header.xml deleted file mode 100644 index b10aabfc..00000000 --- a/app/src/main/res/layout/item_event_timeline_header.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cc2787a2..a96eaa1f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -30,7 +30,7 @@ 출석 결석 지각 - 플랫폼별 출석현황 보러가기 + 상세 스케줄 보러가기 로그아웃 회원탈퇴 매시업을 잊지말아죠... @@ -44,10 +44,10 @@ 다시시도 돌아가기 - 일정 준비 중이에요.\n조금만 기다려주세요! - %d기 모든 활동이\n종료되었어요. - %s님의\n출석을 기다리고 있어요!]]> - 열심히 일정을 준비하고 있어요\n조금만 기다려 주세요! + 조금만 기다려주세요!]]> + 종료되었어요.]]> + %s님의\n출석을 기다리고 있어요!]]> + D-? 등록된 일정이 없어요 매시업 크루들의 출석현황이 궁금하다면? diff --git a/app/src/release/java/com/mashup/data/network/NetworkConstanst.kt b/app/src/release/java/com/mashup/data/network/NetworkConstanst.kt index 782df926..0157244a 100644 --- a/app/src/release/java/com/mashup/data/network/NetworkConstanst.kt +++ b/app/src/release/java/com/mashup/data/network/NetworkConstanst.kt @@ -1,3 +1,4 @@ package com.mashup.data.network -const val API_HOST = "https://api.member.mash-up.kr" +const val API_HOST = "https://api.member.mash-up.kr/" +const val WEB_HOST = "https://app.mash-up.kr/" diff --git a/core/common/src/main/java/com/mashup/core/common/bridge/MashupBridge.kt b/core/common/src/main/java/com/mashup/core/common/bridge/MashupBridge.kt new file mode 100644 index 00000000..d5fafd36 --- /dev/null +++ b/core/common/src/main/java/com/mashup/core/common/bridge/MashupBridge.kt @@ -0,0 +1,38 @@ +package com.mashup.core.common.bridge + +import android.content.Context +import android.webkit.JavascriptInterface +import android.widget.Toast + +class MashupBridge( + private val context: Context, + private val onBackPressed: () -> Unit = {}, + private val onNavigateDanggn: () -> Unit = {} +) : MashupBridgeInterface() { + @JavascriptInterface + override fun showToast(toast: String) { + Toast.makeText(context, toast, Toast.LENGTH_SHORT).show() + } + + @JavascriptInterface + override fun step(type: String) { + when (Type.values().find { it.name == type.uppercase() }) { + Type.BACK -> onBackPressed() + Type.DANGGN -> onNavigateDanggn() + else -> {} + } + } + + companion object { + const val name = "MashupBridge" + } +} + +abstract class MashupBridgeInterface { + open fun showToast(toast: String) {} + open fun step(type: String) {} +} + +enum class Type { + BACK, DANGGN +} diff --git a/core/common/src/main/java/com/mashup/core/common/extensions/DateExt.kt b/core/common/src/main/java/com/mashup/core/common/extensions/DateExt.kt index c0c8fdbe..d67df070 100644 --- a/core/common/src/main/java/com/mashup/core/common/extensions/DateExt.kt +++ b/core/common/src/main/java/com/mashup/core/common/extensions/DateExt.kt @@ -1,6 +1,7 @@ package com.mashup.core.common.extensions import java.text.SimpleDateFormat +import java.util.Calendar import java.util.Date import java.util.Locale import java.util.TimeZone @@ -19,3 +20,36 @@ fun Date.getTimeFormat(): String { "??:??" } } + +fun Date.year(): Int { + val calendar = Calendar.getInstance() + calendar.time = this + return calendar.get(Calendar.YEAR) +} + +fun Date.month(): Int { + val calendar = Calendar.getInstance() + calendar.time = this + return calendar.get(Calendar.MONTH) + 1 +} + +fun Date.day(): Int { + val calendar = Calendar.getInstance() + calendar.time = this + return calendar.get(Calendar.DATE) +} + +fun Date.week(): String { + val calendar = Calendar.getInstance() + calendar.time = this + return when (calendar.get(Calendar.DAY_OF_WEEK)) { + Calendar.MONDAY -> "월" + Calendar.TUESDAY -> "화" + Calendar.WEDNESDAY -> "수" + Calendar.THURSDAY -> "목" + Calendar.FRIDAY -> "금" + Calendar.SATURDAY -> "토" + Calendar.SUNDAY -> "일" + else -> "" + } +} diff --git a/core/common/src/main/res/drawable/ic_clock.xml b/core/common/src/main/res/drawable/ic_clock.xml index cf58d00a..1e15c2e2 100644 --- a/core/common/src/main/res/drawable/ic_clock.xml +++ b/core/common/src/main/res/drawable/ic_clock.xml @@ -1,14 +1,17 @@ - - + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> + + diff --git a/core/common/src/main/res/drawable/ic_mappin.xml b/core/common/src/main/res/drawable/ic_mappin.xml index e4a6a5cb..a2852d3a 100644 --- a/core/common/src/main/res/drawable/ic_mappin.xml +++ b/core/common/src/main/res/drawable/ic_mappin.xml @@ -1,14 +1,14 @@ + android:width="21dp" + android:height="21dp" + android:viewportWidth="21" + android:viewportHeight="21"> diff --git a/core/common/src/main/res/drawable/ic_more.xml b/core/common/src/main/res/drawable/ic_more.xml new file mode 100644 index 00000000..bc35f209 --- /dev/null +++ b/core/common/src/main/res/drawable/ic_more.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/core/common/src/main/res/drawable/img_empty_schdule.png b/core/common/src/main/res/drawable/img_empty_schdule.png new file mode 100644 index 00000000..5e70c6c8 Binary files /dev/null and b/core/common/src/main/res/drawable/img_empty_schdule.png differ diff --git a/core/common/src/main/res/drawable/img_placeholder_sleeping.png b/core/common/src/main/res/drawable/img_placeholder_sleeping.png index aea507e8..8a2ca245 100644 Binary files a/core/common/src/main/res/drawable/img_placeholder_sleeping.png and b/core/common/src/main/res/drawable/img_placeholder_sleeping.png differ diff --git a/core/data/src/main/java/com/mashup/core/data/repository/MetaRepository.kt b/core/data/src/main/java/com/mashup/core/data/repository/MetaRepository.kt new file mode 100644 index 00000000..56487fc9 --- /dev/null +++ b/core/data/src/main/java/com/mashup/core/data/repository/MetaRepository.kt @@ -0,0 +1,14 @@ +package com.mashup.core.data.repository + +import com.mashup.core.network.Response +import com.mashup.core.network.dao.MetaDao +import com.mashup.core.network.dto.RnbResponse +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class MetaRepository @Inject constructor( + private val metaDao: MetaDao +) { + suspend fun getRnb(): Response = metaDao.getRnb() +} diff --git a/core/data/src/main/java/com/mashup/core/data/repository/PushHistoryRepository.kt b/core/data/src/main/java/com/mashup/core/data/repository/PushHistoryRepository.kt new file mode 100644 index 00000000..cfa9def5 --- /dev/null +++ b/core/data/src/main/java/com/mashup/core/data/repository/PushHistoryRepository.kt @@ -0,0 +1,26 @@ +package com.mashup.core.data.repository + +import com.mashup.core.network.Response +import com.mashup.core.network.dao.PushHistoryDao +import com.mashup.core.network.dto.PushHistoryResponse +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class PushHistoryRepository @Inject constructor( + private val pushHistoryDao: PushHistoryDao +) { + suspend fun getPushHistory( + page: Int, + size: Int, + sort: String? = null + ): Response = + pushHistoryDao.getPushHistory(page, size, sort) + + suspend fun postPushHistoryCheck( + page: Int, + size: Int, + sort: String? = null + ): Response = + pushHistoryDao.postPushHistoryCheck(page, size, sort) +} diff --git a/core/network/build.gradle b/core/network/build.gradle index 7676640f..0563dada 100644 --- a/core/network/build.gradle +++ b/core/network/build.gradle @@ -10,7 +10,6 @@ android { compileSdk compileVersion defaultConfig { - minSdk minVersion targetSdk targetVersion } compileOptions { diff --git a/core/network/src/main/java/com/mashup/core/network/dao/MetaDao.kt b/core/network/src/main/java/com/mashup/core/network/dao/MetaDao.kt new file mode 100644 index 00000000..6b38ca48 --- /dev/null +++ b/core/network/src/main/java/com/mashup/core/network/dao/MetaDao.kt @@ -0,0 +1,10 @@ +package com.mashup.core.network.dao + +import com.mashup.core.network.Response +import com.mashup.core.network.dto.RnbResponse +import retrofit2.http.GET + +interface MetaDao { + @GET("/api/v1/meta/rnb") + suspend fun getRnb(): Response +} diff --git a/core/network/src/main/java/com/mashup/core/network/dao/PushHistoryDao.kt b/core/network/src/main/java/com/mashup/core/network/dao/PushHistoryDao.kt new file mode 100644 index 00000000..ca0bd32f --- /dev/null +++ b/core/network/src/main/java/com/mashup/core/network/dao/PushHistoryDao.kt @@ -0,0 +1,23 @@ +package com.mashup.core.network.dao + +import com.mashup.core.network.Response +import com.mashup.core.network.dto.PushHistoryResponse +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Query + +interface PushHistoryDao { + @GET("/api/v1/push-histories") + suspend fun getPushHistory( + @Query("page") page: Int, + @Query("size") size: Int, + @Query("sort") sort: String? + ): Response + + @POST("/api/v1/push-histories") + suspend fun postPushHistoryCheck( + @Query("page") page: Int, + @Query("size") size: Int, + @Query("sort") sort: String? + ): Response +} diff --git a/core/network/src/main/java/com/mashup/core/network/dto/PushHistoryResponse.kt b/core/network/src/main/java/com/mashup/core/network/dto/PushHistoryResponse.kt new file mode 100644 index 00000000..5ad782d3 --- /dev/null +++ b/core/network/src/main/java/com/mashup/core/network/dto/PushHistoryResponse.kt @@ -0,0 +1,18 @@ +package com.mashup.core.network.dto + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class PushHistoryResponse( + val read: List, + val unread: List +) { + @JsonClass(generateAdapter = true) + data class Notice( + val pushType: String, + val title: String, + val body: String, + val linkType: String, + val sendTime: String + ) +} diff --git a/core/network/src/main/java/com/mashup/core/network/dto/RnbResponse.kt b/core/network/src/main/java/com/mashup/core/network/dto/RnbResponse.kt new file mode 100644 index 00000000..cae2bbc8 --- /dev/null +++ b/core/network/src/main/java/com/mashup/core/network/dto/RnbResponse.kt @@ -0,0 +1,8 @@ +package com.mashup.core.network.dto + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class RnbResponse( + val menus: List +) diff --git a/core/ui/src/main/java/com/mashup/core/ui/widget/MashUpButton.kt b/core/ui/src/main/java/com/mashup/core/ui/widget/MashUpButton.kt index 1851f688..ba7213b7 100644 --- a/core/ui/src/main/java/com/mashup/core/ui/widget/MashUpButton.kt +++ b/core/ui/src/main/java/com/mashup/core/ui/widget/MashUpButton.kt @@ -46,7 +46,10 @@ import com.mashup.core.ui.colors.White import com.mashup.core.ui.theme.MashUpTheme import com.mashup.core.ui.typography.Body1 -enum class ButtonStyle(val backgroundColor: Color, val textColor: Color) { +enum class ButtonStyle( + val backgroundColor: Color, + val textColor: Color +) { PRIMARY(backgroundColor = Brand500, textColor = White), INVERSE(backgroundColor = Brand100, textColor = Brand500), DISABLE(backgroundColor = Brand300, textColor = White), @@ -116,7 +119,8 @@ fun ButtonCircularProgressbar( initialValue = 0f, targetValue = 360f, animationSpec = infiniteRepeatable( - animation = keyframes { + animation = + keyframes { durationMillis = progressDuration } ) diff --git a/core/ui/src/main/java/com/mashup/core/ui/widget/MashUpGradientButton.kt b/core/ui/src/main/java/com/mashup/core/ui/widget/MashUpGradientButton.kt new file mode 100644 index 00000000..5c4edf04 --- /dev/null +++ b/core/ui/src/main/java/com/mashup/core/ui/widget/MashUpGradientButton.kt @@ -0,0 +1,59 @@ +package com.mashup.core.ui.widget + +import androidx.compose.foundation.background +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.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +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.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.mashup.core.ui.typography.Body3 + +@Composable +fun MashUpGradientButton( + modifier: Modifier = Modifier, + text: String = "", + onClick: () -> Unit = {}, + isEnabled: Boolean = true, + gradientColors: List = listOf() +) { + Box( + modifier = modifier + .clip(RoundedCornerShape(12.dp)) + .background( + brush = Brush.linearGradient( + colors = gradientColors + ) + ) + .padding(horizontal = 20.dp) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + enabled = isEnabled, + onClick = onClick + ), + contentAlignment = Alignment.Center + ) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = text, + style = Body3.copy( + color = Color.White + ) + ) + } + } +} diff --git a/core/ui/src/main/java/com/mashup/core/ui/widget/MashUpHtmlText.kt b/core/ui/src/main/java/com/mashup/core/ui/widget/MashUpHtmlText.kt new file mode 100644 index 00000000..65d7992e --- /dev/null +++ b/core/ui/src/main/java/com/mashup/core/ui/widget/MashUpHtmlText.kt @@ -0,0 +1,40 @@ +package com.mashup.core.ui.widget + +import android.text.Spanned +import androidx.annotation.StyleRes +import androidx.appcompat.widget.AppCompatTextView +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView + +/** + * MashUpHtmlText.kt + * + * Created by Minji Jeong on 2024/06/29 + * Copyright © 2024 MashUp All rights reserved. + */ + +@Composable +fun MashUpHtmlText( + content: Spanned, + modifier: Modifier = Modifier, + @StyleRes textAppearance: Int? = null +) { + AndroidView( + factory = { context -> + AppCompatTextView( + context + ).apply { + if (textAppearance != null) { + setTextAppearance(textAppearance) + } + text = content + includeFontPadding = false + } + }, + modifier = modifier, + update = { view -> + view.text = content + } + ) +} diff --git a/core/ui/src/main/java/com/mashup/core/ui/widget/MashupPlatformBadge.kt b/core/ui/src/main/java/com/mashup/core/ui/widget/MashupPlatformBadge.kt new file mode 100644 index 00000000..195a2596 --- /dev/null +++ b/core/ui/src/main/java/com/mashup/core/ui/widget/MashupPlatformBadge.kt @@ -0,0 +1,82 @@ +package com.mashup.core.ui.widget + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.mashup.core.ui.R +import com.mashup.core.ui.typography.Body3 + +@Composable +fun MashupPlatformBadge( + platform: String, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .background( + color = platform.getColor(), + shape = RoundedCornerShape(100.dp) + ) + .padding(horizontal = 10.dp, vertical = 6.5.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Image( + modifier = Modifier.size(12.dp), + painter = painterResource(id = platform.getIcon()), + contentDescription = null + ) + Spacer( + modifier = Modifier.width(4.dp) + ) + Text( + text = platform.convertCamelCase().name, + style = Body3, + color = Color.White + ) + } +} + +private fun String.getIcon(): Int { + return when (this) { + "ALL" -> R.drawable.ic_semina + else -> R.drawable.ic_platform + } +} + +enum class PlatformType { + Seminar, Design, Spring, Ios, Android, Web, Node +} + +private fun String.getColor(): Color { + return when (this) { + "ALL" -> Color(0xFF7CD5FF) + else -> Color(0xFF8A61FF) + } +} + +private fun String.convertCamelCase(): PlatformType { + return when (this) { + "ALL" -> PlatformType.Seminar + "DESIGN" -> PlatformType.Design + "SPRING" -> PlatformType.Spring + "IOS" -> PlatformType.Ios + "ANDROID" -> PlatformType.Android + "WEB" -> PlatformType.Web + "NODE" -> PlatformType.Node + else -> PlatformType.Seminar + } +} diff --git a/core/ui/src/main/res/drawable-hdpi/img_birthday_mashong.png b/core/ui/src/main/res/drawable-hdpi/img_birthday_mashong.png new file mode 100644 index 00000000..062c0492 Binary files /dev/null and b/core/ui/src/main/res/drawable-hdpi/img_birthday_mashong.png differ diff --git a/core/ui/src/main/res/drawable-hdpi/img_error.png b/core/ui/src/main/res/drawable-hdpi/img_error.png new file mode 100644 index 00000000..51d27e13 Binary files /dev/null and b/core/ui/src/main/res/drawable-hdpi/img_error.png differ diff --git a/core/ui/src/main/res/drawable-hdpi/img_noalert.png b/core/ui/src/main/res/drawable-hdpi/img_noalert.png new file mode 100644 index 00000000..cb58e362 Binary files /dev/null and b/core/ui/src/main/res/drawable-hdpi/img_noalert.png differ diff --git a/core/ui/src/main/res/drawable-mdpi/img_birthday_mashong.png b/core/ui/src/main/res/drawable-mdpi/img_birthday_mashong.png new file mode 100644 index 00000000..d38c1d3a Binary files /dev/null and b/core/ui/src/main/res/drawable-mdpi/img_birthday_mashong.png differ diff --git a/core/ui/src/main/res/drawable-mdpi/img_error.png b/core/ui/src/main/res/drawable-mdpi/img_error.png new file mode 100644 index 00000000..831894fb Binary files /dev/null and b/core/ui/src/main/res/drawable-mdpi/img_error.png differ diff --git a/core/ui/src/main/res/drawable-mdpi/img_noalert.png b/core/ui/src/main/res/drawable-mdpi/img_noalert.png new file mode 100644 index 00000000..7621f56b Binary files /dev/null and b/core/ui/src/main/res/drawable-mdpi/img_noalert.png differ diff --git a/core/ui/src/main/res/drawable-xhdpi/img_birthday_mashong.png b/core/ui/src/main/res/drawable-xhdpi/img_birthday_mashong.png new file mode 100644 index 00000000..c131eee5 Binary files /dev/null and b/core/ui/src/main/res/drawable-xhdpi/img_birthday_mashong.png differ diff --git a/core/ui/src/main/res/drawable-xhdpi/img_error.png b/core/ui/src/main/res/drawable-xhdpi/img_error.png new file mode 100644 index 00000000..4f397ac4 Binary files /dev/null and b/core/ui/src/main/res/drawable-xhdpi/img_error.png differ diff --git a/core/ui/src/main/res/drawable-xhdpi/img_noalert.png b/core/ui/src/main/res/drawable-xhdpi/img_noalert.png new file mode 100644 index 00000000..2d1740b2 Binary files /dev/null and b/core/ui/src/main/res/drawable-xhdpi/img_noalert.png differ diff --git a/core/ui/src/main/res/drawable-xxhdpi/img_birthday_mashong.png b/core/ui/src/main/res/drawable-xxhdpi/img_birthday_mashong.png new file mode 100644 index 00000000..7cc7744f Binary files /dev/null and b/core/ui/src/main/res/drawable-xxhdpi/img_birthday_mashong.png differ diff --git a/core/ui/src/main/res/drawable-xxhdpi/img_error.png b/core/ui/src/main/res/drawable-xxhdpi/img_error.png new file mode 100644 index 00000000..52155799 Binary files /dev/null and b/core/ui/src/main/res/drawable-xxhdpi/img_error.png differ diff --git a/core/ui/src/main/res/drawable-xxhdpi/img_noalert.png b/core/ui/src/main/res/drawable-xxhdpi/img_noalert.png new file mode 100644 index 00000000..960e34ca Binary files /dev/null and b/core/ui/src/main/res/drawable-xxhdpi/img_noalert.png differ diff --git a/core/ui/src/main/res/drawable-xxxhdpi/img_birthday_mashong.png b/core/ui/src/main/res/drawable-xxxhdpi/img_birthday_mashong.png new file mode 100644 index 00000000..e1981506 Binary files /dev/null and b/core/ui/src/main/res/drawable-xxxhdpi/img_birthday_mashong.png differ diff --git a/core/ui/src/main/res/drawable-xxxhdpi/img_error.png b/core/ui/src/main/res/drawable-xxxhdpi/img_error.png new file mode 100644 index 00000000..9e7ac234 Binary files /dev/null and b/core/ui/src/main/res/drawable-xxxhdpi/img_error.png differ diff --git a/core/ui/src/main/res/drawable-xxxhdpi/img_noalert.png b/core/ui/src/main/res/drawable-xxxhdpi/img_noalert.png new file mode 100644 index 00000000..50e66c91 Binary files /dev/null and b/core/ui/src/main/res/drawable-xxxhdpi/img_noalert.png differ diff --git a/core/ui/src/main/res/drawable/ic_alarm.xml b/core/ui/src/main/res/drawable/ic_alarm.xml new file mode 100644 index 00000000..7a744b8c --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_alarm.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_birthday.xml b/core/ui/src/main/res/drawable/ic_birthday.xml new file mode 100644 index 00000000..1e05e756 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_birthday.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_carrot.xml b/core/ui/src/main/res/drawable/ic_carrot.xml new file mode 100644 index 00000000..58441ee5 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_carrot.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_etc.xml b/core/ui/src/main/res/drawable/ic_etc.xml new file mode 100644 index 00000000..9f12ac8b --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_etc.xml @@ -0,0 +1,12 @@ + + + + diff --git a/core/ui/src/main/res/drawable/ic_mashong.xml b/core/ui/src/main/res/drawable/ic_mashong.xml new file mode 100644 index 00000000..9d4dccc1 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_mashong.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_new.xml b/core/ui/src/main/res/drawable/ic_new.xml new file mode 100644 index 00000000..61f78adb --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_new.xml @@ -0,0 +1,12 @@ + + + + diff --git a/core/ui/src/main/res/drawable/ic_platform.xml b/core/ui/src/main/res/drawable/ic_platform.xml new file mode 100644 index 00000000..cf524524 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_platform.xml @@ -0,0 +1,11 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_semina.xml b/core/ui/src/main/res/drawable/ic_semina.xml new file mode 100644 index 00000000..e45ae499 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_semina.xml @@ -0,0 +1,16 @@ + + + + diff --git a/core/ui/src/main/res/drawable/ic_setting.xml b/core/ui/src/main/res/drawable/ic_setting.xml new file mode 100644 index 00000000..95fe7fe3 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_setting.xml @@ -0,0 +1,10 @@ + + + diff --git a/feature/moreMenu/.gitignore b/feature/moreMenu/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/moreMenu/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/moreMenu/build.gradle b/feature/moreMenu/build.gradle new file mode 100644 index 00000000..dba523e5 --- /dev/null +++ b/feature/moreMenu/build.gradle @@ -0,0 +1,65 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' + id 'kotlin-kapt' + id 'com.google.dagger.hilt.android' +} + +android { + namespace 'com.mashup.feature.moreMenu' + compileSdk compileVersion + + defaultConfig { + minSdk minVersion + targetSdk targetVersion + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + buildFeatures { + compose true + } + composeOptions { + kotlinCompilerExtensionVersion composeCompiler + } + + kotlinOptions { + jvmTarget = '1.8' + } + kapt { + correctErrorTypes = true + } +} + +dependencies { + + implementation project(":core:common") + implementation project(':core:ui') + implementation project(":core:datastore") + implementation project(":core:model") + implementation project(':core:network') + implementation project(':core:data') + implementation 'androidx.activity:activity-compose:1.9.0' + implementation platform('androidx.compose:compose-bom:2023.08.00') + implementation 'androidx.compose.ui:ui' + implementation 'androidx.compose.ui:ui-graphics' + implementation 'androidx.compose.ui:ui-tooling-preview' + implementation 'androidx.compose.material3:material3' + implementation project(':feature:moreMenu:notice') + androidTestImplementation platform('androidx.compose:compose-bom:2023.08.00') + androidTestImplementation 'androidx.compose.ui:ui-test-junit4' + debugImplementation project(':core:testing') + + implementation "com.google.dagger:hilt-android:$hiltVersion" + debugImplementation 'androidx.compose.ui:ui-tooling' + debugImplementation 'androidx.compose.ui:ui-test-manifest' + kapt "com.google.dagger:hilt-compiler:$hiltVersion" + + implementation "com.squareup.moshi:moshi-kotlin:$moshiVersion" + implementation "com.squareup.moshi:moshi-adapters:$moshiVersion" + kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshiVersion" +} \ No newline at end of file diff --git a/feature/moreMenu/consumer-rules.pro b/feature/moreMenu/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/feature/moreMenu/notice/.gitignore b/feature/moreMenu/notice/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/moreMenu/notice/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/moreMenu/notice/build.gradle b/feature/moreMenu/notice/build.gradle new file mode 100644 index 00000000..893a572a --- /dev/null +++ b/feature/moreMenu/notice/build.gradle @@ -0,0 +1,63 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' + id 'kotlin-kapt' + id 'com.google.dagger.hilt.android' +} + +android { + namespace 'com.mashup.feature.moreMenu.notice' + compileSdk compileVersion + + defaultConfig { + minSdk minVersion + targetSdk targetVersion + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + buildFeatures { + compose true + } + composeOptions { + kotlinCompilerExtensionVersion composeCompiler + } + + kotlinOptions { + jvmTarget = '1.8' + } + kapt { + correctErrorTypes = true + } +} + +dependencies { + implementation project(":core:common") + implementation project(':core:ui') + implementation project(":core:datastore") + implementation project(":core:model") + implementation project(':core:network') + implementation project(':core:data') + implementation 'androidx.activity:activity-compose:1.9.0' + implementation platform('androidx.compose:compose-bom:2023.08.00') + implementation 'androidx.compose.ui:ui' + implementation 'androidx.compose.ui:ui-graphics' + implementation 'androidx.compose.ui:ui-tooling-preview' + implementation 'androidx.compose.material3:material3' + androidTestImplementation platform('androidx.compose:compose-bom:2023.08.00') + androidTestImplementation 'androidx.compose.ui:ui-test-junit4' + debugImplementation project(':core:testing') + + implementation "com.google.dagger:hilt-android:$hiltVersion" + debugImplementation 'androidx.compose.ui:ui-tooling' + debugImplementation 'androidx.compose.ui:ui-test-manifest' + kapt "com.google.dagger:hilt-compiler:$hiltVersion" + + implementation "com.squareup.moshi:moshi-kotlin:$moshiVersion" + implementation "com.squareup.moshi:moshi-adapters:$moshiVersion" + kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshiVersion" +} \ No newline at end of file diff --git a/feature/moreMenu/notice/consumer-rules.pro b/feature/moreMenu/notice/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/feature/moreMenu/notice/proguard-rules.pro b/feature/moreMenu/notice/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/moreMenu/notice/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/feature/moreMenu/notice/src/androidTest/java/com/example/notice/ExampleInstrumentedTest.kt b/feature/moreMenu/notice/src/androidTest/java/com/example/notice/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..7d67c1cf --- /dev/null +++ b/feature/moreMenu/notice/src/androidTest/java/com/example/notice/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package com.example.notice + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.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.example.alarm.test", appContext.packageName) + } +} diff --git a/feature/moreMenu/notice/src/main/AndroidManifest.xml b/feature/moreMenu/notice/src/main/AndroidManifest.xml new file mode 100644 index 00000000..d0a21e08 --- /dev/null +++ b/feature/moreMenu/notice/src/main/AndroidManifest.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/feature/moreMenu/notice/src/main/java/com/example/notice/NoticeScreen.kt b/feature/moreMenu/notice/src/main/java/com/example/notice/NoticeScreen.kt new file mode 100644 index 00000000..ac1f8d3d --- /dev/null +++ b/feature/moreMenu/notice/src/main/java/com/example/notice/NoticeScreen.kt @@ -0,0 +1,219 @@ +package com.example.notice + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.example.notice.components.NoticeItem +import com.example.notice.model.NoticeState +import com.example.notice.model.NoticeState.Companion.isNoticeEmpty +import com.mashup.core.network.dto.PushHistoryResponse +import com.mashup.core.ui.R +import com.mashup.core.ui.colors.Gray600 +import com.mashup.core.ui.theme.MashUpTheme +import com.mashup.core.ui.typography.Body5 +import com.mashup.core.ui.typography.Title3 +import com.mashup.core.ui.widget.MashUpToolbar + +@Composable +fun NoticeRoute( + modifier: Modifier = Modifier, + noticeState: NoticeState = NoticeState(), + onBackPressed: () -> Unit = {}, + onLoadNextNotice: () -> Unit = {}, + onClickNoticeItem: (PushHistoryResponse.Notice) -> Unit = {} +) { + NoticeScreen( + modifier = modifier, + noticeState = noticeState, + onBackPressed = onBackPressed, + onLoadNextNotice = onLoadNextNotice, + onClickNoticeItem = onClickNoticeItem + ) +} + +@Composable +fun NoticeScreen( + modifier: Modifier = Modifier, + noticeState: NoticeState = NoticeState(), + onBackPressed: () -> Unit = {}, + onLoadNextNotice: () -> Unit = {}, + onClickNoticeItem: (PushHistoryResponse.Notice) -> Unit = {} +) { + Column(modifier = modifier) { + MashUpToolbar( + modifier = Modifier.fillMaxWidth(), + title = "알림", + showBackButton = true, + onClickBackButton = onBackPressed + ) + + val scrollState = rememberLazyListState() + val lastItemReached by remember { + derivedStateOf { + val lastVisibleItem = scrollState.layoutInfo.visibleItemsInfo.lastOrNull() + val lastItemIndex = scrollState.layoutInfo.totalItemsCount - 1 + lastVisibleItem?.index == lastItemIndex + } + } + + LaunchedEffect(lastItemReached) { + if (lastItemReached) { + onLoadNextNotice() + } + } + + if (noticeState.isError) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Image( + painter = painterResource(id = R.drawable.img_error), + contentDescription = null + ) + Text( + text = "오류가 발생했어요...", + color = Gray600, + style = Body5 + ) + return + } + } + + if (noticeState.isNoticeEmpty()) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Image( + painter = painterResource(id = R.drawable.img_noalert), + contentDescription = null + ) + Text( + text = "아직 도착한 알림이 없어요", + color = Gray600, + style = Body5 + ) + return + } + } + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding( + horizontal = 20.dp + ), + state = scrollState + ) { + if (noticeState.newNoticeList.isNotEmpty()) { + item { + Text( + text = "새로운 알림", + style = Title3, + fontWeight = FontWeight.Bold, + color = Color(0xFF2C3037) + ) + Spacer( + modifier = Modifier.height(16.dp) + ) + } + items(noticeState.newNoticeList) { + NoticeItem( + notice = it, + modifier = Modifier + .fillMaxWidth().clickable { + onClickNoticeItem(it) + } + ) + Spacer( + modifier = Modifier.height(12.dp) + ) + } + } + + if (noticeState.oldNoticeList.isNotEmpty()) { + item { + Spacer( + modifier = Modifier.height(12.dp) + ) + } + item { + Text( + text = "지난 알림", + style = Title3, + fontWeight = FontWeight.Bold, + color = Color(0xFF2C3037) + ) + Spacer( + modifier = Modifier.height(16.dp) + ) + } + items(noticeState.oldNoticeList) { + NoticeItem( + notice = it, + modifier = Modifier + .fillMaxWidth().clickable { + onClickNoticeItem(it) + } + ) + Spacer( + modifier = Modifier.height(12.dp) + ) + } + } + } + } +} + +@Preview +@Composable +private fun PreviewNoticeScreen() { + MashUpTheme { + NoticeScreen( + modifier = Modifier + .fillMaxSize() + .background(color = Color(0xFFF8F7FC)) + ) + } +} + +@Preview +@Composable +private fun PreviewNoticeScreenError() { + MashUpTheme { + NoticeScreen( + modifier = Modifier + .fillMaxSize() + .background(color = Color(0xFFF8F7FC)), + noticeState = NoticeState().copy( + isError = true + ) + ) + } +} diff --git a/feature/moreMenu/notice/src/main/java/com/example/notice/components/NoticeItem.kt b/feature/moreMenu/notice/src/main/java/com/example/notice/components/NoticeItem.kt new file mode 100644 index 00000000..6f012bae --- /dev/null +++ b/feature/moreMenu/notice/src/main/java/com/example/notice/components/NoticeItem.kt @@ -0,0 +1,102 @@ +package com.example.notice.components + +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.Spacer +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.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.example.notice.util.getNoticeTime +import com.example.notice.util.getPushImage +import com.mashup.core.network.dto.PushHistoryResponse +import com.mashup.core.ui.colors.Gray400 +import com.mashup.core.ui.colors.Gray500 +import com.mashup.core.ui.colors.Gray950 +import com.mashup.core.ui.typography.Body5 +import com.mashup.core.ui.typography.Caption1 +import com.mashup.core.ui.typography.Caption2 + +@Composable +fun NoticeItem( + notice: PushHistoryResponse.Notice, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .fillMaxWidth() + .background( + color = Color.White, + shape = RoundedCornerShape(8.dp) + ) + .clip(RoundedCornerShape(8.dp)) + .padding(horizontal = 14.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Image( + modifier = Modifier + .size(26.dp) + .align(Alignment.Top), + painter = painterResource(notice.getPushImage()), + contentDescription = null + ) + Column( + modifier = Modifier + .width(254.dp) + ) { + Text( + text = notice.title, + style = Caption1, + color = Gray500 + ) + Spacer( + modifier = Modifier.height(4.dp) + ) + Text( + text = notice.body, + style = Body5.copy( + fontWeight = FontWeight.W600 + ), + color = Gray950 + ) + Spacer( + modifier = Modifier.height(8.dp) + ) + Text( + text = notice.sendTime.getNoticeTime(), + style = Caption2, + color = Gray400 + ) + } + } +} + +@Preview +@Composable +private fun PreviewNoticeItem() { + NoticeItem( + modifier = Modifier.background(color = Color.White), + notice = PushHistoryResponse.Notice( + title = "", + body = "", + sendTime = "", + pushType = "", + linkType = "" + ) + ) +} diff --git a/feature/moreMenu/notice/src/main/java/com/example/notice/model/NoticeSideEffect.kt b/feature/moreMenu/notice/src/main/java/com/example/notice/model/NoticeSideEffect.kt new file mode 100644 index 00000000..926501e3 --- /dev/null +++ b/feature/moreMenu/notice/src/main/java/com/example/notice/model/NoticeSideEffect.kt @@ -0,0 +1,8 @@ +package com.example.notice.model + +import com.mashup.core.network.dto.PushHistoryResponse + +sealed interface NoticeSideEffect { + object OnBackPressed : NoticeSideEffect + data class OnNavigateMenu(val notice: PushHistoryResponse.Notice) : NoticeSideEffect +} diff --git a/feature/moreMenu/notice/src/main/java/com/example/notice/model/NoticeState.kt b/feature/moreMenu/notice/src/main/java/com/example/notice/model/NoticeState.kt new file mode 100644 index 00000000..f18c82d1 --- /dev/null +++ b/feature/moreMenu/notice/src/main/java/com/example/notice/model/NoticeState.kt @@ -0,0 +1,15 @@ +package com.example.notice.model + +import com.mashup.core.network.dto.PushHistoryResponse + +data class NoticeState( + val newNoticeList: List = emptyList(), + val oldNoticeList: List = emptyList(), + val isError: Boolean = false +) { + companion object { + fun NoticeState.isNoticeEmpty(): Boolean { + return this.oldNoticeList.isEmpty() && this.newNoticeList.isEmpty() + } + } +} diff --git a/feature/moreMenu/notice/src/main/java/com/example/notice/util/GetNoticeTime.kt b/feature/moreMenu/notice/src/main/java/com/example/notice/util/GetNoticeTime.kt new file mode 100644 index 00000000..7531c6cd --- /dev/null +++ b/feature/moreMenu/notice/src/main/java/com/example/notice/util/GetNoticeTime.kt @@ -0,0 +1,25 @@ +package com.example.notice.util + +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.concurrent.TimeUnit + +fun String.getNoticeTime(): String { + val formatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()) + val parsedTime = formatter.parse(this) ?: return "" + val currentTime = Date() + val diff = currentTime.time - parsedTime.time + + val hours = TimeUnit.MILLISECONDS.toHours(diff) + val minutes = TimeUnit.MILLISECONDS.toMinutes(diff) % 60 + + return when { + hours < 1 -> "${minutes}분 전" + hours < 24 -> "${hours}시간 ${minutes}분 전" + else -> { + val monthDayFormatter = SimpleDateFormat("MM월 dd일", Locale.getDefault()) + monthDayFormatter.format(parsedTime) + } + } +} diff --git a/feature/moreMenu/notice/src/main/java/com/example/notice/util/GetNoticeTitle.kt b/feature/moreMenu/notice/src/main/java/com/example/notice/util/GetNoticeTitle.kt new file mode 100644 index 00000000..7296386c --- /dev/null +++ b/feature/moreMenu/notice/src/main/java/com/example/notice/util/GetNoticeTitle.kt @@ -0,0 +1,23 @@ +package com.example.notice.util + +import com.mashup.core.network.dto.PushHistoryResponse + +fun PushHistoryResponse.Notice.getNoticeTitle(): String { + return when (pushType) { + "BIRTHDAY" -> { + "생일 축하" + } + + "MASHONG" -> { + "매숑이 키우기" + } + + "DANGGN" -> { + "당근 흔들기" + } + + else -> { + "세미나 알림" + } + } +} diff --git a/feature/moreMenu/notice/src/main/java/com/example/notice/util/GetPushImage.kt b/feature/moreMenu/notice/src/main/java/com/example/notice/util/GetPushImage.kt new file mode 100644 index 00000000..96d11e22 --- /dev/null +++ b/feature/moreMenu/notice/src/main/java/com/example/notice/util/GetPushImage.kt @@ -0,0 +1,33 @@ +package com.example.notice.util + +import androidx.annotation.DrawableRes +import com.mashup.core.network.dto.PushHistoryResponse + +@DrawableRes +fun PushHistoryResponse.Notice.getPushImage(): Int { + return when (pushType) { + "BIRTHDAY" -> { + com.mashup.core.ui.R.drawable.ic_birthday + } + + "MASHONG" -> { + com.mashup.core.ui.R.drawable.ic_mashong + } + + "DANGGN" -> { + com.mashup.core.ui.R.drawable.ic_carrot + } + + "NOTI" -> { + com.mashup.core.ui.R.drawable.ic_alarm + } + + "SETTING" -> { + com.mashup.core.ui.R.drawable.ic_setting + } + + else -> { + com.mashup.core.ui.R.drawable.ic_etc + } + } +} diff --git a/feature/moreMenu/notice/src/main/res/values/strings.xml b/feature/moreMenu/notice/src/main/res/values/strings.xml new file mode 100644 index 00000000..f599d6c4 --- /dev/null +++ b/feature/moreMenu/notice/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + NoticeActivity + \ No newline at end of file diff --git a/feature/moreMenu/notice/src/main/res/values/themes.xml b/feature/moreMenu/notice/src/main/res/values/themes.xml new file mode 100644 index 00000000..d267c0f5 --- /dev/null +++ b/feature/moreMenu/notice/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +