diff --git a/app-compose/build.gradle.kts b/app-compose/build.gradle.kts
index be3fc11b6..bdcaeed01 100644
--- a/app-compose/build.gradle.kts
+++ b/app-compose/build.gradle.kts
@@ -18,6 +18,7 @@ android {
}
dependencies {
+ implementation(projects.core.designsystem)
implementation(projects.feature.navigator)
implementation(projects.remote.openmajor)
@@ -28,10 +29,12 @@ dependencies {
implementation(projects.remote.signup)
implementation(projects.remote.notice)
implementation(projects.remote.user)
+ implementation(projects.remote.login)
implementation(projects.local.openmajor)
implementation(projects.local.timetable)
implementation(projects.local.user)
+ implementation(projects.local.login)
implementation(projects.data.openmajor)
implementation(projects.data.timetable)
@@ -41,6 +44,7 @@ dependencies {
implementation(projects.data.user)
implementation(projects.data.notice)
implementation(projects.data.signup)
+ implementation(projects.data.login)
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.crashlytics)
diff --git a/app-compose/src/main/AndroidManifest.xml b/app-compose/src/main/AndroidManifest.xml
index 94776ba9c..2401fef9f 100644
--- a/app-compose/src/main/AndroidManifest.xml
+++ b/app-compose/src/main/AndroidManifest.xml
@@ -7,11 +7,12 @@
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="false"
- android:icon="@mipmap/ic_launcher"
+ android:icon="@mipmap/ic_logo"
android:label="@string/app_name"
- android:roundIcon="@mipmap/ic_launcher_round"
+ android:roundIcon="@mipmap/ic_logo"
android:supportsRtl="true"
android:theme="@style/Theme.Suwiki"
+ android:usesCleartextTraffic="true"
tools:replace="android:allowBackup"
tools:targetApi="s" />
diff --git a/app-compose/src/main/ic_logo-playstore.png b/app-compose/src/main/ic_logo-playstore.png
new file mode 100644
index 000000000..13712366c
Binary files /dev/null and b/app-compose/src/main/ic_logo-playstore.png differ
diff --git a/core/android/src/main/java/com/suwiki/core/android/ThrowUnknownException.kt b/core/android/src/main/java/com/suwiki/core/android/ThrowUnknownException.kt
index 1bf96c61a..7e64a8095 100644
--- a/core/android/src/main/java/com/suwiki/core/android/ThrowUnknownException.kt
+++ b/core/android/src/main/java/com/suwiki/core/android/ThrowUnknownException.kt
@@ -2,11 +2,9 @@ package com.suwiki.core.android
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase
-import com.suwiki.core.model.exception.UnknownException
-fun throwUnknownException(
+fun recordException(
e: Throwable,
) {
Firebase.crashlytics.recordException(e)
- throw e.message?.let { UnknownException(it) } ?: UnknownException()
}
diff --git a/core/designsystem/src/main/ic_logo-playstore.png b/core/designsystem/src/main/ic_logo-playstore.png
new file mode 100644
index 000000000..13712366c
Binary files /dev/null and b/core/designsystem/src/main/ic_logo-playstore.png differ
diff --git a/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/bottomsheet/SuwikiBottomSheet.kt b/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/bottomsheet/SuwikiBottomSheet.kt
index 497c26710..acbadcd80 100644
--- a/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/bottomsheet/SuwikiBottomSheet.kt
+++ b/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/bottomsheet/SuwikiBottomSheet.kt
@@ -1,24 +1,24 @@
package com.suwiki.core.designsystem.component.bottomsheet
import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.ColumnScope
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.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.SheetState
import androidx.compose.material3.Text
+import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
-import com.suwiki.core.designsystem.component.align.SuwikiAlignContainer
import com.suwiki.core.designsystem.theme.Gray95
import com.suwiki.core.designsystem.theme.SuwikiTheme
import com.suwiki.core.designsystem.theme.White
@@ -26,20 +26,20 @@ import com.suwiki.core.designsystem.theme.White
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SuwikiBottomSheet(
+ sheetState: SheetState = rememberModalBottomSheetState(),
isSheetOpen: Boolean,
onDismissRequest: () -> Unit = {},
- bottomSheetItem: List<@Composable () -> Unit>,
+ content: @Composable ColumnScope.() -> Unit,
) {
if (isSheetOpen) {
ModalBottomSheet(
+ sheetState = sheetState,
+ shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp),
onDismissRequest = onDismissRequest,
containerColor = White,
dragHandle = null,
) {
- Spacer(modifier = Modifier.height(36.dp))
- bottomSheetItem.forEach { item ->
- item()
- }
+ content()
}
}
}
@@ -60,10 +60,10 @@ fun SuwikiBottomSheetItem(
)
}
+@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun SuwikiBottomSheetPreview() {
- var isChecked by remember { mutableStateOf(false) }
var isSheetOpen by rememberSaveable { mutableStateOf(false) }
// 테스트용 버튼
@@ -75,16 +75,7 @@ fun SuwikiBottomSheetPreview() {
SuwikiBottomSheet(
isSheetOpen = isSheetOpen,
onDismissRequest = { isSheetOpen = !isSheetOpen },
- bottomSheetItem = listOf(
- { SuwikiBottomSheetItem(title = "타이틀") },
- {
- SuwikiAlignContainer(
- text = "메뉴",
- isChecked = isChecked,
- onClick = { isChecked = !isChecked },
- )
- },
- ),
+ content = {},
)
}
}
diff --git a/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/dialog/SuwikiDialog.kt b/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/dialog/SuwikiDialog.kt
index 72a10e582..5cb3da9f5 100644
--- a/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/dialog/SuwikiDialog.kt
+++ b/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/dialog/SuwikiDialog.kt
@@ -30,10 +30,10 @@ fun SuwikiDialog(
headerText: String,
bodyText: String,
confirmButtonText: String,
- dismissButtonText: String,
+ dismissButtonText: String? = null,
onDismissRequest: () -> Unit,
onClickConfirm: () -> Unit,
- onClickDismiss: () -> Unit,
+ onClickDismiss: () -> Unit = {},
) {
Dialog(
onDismissRequest = onDismissRequest,
@@ -60,12 +60,14 @@ fun SuwikiDialog(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
) {
- Text(
- modifier = Modifier.suwikiClickable(rippleEnabled = false, onClick = onClickDismiss),
- text = dismissButtonText,
- style = SuwikiTheme.typography.body4,
- color = Gray95,
- )
+ if (dismissButtonText != null) {
+ Text(
+ modifier = Modifier.suwikiClickable(rippleEnabled = false, onClick = onClickDismiss),
+ text = dismissButtonText,
+ style = SuwikiTheme.typography.body4,
+ color = Gray95,
+ )
+ }
Spacer(modifier = Modifier.width(30.dp))
diff --git a/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/loading/LoadingScreen.kt b/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/loading/LoadingScreen.kt
new file mode 100644
index 000000000..bc4b030e0
--- /dev/null
+++ b/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/loading/LoadingScreen.kt
@@ -0,0 +1,28 @@
+package com.suwiki.core.designsystem.component.loading
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.CircularProgressIndicator
+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.unit.dp
+import com.suwiki.core.designsystem.theme.Primary
+import com.suwiki.core.ui.extension.suwikiClickable
+
+@Composable
+fun LoadingScreen(modifier: Modifier = Modifier) {
+ Box(
+ modifier = modifier.fillMaxSize().suwikiClickable(rippleEnabled = false, onClick = {}),
+ contentAlignment = Alignment.Center,
+ ) {
+ CircularProgressIndicator(
+ modifier = Modifier.size(36.dp),
+ strokeWidth = 4.dp,
+ color = Primary,
+ trackColor = Color.Transparent,
+ )
+ }
+}
diff --git a/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/textfield/SuwikiTextFieldRegular.kt b/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/textfield/SuwikiRegularTextField.kt
similarity index 79%
rename from core/designsystem/src/main/java/com/suwiki/core/designsystem/component/textfield/SuwikiTextFieldRegular.kt
rename to core/designsystem/src/main/java/com/suwiki/core/designsystem/component/textfield/SuwikiRegularTextField.kt
index 3e7e55018..cde20cc7d 100644
--- a/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/textfield/SuwikiTextFieldRegular.kt
+++ b/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/textfield/SuwikiRegularTextField.kt
@@ -12,10 +12,12 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -24,9 +26,14 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import com.suwiki.core.designsystem.R
import com.suwiki.core.designsystem.component.button.TextFieldClearButton
import com.suwiki.core.designsystem.theme.Black
import com.suwiki.core.designsystem.theme.Error
@@ -36,9 +43,10 @@ import com.suwiki.core.designsystem.theme.GrayF6
import com.suwiki.core.designsystem.theme.Primary
import com.suwiki.core.designsystem.theme.SuwikiTheme
import com.suwiki.core.designsystem.theme.White
+import com.suwiki.core.ui.extension.suwikiClickable
@Composable
-fun SuwikiTextFieldRegular(
+fun SuwikiRegularTextField(
modifier: Modifier = Modifier,
label: String? = "",
placeholder: String = "",
@@ -51,6 +59,9 @@ fun SuwikiTextFieldRegular(
minLines: Int = 1,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
+ showEyeIcon: Boolean = false,
+ onClickEyeIcon: () -> Unit = {},
+ showValue: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
val isFocused by interactionSource.collectIsFocusedAsState()
@@ -78,6 +89,7 @@ fun SuwikiTextFieldRegular(
textStyle = SuwikiTheme.typography.header4.copy(color = Black),
interactionSource = interactionSource,
cursorBrush = SolidColor(Primary),
+ visualTransformation = if (showValue) VisualTransformation.None else PasswordVisualTransformation(),
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
decorationBox = { innerText ->
@@ -116,9 +128,25 @@ fun SuwikiTextFieldRegular(
}
if (value.isNotEmpty()) {
- TextFieldClearButton(
- onClick = onClickClearButton,
- )
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ ) {
+ if (showEyeIcon) {
+ Icon(
+ modifier = modifier
+ .size(24.dp)
+ .clip(CircleShape)
+ .suwikiClickable(onClick = onClickEyeIcon),
+ painter = painterResource(id = if (showValue) R.drawable.ic_eye_off else R.drawable.ic_eye_on),
+ tint = Gray95,
+ contentDescription = "",
+ )
+ }
+
+ TextFieldClearButton(
+ onClick = onClickClearButton,
+ )
+ }
}
}
@@ -145,7 +173,7 @@ fun SuwikiTextFieldRegular(
@Preview(showBackground = true, backgroundColor = 0xFFFFFF)
@Composable
-fun SuwikiTextFieldRegularPreview() {
+fun SuwikiRegularTextFieldPreview() {
SuwikiTheme {
var normalValue by remember {
mutableStateOf("")
@@ -159,7 +187,7 @@ fun SuwikiTextFieldRegularPreview() {
modifier = Modifier.background(White),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
- SuwikiTextFieldRegular(
+ SuwikiRegularTextField(
label = "라벨",
placeholder = "플레이스 홀더",
value = normalValue,
@@ -168,7 +196,7 @@ fun SuwikiTextFieldRegularPreview() {
helperText = "도움말 메세지",
)
- SuwikiTextFieldRegular(
+ SuwikiRegularTextField(
label = "라벨",
placeholder = "플레이스 홀더",
value = errorValue,
diff --git a/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/textfield/SuwikiTextFieldReview.kt b/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/textfield/SuwikiReviewInputBox.kt
similarity index 96%
rename from core/designsystem/src/main/java/com/suwiki/core/designsystem/component/textfield/SuwikiTextFieldReview.kt
rename to core/designsystem/src/main/java/com/suwiki/core/designsystem/component/textfield/SuwikiReviewInputBox.kt
index bda5e7825..75e6252ab 100644
--- a/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/textfield/SuwikiTextFieldReview.kt
+++ b/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/textfield/SuwikiReviewInputBox.kt
@@ -31,7 +31,7 @@ import com.suwiki.core.designsystem.theme.Primary
import com.suwiki.core.designsystem.theme.SuwikiTheme
@Composable
-fun SuwikiTextFieldReview(
+fun SuwikiReviewInputBox(
modifier: Modifier = Modifier,
hint: String = "",
value: String = "",
@@ -74,7 +74,7 @@ fun SuwikiTextFieldReview(
@Preview(showBackground = true, backgroundColor = 0xFFFFFF)
@Composable
-fun SuwikiTextFieldReviewPreview() {
+fun SuwikiReviewInputBoxPreview() {
SuwikiTheme {
var normalValue by remember {
mutableStateOf("")
@@ -84,13 +84,13 @@ fun SuwikiTextFieldReviewPreview() {
modifier = Modifier.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
- SuwikiTextFieldReview(
+ SuwikiReviewInputBox(
hint = "강의평가를 작성해주세요",
value = normalValue,
onValueChange = { normalValue = it },
)
- SuwikiTextFieldReview(
+ SuwikiReviewInputBox(
hint = "강의평가를 작성해주세요",
value = normalValue,
isError = true,
diff --git a/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/textfield/SuwikiTextFieldSmall.kt b/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/textfield/SuwikiSmallTextField.kt
similarity index 93%
rename from core/designsystem/src/main/java/com/suwiki/core/designsystem/component/textfield/SuwikiTextFieldSmall.kt
rename to core/designsystem/src/main/java/com/suwiki/core/designsystem/component/textfield/SuwikiSmallTextField.kt
index a377296ca..ba63b0368 100644
--- a/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/textfield/SuwikiTextFieldSmall.kt
+++ b/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/textfield/SuwikiSmallTextField.kt
@@ -26,6 +26,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.suwiki.core.designsystem.component.button.TextFieldClearButton
@@ -38,7 +39,7 @@ import com.suwiki.core.designsystem.theme.SuwikiTheme
import com.suwiki.core.designsystem.theme.White
@Composable
-fun SuwikiTextFieldSmall(
+fun SuwikiSmallTextField(
modifier: Modifier = Modifier,
placeholder: String = "",
value: String = "",
@@ -48,6 +49,7 @@ fun SuwikiTextFieldSmall(
minLines: Int = 1,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
+ visualTransformation: VisualTransformation = VisualTransformation.None,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
val isFocused by interactionSource.collectIsFocusedAsState()
@@ -70,6 +72,7 @@ fun SuwikiTextFieldSmall(
cursorBrush = SolidColor(Primary),
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
+ visualTransformation = visualTransformation,
decorationBox = { innerText ->
Column {
Row(
@@ -117,7 +120,7 @@ fun SuwikiTextFieldSmall(
@Preview(showBackground = true, backgroundColor = 0xFFFFFF)
@Composable
-fun SuwikiTextFieldSmallPreview() {
+fun SuwikiSmallTextFieldPreview() {
SuwikiTheme {
var normalValue by remember {
mutableStateOf("")
@@ -129,14 +132,14 @@ fun SuwikiTextFieldSmallPreview() {
.padding(vertical = 10.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
- SuwikiTextFieldSmall(
+ SuwikiSmallTextField(
placeholder = "플레이스 홀더",
value = normalValue,
onValueChange = { normalValue = it },
onClickClearButton = { normalValue = "" },
)
- SuwikiTextFieldSmall(
+ SuwikiSmallTextField(
placeholder = "플레이스 홀더",
value = normalValue,
onValueChange = { normalValue = it },
diff --git a/core/designsystem/src/main/java/com/suwiki/core/designsystem/theme/Typography.kt b/core/designsystem/src/main/java/com/suwiki/core/designsystem/theme/Typography.kt
index d036f25e9..2cf938b1b 100644
--- a/core/designsystem/src/main/java/com/suwiki/core/designsystem/theme/Typography.kt
+++ b/core/designsystem/src/main/java/com/suwiki/core/designsystem/theme/Typography.kt
@@ -29,6 +29,7 @@ private val notoSansStyle = TextStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
+ color = Black,
)
internal val Typography = SuwikiTypography(
diff --git a/core/designsystem/src/main/res/drawable/ic_eye_off.xml b/core/designsystem/src/main/res/drawable/ic_eye_off.xml
new file mode 100644
index 000000000..7454717d8
--- /dev/null
+++ b/core/designsystem/src/main/res/drawable/ic_eye_off.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/core/designsystem/src/main/res/drawable/ic_eye_on.xml b/core/designsystem/src/main/res/drawable/ic_eye_on.xml
new file mode 100644
index 000000000..eedc96fb9
--- /dev/null
+++ b/core/designsystem/src/main/res/drawable/ic_eye_on.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/core/designsystem/src/main/res/drawable/ic_logo.xml b/core/designsystem/src/main/res/drawable/ic_logo.xml
new file mode 100644
index 000000000..371e4c772
--- /dev/null
+++ b/core/designsystem/src/main/res/drawable/ic_logo.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
diff --git a/core/designsystem/src/main/res/mipmap-anydpi-v26/ic_logo.xml b/core/designsystem/src/main/res/mipmap-anydpi-v26/ic_logo.xml
new file mode 100644
index 000000000..38d66407d
--- /dev/null
+++ b/core/designsystem/src/main/res/mipmap-anydpi-v26/ic_logo.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/core/designsystem/src/main/res/mipmap-anydpi-v26/ic_logo_round.xml b/core/designsystem/src/main/res/mipmap-anydpi-v26/ic_logo_round.xml
new file mode 100644
index 000000000..38d66407d
--- /dev/null
+++ b/core/designsystem/src/main/res/mipmap-anydpi-v26/ic_logo_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/core/designsystem/src/main/res/mipmap-hdpi/ic_logo.webp b/core/designsystem/src/main/res/mipmap-hdpi/ic_logo.webp
new file mode 100644
index 000000000..ecca702eb
Binary files /dev/null and b/core/designsystem/src/main/res/mipmap-hdpi/ic_logo.webp differ
diff --git a/core/designsystem/src/main/res/mipmap-hdpi/ic_logo_foreground.webp b/core/designsystem/src/main/res/mipmap-hdpi/ic_logo_foreground.webp
new file mode 100644
index 000000000..ea3a92f2d
Binary files /dev/null and b/core/designsystem/src/main/res/mipmap-hdpi/ic_logo_foreground.webp differ
diff --git a/core/designsystem/src/main/res/mipmap-hdpi/ic_logo_round.webp b/core/designsystem/src/main/res/mipmap-hdpi/ic_logo_round.webp
new file mode 100644
index 000000000..137e84308
Binary files /dev/null and b/core/designsystem/src/main/res/mipmap-hdpi/ic_logo_round.webp differ
diff --git a/core/designsystem/src/main/res/mipmap-mdpi/ic_logo.webp b/core/designsystem/src/main/res/mipmap-mdpi/ic_logo.webp
new file mode 100644
index 000000000..313103c02
Binary files /dev/null and b/core/designsystem/src/main/res/mipmap-mdpi/ic_logo.webp differ
diff --git a/core/designsystem/src/main/res/mipmap-mdpi/ic_logo_foreground.webp b/core/designsystem/src/main/res/mipmap-mdpi/ic_logo_foreground.webp
new file mode 100644
index 000000000..e1b113f49
Binary files /dev/null and b/core/designsystem/src/main/res/mipmap-mdpi/ic_logo_foreground.webp differ
diff --git a/core/designsystem/src/main/res/mipmap-mdpi/ic_logo_round.webp b/core/designsystem/src/main/res/mipmap-mdpi/ic_logo_round.webp
new file mode 100644
index 000000000..b8ff17b65
Binary files /dev/null and b/core/designsystem/src/main/res/mipmap-mdpi/ic_logo_round.webp differ
diff --git a/core/designsystem/src/main/res/mipmap-xhdpi/ic_logo.webp b/core/designsystem/src/main/res/mipmap-xhdpi/ic_logo.webp
new file mode 100644
index 000000000..8a65328a3
Binary files /dev/null and b/core/designsystem/src/main/res/mipmap-xhdpi/ic_logo.webp differ
diff --git a/core/designsystem/src/main/res/mipmap-xhdpi/ic_logo_foreground.webp b/core/designsystem/src/main/res/mipmap-xhdpi/ic_logo_foreground.webp
new file mode 100644
index 000000000..47635e9e4
Binary files /dev/null and b/core/designsystem/src/main/res/mipmap-xhdpi/ic_logo_foreground.webp differ
diff --git a/core/designsystem/src/main/res/mipmap-xhdpi/ic_logo_round.webp b/core/designsystem/src/main/res/mipmap-xhdpi/ic_logo_round.webp
new file mode 100644
index 000000000..2d718794b
Binary files /dev/null and b/core/designsystem/src/main/res/mipmap-xhdpi/ic_logo_round.webp differ
diff --git a/core/designsystem/src/main/res/mipmap-xxhdpi/ic_logo.webp b/core/designsystem/src/main/res/mipmap-xxhdpi/ic_logo.webp
new file mode 100644
index 000000000..7c70ef7bf
Binary files /dev/null and b/core/designsystem/src/main/res/mipmap-xxhdpi/ic_logo.webp differ
diff --git a/core/designsystem/src/main/res/mipmap-xxhdpi/ic_logo_foreground.webp b/core/designsystem/src/main/res/mipmap-xxhdpi/ic_logo_foreground.webp
new file mode 100644
index 000000000..657c61637
Binary files /dev/null and b/core/designsystem/src/main/res/mipmap-xxhdpi/ic_logo_foreground.webp differ
diff --git a/core/designsystem/src/main/res/mipmap-xxhdpi/ic_logo_round.webp b/core/designsystem/src/main/res/mipmap-xxhdpi/ic_logo_round.webp
new file mode 100644
index 000000000..1659df483
Binary files /dev/null and b/core/designsystem/src/main/res/mipmap-xxhdpi/ic_logo_round.webp differ
diff --git a/core/designsystem/src/main/res/mipmap-xxxhdpi/ic_logo.webp b/core/designsystem/src/main/res/mipmap-xxxhdpi/ic_logo.webp
new file mode 100644
index 000000000..7107c9d69
Binary files /dev/null and b/core/designsystem/src/main/res/mipmap-xxxhdpi/ic_logo.webp differ
diff --git a/core/designsystem/src/main/res/mipmap-xxxhdpi/ic_logo_foreground.webp b/core/designsystem/src/main/res/mipmap-xxxhdpi/ic_logo_foreground.webp
new file mode 100644
index 000000000..7d48eb0f9
Binary files /dev/null and b/core/designsystem/src/main/res/mipmap-xxxhdpi/ic_logo_foreground.webp differ
diff --git a/core/designsystem/src/main/res/mipmap-xxxhdpi/ic_logo_round.webp b/core/designsystem/src/main/res/mipmap-xxxhdpi/ic_logo_round.webp
new file mode 100644
index 000000000..cd58834e6
Binary files /dev/null and b/core/designsystem/src/main/res/mipmap-xxxhdpi/ic_logo_round.webp differ
diff --git a/core/designsystem/src/main/res/values/ic_logo_background.xml b/core/designsystem/src/main/res/values/ic_logo_background.xml
new file mode 100644
index 000000000..67b6ee5de
--- /dev/null
+++ b/core/designsystem/src/main/res/values/ic_logo_background.xml
@@ -0,0 +1,4 @@
+
+
+ #FFFFFF
+
\ No newline at end of file
diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts
index 10d937df4..994f30836 100644
--- a/core/network/build.gradle.kts
+++ b/core/network/build.gradle.kts
@@ -1,3 +1,5 @@
+
+
@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed
plugins {
alias(libs.plugins.suwiki.android.library)
@@ -7,6 +9,16 @@ plugins {
android {
namespace = "com.suwiki.core.network"
+
+ buildTypes {
+ getByName("debug") {
+ buildConfigField("String", "BASE_URL", "\"http://54.180.72.97:8080\"")
+ }
+
+ getByName("release") {
+ buildConfigField("String", "BASE_URL", "\"https://api.suwiki.kr\"")
+ }
+ }
}
dependencies {
diff --git a/core/network/src/main/java/com/suwiki/core/network/di/NetworkModule.kt b/core/network/src/main/java/com/suwiki/core/network/di/NetworkModule.kt
index 3a69ea7f7..48474493c 100644
--- a/core/network/src/main/java/com/suwiki/core/network/di/NetworkModule.kt
+++ b/core/network/src/main/java/com/suwiki/core/network/di/NetworkModule.kt
@@ -1,6 +1,7 @@
package com.suwiki.core.network.di
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
+import com.suwiki.core.network.BuildConfig
import com.suwiki.core.network.authenticator.TokenAuthenticator
import com.suwiki.core.network.interceptor.AuthenticationInterceptor
import com.suwiki.core.network.retrofit.ResultCallAdapterFactory
@@ -22,8 +23,6 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
object NetworkModule {
- private const val BASE_URL: String = "https://api.suwiki.kr"
-
@Singleton
@Provides
@NormalOkHttpClient
@@ -79,7 +78,7 @@ object NetworkModule {
json: Json,
): Retrofit {
return Retrofit.Builder()
- .baseUrl(BASE_URL)
+ .baseUrl(BuildConfig.BASE_URL)
.client(okHttpClient)
.addCallAdapterFactory(ResultCallAdapterFactory())
.addConverterFactory(json.asConverterFactory("application/json".toMediaTypeOrNull()!!))
@@ -112,7 +111,7 @@ object NetworkModule {
json: Json,
): Retrofit {
return Retrofit.Builder()
- .baseUrl(BASE_URL)
+ .baseUrl(BuildConfig.BASE_URL)
.client(okHttpClient)
.addCallAdapterFactory(ResultCallAdapterFactory())
.addConverterFactory(json.asConverterFactory("application/json".toMediaTypeOrNull()!!))
diff --git a/data/user/src/main/java/com/suwiki/data/user/repository/UserRepositoryImpl.kt b/data/user/src/main/java/com/suwiki/data/user/repository/UserRepositoryImpl.kt
index 8f51c78ee..7e072786c 100644
--- a/data/user/src/main/java/com/suwiki/data/user/repository/UserRepositoryImpl.kt
+++ b/data/user/src/main/java/com/suwiki/data/user/repository/UserRepositoryImpl.kt
@@ -8,6 +8,7 @@ import com.suwiki.domain.user.repository.UserRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.zip
import javax.inject.Inject
class UserRepositoryImpl @Inject constructor(
@@ -39,8 +40,22 @@ class UserRepositoryImpl @Inject constructor(
emit(localUserInfo)
val remoteUserInfo = remoteUserDataSource.getUserInfo()
- emit(remoteUserInfo)
+ val isTokenExpired = with(securityPreferences) {
+ val (accessToken, refreshToken) = flowAccessToken().zip(flowRefreshToken()) { accessToken, refreshToken ->
+ (accessToken to refreshToken)
+ }.first()
+
+ accessToken.isEmpty() && refreshToken.isEmpty()
+ }
+
+ if (isTokenExpired) {
+ logout()
+ emit(User())
+ return@flow
+ }
+
+ emit(remoteUserInfo)
localUserDataSource.setUserInfo(remoteUserInfo)
}
}
diff --git a/feature/lectureevaluation/viewerreporter/build.gradle.kts b/feature/lectureevaluation/viewerreporter/build.gradle.kts
index f0e430ad2..44bdf64f1 100644
--- a/feature/lectureevaluation/viewerreporter/build.gradle.kts
+++ b/feature/lectureevaluation/viewerreporter/build.gradle.kts
@@ -8,5 +8,6 @@ android {
}
dependencies {
+ implementation(projects.domain.user)
implementation(projects.domain.lectureevaluation.viewerreporter)
}
diff --git a/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/LectureEvaluationContract.kt b/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/LectureEvaluationContract.kt
new file mode 100644
index 000000000..a41ee50df
--- /dev/null
+++ b/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/LectureEvaluationContract.kt
@@ -0,0 +1,10 @@
+package com.suwiki.feature.lectureevaluation.viewerreporter
+
+data class LectureEvaluationState(
+ val showOnboardingBottomSheet: Boolean = false,
+)
+
+sealed interface LectureEvaluationSideEffect {
+ data object NavigateLogin : LectureEvaluationSideEffect
+ data object NavigateSignUp : LectureEvaluationSideEffect
+}
diff --git a/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/LectureEvaluationScreen.kt b/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/LectureEvaluationScreen.kt
index 57d4c189b..203f10ead 100644
--- a/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/LectureEvaluationScreen.kt
+++ b/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/LectureEvaluationScreen.kt
@@ -1,28 +1,86 @@
package com.suwiki.feature.lectureevaluation.viewerreporter
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.Text
+import androidx.compose.foundation.pager.PagerState
+import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
import com.suwiki.core.designsystem.theme.SuwikiTheme
+import com.suwiki.feature.lectureevaluation.viewerreporter.component.ONBOARDING_PAGE_COUNT
+import com.suwiki.feature.lectureevaluation.viewerreporter.component.OnboardingBottomSheet
+import org.orbitmvi.orbit.compose.collectAsState
+import org.orbitmvi.orbit.compose.collectSideEffect
+@OptIn(ExperimentalFoundationApi::class)
@Composable
-fun LectureEvaluationScreen(
+fun LectureEvaluationRoute(
padding: PaddingValues,
+ viewModel: LectureEvaluationViewModel = hiltViewModel(),
+ navigateLogin: () -> Unit,
) {
- Text(
- modifier = Modifier.padding(padding),
- text = "강의평가",
+ val uiState = viewModel.collectAsState().value
+ viewModel.collectSideEffect { sideEffect ->
+ when (sideEffect) {
+ LectureEvaluationSideEffect.NavigateLogin -> navigateLogin()
+ LectureEvaluationSideEffect.NavigateSignUp -> TODO()
+ }
+ }
+
+ LaunchedEffect(key1 = viewModel) {
+ viewModel.checkLoggedInShowBottomSheetIfNeed()
+ }
+
+ val pagerState = rememberPagerState(pageCount = { ONBOARDING_PAGE_COUNT })
+
+ LectureEvaluationScreen(
+ padding = padding,
+ uiState = uiState,
+ pagerState = pagerState,
+ hideOnboardingBottomSheet = viewModel::hideOnboardingBottomSheet,
+ onClickLoginButton = {
+ viewModel.hideOnboardingBottomSheet()
+ viewModel.navigateLogin()
+ },
)
}
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun LectureEvaluationScreen(
+ padding: PaddingValues,
+ uiState: LectureEvaluationState,
+ pagerState: PagerState = rememberPagerState(pageCount = { ONBOARDING_PAGE_COUNT }),
+ hideOnboardingBottomSheet: () -> Unit = {},
+ onClickLoginButton: () -> Unit = {},
+ onClickSignupButton: () -> Unit = {},
+) {
+ Box(modifier = Modifier.fillMaxSize().padding(padding)) {
+ OnboardingBottomSheet(
+ uiState = uiState,
+ hideOnboardingBottomSheet = hideOnboardingBottomSheet,
+ pagerState = pagerState,
+ onClickLoginButton = onClickLoginButton,
+ onClickSignupButton = onClickSignupButton,
+ )
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
@Preview
@Composable
fun LectureEvaluationScreenPreview() {
SuwikiTheme {
- LectureEvaluationScreen(padding = PaddingValues(0.dp))
+ LectureEvaluationScreen(
+ padding = PaddingValues(0.dp),
+ uiState = LectureEvaluationState(),
+ )
}
}
diff --git a/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/LectureEvaluationViewModel.kt b/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/LectureEvaluationViewModel.kt
new file mode 100644
index 000000000..850f290f8
--- /dev/null
+++ b/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/LectureEvaluationViewModel.kt
@@ -0,0 +1,44 @@
+package com.suwiki.feature.lectureevaluation.viewerreporter
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.suwiki.domain.user.usecase.GetUserInfoUseCase
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.lastOrNull
+import kotlinx.coroutines.launch
+import org.orbitmvi.orbit.Container
+import org.orbitmvi.orbit.ContainerHost
+import org.orbitmvi.orbit.syntax.simple.intent
+import org.orbitmvi.orbit.syntax.simple.postSideEffect
+import org.orbitmvi.orbit.syntax.simple.reduce
+import org.orbitmvi.orbit.viewmodel.container
+import javax.inject.Inject
+
+@HiltViewModel
+class LectureEvaluationViewModel @Inject constructor(
+ private val getUserInfoUseCase: GetUserInfoUseCase,
+) : ContainerHost, ViewModel() {
+ override val container: Container =
+ container(LectureEvaluationState())
+
+ private var isLoggedIn: Boolean = false
+ private var isFirstVisit: Boolean = true
+
+ private suspend fun checkLoggedIn() {
+ isLoggedIn = getUserInfoUseCase().catch { }.lastOrNull()?.isLoggedIn == true
+ }
+
+ fun checkLoggedInShowBottomSheetIfNeed() = viewModelScope.launch {
+ checkLoggedIn()
+ if (isLoggedIn.not() && isFirstVisit) {
+ isFirstVisit = false
+ showOnboardingBottomSheet()
+ }
+ }
+
+ fun navigateLogin() = intent { postSideEffect(LectureEvaluationSideEffect.NavigateLogin) }
+
+ private fun showOnboardingBottomSheet() = intent { reduce { state.copy(showOnboardingBottomSheet = true) } }
+ fun hideOnboardingBottomSheet() = intent { reduce { state.copy(showOnboardingBottomSheet = false) } }
+}
diff --git a/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/component/OnboardingBottomSheet.kt b/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/component/OnboardingBottomSheet.kt
new file mode 100644
index 000000000..1dbb80038
--- /dev/null
+++ b/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/component/OnboardingBottomSheet.kt
@@ -0,0 +1,94 @@
+package com.suwiki.feature.lectureevaluation.viewerreporter.component
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.pager.PagerState
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.rememberModalBottomSheetState
+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
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.suwiki.core.designsystem.component.bottomsheet.SuwikiBottomSheet
+import com.suwiki.core.designsystem.component.button.SuwikiContainedLargeButton
+import com.suwiki.core.designsystem.component.button.SuwikiOutlinedButton
+import com.suwiki.core.designsystem.theme.SuwikiTheme
+import com.suwiki.feature.lectureevaluation.viewerreporter.LectureEvaluationState
+import com.suwiki.feature.lectureevaluation.viewerreporter.R
+
+@Composable
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
+fun OnboardingBottomSheet(
+ uiState: LectureEvaluationState,
+ hideOnboardingBottomSheet: () -> Unit,
+ pagerState: PagerState,
+ onClickLoginButton: () -> Unit = {},
+ onClickSignupButton: () -> Unit = {},
+) {
+ SuwikiBottomSheet(
+ sheetState = rememberModalBottomSheetState(
+ skipPartiallyExpanded = true,
+ ),
+ isSheetOpen = uiState.showOnboardingBottomSheet,
+ onDismissRequest = hideOnboardingBottomSheet,
+ ) {
+ OnboardingBottomSheetContent(
+ pagerState = pagerState,
+ onClickLoginButton = onClickLoginButton,
+ onClickSignupButton = onClickSignupButton,
+ )
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun OnboardingBottomSheetContent(
+ pagerState: PagerState = rememberPagerState(pageCount = { ONBOARDING_PAGE_COUNT }),
+ onClickLoginButton: () -> Unit = {},
+ onClickSignupButton: () -> Unit = {},
+) {
+ Column(
+ modifier = Modifier.padding(top = 62.dp, bottom = 50.dp, start = 24.dp, end = 24.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Image(
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ painter = painterResource(id = R.drawable.ic_logo),
+ contentDescription = stringResource(R.string.content_description_logo),
+ )
+
+ Spacer(modifier = Modifier.size(107.dp))
+
+ OnboardingPager(pagerState)
+
+ Spacer(modifier = Modifier.size(34.dp))
+
+ OnboardingPagerIndicator(
+ pageCount = ONBOARDING_PAGE_COUNT,
+ pagerState = pagerState,
+ )
+
+ Spacer(modifier = Modifier.size(50.dp))
+
+ SuwikiContainedLargeButton(text = stringResource(R.string.onboarding_button_signup), onClick = onClickSignupButton)
+ Spacer(modifier = Modifier.size(12.dp))
+ SuwikiOutlinedButton(text = stringResource(R.string.word_login), onClick = onClickLoginButton)
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Preview(showBackground = true)
+@Composable
+fun OnboardingBottomSheetContentPreview() {
+ SuwikiTheme {
+ OnboardingBottomSheetContent()
+ }
+}
diff --git a/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/component/OnboardingPager.kt b/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/component/OnboardingPager.kt
new file mode 100644
index 000000000..327303240
--- /dev/null
+++ b/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/component/OnboardingPager.kt
@@ -0,0 +1,107 @@
+package com.suwiki.feature.lectureevaluation.viewerreporter.component
+
+import androidx.annotation.DrawableRes
+import androidx.compose.foundation.ExperimentalFoundationApi
+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.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.PagerState
+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.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import com.suwiki.core.designsystem.theme.Gray6A
+import com.suwiki.core.designsystem.theme.GrayDA
+import com.suwiki.core.designsystem.theme.Primary
+import com.suwiki.core.designsystem.theme.SuwikiTheme
+import com.suwiki.feature.lectureevaluation.viewerreporter.R
+
+const val ONBOARDING_PAGE_COUNT = 2
+
+@Composable
+@OptIn(ExperimentalFoundationApi::class)
+fun OnboardingPager(pagerState: PagerState) {
+ HorizontalPager(
+ state = pagerState,
+ ) { page ->
+ when (page) {
+ 0 -> {
+ OnboardingPageContent(
+ title = stringResource(R.string.onboarding_title),
+ description = stringResource(R.string.onboarding1_description),
+ icon = R.drawable.ic_onboarding1,
+ )
+ }
+
+ 1 -> {
+ OnboardingPageContent(
+ title = stringResource(R.string.onboarding_title),
+ description = stringResource(R.string.onboarding2_description),
+ icon = R.drawable.ic_onboarding2,
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun OnboardingPageContent(
+ title: String,
+ description: String,
+ @DrawableRes icon: Int,
+) {
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Image(
+ modifier = Modifier.size(160.dp),
+ painter = painterResource(id = icon),
+ contentDescription = "",
+ )
+
+ Spacer(modifier = Modifier.size(42.dp))
+
+ Text(text = title, style = SuwikiTheme.typography.header2, textAlign = TextAlign.Center)
+
+ Spacer(modifier = Modifier.size(7.dp))
+
+ Text(text = description, style = SuwikiTheme.typography.body5, color = Gray6A)
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun OnboardingPagerIndicator(
+ pageCount: Int,
+ pagerState: PagerState,
+) {
+ Row(
+ horizontalArrangement = Arrangement.Center,
+ ) {
+ repeat(pageCount) { iteration ->
+ val color = if (pagerState.currentPage == iteration) Primary else GrayDA
+ Box(
+ modifier = Modifier
+ .padding(horizontal = 4.dp)
+ .clip(CircleShape)
+ .background(color)
+ .size(5.dp),
+ )
+ }
+ }
+}
diff --git a/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/navigation/LectureEvaluationNavigation.kt b/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/navigation/LectureEvaluationNavigation.kt
index 28d1bf054..591c7e837 100644
--- a/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/navigation/LectureEvaluationNavigation.kt
+++ b/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/navigation/LectureEvaluationNavigation.kt
@@ -5,7 +5,7 @@ import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.compose.composable
-import com.suwiki.feature.lectureevaluation.viewerreporter.LectureEvaluationScreen
+import com.suwiki.feature.lectureevaluation.viewerreporter.LectureEvaluationRoute
fun NavController.navigateLectureEvaluation(navOptions: NavOptions) {
navigate(LectureEvaluationRoute.route, navOptions)
@@ -13,9 +13,13 @@ fun NavController.navigateLectureEvaluation(navOptions: NavOptions) {
fun NavGraphBuilder.lectureEvaluationNavGraph(
padding: PaddingValues,
+ navigateLogin: () -> Unit,
) {
composable(route = LectureEvaluationRoute.route) {
- LectureEvaluationScreen(padding)
+ LectureEvaluationRoute(
+ padding = padding,
+ navigateLogin = navigateLogin,
+ )
}
}
diff --git a/feature/lectureevaluation/viewerreporter/src/main/res/drawable/ic_onboarding1.png b/feature/lectureevaluation/viewerreporter/src/main/res/drawable/ic_onboarding1.png
new file mode 100644
index 000000000..b35a6a779
Binary files /dev/null and b/feature/lectureevaluation/viewerreporter/src/main/res/drawable/ic_onboarding1.png differ
diff --git a/feature/lectureevaluation/viewerreporter/src/main/res/drawable/ic_onboarding2.png b/feature/lectureevaluation/viewerreporter/src/main/res/drawable/ic_onboarding2.png
new file mode 100644
index 000000000..8fe8800c7
Binary files /dev/null and b/feature/lectureevaluation/viewerreporter/src/main/res/drawable/ic_onboarding2.png differ
diff --git a/feature/lectureevaluation/viewerreporter/src/main/res/values/strings.xml b/feature/lectureevaluation/viewerreporter/src/main/res/values/strings.xml
new file mode 100644
index 000000000..42a81021c
--- /dev/null
+++ b/feature/lectureevaluation/viewerreporter/src/main/res/values/strings.xml
@@ -0,0 +1,9 @@
+
+
+ 로고
+ 로그인
+ 가입하기
+ 수원대 학생이라면\n수위키로 한 번에 해결!
+ 재학생들의 솔직한 강의평가 확인하기
+ 재학생들의 솔직한 강의평가 확인하기
+
diff --git a/feature/login/src/main/java/com/suwiki/feature/login/LoginContract.kt b/feature/login/src/main/java/com/suwiki/feature/login/LoginContract.kt
new file mode 100644
index 000000000..59a464a02
--- /dev/null
+++ b/feature/login/src/main/java/com/suwiki/feature/login/LoginContract.kt
@@ -0,0 +1,20 @@
+package com.suwiki.feature.login
+
+data class LoginState(
+ val showEmailNotAuthDialog: Boolean = false,
+ val showLoginFailDialog: Boolean = false,
+ val id: String = "",
+ val password: String = "",
+ val showPassword: Boolean = false,
+ val isLoading: Boolean = false,
+) {
+ val loginButtonEnable = id.isNotBlank() && password.isNotBlank()
+}
+
+sealed interface LoginSideEffect {
+ data class HandleException(val throwable: Throwable) : LoginSideEffect
+ data object PopBackStack : LoginSideEffect
+ data object NavigateFindId : LoginSideEffect
+ data object NavigateFindPassword : LoginSideEffect
+ data object NavigateSignUp : LoginSideEffect
+}
diff --git a/feature/login/src/main/java/com/suwiki/feature/login/LoginScreen.kt b/feature/login/src/main/java/com/suwiki/feature/login/LoginScreen.kt
new file mode 100644
index 000000000..7101b27e2
--- /dev/null
+++ b/feature/login/src/main/java/com/suwiki/feature/login/LoginScreen.kt
@@ -0,0 +1,199 @@
+package com.suwiki.feature.login
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Text
+import androidx.compose.material3.VerticalDivider
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.suwiki.core.designsystem.component.button.SuwikiContainedLargeButton
+import com.suwiki.core.designsystem.component.dialog.SuwikiDialog
+import com.suwiki.core.designsystem.component.loading.LoadingScreen
+import com.suwiki.core.designsystem.component.textfield.SuwikiRegularTextField
+import com.suwiki.core.designsystem.theme.Gray6A
+import com.suwiki.core.designsystem.theme.GrayF6
+import com.suwiki.core.designsystem.theme.Primary
+import com.suwiki.core.designsystem.theme.SuwikiTheme
+import com.suwiki.core.ui.extension.suwikiClickable
+import org.orbitmvi.orbit.compose.collectAsState
+import org.orbitmvi.orbit.compose.collectSideEffect
+
+@Composable
+fun LoginRoute(
+ viewModel: LoginViewModel = hiltViewModel(),
+ popBackStack: () -> Unit,
+ navigateFindId: () -> Unit,
+ navigateFindPassword: () -> Unit,
+ navigateSignup: () -> Unit,
+ handleException: (Throwable) -> Unit,
+) {
+ val uiState = viewModel.collectAsState().value
+ viewModel.collectSideEffect { sideEffect ->
+ when (sideEffect) {
+ is LoginSideEffect.HandleException -> handleException(sideEffect.throwable)
+ LoginSideEffect.NavigateFindId -> navigateFindId()
+ LoginSideEffect.NavigateFindPassword -> navigateFindPassword()
+ LoginSideEffect.NavigateSignUp -> navigateSignup()
+ LoginSideEffect.PopBackStack -> popBackStack()
+ }
+ }
+
+ LoginScreen(
+ uiState = uiState,
+ onIdTextFieldValueChange = viewModel::updateId,
+ onPasswordTextFieldValueChange = viewModel::updatePassword,
+ onClickIdClearButton = { viewModel.updateId("") },
+ onClickPasswordClearButton = { viewModel.updatePassword("") },
+ onClickPasswordEyeIcon = viewModel::toggleShowPassword,
+ onClickFindIdText = { /* TODO */ },
+ onClickFindPasswordText = { /* TODO */ },
+ onClickSignupText = { /* TODO */ },
+ onClickLoginButton = viewModel::login,
+ onClickLoginFailDialogButton = viewModel::hideLoginFailDialog,
+ onClickEmailNotAuthDialogButton = viewModel::hideEmailNotAuthDialog,
+ )
+}
+
+@Composable
+fun LoginScreen(
+ uiState: LoginState = LoginState(),
+ onIdTextFieldValueChange: (String) -> Unit = {},
+ onPasswordTextFieldValueChange: (String) -> Unit = {},
+ onClickIdClearButton: () -> Unit = {},
+ onClickPasswordClearButton: () -> Unit = {},
+ onClickPasswordEyeIcon: () -> Unit = {},
+ onClickFindIdText: () -> Unit = {},
+ onClickFindPasswordText: () -> Unit = {},
+ onClickSignupText: () -> Unit = {},
+ onClickLoginButton: () -> Unit = {},
+ onClickLoginFailDialogButton: () -> Unit = {},
+ onClickEmailNotAuthDialogButton: () -> Unit = {},
+) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(top = 63.dp, bottom = 30.dp, start = 24.dp, end = 24.dp),
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(text = stringResource(R.string.word_login), style = SuwikiTheme.typography.header1)
+
+ Spacer(modifier = Modifier.size(26.dp))
+
+ SuwikiRegularTextField(
+ value = uiState.id,
+ onValueChange = onIdTextFieldValueChange,
+ onClickClearButton = onClickIdClearButton,
+ label = stringResource(R.string.word_id),
+ placeholder = stringResource(R.string.login_screen_id_textfield_placeholder),
+ )
+
+ Spacer(modifier = Modifier.size(20.dp))
+
+ SuwikiRegularTextField(
+ value = uiState.password,
+ onValueChange = onPasswordTextFieldValueChange,
+ onClickClearButton = onClickPasswordClearButton,
+ showEyeIcon = true,
+ showValue = uiState.showPassword,
+ onClickEyeIcon = onClickPasswordEyeIcon,
+ label = stringResource(R.string.word_password),
+ placeholder = stringResource(R.string.login_screen_password_textfield_placeholder),
+ )
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ Row(
+ modifier = Modifier.height(IntrinsicSize.Min),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ Text(
+ modifier = Modifier.suwikiClickable(onClick = onClickFindIdText),
+ text = stringResource(R.string.login_screen_find_id),
+ style = SuwikiTheme.typography.body5,
+ color = Gray6A,
+ )
+ VerticalDivider(
+ modifier = Modifier.padding(vertical = 4.5.dp),
+ thickness = 1.dp,
+ color = GrayF6,
+ )
+ Text(
+ modifier = Modifier.suwikiClickable(onClick = onClickFindPasswordText),
+ text = stringResource(R.string.login_screen_find_password),
+ style = SuwikiTheme.typography.body5,
+ color = Gray6A,
+ )
+ VerticalDivider(
+ modifier = Modifier.padding(vertical = 4.5.dp),
+ thickness = 1.dp,
+ color = GrayF6,
+ )
+ Text(
+ modifier = Modifier.suwikiClickable(onClick = onClickSignupText),
+ text = stringResource(R.string.word_signup),
+ style = SuwikiTheme.typography.body4,
+ color = Primary,
+ )
+ }
+
+ Spacer(modifier = Modifier.size(20.dp))
+
+ SuwikiContainedLargeButton(
+ modifier = Modifier.imePadding(),
+ clickable = uiState.loginButtonEnable,
+ enabled = uiState.loginButtonEnable,
+ text = stringResource(R.string.word_login),
+ onClick = onClickLoginButton,
+ )
+ }
+
+ if (uiState.showLoginFailDialog) {
+ SuwikiDialog(
+ headerText = stringResource(R.string.login_screen_dialog_login_fail_title),
+ bodyText = stringResource(R.string.login_screen_dialog_login_fail_body),
+ confirmButtonText = stringResource(R.string.word_confirm),
+ onDismissRequest = onClickLoginFailDialogButton,
+ onClickConfirm = onClickLoginFailDialogButton,
+ )
+ }
+
+ if (uiState.showEmailNotAuthDialog) {
+ SuwikiDialog(
+ headerText = stringResource(R.string.login_screen_dialog_email_not_auth_title),
+ bodyText = stringResource(R.string.login_screen_dialog_email_not_auth_body),
+ confirmButtonText = stringResource(R.string.word_confirm),
+ onDismissRequest = onClickEmailNotAuthDialogButton,
+ onClickConfirm = onClickEmailNotAuthDialogButton,
+ )
+ }
+
+ if (uiState.isLoading) {
+ LoadingScreen()
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun LectureEvaluationScreenPreview() {
+ SuwikiTheme {
+ LoginScreen()
+ }
+}
diff --git a/feature/login/src/main/java/com/suwiki/feature/login/LoginViewModel.kt b/feature/login/src/main/java/com/suwiki/feature/login/LoginViewModel.kt
new file mode 100644
index 000000000..89c54886c
--- /dev/null
+++ b/feature/login/src/main/java/com/suwiki/feature/login/LoginViewModel.kt
@@ -0,0 +1,58 @@
+package com.suwiki.feature.login
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.suwiki.core.model.exception.EmailNotAuthedException
+import com.suwiki.core.model.exception.LoginFailedException
+import com.suwiki.core.model.exception.PasswordErrorException
+import com.suwiki.domain.login.usecase.LoginUseCase
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import org.orbitmvi.orbit.Container
+import org.orbitmvi.orbit.ContainerHost
+import org.orbitmvi.orbit.annotation.OrbitExperimental
+import org.orbitmvi.orbit.syntax.simple.blockingIntent
+import org.orbitmvi.orbit.syntax.simple.intent
+import org.orbitmvi.orbit.syntax.simple.postSideEffect
+import org.orbitmvi.orbit.syntax.simple.reduce
+import org.orbitmvi.orbit.viewmodel.container
+import javax.inject.Inject
+
+@HiltViewModel
+class LoginViewModel @Inject constructor(
+ private val loginUseCase: LoginUseCase,
+) : ContainerHost, ViewModel() {
+ override val container: Container = container(LoginState())
+
+ fun login() = intent {
+ viewModelScope.launch {
+ reduce { state.copy(isLoading = true) }
+ loginUseCase(loginId = state.id, password = state.password)
+ .onSuccess {
+ postSideEffect(LoginSideEffect.PopBackStack)
+ }
+ .onFailure {
+ when (it) {
+ is LoginFailedException -> reduce { state.copy(showLoginFailDialog = true) }
+ is PasswordErrorException -> reduce { state.copy(showLoginFailDialog = true) }
+ is EmailNotAuthedException -> reduce { state.copy(showEmailNotAuthDialog = true) }
+ else -> postSideEffect(LoginSideEffect.HandleException(it))
+ }
+ }
+
+ reduce { state.copy(isLoading = false) }
+ }
+ }
+
+ fun toggleShowPassword() = intent { reduce { state.copy(showPassword = !state.showPassword) } }
+
+ fun hideLoginFailDialog() = intent { reduce { state.copy(showLoginFailDialog = false) } }
+
+ fun hideEmailNotAuthDialog() = intent { reduce { state.copy(showEmailNotAuthDialog = false) } }
+
+ @OptIn(OrbitExperimental::class)
+ fun updateId(id: String) = blockingIntent { reduce { state.copy(id = id) } }
+
+ @OptIn(OrbitExperimental::class)
+ fun updatePassword(password: String) = blockingIntent { reduce { state.copy(password = password) } }
+}
diff --git a/feature/login/src/main/java/com/suwiki/feature/login/navigation/LoginNavigation.kt b/feature/login/src/main/java/com/suwiki/feature/login/navigation/LoginNavigation.kt
new file mode 100644
index 000000000..a876c82b8
--- /dev/null
+++ b/feature/login/src/main/java/com/suwiki/feature/login/navigation/LoginNavigation.kt
@@ -0,0 +1,32 @@
+package com.suwiki.feature.login.navigation
+
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.compose.composable
+import com.suwiki.feature.login.LoginRoute
+
+fun NavController.navigateLogin() {
+ navigate(LoginRoute.route)
+}
+
+fun NavGraphBuilder.loginNavGraph(
+ popBackStack: () -> Unit,
+ navigateFindId: () -> Unit,
+ navigateFindPassword: () -> Unit,
+ navigateSignup: () -> Unit,
+ handleException: (Throwable) -> Unit,
+) {
+ composable(route = LoginRoute.route) {
+ LoginRoute(
+ popBackStack = popBackStack,
+ navigateFindId = navigateFindId,
+ navigateFindPassword = navigateFindPassword,
+ navigateSignup = navigateSignup,
+ handleException = handleException,
+ )
+ }
+}
+
+object LoginRoute {
+ const val route = "login"
+}
diff --git a/feature/login/src/main/res/values/strings.xml b/feature/login/src/main/res/values/strings.xml
new file mode 100644
index 000000000..5de9774e4
--- /dev/null
+++ b/feature/login/src/main/res/values/strings.xml
@@ -0,0 +1,16 @@
+
+
+ 로그인
+ 아이디
+ 비밀번호
+ 아이디를 입력하세요
+ 비밀번호를 입력하세요
+ 아이디 찾기
+ 비밀번호 찾기
+ 회원가입
+ 잘못된 아이디 혹은 비밀번호입니다.
+ 입력한 내용을 다시 확인해주세요.
+ 확인
+ 웹메일 인증 절차가 필요합니다.
+ 학교 웹미일 인증 진행 후 로그인 가능합니다.
+
diff --git a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainContract.kt b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainContract.kt
new file mode 100644
index 000000000..375ef831d
--- /dev/null
+++ b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainContract.kt
@@ -0,0 +1,9 @@
+package com.suwiki.feature.navigator
+
+data class MainState(
+ val toastMessage: String = "",
+ val toastVisible: Boolean = false,
+ val showNetworkErrorDialog: Boolean = false,
+)
+
+sealed interface MainSideEffect
diff --git a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainNavigator.kt b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainNavigator.kt
index 291833e9d..2873a2f22 100644
--- a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainNavigator.kt
+++ b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainNavigator.kt
@@ -9,6 +9,7 @@ import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navOptions
import com.suwiki.feature.lectureevaluation.viewerreporter.navigation.navigateLectureEvaluation
+import com.suwiki.feature.login.navigation.navigateLogin
import com.suwiki.feature.myinfo.navigation.navigateMyInfo
import com.suwiki.feature.timetable.navigation.TimetableRoute
import com.suwiki.feature.timetable.navigation.navigateTimetable
@@ -43,6 +44,10 @@ internal class MainNavigator(
}
}
+ fun navigateLogin() {
+ navController.navigateLogin()
+ }
+
fun popBackStackIfNotHome() {
if (!isSameCurrentDestination(TimetableRoute.route)) {
navController.popBackStack()
diff --git a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainScreen.kt b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainScreen.kt
index 189ba451a..17fad572f 100644
--- a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainScreen.kt
+++ b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainScreen.kt
@@ -13,71 +13,45 @@ import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxHeight
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.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
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.rememberCoroutineScope
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.tooling.preview.Preview
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.compose.NavHost
+import com.suwiki.core.designsystem.component.dialog.SuwikiDialog
import com.suwiki.core.designsystem.component.toast.SuwikiToast
import com.suwiki.core.designsystem.shadow.bottomNavigationShadow
import com.suwiki.core.designsystem.theme.GrayDA
import com.suwiki.core.designsystem.theme.Primary
-import com.suwiki.core.designsystem.theme.SuwikiTheme
import com.suwiki.core.designsystem.theme.White
import com.suwiki.core.ui.extension.suwikiClickable
import com.suwiki.feature.lectureevaluation.viewerreporter.navigation.lectureEvaluationNavGraph
+import com.suwiki.feature.login.R
+import com.suwiki.feature.login.navigation.loginNavGraph
+import com.suwiki.feature.login.navigation.navigateLogin
import com.suwiki.feature.myinfo.navigation.myInfoNavGraph
import com.suwiki.feature.timetable.navigation.timetableNavGraph
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.sync.Mutex
-
-private const val SHOW_TOAST_LENGTH = 2000L
-val mutex = Mutex()
+import org.orbitmvi.orbit.compose.collectAsState
@Composable
internal fun MainScreen(
modifier: Modifier = Modifier,
+ viewModel: MainViewModel = hiltViewModel(),
navigator: MainNavigator = rememberMainNavigator(),
) {
- val coroutineScope = rememberCoroutineScope()
-
- var toastMessage: String? by remember { mutableStateOf(null) }
- var toastVisible by remember { mutableStateOf(false) }
-
- // TODO REMOVE
- @Suppress("detekt:UnusedPrivateProperty")
- val onShowToast: (message: String) -> Unit = { message ->
- coroutineScope.launch {
- mutex.lock()
- toastMessage = message
- }
- }
-
- LaunchedEffect(key1 = toastMessage) {
- if (toastMessage == null) return@LaunchedEffect
-
- toastVisible = true
- delay(SHOW_TOAST_LENGTH)
- toastVisible = false
- if (mutex.isLocked) mutex.unlock()
- }
+ val uiState = viewModel.collectAsState().value
Scaffold(
modifier = modifier,
@@ -86,12 +60,21 @@ internal fun MainScreen(
navController = navigator.navController,
startDestination = navigator.startDestination,
) {
+ loginNavGraph(
+ popBackStack = navigator::popBackStackIfNotHome,
+ navigateFindId = { /* TODO */ },
+ navigateFindPassword = { /* TODO */ },
+ navigateSignup = { /* TODO */ },
+ handleException = viewModel::handleException,
+ )
+
timetableNavGraph(
padding = innerPadding,
)
lectureEvaluationNavGraph(
padding = innerPadding,
+ navigateLogin = navigator::navigateLogin,
)
myInfoNavGraph(
@@ -99,9 +82,19 @@ internal fun MainScreen(
)
}
+ if (uiState.showNetworkErrorDialog) {
+ SuwikiDialog(
+ headerText = stringResource(com.suwiki.feature.navigator.R.string.dialog_network_header),
+ bodyText = stringResource(com.suwiki.feature.navigator.R.string.dialog_network_body),
+ confirmButtonText = stringResource(id = R.string.word_confirm),
+ onDismissRequest = viewModel::hideNetworkErrorDialog,
+ onClickConfirm = viewModel::hideNetworkErrorDialog,
+ )
+ }
+
SuwikiToast(
- visible = toastVisible,
- message = toastMessage ?: "",
+ visible = uiState.toastVisible,
+ message = uiState.toastMessage,
)
},
bottomBar = {
@@ -115,14 +108,6 @@ internal fun MainScreen(
)
}
-@Preview
-@Composable
-fun MainScreenPreview() {
- SuwikiTheme {
- MainScreen()
- }
-}
-
@Composable
private fun MainBottomBar(
visible: Boolean,
diff --git a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainViewModel.kt b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainViewModel.kt
new file mode 100644
index 000000000..49520fbb0
--- /dev/null
+++ b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainViewModel.kt
@@ -0,0 +1,53 @@
+package com.suwiki.feature.navigator
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.suwiki.core.android.recordException
+import com.suwiki.core.model.exception.NetworkException
+import com.suwiki.core.model.exception.UnknownException
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import org.orbitmvi.orbit.Container
+import org.orbitmvi.orbit.ContainerHost
+import org.orbitmvi.orbit.syntax.simple.intent
+import org.orbitmvi.orbit.syntax.simple.reduce
+import org.orbitmvi.orbit.viewmodel.container
+import java.net.ConnectException
+import javax.inject.Inject
+
+@HiltViewModel
+class MainViewModel @Inject constructor() : ContainerHost, ViewModel() {
+ override val container: Container = container(MainState())
+
+ private val mutex = Mutex()
+
+ fun onShowToast(msg: String) = intent {
+ viewModelScope.launch {
+ mutex.withLock {
+ reduce { state.copy(toastMessage = msg, toastVisible = true) }
+ delay(SHOW_TOAST_LENGTH)
+ reduce { state.copy(toastVisible = false) }
+ }
+ }
+ }
+
+ fun handleException(throwable: Throwable) = intent {
+ when (throwable) {
+ is NetworkException -> reduce { state.copy(showNetworkErrorDialog = true) }
+ is ConnectException -> reduce { state.copy(showNetworkErrorDialog = true) }
+ else -> {
+ onShowToast(throwable.message ?: UnknownException().message)
+ recordException(throwable)
+ }
+ }
+ }
+
+ fun hideNetworkErrorDialog() = intent { reduce { state.copy(showNetworkErrorDialog = false) } }
+
+ companion object {
+ private const val SHOW_TOAST_LENGTH = 2000L
+ }
+}
diff --git a/feature/navigator/src/main/res/values/strings.xml b/feature/navigator/src/main/res/values/strings.xml
index b4075cad7..9a477b064 100644
--- a/feature/navigator/src/main/res/values/strings.xml
+++ b/feature/navigator/src/main/res/values/strings.xml
@@ -1,4 +1,6 @@
SUWIKI
+ 네트워크에 문제가 있어요.
+ 네트워크 연결 상태 또는 수위키 서버에 문제가 있어요. 문제가 지속된다면 내 정보 -> 문의하기를 통해 연락 주세요.
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 0f34bb570..23399d2a0 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -189,7 +189,7 @@ firebase = ["firebase-analytics", "firebase-database"]
androidx-lifecycle = ["androidx-lifecycle-runtime-ktx", "androidx-lifecycle-viewmodel-ktx"]
androidx-navigation = ["navigation-fragment-ktx", "navigation-ui-ktx"]
coroutine = ["kotlinx-coroutines-android", "kotlinx-coroutines-core"]
-compose = ["ui", "ui-graphics", "ui-tooling-preview", "material3-compose", "coil-compose", "ui-foundation", "activity-compose", "lifecycle-compose", "navigation-compose"]
+compose = ["ui", "ui-graphics", "ui-tooling-preview", "material3-compose", "coil-compose", "ui-foundation", "activity-compose", "lifecycle-compose", "navigation-compose", "hilt-navigation-compose"]
compose-debug = ["ui-tooling", "ui-test-manifest"]
orbit = ["orbit-core", "orbit-viewmodel", "orbit-compose"]