diff --git a/android/README.md b/android/README.md
new file mode 100644
index 00000000..13a641cf
--- /dev/null
+++ b/android/README.md
@@ -0,0 +1,68 @@
+# 울반 - NFC기반 학급 소통 앱
+![](https://blog.kakaocdn.net/dn/kTWTG/btsHtPbRbMO/ApDuCfEaKyqA57ZlnGnkU0/img.png)
+
+
+
📱 NFC, 블루투스 기반으로 소통해요
+
+🎯 친구들과 채팅하고 글을 쓰며 일상을 공유해요
+
+👥 친구들과의 친밀도를 확인해요
+
+🥇 선생님은 학생들의 소통 통계를 확인해요
+
+
+
+## 개요
+
+- **한 줄 요약** : *울반* 프로젝트는 NFC와 BLE 통신을 기반으로 한 학급 소통 앱입니다.
+
+- **기획의도** : 코로나 이후 악화된 교우관개를 개선을 위해 제작되었습니다.
+
+- **개발 인원 및 기간**
+
+ - **개발 인원** : Android 3명, BackEnd 3명
+
+ - **프로젝트 기간** : 2024.04.08 ~ 2024.05.19
+
+- **주요 기능**
+
+ - NFC 기반의 태깅인사, 이어달리기
+
+ - 블루투스 기반의 함께달리기
+
+ - 학급별 채팅, 게시판, 알림장
+
+ - 학급별 소통 통계
+
+
+
+
+# 프로젝트 구조
+![](https://blog.kakaocdn.net/dn/17Gn2/btsHtQhx6Ti/nfrxsULAZmdlsdLl3SJZxK/img.png)
+
+### 기술
+- Android: Hilt, Jetpack AAC(ViewModel, Room), Jetpack Compose, Paging
+- Kotlin : Coroutine, Flow, KotlinSerialization
+- Library : Retrofit, Coil, FCM, KakaoSocialAuth, KrossBow(Stomp)
+- Architecture : MVI, MultiModule, CleanArchitecture
+- Connection : NFC, BlueTooth
+
+
+
+# 동작 화면
+
+**주요 동작화면은 추후 추가 예정입니다.**
+
+### [피그마](https://www.figma.com/design/yfm5gTmRJED2uAdm7H70YC/6-kids-on-the-block?node-id=0%3A1&t=5blyLSniokJVPpQR-1)
+
+
+
+## 개발 환경
+
+- Android Studio : Iguana | 2023.2.1 Patch 2
+- Gradle JDK : jbr-17(JetBrains Runtime version 17.0.6)
+- Android Gradle Plugin Version : 8.1.3
+- Gradle Version : 8.1
+- Kotlin version : 1.8.0
+
+## 역할
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 6f05d812..dc623bcd 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -9,15 +9,16 @@
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
- android:icon="@mipmap/ic_launcher"
+ android:icon="@mipmap/ic_app_icon"
android:label="@string/app_name"
- android:roundIcon="@mipmap/ic_launcher_round"
+ android:roundIcon="@mipmap/ic_app_icon_round"
android:supportsRtl="true"
tools:targetApi="31">
+ android:exported="true"
+ android:screenOrientation="portrait">
diff --git a/android/app/src/main/ic_app_icon-playstore.png b/android/app/src/main/ic_app_icon-playstore.png
new file mode 100644
index 00000000..22754ad4
Binary files /dev/null and b/android/app/src/main/ic_app_icon-playstore.png differ
diff --git a/android/app/src/main/res/drawable/ic_app_icon_background.xml b/android/app/src/main/res/drawable/ic_app_icon_background.xml
new file mode 100644
index 00000000..ca3826a4
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_app_icon_background.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_app_icon.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_app_icon.xml
new file mode 100644
index 00000000..6cb34a23
--- /dev/null
+++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_app_icon.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_app_icon_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_app_icon_round.xml
new file mode 100644
index 00000000..6cb34a23
--- /dev/null
+++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_app_icon_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_app_icon.webp b/android/app/src/main/res/mipmap-hdpi/ic_app_icon.webp
new file mode 100644
index 00000000..21939df9
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_app_icon.webp differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_app_icon_foreground.webp b/android/app/src/main/res/mipmap-hdpi/ic_app_icon_foreground.webp
new file mode 100644
index 00000000..e71d8f54
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_app_icon_foreground.webp differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_app_icon_round.webp b/android/app/src/main/res/mipmap-hdpi/ic_app_icon_round.webp
new file mode 100644
index 00000000..e72bb0b6
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_app_icon_round.webp differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_app_icon.webp b/android/app/src/main/res/mipmap-mdpi/ic_app_icon.webp
new file mode 100644
index 00000000..62454840
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_app_icon.webp differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_app_icon_foreground.webp b/android/app/src/main/res/mipmap-mdpi/ic_app_icon_foreground.webp
new file mode 100644
index 00000000..6c4afc2c
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_app_icon_foreground.webp differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_app_icon_round.webp b/android/app/src/main/res/mipmap-mdpi/ic_app_icon_round.webp
new file mode 100644
index 00000000..488e3a6a
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_app_icon_round.webp differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_app_icon.webp b/android/app/src/main/res/mipmap-xhdpi/ic_app_icon.webp
new file mode 100644
index 00000000..c565e2f2
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_app_icon.webp differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_app_icon_foreground.webp b/android/app/src/main/res/mipmap-xhdpi/ic_app_icon_foreground.webp
new file mode 100644
index 00000000..ad06040f
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_app_icon_foreground.webp differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_app_icon_round.webp b/android/app/src/main/res/mipmap-xhdpi/ic_app_icon_round.webp
new file mode 100644
index 00000000..c4fdbecc
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_app_icon_round.webp differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_app_icon.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_app_icon.webp
new file mode 100644
index 00000000..0d9f7952
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_app_icon.webp differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_app_icon_foreground.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_app_icon_foreground.webp
new file mode 100644
index 00000000..5b470a91
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_app_icon_foreground.webp differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_app_icon_round.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_app_icon_round.webp
new file mode 100644
index 00000000..bb832098
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_app_icon_round.webp differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_app_icon.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_app_icon.webp
new file mode 100644
index 00000000..64893d8b
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_app_icon.webp differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_app_icon_foreground.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_app_icon_foreground.webp
new file mode 100644
index 00000000..7cdb203c
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_app_icon_foreground.webp differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_app_icon_round.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_app_icon_round.webp
new file mode 100644
index 00000000..de1ef0b2
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_app_icon_round.webp differ
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index a0b66fce..1f6fc661 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -1,3 +1,3 @@
- ulban
+ 울반
\ No newline at end of file
diff --git a/android/build.gradle.kts b/android/build.gradle.kts
index ec51d515..750638f1 100644
--- a/android/build.gradle.kts
+++ b/android/build.gradle.kts
@@ -8,3 +8,11 @@ plugins {
alias(libs.plugins.hilt) apply false
alias(libs.plugins.googleServices) apply false
}
+
+buildscript{
+ repositories {
+ mavenCentral()
+ google()
+ maven ( url = "https://jitpack.io" )
+ }
+}
\ No newline at end of file
diff --git a/android/core/bluetooth/.gitignore b/android/core/bluetooth/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/android/core/bluetooth/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/android/core/bluetooth/build.gradle.kts b/android/core/bluetooth/build.gradle.kts
new file mode 100644
index 00000000..656d03eb
--- /dev/null
+++ b/android/core/bluetooth/build.gradle.kts
@@ -0,0 +1,15 @@
+plugins {
+ alias(libs.plugins.sixkids.android.library)
+ alias(libs.plugins.sixkids.android.hilt)
+}
+
+android {
+ namespace = "com.sixkids.core.bluetooth"
+
+}
+
+dependencies {
+ implementation(libs.androidx.annotation.jvm)
+ implementation(libs.kotlinx.coroutines.core)
+ implementation(projects.core.model)
+}
diff --git a/android/core/bluetooth/consumer-rules.pro b/android/core/bluetooth/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/android/core/bluetooth/proguard-rules.pro b/android/core/bluetooth/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/android/core/bluetooth/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/android/core/bluetooth/src/main/AndroidManifest.xml b/android/core/bluetooth/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..90ceb4b6
--- /dev/null
+++ b/android/core/bluetooth/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/android/core/bluetooth/src/main/kotlin/com/sixkids/core/bluetooth/BluetoothScanner.kt b/android/core/bluetooth/src/main/kotlin/com/sixkids/core/bluetooth/BluetoothScanner.kt
new file mode 100644
index 00000000..5aaade84
--- /dev/null
+++ b/android/core/bluetooth/src/main/kotlin/com/sixkids/core/bluetooth/BluetoothScanner.kt
@@ -0,0 +1,73 @@
+package com.sixkids.core.bluetooth
+
+import android.Manifest
+import android.bluetooth.BluetoothManager
+import android.bluetooth.le.BluetoothLeScanner
+import android.bluetooth.le.ScanCallback
+import android.bluetooth.le.ScanResult
+import android.content.Context
+import android.os.ParcelUuid
+import android.util.Log
+import androidx.annotation.RequiresPermission
+import com.sixkids.core.bluetooth.BluetoothServer.Companion.ULBAN_UUID
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.update
+import java.nio.ByteBuffer
+import java.util.UUID
+
+private const val TAG = "D107 ble"
+
+class BluetoothScanner(context: Context) {
+ private val bluetooth = context.getSystemService(Context.BLUETOOTH_SERVICE)
+ as? BluetoothManager ?: throw Exception("This device doesn't support Bluetooth")
+
+ private val scanner: BluetoothLeScanner
+ get() = bluetooth.adapter.bluetoothLeScanner
+
+ val isScanning = MutableStateFlow(false)
+ val foundDevices = MutableStateFlow>(emptyList())
+
+ private val scanCallback = object : ScanCallback() {
+ @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+ override fun onScanResult(callbackType: Int, result: ScanResult?) {
+ super.onScanResult(callbackType, result)
+ result ?: return
+
+ val bytes = result.scanRecord?.getServiceData(ParcelUuid(UUID.fromString(ULBAN_UUID)))
+
+ bytes?.let {
+ val memberId = ByteBuffer.wrap(bytes).long
+ if (!foundDevices.value.contains(memberId)) {
+ foundDevices.update { it + memberId }
+ }
+ }
+ }
+
+ override fun onBatchScanResults(results: MutableList?) {
+ super.onBatchScanResults(results)
+ }
+
+ override fun onScanFailed(errorCode: Int) {
+ super.onScanFailed(errorCode)
+ Log.d(TAG, "onScanFailed: 실패")
+ isScanning.value = false
+ }
+ }
+
+ @RequiresPermission(Manifest.permission.BLUETOOTH_SCAN)
+ fun startScanning() {
+ scanner.startScan(scanCallback)
+ isScanning.value = true
+ }
+
+ @RequiresPermission(Manifest.permission.BLUETOOTH_SCAN)
+ fun stopScanning() {
+ scanner.stopScan(scanCallback)
+ isScanning.value = false
+ }
+
+ fun removeDevice(memberId: Long) {
+ foundDevices.update { it - memberId }
+ }
+
+}
diff --git a/android/core/bluetooth/src/main/kotlin/com/sixkids/core/bluetooth/BluetoothServer.kt b/android/core/bluetooth/src/main/kotlin/com/sixkids/core/bluetooth/BluetoothServer.kt
new file mode 100644
index 00000000..afa85bf3
--- /dev/null
+++ b/android/core/bluetooth/src/main/kotlin/com/sixkids/core/bluetooth/BluetoothServer.kt
@@ -0,0 +1,89 @@
+package com.sixkids.core.bluetooth
+
+import android.Manifest
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothManager
+import android.bluetooth.le.AdvertiseCallback
+import android.bluetooth.le.AdvertiseData
+import android.bluetooth.le.AdvertiseSettings
+import android.bluetooth.le.BluetoothLeAdvertiser
+import android.content.Context
+import android.os.Build
+import android.os.ParcelUuid
+import androidx.annotation.RequiresPermission
+import java.nio.ByteBuffer
+import java.util.UUID
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+
+class BluetoothServer(context: Context) {
+ private val bluetooth = context.getSystemService(Context.BLUETOOTH_SERVICE)
+ as? BluetoothManager ?: throw Exception("This device doesn't support Bluetooth")
+
+ private val bluetoothAdapter by lazy {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
+ bluetooth.adapter
+ else {
+ BluetoothAdapter.getDefaultAdapter()
+ }
+ }
+
+ private var advertiseCallback: AdvertiseCallback? = null
+
+ @RequiresPermission(allOf = [Manifest.permission.BLUETOOTH_ADVERTISE, Manifest.permission.BLUETOOTH_CONNECT])
+ suspend fun startAdvertising(memberId: Long) {
+ val advertiser: BluetoothLeAdvertiser = bluetoothAdapter.bluetoothLeAdvertiser
+ ?: throw Exception("This device doesn't support Bluetooth advertising")
+
+ //if already advertising, ignore
+ if (advertiseCallback != null) {
+ return
+ }
+
+ val memberIdBytes = ByteBuffer.allocate(Long.SIZE_BYTES).putLong(memberId).array()
+ val uuid = UUID.fromString(ULBAN_UUID) // 고유 UUID
+
+
+ val settings = AdvertiseSettings.Builder()
+ .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
+ .setConnectable(true)
+ .setTimeout(0)
+ .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM)
+ .build()
+
+ val data = AdvertiseData.Builder()
+ .setIncludeDeviceName(false)
+ .setIncludeTxPowerLevel(false)
+ .addServiceData(ParcelUuid(uuid), memberIdBytes)
+ .build()
+
+ advertiseCallback = suspendCoroutine { continuation ->
+ val advertiseCallback = object : AdvertiseCallback() {
+ override fun onStartSuccess(settingsInEffect: AdvertiseSettings?) {
+ continuation.resume(this)
+ }
+
+ override fun onStartFailure(errorCode: Int) {
+ super.onStartFailure(errorCode)
+ throw Exception("Unable to start advertising, errorCode: $errorCode")
+ }
+ }
+ advertiser.startAdvertising(settings, data, advertiseCallback)
+ }
+ }
+
+ @RequiresPermission(Manifest.permission.BLUETOOTH_ADVERTISE)
+ fun stopAdvertising() {
+ val advertiser: BluetoothLeAdvertiser = bluetoothAdapter.bluetoothLeAdvertiser
+ ?: throw Exception("This device doesn't support Bluetooth advertising")
+
+ advertiseCallback?.let {
+ advertiser.stopAdvertising(it)
+ advertiseCallback = null
+ }
+ }
+
+ companion object {
+ const val ULBAN_UUID = "0461c2e0-7d45-437f-929b-72aa4b355a96"
+ }
+}
diff --git a/android/core/bluetooth/src/main/kotlin/com/sixkids/core/bluetooth/di/BluetoothModule.kt b/android/core/bluetooth/src/main/kotlin/com/sixkids/core/bluetooth/di/BluetoothModule.kt
new file mode 100644
index 00000000..62fc7936
--- /dev/null
+++ b/android/core/bluetooth/src/main/kotlin/com/sixkids/core/bluetooth/di/BluetoothModule.kt
@@ -0,0 +1,33 @@
+package com.sixkids.core.bluetooth.di
+
+import android.content.Context
+import com.sixkids.core.bluetooth.BluetoothScanner
+import com.sixkids.core.bluetooth.BluetoothServer
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object BluetoothModule {
+
+ @Provides
+ @Singleton
+ fun provideBluetoothScanner(
+ @ApplicationContext context: Context
+ ): BluetoothScanner {
+ return BluetoothScanner(context)
+ }
+
+ @Provides
+ @Singleton
+ fun provideBluetoothServer(
+ @ApplicationContext context: Context
+ ): BluetoothServer {
+ return BluetoothServer(context)
+ }
+
+}
diff --git a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/appbar/BasicAppBar.kt b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/appbar/BasicAppBar.kt
index 89fef034..ae06f149 100644
--- a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/appbar/BasicAppBar.kt
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/appbar/BasicAppBar.kt
@@ -51,7 +51,7 @@ fun BasicAppBar(
)
val animatedHeight by animateDpAsState(
- targetValue = if (expanded) 180.dp else 60.dp,
+ targetValue = if (expanded) 210.dp else 60.dp,
animationSpec = TweenSpec(durationMillis = 300),
label = "앱바 크기"
)
@@ -88,7 +88,7 @@ fun BasicAppBar(
Icon(
modifier = Modifier
.fillMaxHeight()
- .padding(vertical = 16.dp)
+ .padding(vertical = 20.dp)
.aspectRatio(1f),
painter = painterResource(id = leftIcon),
contentDescription = "로고",
diff --git a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/appbar/UlbanDefaultAppBar.kt b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/appbar/UlbanDefaultAppBar.kt
index 4743b730..51f27c03 100644
--- a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/appbar/UlbanDefaultAppBar.kt
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/appbar/UlbanDefaultAppBar.kt
@@ -1,14 +1,18 @@
package com.sixkids.designsystem.component.appbar
import androidx.annotation.DrawableRes
+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.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
import com.sixkids.designsystem.theme.AppBarTypography
+import com.sixkids.designsystem.theme.UlbanTypography
@Composable
fun UlbanDefaultAppBar(
@@ -16,9 +20,10 @@ fun UlbanDefaultAppBar(
@DrawableRes leftIcon: Int,
title: String,
content: String,
+ body: String = "",
color: Color,
expanded: Boolean = true,
- onclick: () -> Unit,
+ onclick: () -> Unit = {},
) {
BasicAppBar(
modifier = modifier,
@@ -27,9 +32,18 @@ fun UlbanDefaultAppBar(
content = {
Row(modifier = Modifier.fillMaxWidth()) {
Spacer(modifier = Modifier.weight(1f))
- Text(
- text = content, style = AppBarTypography.titleLarge,
- )
+ Column {
+ Text(
+ text = content, style = UlbanTypography.titleLarge,
+ )
+ if (body.isNotEmpty()) {
+ Text(
+ text = body, style = UlbanTypography.bodyLarge,
+ modifier = Modifier.padding(top = 4.dp)
+ )
+ }
+ }
+
Spacer(modifier = Modifier.weight(3f))
}
},
diff --git a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/appbar/UlbanDetailAppBar.kt b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/appbar/UlbanDetailAppBar.kt
index 85e2aef4..9646a35f 100644
--- a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/appbar/UlbanDetailAppBar.kt
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/appbar/UlbanDetailAppBar.kt
@@ -22,6 +22,7 @@ import androidx.compose.ui.unit.dp
import com.sixkids.designsystem.theme.AppBarTypography
import com.sixkids.designsystem.theme.RedDark
import com.sixkids.designsystem.theme.UlbanTheme
+import com.sixkids.designsystem.theme.UlbanTypography
@Composable
fun UlbanDetailAppBar(
@@ -92,15 +93,16 @@ fun AppBarDetailInfo(
}
}
}
+ Spacer(modifier = modifier.height(4.dp))
Text(
text = title,
style = AppBarTypography.titleSmall,
modifier = Modifier.fillMaxWidth()
)
- Spacer(modifier = modifier.height(8.dp))
+ Spacer(modifier = modifier.height(12.dp))
Text(
text = bottomDescription,
- style = AppBarTypography.bodyMedium,
+ style = UlbanTypography.bodyMedium,
modifier = modifier.fillMaxWidth()
)
}
diff --git a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/appbar/UlbanDetailWithProgressAppBar.kt b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/appbar/UlbanDetailWithProgressAppBar.kt
index e97c14d9..8990f21e 100644
--- a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/appbar/UlbanDetailWithProgressAppBar.kt
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/appbar/UlbanDetailWithProgressAppBar.kt
@@ -9,6 +9,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
+import com.sixkids.designsystem.theme.RedDark
import com.sixkids.designsystem.theme.component.progressbar.StudentProgressBar
@Composable
@@ -23,6 +24,7 @@ fun UlbanDetailWithProgressAppBar(
totalCnt: Int,
successCnt: Int,
color: Color,
+ progressBarColor: Color = RedDark,
expanded: Boolean = true,
onclick: () -> Unit,
) {
@@ -44,7 +46,8 @@ fun UlbanDetailWithProgressAppBar(
StudentProgressBar(
modifier = modifier.padding(top = 8.dp, end = 16.dp),
totalStudentCount = totalCnt,
- successStudentCount = successCnt
+ successStudentCount = successCnt,
+ progressBarColor = progressBarColor
)
}
},
diff --git a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/button/EditFAB.kt b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/button/EditFAB.kt
new file mode 100644
index 00000000..bc048d4f
--- /dev/null
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/button/EditFAB.kt
@@ -0,0 +1,39 @@
+package com.sixkids.designsystem.component.button
+
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.Icon
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.tooling.preview.Preview
+import com.sixkids.designsystem.R
+import com.sixkids.designsystem.theme.Cream
+import com.sixkids.designsystem.theme.Gray
+
+@Composable
+fun EditFAB(
+ modifier: Modifier = Modifier,
+ buttonColor: Color = Cream,
+ iconColor: Color = Gray,
+ onClick: () -> Unit = {}
+) {
+ FloatingActionButton(
+ modifier = modifier,
+ containerColor = buttonColor,
+ onClick = onClick,
+ ) {
+ Icon(
+ imageVector = ImageVector.vectorResource(id = R.drawable.ic_edit),
+ contentDescription = null,
+ tint = iconColor
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun EditFABPreview() {
+ EditFAB()
+}
\ No newline at end of file
diff --git a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/card/ContentCard.kt b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/card/ContentCard.kt
index df8d8bf3..a2480f5a 100644
--- a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/card/ContentCard.kt
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/card/ContentCard.kt
@@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -21,18 +22,19 @@ 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.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.sixkids.designsystem.R
import com.sixkids.designsystem.theme.Cream
import com.sixkids.designsystem.theme.Red
import com.sixkids.designsystem.theme.UlbanTheme
+import com.sixkids.designsystem.theme.UlbanTypography
import com.sixkids.designsystem.theme.Yellow
import com.sixkids.designsystem.theme.component.progressbar.StudentProgressBar
import com.sixkids.designsystem.theme.npsFont
@@ -97,7 +99,13 @@ fun ContentCardViewPreview() {
@Composable
fun RankCardViewPreview() {
UlbanTheme {
- RankCard()
+ ContentVerticalCard(
+ cardModifier = Modifier
+ .padding(20.dp)
+ .aspectRatio(1f),
+ imageDrawable = R.drawable.rank,
+ text = "랭킹"
+ )
}
}
@@ -114,12 +122,14 @@ fun ContentCard(
contentAligment: ContentAligment,
cardColor: Color,
contentName: String,
+ textColor: Color = Color.Black,
@DrawableRes contentImageId: Int,
runningState: RunningState? = null,
- onclick: () -> Unit = {}
+ onclick: () -> Unit = {},
+ cardHeight: Dp = 160.dp
) {
Card(
- modifier = modifier.height(160.dp),
+ modifier = modifier.height(cardHeight),
colors = CardDefaults.cardColors(
containerColor = cardColor
),
@@ -127,6 +137,7 @@ fun ContentCard(
defaultElevation = 4.dp,
pressedElevation = 8.dp
),
+ shape = RoundedCornerShape(24.dp),
onClick = onclick
) {
Column {
@@ -149,12 +160,13 @@ fun ContentCard(
Text(
text = contentName,
modifier = Modifier
- .padding(20.dp)
+ .weight(1f)
.wrapContentHeight(),
- textAlign = TextAlign.Start,
- fontSize = 34.sp,
- fontWeight = FontWeight.Medium,
- fontFamily = npsFont
+ textAlign = TextAlign.Center,
+ style = UlbanTypography.titleLarge.copy(
+ fontSize = 26.sp,
+ color = textColor
+ )
)
} else {
RunningText(
@@ -168,12 +180,14 @@ fun ContentCard(
Text(
text = contentName,
modifier = Modifier
- .padding(20.dp)
+// .padding(20.dp)
+ .weight(1f)
.wrapContentHeight(),
- textAlign = TextAlign.Start,
- fontSize = 34.sp,
- fontWeight = FontWeight.Medium,
- fontFamily = npsFont
+ textAlign = TextAlign.Center,
+ style = UlbanTypography.titleLarge.copy(
+ fontSize = 26.sp,
+ color = textColor
+ )
)
} else {
RunningText(
@@ -244,16 +258,18 @@ fun RunningText(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun RankCard(
- modifier: Modifier = Modifier,
+fun ContentVerticalCard(
+ cardModifier: Modifier = Modifier,
+ cardColor: Color = Yellow,
+ textColor: Color = Color.Black,
+ imageDrawable: Int = R.drawable.rank,
+ text: String = "랭킹",
onClick: () -> Unit = {}
) {
Card(
- modifier = modifier
- .height(160.dp)
- .aspectRatio(1f),
+ modifier = cardModifier,
colors = CardDefaults.cardColors(
- containerColor = Yellow
+ containerColor = cardColor
),
elevation = CardDefaults.cardElevation(
defaultElevation = 4.dp,
@@ -269,14 +285,16 @@ fun RankCard(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
- painter = painterResource(id = R.drawable.rank),
+ painter = painterResource(id = imageDrawable),
contentDescription = null,
)
+
+ Spacer(modifier = Modifier.height(4.dp))
Text(
- text = "랭킹",
- fontSize = 30.sp,
- fontWeight = FontWeight.Medium,
- fontFamily = npsFont
+ text = text,
+ style = UlbanTypography.titleLarge.copy(
+ color = textColor
+ )
)
}
}
diff --git a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/card/UlbanMissionCard.kt b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/card/UlbanMissionCard.kt
index 912ed64e..1b1fc4e0 100644
--- a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/card/UlbanMissionCard.kt
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/card/UlbanMissionCard.kt
@@ -9,7 +9,9 @@ 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.offset
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.GenericShape
import androidx.compose.material3.Card
@@ -26,6 +28,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.sixkids.designsystem.R
+import com.sixkids.designsystem.theme.Orange
import com.sixkids.designsystem.theme.Red
import com.sixkids.designsystem.theme.UlbanTheme
import com.sixkids.designsystem.theme.UlbanTypography
@@ -81,7 +84,7 @@ fun UlbanMissionCard(
Box(
modifier = modifier.clickable {
onClick()
- },
+ }
) {
Card(
shape = customShape,
@@ -89,13 +92,12 @@ fun UlbanMissionCard(
containerColor = backGroundColor,
),
modifier = Modifier
+ .padding(40.dp)
.width(200.dp)
.height(300.dp)
- .padding(16.dp)
- .align(Alignment.BottomCenter)
) {
Column {
- Spacer(modifier = Modifier.height(170.dp))
+ Spacer(modifier = modifier.height(170.dp))
Text(
modifier = Modifier.fillMaxWidth(),
text = title,
@@ -118,19 +120,18 @@ fun UlbanMissionCard(
}
}
}
- Image(
+ Box(
modifier = Modifier
- .padding(bottom = 32.dp, end = 32.dp)
- .width(250.dp)
- .height(400.dp)
.align(Alignment.TopCenter),
- painter = painterResource(id = imgRes),
- contentDescription = null
- )
-
+ contentAlignment = Alignment.Center
+ ) {
+ Image(
+ modifier = Modifier.run { size(230.dp).offset(x = -(20.dp), y = -(20.dp)) },
+ painter = painterResource(id = imgRes),
+ contentDescription = null
+ )
+ }
}
-
-
}
@Preview(showBackground = true)
@@ -145,3 +146,13 @@ fun UlbanMissionCardPreview() {
)
}
}
+
+@Preview(showBackground = true)
+@Composable
+fun UlbanPreview2(){
+ UlbanMissionCard(
+ imgRes = R.drawable.relay,
+ title = "친구에게 전달해 봐요!",
+ backGroundColor = Orange
+ )
+}
\ No newline at end of file
diff --git a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/checkbox/TextRadioButton.kt b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/checkbox/TextRadioButton.kt
new file mode 100644
index 00000000..b3e88c02
--- /dev/null
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/checkbox/TextRadioButton.kt
@@ -0,0 +1,57 @@
+package com.sixkids.designsystem.component.checkbox
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.selection.selectable
+import androidx.compose.material3.RadioButton
+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.scale
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.sixkids.designsystem.theme.UlbanTypography
+
+@Composable
+fun TextRadioButton(
+ selected: Boolean = false,
+ onClick: () -> Unit = { },
+ text: String = ""
+) {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .selectable(
+ selected = selected,
+ onClick = onClick,
+ role = Role.RadioButton,
+ ),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ // 익명 체크박스
+ RadioButton(
+ modifier = Modifier
+ .scale(1.2f)
+ .requiredSize(30.dp),
+ selected = selected,
+ onClick = null,
+ )
+ if(text.isNotEmpty()) {
+ Text(
+ text = text,
+ style = UlbanTypography.bodyLarge
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun TextCheckBoxPreview() {
+ TextRadioButton(
+ text = "라디오버튼"
+ )
+}
diff --git a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/dialog/UlbanBisicDialog.kt b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/dialog/UlbanBisicDialog.kt
index 7923ebed..768c9984 100644
--- a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/dialog/UlbanBisicDialog.kt
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/dialog/UlbanBisicDialog.kt
@@ -27,8 +27,7 @@ fun UlbanBasicDialog(
) {
Dialog(onDismissRequest = onDismiss) {
Card(
- modifier = modifier
- .fillMaxWidth(),
+ modifier = modifier,
colors = CardDefaults.cardColors(containerColor = backGroundColor),
shape = RoundedCornerShape(16.dp)
) {
diff --git a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/dialog/UlbanDatePickerDialog.kt b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/dialog/UlbanDatePickerDialog.kt
index f22b6503..f8bc2300 100644
--- a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/dialog/UlbanDatePickerDialog.kt
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/dialog/UlbanDatePickerDialog.kt
@@ -35,8 +35,9 @@ fun UlbanDatePickerDialog(
val datePickerState = rememberDatePickerState(
initialDisplayMode = DisplayMode.Picker,
- initialSelectedDateMillis = selectedDate.atStartOfDay().atZone(ZoneId.systemDefault())
- .toInstant().toEpochMilli(),
+ initialSelectedDateMillis = selectedDate.plusDays(1)
+ .atStartOfDay(ZoneId.systemDefault()).toInstant()
+ .toEpochMilli()
)
diff --git a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/item/MemberSimpleItem.kt b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/item/MemberSimpleItem.kt
index 2fe26e0e..c86dfe98 100644
--- a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/item/MemberSimpleItem.kt
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/item/MemberSimpleItem.kt
@@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.sixkids.designsystem.R
@@ -55,3 +56,16 @@ interface DisplayableMember {
val photo: String
val isLeader: Boolean
}
+
+
+@Preview(showBackground = true)
+@Composable
+fun MemberSimpleItemPreview() {
+ MemberSimpleItem(
+ member = object : DisplayableMember {
+ override val name: String = "김철수"
+ override val photo: String = ""
+ override val isLeader: Boolean = true
+ }
+ )
+}
diff --git a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/item/StudentSimpleCardItem.kt b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/item/StudentSimpleCardItem.kt
index 9b465e46..5cee18b7 100644
--- a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/item/StudentSimpleCardItem.kt
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/item/StudentSimpleCardItem.kt
@@ -1,16 +1,13 @@
package com.sixkids.designsystem.component.item
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
-import androidx.compose.foundation.lazy.grid.items
-import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -20,6 +17,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
@@ -30,10 +28,12 @@ import com.sixkids.designsystem.theme.UlbanTypography
@Composable
fun StudentSimpleCardItem(
modifier: Modifier = Modifier,
+ id: Long = 0,
name: String = "",
photo: String = "",
score: Int? = null,
- onClick: () -> Unit = {}
+ onClick: (Long) -> Unit = {},
+ isCount: Boolean = false
) {
Card(
modifier = modifier,
@@ -44,31 +44,36 @@ fun StudentSimpleCardItem(
defaultElevation = 4.dp,
pressedElevation = 8.dp
),
- onClick = onClick
+ onClick = {onClick(id)}
) {
Column(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
- .padding(4.dp),
+ .padding(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
AsyncImage(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
- .padding(8.dp),
+ .padding(8.dp)
+ .clip(RoundedCornerShape(16.dp)),
model = photo,
+
contentScale = ContentScale.Crop,
contentDescription = "프로필 사진"
)
Text(
text = name,
- style = UlbanTypography.bodyMedium
+ style = UlbanTypography.bodyMedium,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ modifier = Modifier.padding(bottom = 4.dp)
)
if (score != null) {
Text(
- text = "${score}점",
+ text = if (isCount)"${score}회" else "${score}점",
style = UlbanTypography.bodySmall
)
}
@@ -81,15 +86,14 @@ fun StudentSimpleCardItem(
fun StudentSimpleCardItemPreview() {
LazyVerticalGrid(
columns = GridCells.Fixed(3),
-
) {
items(25){
StudentSimpleCardItem(
modifier = Modifier.padding(4.dp),
name = "김철수",
- photo = "https://i.pinimg.com/564x/f9/e5/c1/f9e5c19d2a51bda108e5ea536d7745c1.jpg",
+// photo = "https://i.pinimg.com/564x/f9/e5/c1/f9e5c19d2a51bda108e5ea536d7745c1.jpg",
score = 97
)
}
}
-}
\ No newline at end of file
+}
diff --git a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/item/UlbanRelayItem.kt b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/item/UlbanRelayItem.kt
new file mode 100644
index 00000000..eacd5b89
--- /dev/null
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/item/UlbanRelayItem.kt
@@ -0,0 +1,110 @@
+package com.sixkids.designsystem.component.item
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+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.material3.Card
+import androidx.compose.material3.CardDefaults
+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.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import com.sixkids.designsystem.R
+import com.sixkids.designsystem.theme.Cream
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.ui.util.formatToMonthDayTime
+import java.time.LocalDateTime
+
+@Composable
+fun UlbanRelayItem(
+ modifier: Modifier = Modifier,
+ padding: PaddingValues = PaddingValues(
+ horizontal = 16.dp,
+ vertical = 16.dp
+ ),
+ startDate: LocalDateTime,
+ endDate: LocalDateTime,
+ userCount: Int,
+ color: Color = Cream,
+ lastMemberName: String,
+ onClick: () -> Unit = {}
+) {
+ Card(
+ modifier = modifier
+ .fillMaxWidth()
+ .clickable {
+ onClick()
+ },
+ colors = CardDefaults.cardColors(
+ containerColor = color
+ ),
+ elevation = CardDefaults.cardElevation(
+ defaultElevation = 8.dp
+ ),
+ shape = CardDefaults.outlinedShape,
+ ) {
+ Column(
+ modifier = modifier.padding(
+ padding
+ )
+ ) {
+ Text(
+ text = stringResource(R.string.relay),
+ style = UlbanTypography.titleSmall
+ )
+ Spacer(modifier = modifier.padding(8.dp))
+
+ Text(
+ text = "${startDate.formatToMonthDayTime()} ~ ${endDate.formatToMonthDayTime()}",
+ style = UlbanTypography.bodySmall
+ )
+ Spacer(modifier = modifier.padding(8.dp))
+
+ Row (modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically){
+ AsyncImage(
+ model = R.drawable.bomb,
+ placeholder = painterResource(id =R.drawable.bomb),
+ modifier = Modifier.size(42.dp),
+ contentDescription = "bomb")
+
+ Spacer(modifier = modifier.padding(4.dp))
+
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = stringResource(R.string.relay_item_count, userCount),
+ style = UlbanTypography.bodySmall
+ )
+ Spacer(modifier = modifier.padding(2.dp))
+ Text(
+ text = stringResource(R.string.relay_item_last_member, lastMemberName),
+ style = UlbanTypography.bodyMedium
+ )
+ }
+ }
+
+ }
+ }
+
+}
+
+@Composable
+@Preview(showBackground = true)
+fun UlbanChallengeItemPreview() {
+ UlbanRelayItem(
+ startDate = LocalDateTime.now(),
+ endDate = LocalDateTime.now(),
+ userCount = 0,
+ lastMemberName = "홍유준"
+ )
+}
diff --git a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/item/UlbanReportItem.kt b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/item/UlbanReportItem.kt
index fd251277..0db54da4 100644
--- a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/item/UlbanReportItem.kt
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/item/UlbanReportItem.kt
@@ -56,9 +56,8 @@ fun UlbanReportItem(
shape = CardDefaults.outlinedShape
) {
Column(
- modifier = modifier.padding(
- padding
- )
+ modifier = modifier
+ .padding(padding)
) {
Spacer(modifier = modifier.padding(4.dp))
Row {
@@ -79,8 +78,9 @@ fun UlbanReportItem(
model = file,
contentDescription = "과제 사진",
modifier = modifier
+ .fillMaxWidth()
.height(240.dp)
- .padding(vertical = 8.dp)
+ .padding(vertical = 8.dp)
)
MemberSimpleList(
modifier = modifier.padding(vertical = 8.dp),
diff --git a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/item/UlbanRunnerItem.kt b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/item/UlbanRunnerItem.kt
new file mode 100644
index 00000000..480f012b
--- /dev/null
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/item/UlbanRunnerItem.kt
@@ -0,0 +1,131 @@
+package com.sixkids.designsystem.component.item
+
+import androidx.compose.foundation.Image
+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.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+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.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import com.sixkids.designsystem.R
+import com.sixkids.designsystem.theme.Cream
+import com.sixkids.designsystem.theme.Gray
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.ui.util.formatToMonthDayTime
+import java.time.LocalDateTime
+
+@Composable
+fun UlbanRunnerItem(
+ modifier: Modifier = Modifier,
+ padding: PaddingValues = PaddingValues(
+ horizontal = 16.dp,
+ vertical = 20.dp
+ ),
+ time: LocalDateTime,
+ memberName: String,
+ memberPhoto: String,
+ question: String,
+ isLastTurn: Boolean = false,
+ color: Color = Cream
+) {
+ Column(modifier = modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
+ Box(modifier = Modifier.wrapContentSize().padding(bottom = 10.dp)) {
+ Card(
+ colors = CardDefaults.cardColors(
+ containerColor = color
+ ),
+ elevation = CardDefaults.cardElevation(
+ defaultElevation = 8.dp
+ ),
+ shape = CardDefaults.outlinedShape,
+ ) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(padding),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ AsyncImage(
+ model = memberPhoto,
+ placeholder = painterResource(id = R.drawable.student_boy),
+ contentDescription = null,
+ modifier = Modifier.size(80.dp),
+ contentScale = ContentScale.Crop
+ )
+ Column(
+ modifier = Modifier.padding(start = 16.dp),
+ verticalArrangement = Arrangement.spacedBy(6.dp)
+ ) {
+ Text(
+ text = memberName,
+ style = UlbanTypography.titleSmall
+ )
+ Text(
+ text = time.formatToMonthDayTime(),
+ style = UlbanTypography.bodySmall
+ )
+ Text(
+// text = stringResource(R.string.runner_question),
+ text = "받은 질문",
+ style = UlbanTypography.bodySmall.copy(color = Gray)
+ )
+ Text(
+ text = question,
+ style = UlbanTypography.bodyMedium,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ }
+ }
+ if (isLastTurn) {
+ AsyncImage(
+ model = R.drawable.bomb,
+ placeholder = painterResource(id = R.drawable.bomb),
+ contentDescription = "bomb",
+ modifier = Modifier
+ .size(72.dp)
+ .align(Alignment.TopEnd)
+ .padding(12.dp)
+ )
+ }
+ }
+ if (!isLastTurn) {
+ Image(
+ painter = painterResource(id = R.drawable.ic_down_arrow),
+ contentDescription = "next",
+ modifier = Modifier
+ .padding(10.dp)
+ .size(32.dp)
+ )
+ }
+ }
+
+}
+
+@Composable
+@Preview(showBackground = true)
+fun UlbanRunnerItemPreview() {
+ UlbanRunnerItem(
+ time = LocalDateTime.now(),
+ memberName = "홍유준",
+ memberPhoto = "https://ulvanbucket.s3.ap-northeast-2.amazonaws.com/c5cef8d2-f085-45ca-b359-f76e39f2bca3_profile.jpg",
+ question = "이번 턴은 어떤"
+ )
+}
\ No newline at end of file
diff --git a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/progressbar/StudentProgressBar.kt b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/progressbar/StudentProgressBar.kt
index 3dc02f4b..e0d282ef 100644
--- a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/progressbar/StudentProgressBar.kt
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/progressbar/StudentProgressBar.kt
@@ -14,6 +14,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.sixkids.designsystem.theme.RedDark
+import com.sixkids.designsystem.theme.UlbanTypography
@Preview(showBackground = true, backgroundColor = 0xFFFFF6F0)
@Composable
@@ -29,7 +30,8 @@ fun StudentProgressBarPreview() {
fun StudentProgressBar(
modifier: Modifier = Modifier,
totalStudentCount: Int,
- successStudentCount: Int
+ successStudentCount: Int,
+ progressBarColor: Color = RedDark
){
Column(
modifier = modifier,
@@ -37,10 +39,11 @@ fun StudentProgressBar(
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
+ .padding(bottom = 4.dp)
.height(4.dp),
progress = (successStudentCount.toFloat()/totalStudentCount.toFloat()),
trackColor = Color.White,
- color = RedDark
+ color = progressBarColor
)
Text(
modifier = Modifier
@@ -48,6 +51,7 @@ fun StudentProgressBar(
text = "${totalStudentCount}명 중 ${successStudentCount}명이 진행했어요",
textAlign = TextAlign.End,
fontSize = 14.sp,
+ style = UlbanTypography.bodySmall
)
}
}
\ No newline at end of file
diff --git a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/screen/GreetingScreen.kt b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/screen/GreetingScreen.kt
new file mode 100644
index 00000000..adf59e6e
--- /dev/null
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/screen/GreetingScreen.kt
@@ -0,0 +1,71 @@
+package com.sixkids.designsystem.component.screen
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+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.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
+import androidx.compose.ui.unit.dp
+import com.sixkids.designsystem.R
+import com.sixkids.designsystem.component.button.UlbanFilledButton
+import com.sixkids.designsystem.theme.UlbanTypography
+
+@Composable
+fun GreetingScreen(
+ isSender: Boolean = true,
+ onClick: () -> Unit = {}
+) {
+ val title =
+ if (isSender) "친구에게 인사 보내기"
+ else "친구의 인사 받기"
+
+
+ val body =
+ if (isSender) "친구와 핸드폰 뒷면을 맞대서 인사를 건네요"
+ else "친구와 핸드폰 뒷면을 맞대서 인사를 받아요"
+
+ Column(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ Text(
+ text = title,
+ style = UlbanTypography.titleMedium,
+ modifier = Modifier.padding(30.dp, 40.dp, 30.dp, 15.dp)
+ )
+
+
+ Spacer(modifier = Modifier.height(100.dp))
+
+ Image(painter = painterResource(id = R.drawable.relay_tag), contentDescription = "tagging",
+ modifier = Modifier
+ .fillMaxWidth()
+ .aspectRatio(1f)
+ )
+
+
+ Text(
+ text = body, style = UlbanTypography.bodyMedium,
+ modifier = Modifier.align(Alignment.CenterHorizontally)
+ )
+
+ Spacer(modifier = Modifier.weight(1f))
+ if (isSender) {
+ UlbanFilledButton(
+ text = "완료",
+ onClick = { onClick() },
+ modifier = Modifier
+ .padding(horizontal = 30.dp, vertical = 40.dp)
+ .fillMaxWidth()
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/screen/RelayPassResultScreen.kt b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/screen/RelayPassResultScreen.kt
new file mode 100644
index 00000000..7d1ddff8
--- /dev/null
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/screen/RelayPassResultScreen.kt
@@ -0,0 +1,153 @@
+package com.sixkids.designsystem.component.screen
+
+import androidx.annotation.DrawableRes
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+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.offset
+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
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.sixkids.designsystem.R
+import com.sixkids.designsystem.component.button.UlbanFilledButton
+import com.sixkids.designsystem.theme.Orange
+import com.sixkids.designsystem.theme.Purple
+import com.sixkids.designsystem.theme.UlbanTheme
+import com.sixkids.designsystem.theme.UlbanTypography
+
+@Composable
+fun RelayPassResultScreen(
+ paddingValues: PaddingValues = PaddingValues(horizontal = 30.dp, vertical = 40.dp),
+ title: String = stringResource(R.string.relay_pass_result_title),
+ subTitle: String = stringResource(R.string.relay_pass_result_subtitle_sender),
+ bodyTop: String = stringResource(R.string.relay_pass_result_body_top_receiver),
+ bodyMiddle: String,
+ bodyBottom: String = stringResource(R.string.relay_pass_result_body_bottom_sender),
+ @DrawableRes imgRes: Int = R.drawable.relay_success,
+ backgroundColor: Color = Orange,
+ onClick: () -> Unit = {}
+) {
+ val screenWidthDp = with(LocalDensity.current) {
+ LocalContext.current.resources.displayMetrics.widthPixels.toDp()
+ }
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues),
+ ) {
+ Text(
+ text = title,
+ style = UlbanTypography.titleMedium,
+ modifier = Modifier.padding(bottom = 15.dp)
+ )
+
+ Text(text = subTitle, style = UlbanTypography.bodyMedium)
+
+ Spacer(modifier = Modifier.height(100.dp))
+
+ BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
+ Box(modifier = Modifier.align(Alignment.Center)) {
+ Box(
+ modifier = Modifier
+ .size(screenWidthDp / 2)
+ .clip(RoundedCornerShape(28.dp))
+ .background(backgroundColor)
+ .align(Alignment.Center)
+ )
+
+ Image(
+ painter = painterResource(id = imgRes),
+ contentDescription = "success",
+ modifier = Modifier
+ .size(screenWidthDp / 2 + screenWidthDp / 7)
+ .offset(y = -screenWidthDp / 10)
+ .align(Alignment.Center)
+ )
+ }
+ }
+
+ Column(
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = bodyTop,
+ style = UlbanTypography.bodyLarge,
+ modifier = Modifier.padding(top = 30.dp),
+ textAlign = TextAlign.Center
+ )
+
+ Text(
+ text = bodyMiddle,
+ style = UlbanTypography.titleMedium.copy(lineHeight = 30.sp),
+ modifier = Modifier.padding(top = 30.dp, start = 20.dp, end = 20.dp),
+ textAlign = TextAlign.Center
+ )
+
+ Text(
+ text = bodyBottom,
+ style = UlbanTypography.bodyLarge,
+ modifier = Modifier.padding(top = 30.dp),
+ textAlign = TextAlign.Center
+ )
+
+ }
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ UlbanFilledButton(
+ modifier = Modifier.fillMaxWidth(),
+ text = stringResource(R.string.relay_pass_result_button_done),
+ onClick = { onClick() }
+ )
+
+ }
+}
+
+@Composable
+@Preview(showBackground = true)
+fun RelayPassResultScreenPreview() {
+ UlbanTheme {
+ RelayPassResultScreen(
+ bodyTop = "오하빈 학생이",
+ bodyMiddle = "\"우리반에서 가장 공부를 잘 하는 친구는 누구야?\"",
+ )
+ }
+}
+
+@Composable
+@Preview(showBackground = true)
+fun RelayPassResultBombScreenPreview() {
+ UlbanTheme {
+ RelayPassResultScreen(
+ title = "이런! 폭탄이 터졌어요",
+ subTitle = "-100 exp",
+ bodyTop = "오하빈 학생이",
+ bodyMiddle = "\"우리반에서 가장 공부를 잘 하는 친구는 누구야?\"",
+ imgRes = R.drawable.bomb,
+ backgroundColor = Purple
+ )
+ }
+}
\ No newline at end of file
diff --git a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/screen/RelayTaggingScreen.kt b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/screen/RelayTaggingScreen.kt
new file mode 100644
index 00000000..89649fbd
--- /dev/null
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/screen/RelayTaggingScreen.kt
@@ -0,0 +1,86 @@
+package com.sixkids.designsystem.component.screen
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+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.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.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.sixkids.designsystem.R
+import com.sixkids.designsystem.component.button.UlbanFilledButton
+import com.sixkids.designsystem.theme.UlbanTypography
+
+@Composable
+fun RelayTaggingScreen(
+ isSender: Boolean = true,
+ onClick: () -> Unit = {}
+) {
+ val title =
+ if (isSender) stringResource(R.string.relay_tagging_title_sender)
+ else stringResource(R.string.relay_tagging_title_receiver)
+
+ val subTitle =
+ if (isSender) stringResource(R.string.relay_tagging_sub_title_sender)
+ else stringResource(R.string.relay_tagging_sub_title_receiver)
+
+ val body =
+ if (isSender) stringResource(R.string.relay_tagging_body_sender)
+ else stringResource(R.string.relay_tagging_body_receiver)
+
+ Column(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ Text(
+ text = title,
+ style = UlbanTypography.titleMedium,
+ modifier = Modifier.padding(30.dp, 40.dp, 30.dp, 15.dp)
+ )
+
+ Text(
+ text = subTitle, style = UlbanTypography.bodyMedium,
+ modifier = Modifier.padding(start = 30.dp)
+ )
+
+ Spacer(modifier = Modifier.height(100.dp))
+
+ Image(painter = painterResource(id = R.drawable.relay_tag), contentDescription = "tagging",
+ modifier = Modifier
+ .fillMaxWidth()
+ .aspectRatio(1f)
+ )
+
+
+ Text(
+ text = body, style = UlbanTypography.bodyMedium,
+ modifier = Modifier.align(Alignment.CenterHorizontally)
+ )
+
+ Spacer(modifier = Modifier.weight(1f))
+ if (isSender) {
+ UlbanFilledButton(
+ text = "다음",
+ onClick = { onClick() },
+ modifier = Modifier
+ .padding(horizontal = 30.dp, vertical = 40.dp)
+ .fillMaxWidth()
+ )
+ }
+ }
+}
+
+@Composable
+@Preview(showBackground = true)
+fun RelayTaggingScreenPreview() {
+ RelayTaggingScreen(false)
+}
\ No newline at end of file
diff --git a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/textfield/UlbanUnderLineTextField.kt b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/textfield/UlbanUnderLineTextField.kt
index 66c3c482..5acd589c 100644
--- a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/textfield/UlbanUnderLineTextField.kt
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/textfield/UlbanUnderLineTextField.kt
@@ -164,7 +164,7 @@ fun UlbanUnderLineTextFieldPreview() {
inputTextType = InputTextType.TEXT
)
- Text(text = "포인트를 입력해 주세요", style = UlbanTypography.titleSmall)
+ Text(text = "점수를 입력해 주세요", style = UlbanTypography.titleSmall)
UlbanUnderLineTextField(
text = point,
onTextChange = { point = it },
diff --git a/android/core/designsystem/src/main/java/com/sixkids/designsystem/theme/Color.kt b/android/core/designsystem/src/main/java/com/sixkids/designsystem/theme/Color.kt
index 6e295aa8..ed547889 100644
--- a/android/core/designsystem/src/main/java/com/sixkids/designsystem/theme/Color.kt
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/theme/Color.kt
@@ -26,6 +26,13 @@ val BlueDark = Color(0XFF152B65)
val GreenDark = Color(0XFF0B4C3D)
val PurpleDark = Color(0XFF52148E)
+val RedText = Color(0xFF52000D)
+val OrangeText = Color(0xFF492800)
+val YellowText = Color(0xFF4E3401)
+val PurpleText = Color(0xFF300153)
+val GrayLight = Color(0XFFF1F1F1)
+val BlueText = Color(0xFF010F3D)
+
val LoadingBackground = Color(0xA6000000)
diff --git a/android/core/designsystem/src/main/java/com/sixkids/designsystem/theme/Type.kt b/android/core/designsystem/src/main/java/com/sixkids/designsystem/theme/Type.kt
index 042b3f35..60c0f3df 100644
--- a/android/core/designsystem/src/main/java/com/sixkids/designsystem/theme/Type.kt
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/theme/Type.kt
@@ -77,7 +77,7 @@ val UlbanTypography = Typography(
fontFamily = npsFont,
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
- lineHeight = 20.sp,
+ lineHeight = 26.sp,
letterSpacing = 0.8.sp
),
diff --git a/android/core/designsystem/src/main/res/drawable/board.png b/android/core/designsystem/src/main/res/drawable/board.png
index 763ec4cf..d6a0bf9f 100644
Binary files a/android/core/designsystem/src/main/res/drawable/board.png and b/android/core/designsystem/src/main/res/drawable/board.png differ
diff --git a/android/core/designsystem/src/main/res/drawable/ic_cancel_post.xml b/android/core/designsystem/src/main/res/drawable/ic_cancel_post.xml
new file mode 100644
index 00000000..a96683a4
--- /dev/null
+++ b/android/core/designsystem/src/main/res/drawable/ic_cancel_post.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/android/core/designsystem/src/main/res/drawable/ic_chat_bubble.xml b/android/core/designsystem/src/main/res/drawable/ic_chat_bubble.xml
new file mode 100644
index 00000000..3699a89c
--- /dev/null
+++ b/android/core/designsystem/src/main/res/drawable/ic_chat_bubble.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/android/core/designsystem/src/main/res/drawable/ic_chat_bubble_outline.xml b/android/core/designsystem/src/main/res/drawable/ic_chat_bubble_outline.xml
new file mode 100644
index 00000000..264adf10
--- /dev/null
+++ b/android/core/designsystem/src/main/res/drawable/ic_chat_bubble_outline.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/android/core/designsystem/src/main/res/drawable/ic_close_filled.xml b/android/core/designsystem/src/main/res/drawable/ic_close_filled.xml
new file mode 100644
index 00000000..e1a18e26
--- /dev/null
+++ b/android/core/designsystem/src/main/res/drawable/ic_close_filled.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/android/core/designsystem/src/main/res/drawable/ic_copy.xml b/android/core/designsystem/src/main/res/drawable/ic_copy.xml
new file mode 100644
index 00000000..942aeb96
--- /dev/null
+++ b/android/core/designsystem/src/main/res/drawable/ic_copy.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/android/core/designsystem/src/main/res/drawable/ic_delete.xml b/android/core/designsystem/src/main/res/drawable/ic_delete.xml
new file mode 100644
index 00000000..7d8a4e7f
--- /dev/null
+++ b/android/core/designsystem/src/main/res/drawable/ic_delete.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/android/core/designsystem/src/main/res/drawable/ic_down_arrow.xml b/android/core/designsystem/src/main/res/drawable/ic_down_arrow.xml
new file mode 100644
index 00000000..c72e6941
--- /dev/null
+++ b/android/core/designsystem/src/main/res/drawable/ic_down_arrow.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/android/core/designsystem/src/main/res/drawable/ic_edit.xml b/android/core/designsystem/src/main/res/drawable/ic_edit.xml
new file mode 100644
index 00000000..e59da99e
--- /dev/null
+++ b/android/core/designsystem/src/main/res/drawable/ic_edit.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/android/core/designsystem/src/main/res/drawable/ic_photo_camera.xml b/android/core/designsystem/src/main/res/drawable/ic_photo_camera.xml
new file mode 100644
index 00000000..7283b1a9
--- /dev/null
+++ b/android/core/designsystem/src/main/res/drawable/ic_photo_camera.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/android/core/designsystem/src/main/res/drawable/ic_recomment.xml b/android/core/designsystem/src/main/res/drawable/ic_recomment.xml
new file mode 100644
index 00000000..c5fc3e63
--- /dev/null
+++ b/android/core/designsystem/src/main/res/drawable/ic_recomment.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/android/core/designsystem/src/main/res/drawable/quiz.png b/android/core/designsystem/src/main/res/drawable/quiz.png
new file mode 100644
index 00000000..52dfb441
Binary files /dev/null and b/android/core/designsystem/src/main/res/drawable/quiz.png differ
diff --git a/android/core/designsystem/src/main/res/drawable/relay_tag.png b/android/core/designsystem/src/main/res/drawable/relay_tag.png
index 02485521..fa5e3b77 100644
Binary files a/android/core/designsystem/src/main/res/drawable/relay_tag.png and b/android/core/designsystem/src/main/res/drawable/relay_tag.png differ
diff --git a/android/core/designsystem/src/main/res/values/strings.xml b/android/core/designsystem/src/main/res/values/strings.xml
index 1b162741..3e68bcb5 100644
--- a/android/core/designsystem/src/main/res/values/strings.xml
+++ b/android/core/designsystem/src/main/res/values/strings.xml
@@ -1,10 +1,30 @@
%d명 참여
- " POINT"
+ " 점"
명
" 학년"
" 반"
확인
취소
+ 이어 달리기
+ %s 학생이 폭탄을 터트렸어요
+ %d번
+ 받은 질문
+ 이어 달리기 전달 성공
+ 이런 폭탄이 터졌어요!
+ 다음 친구에게 잘 전달 됐어요
+ 친구에게 잘 전달 받았어요
+ %d exp
+ 질문에 나를 지목했어요!
+ 어떤 질문에 나를 지목했는지\n확인할 수 있어요!
+ 다음 친구에게 전달하면
+ 완료
+ 이어 달리기 전달하기
+ 이어 달리기 전달 받기
+ 내가 받은 질문에 맞는 친구에게 전달 해봐요
+ 친구에게 이어 달리기를 전달 받아요
+ 친구와 핸드폰 뒷면을 맞대서 전달해요
+ 친구와 핸드폰 뒷면을 맞대서 전달 받아요
+
diff --git a/android/core/model/src/main/java/com/sixkids/model/AcceptStatus.kt b/android/core/model/src/main/java/com/sixkids/model/AcceptStatus.kt
new file mode 100644
index 00000000..43ef19ae
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/AcceptStatus.kt
@@ -0,0 +1,7 @@
+package com.sixkids.model
+
+enum class AcceptStatus {
+ BEFORE,
+ APPROVE,
+ REFUSE
+}
diff --git a/android/core/model/src/main/java/com/sixkids/model/Challenge.kt b/android/core/model/src/main/java/com/sixkids/model/Challenge.kt
index 9bc1cd15..23ff1ef0 100644
--- a/android/core/model/src/main/java/com/sixkids/model/Challenge.kt
+++ b/android/core/model/src/main/java/com/sixkids/model/Challenge.kt
@@ -3,10 +3,12 @@ package com.sixkids.model
import java.time.LocalDateTime
data class Challenge(
- val id: Int = 0,
+ val id: Long = 0,
val title: String = "",
val content: String = "",
val headCount: Int = 0,
+ val reward: Int = 0,
val startTime: LocalDateTime = LocalDateTime.now(),
val endTime: LocalDateTime = LocalDateTime.now(),
+ val totalCount: Int = 0
)
diff --git a/android/core/model/src/main/java/com/sixkids/model/ChallengeDetail.kt b/android/core/model/src/main/java/com/sixkids/model/ChallengeDetail.kt
index ce16a27c..0cc138aa 100644
--- a/android/core/model/src/main/java/com/sixkids/model/ChallengeDetail.kt
+++ b/android/core/model/src/main/java/com/sixkids/model/ChallengeDetail.kt
@@ -3,12 +3,12 @@ package com.sixkids.model
import java.time.LocalDateTime
data class ChallengeDetail(
- val id: Int = 0,
+ val id: Long = 0,
val title: String = "",
- val description: String = "",
- val startDate: LocalDateTime = LocalDateTime.now(),
- val endDate: LocalDateTime = LocalDateTime.now(),
- val userCount: Int = 0,
+ val content: String = "",
+ val startTime: LocalDateTime = LocalDateTime.now(),
+ val endTime: LocalDateTime = LocalDateTime.now(),
+ val headCount: Int = 0,
val teamCount: Int = 0,
val reportList: List = emptyList(),
)
diff --git a/android/core/model/src/main/java/com/sixkids/model/ChallengeGroup.kt b/android/core/model/src/main/java/com/sixkids/model/ChallengeGroup.kt
new file mode 100644
index 00000000..1f92b934
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/ChallengeGroup.kt
@@ -0,0 +1,6 @@
+package com.sixkids.model
+
+data class ChallengeGroup(
+ val headCount: Int,
+ val memberList: List,
+)
\ No newline at end of file
diff --git a/android/core/model/src/main/java/com/sixkids/model/Chat.kt b/android/core/model/src/main/java/com/sixkids/model/Chat.kt
new file mode 100644
index 00000000..d422d357
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/Chat.kt
@@ -0,0 +1,10 @@
+package com.sixkids.model
+
+data class Chat(
+ val memberId: Long,
+ val memberName: String,
+ val memberImageUrl: String,
+ val content: String,
+ val sendDateTime: Long,
+ val readCount: Int = 1
+)
diff --git a/android/core/model/src/main/java/com/sixkids/model/ChatFilterWord.kt b/android/core/model/src/main/java/com/sixkids/model/ChatFilterWord.kt
new file mode 100644
index 00000000..58a76a3c
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/ChatFilterWord.kt
@@ -0,0 +1,6 @@
+package com.sixkids.model
+
+data class ChatFilterWord(
+ val id: Long,
+ val badWord: String,
+)
\ No newline at end of file
diff --git a/android/core/model/src/main/java/com/sixkids/model/ChatMessage.kt b/android/core/model/src/main/java/com/sixkids/model/ChatMessage.kt
new file mode 100644
index 00000000..3bdef83c
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/ChatMessage.kt
@@ -0,0 +1,7 @@
+package com.sixkids.model
+
+data class ChatMessage(
+ val roomId: Long,
+ val memberImageUrl: String,
+ val content: String
+)
diff --git a/android/core/model/src/main/java/com/sixkids/model/ClassSummary.kt b/android/core/model/src/main/java/com/sixkids/model/ClassSummary.kt
new file mode 100644
index 00000000..94274619
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/ClassSummary.kt
@@ -0,0 +1,7 @@
+package com.sixkids.model
+
+data class ClassSummary(
+ val challengeCounts: List = emptyList(),
+ val relayCounts: List = emptyList(),
+ val postsCounts: List = emptyList(),
+)
\ No newline at end of file
diff --git a/android/core/model/src/main/java/com/sixkids/model/Comment.kt b/android/core/model/src/main/java/com/sixkids/model/Comment.kt
new file mode 100644
index 00000000..dc78c45e
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/Comment.kt
@@ -0,0 +1,21 @@
+package com.sixkids.model
+
+import java.time.LocalDateTime
+
+data class Comment(
+ val id: Long,
+ val member: MemberSimple,
+ val content: String,
+ val createTime: LocalDateTime,
+ val updateTime: LocalDateTime?,
+ val recomments: List,
+)
+
+data class Recomment(
+ val id: Long,
+ val member: MemberSimple,
+ val content: String,
+ val createTime: LocalDateTime,
+ val updateTime: LocalDateTime?,
+ val parentId: Long,
+)
diff --git a/android/core/model/src/main/java/com/sixkids/model/GreetingNFC.kt b/android/core/model/src/main/java/com/sixkids/model/GreetingNFC.kt
new file mode 100644
index 00000000..be1e4589
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/GreetingNFC.kt
@@ -0,0 +1,9 @@
+package com.sixkids.model
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class GreetingNFC(
+ val senderId: Int = -1,
+ val organizationId: Int = -1
+)
diff --git a/android/core/model/src/main/java/com/sixkids/model/GroupSimple.kt b/android/core/model/src/main/java/com/sixkids/model/GroupSimple.kt
index 87cec591..497ab35b 100644
--- a/android/core/model/src/main/java/com/sixkids/model/GroupSimple.kt
+++ b/android/core/model/src/main/java/com/sixkids/model/GroupSimple.kt
@@ -2,6 +2,6 @@ package com.sixkids.model
data class GroupSimple(
val headCount: Int,
- val leaderId: Int,
+ val leaderId: Long,
val students: List,
)
diff --git a/android/core/model/src/main/java/com/sixkids/model/GroupType.kt b/android/core/model/src/main/java/com/sixkids/model/GroupType.kt
new file mode 100644
index 00000000..92e5131e
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/GroupType.kt
@@ -0,0 +1,6 @@
+package com.sixkids.model
+
+enum class GroupType {
+ FREE,
+ DESIGN
+}
diff --git a/android/core/model/src/main/java/com/sixkids/model/MatchingRoom.kt b/android/core/model/src/main/java/com/sixkids/model/MatchingRoom.kt
new file mode 100644
index 00000000..4df7eda9
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/MatchingRoom.kt
@@ -0,0 +1,6 @@
+package com.sixkids.model
+
+data class MatchingRoom(
+ val dataKey: String,
+ val minCount: Int
+)
diff --git a/android/core/model/src/main/java/com/sixkids/model/MemberDetail.kt b/android/core/model/src/main/java/com/sixkids/model/MemberDetail.kt
new file mode 100644
index 00000000..ab50602a
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/MemberDetail.kt
@@ -0,0 +1,12 @@
+package com.sixkids.model
+
+data class MemberDetail(
+ val name: String = "",
+ val photo: String = "",
+ val isolationPoint: Double = 0.0,
+ val isolationRank: Int = -1,
+ val exp: Int = -1,
+ val challengeCount: Int = -1,
+ val relayCount: Int = -1,
+ val postCount: Int = -1,
+)
diff --git a/android/core/model/src/main/java/com/sixkids/model/MemberRankItem.kt b/android/core/model/src/main/java/com/sixkids/model/MemberRankItem.kt
new file mode 100644
index 00000000..ca2ff2f1
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/MemberRankItem.kt
@@ -0,0 +1,7 @@
+package com.sixkids.model
+
+data class MemberRankItem (
+ val name: String,
+ val exp: Int,
+ val rank: Int
+)
\ No newline at end of file
diff --git a/android/core/model/src/main/java/com/sixkids/model/MemberSimple.kt b/android/core/model/src/main/java/com/sixkids/model/MemberSimple.kt
index 017f2070..7b005fac 100644
--- a/android/core/model/src/main/java/com/sixkids/model/MemberSimple.kt
+++ b/android/core/model/src/main/java/com/sixkids/model/MemberSimple.kt
@@ -1,7 +1,10 @@
package com.sixkids.model
+import kotlinx.serialization.Serializable
+
+@Serializable
data class MemberSimple(
- val id: Long,
- val name: String,
- val photo: String
+ val id: Long = 0L,
+ val name: String = "",
+ val photo: String = "",
)
diff --git a/android/core/model/src/main/java/com/sixkids/model/MemberSimpleClassSummary.kt b/android/core/model/src/main/java/com/sixkids/model/MemberSimpleClassSummary.kt
new file mode 100644
index 00000000..37f5a5c4
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/MemberSimpleClassSummary.kt
@@ -0,0 +1,6 @@
+package com.sixkids.model
+
+data class MemberSimpleClassSummary (
+ val member: MemberSimple,
+ val count: Int
+)
\ No newline at end of file
diff --git a/android/core/model/src/main/java/com/sixkids/model/MemberSimpleWithScore.kt b/android/core/model/src/main/java/com/sixkids/model/MemberSimpleWithScore.kt
new file mode 100644
index 00000000..e24a057d
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/MemberSimpleWithScore.kt
@@ -0,0 +1,6 @@
+package com.sixkids.model
+
+data class MemberSimpleWithScore(
+ val memberSimple: MemberSimple,
+ val relationPoint: Int
+)
\ No newline at end of file
diff --git a/android/core/model/src/main/java/com/sixkids/model/Post.kt b/android/core/model/src/main/java/com/sixkids/model/Post.kt
new file mode 100644
index 00000000..1ffa5327
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/Post.kt
@@ -0,0 +1,11 @@
+package com.sixkids.model
+
+import java.time.LocalDateTime
+
+data class Post(
+ val id: Long,
+ val title: String,
+ val writer: String,
+ val time: LocalDateTime,
+ val commentCount: Int
+)
diff --git a/android/core/model/src/main/java/com/sixkids/model/PostCategory.kt b/android/core/model/src/main/java/com/sixkids/model/PostCategory.kt
new file mode 100644
index 00000000..e7df77b0
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/PostCategory.kt
@@ -0,0 +1,6 @@
+package com.sixkids.model
+
+object PostCategory {
+ const val FREE = "FREE"
+ const val NOTICE = "NOTICE"
+}
\ No newline at end of file
diff --git a/android/core/model/src/main/java/com/sixkids/model/PostDetail.kt b/android/core/model/src/main/java/com/sixkids/model/PostDetail.kt
new file mode 100644
index 00000000..171564ac
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/PostDetail.kt
@@ -0,0 +1,12 @@
+package com.sixkids.model
+
+import java.time.LocalDateTime
+
+data class PostDetail(
+ val writeMember: MemberSimple = MemberSimple(0, "", ""),
+ val createTime: LocalDateTime = LocalDateTime.now(),
+ val title: String = "",
+ val content: String = "",
+ val imageUri: String = "",
+ val comments: List = listOf(),
+)
diff --git a/android/core/model/src/main/java/com/sixkids/model/Relay.kt b/android/core/model/src/main/java/com/sixkids/model/Relay.kt
new file mode 100644
index 00000000..9cc5354d
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/Relay.kt
@@ -0,0 +1,12 @@
+package com.sixkids.model
+
+import java.time.LocalDateTime
+
+data class Relay(
+ val id: Long = 0,
+ val startTime: LocalDateTime = LocalDateTime.now(),
+ val endTime: LocalDateTime = LocalDateTime.now(),
+ val lastTurn: Int = 0,
+ val lastMemberName: String = "",
+ val totalCount: Int = 0
+)
diff --git a/android/core/model/src/main/java/com/sixkids/model/RelayDetail.kt b/android/core/model/src/main/java/com/sixkids/model/RelayDetail.kt
new file mode 100644
index 00000000..98c8ca5b
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/RelayDetail.kt
@@ -0,0 +1,13 @@
+package com.sixkids.model
+
+import java.time.LocalDateTime
+
+data class RelayDetail(
+ val id: Long = 0,
+ val startTime: LocalDateTime = LocalDateTime.now(),
+ val endTime: LocalDateTime = LocalDateTime.now(),
+ val lastTurn: Int = 0,
+ val lastMemberName: String = "",
+ val runnerList: List = emptyList(),
+
+)
diff --git a/android/core/model/src/main/java/com/sixkids/model/RelayQuestion.kt b/android/core/model/src/main/java/com/sixkids/model/RelayQuestion.kt
new file mode 100644
index 00000000..a6354e0b
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/RelayQuestion.kt
@@ -0,0 +1,14 @@
+package com.sixkids.model
+
+import java.time.LocalDateTime
+
+data class RelayQuestion(
+ val id: Long = 0,
+ val memberId: Long = 0,
+ val memberName: String = "",
+ val memberPhoto: String = "",
+ val time: LocalDateTime = LocalDateTime.now(),
+ val question: String = "",
+ val turn: Int = 0,
+ val endStatus: Boolean = false,
+)
diff --git a/android/core/model/src/main/java/com/sixkids/model/RelayReceive.kt b/android/core/model/src/main/java/com/sixkids/model/RelayReceive.kt
new file mode 100644
index 00000000..8ea53d33
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/RelayReceive.kt
@@ -0,0 +1,8 @@
+package com.sixkids.model
+
+data class RelayReceive(
+ val senderName: String = "",
+ val question: String = "",
+ val lastStatus: Boolean = false,
+ val demerit: Int = 0,
+)
diff --git a/android/core/model/src/main/java/com/sixkids/model/RelaySend.kt b/android/core/model/src/main/java/com/sixkids/model/RelaySend.kt
new file mode 100644
index 00000000..367f9aad
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/RelaySend.kt
@@ -0,0 +1,6 @@
+package com.sixkids.model
+
+data class RelaySend(
+ val prevMemberName: String = "",
+ val prevQuestion: String = ""
+)
\ No newline at end of file
diff --git a/android/core/model/src/main/java/com/sixkids/model/Report.kt b/android/core/model/src/main/java/com/sixkids/model/Report.kt
index 95a69125..dab6342e 100644
--- a/android/core/model/src/main/java/com/sixkids/model/Report.kt
+++ b/android/core/model/src/main/java/com/sixkids/model/Report.kt
@@ -3,11 +3,11 @@ package com.sixkids.model
import java.time.LocalDateTime
data class Report(
- val id: Int = 0,
+ val id: Long = 0,
val group: Group = Group(),
- val startDate: LocalDateTime = LocalDateTime.now(),
- val endDate: LocalDateTime = LocalDateTime.now(),
+ val startTime: LocalDateTime = LocalDateTime.now(),
+ val endTime: LocalDateTime = LocalDateTime.now(),
val file : String = "",
val content: String = "",
- val accepted: Boolean = false,
+ val acceptStatus: AcceptStatus = AcceptStatus.BEFORE,
)
diff --git a/android/core/model/src/main/java/com/sixkids/model/RunningChallenge.kt b/android/core/model/src/main/java/com/sixkids/model/RunningChallenge.kt
index b33f0d19..d0ecb6dd 100644
--- a/android/core/model/src/main/java/com/sixkids/model/RunningChallenge.kt
+++ b/android/core/model/src/main/java/com/sixkids/model/RunningChallenge.kt
@@ -3,7 +3,7 @@ package com.sixkids.model
import java.time.LocalDateTime
data class RunningChallenge(
- val id: Int = 0,
+ val id: Long = 0,
val title: String = "",
val content: String = "",
val totalMemberCount: Int = 0,
diff --git a/android/core/model/src/main/java/com/sixkids/model/RunningChallengeByStudent.kt b/android/core/model/src/main/java/com/sixkids/model/RunningChallengeByStudent.kt
new file mode 100644
index 00000000..8690ce8f
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/RunningChallengeByStudent.kt
@@ -0,0 +1,12 @@
+package com.sixkids.model
+
+import java.time.LocalDateTime
+
+data class RunningChallengeByStudent(
+ val challenge: Challenge,
+ val leaderStatus: Boolean? = null,
+ val memberNames: List,
+ val type: GroupType,
+ val createTime: LocalDateTime?,
+ val endStatus: Boolean?
+)
diff --git a/android/core/model/src/main/java/com/sixkids/model/RunningRelay.kt b/android/core/model/src/main/java/com/sixkids/model/RunningRelay.kt
new file mode 100644
index 00000000..c78e3595
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/RunningRelay.kt
@@ -0,0 +1,13 @@
+package com.sixkids.model
+
+import java.time.LocalDateTime
+
+data class RunningRelay(
+ val id: Long = 0,
+ val totalMemberCount: Int = 0, // 전체 턴 횟수 (선생님만 볼 수 있음)
+ val doneMemberCount: Int = 0, // 현재 진행된 턴 횟수 (선생님만 볼 수 있음)
+ val startTime: LocalDateTime = LocalDateTime.now(),
+ val endTime: LocalDateTime = LocalDateTime.now(),
+ val curMemberNickname: String = "",
+ val myTurnStatus : Boolean = false
+)
diff --git a/android/core/model/src/main/java/com/sixkids/model/SSeData.kt b/android/core/model/src/main/java/com/sixkids/model/SSeData.kt
new file mode 100644
index 00000000..6c1bbf45
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/SSeData.kt
@@ -0,0 +1,16 @@
+package com.sixkids.model
+
+import kotlinx.datetime.toKotlinLocalDateTime
+import kotlinx.serialization.Serializable
+import java.time.LocalDateTime
+import kotlinx.datetime.LocalDateTime as KotlinLocalDateTime
+
+@Serializable
+data class SseData(
+ val eventType: String,
+ val receiverId: Long,
+ val url: Long?,
+ val data: String?,
+ val time: KotlinLocalDateTime = LocalDateTime.now().toKotlinLocalDateTime()
+
+)
diff --git a/android/core/model/src/main/java/com/sixkids/model/SseEventType.kt b/android/core/model/src/main/java/com/sixkids/model/SseEventType.kt
new file mode 100644
index 00000000..453160fd
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/SseEventType.kt
@@ -0,0 +1,9 @@
+package com.sixkids.model
+
+enum class SseEventType {
+ SSE_CONNECT,
+ INVITE_REQUEST,
+ INVITE_RESPONSE,
+ KICK_MEMBER,
+ CREATE_GROUP,
+}
diff --git a/android/core/model/src/main/java/com/sixkids/model/StudentHomeInfo.kt b/android/core/model/src/main/java/com/sixkids/model/StudentHomeInfo.kt
new file mode 100644
index 00000000..96740a4e
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/StudentHomeInfo.kt
@@ -0,0 +1,10 @@
+package com.sixkids.model
+
+data class StudentHomeInfo (
+ val name: String,
+ val photo: String,
+ val className: String,
+ val exp: Int,
+ val notifyCount: Int,
+ val relations: List,
+)
\ No newline at end of file
diff --git a/android/core/model/src/main/java/com/sixkids/model/StudentRelation.kt b/android/core/model/src/main/java/com/sixkids/model/StudentRelation.kt
new file mode 100644
index 00000000..9764dad2
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/StudentRelation.kt
@@ -0,0 +1,11 @@
+package com.sixkids.model
+
+data class StudentRelation(
+ val id: Long = -1,
+ val name: String = "",
+ val relationPoint: Int = 0,
+ val tagGreetingCount: Int = 0,
+ val groupCount: Int = 0,
+ val receiveCount: Int = 0,
+ val sendCount: Int = 0,
+)
diff --git a/android/core/nfc/.gitignore b/android/core/nfc/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/android/core/nfc/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/android/core/nfc/build.gradle.kts b/android/core/nfc/build.gradle.kts
new file mode 100644
index 00000000..60961426
--- /dev/null
+++ b/android/core/nfc/build.gradle.kts
@@ -0,0 +1,11 @@
+plugins {
+ alias(libs.plugins.sixkids.android.library)
+}
+
+android {
+ namespace = "com.sixkids.core.nfc"
+
+}
+
+dependencies {
+}
diff --git a/android/core/nfc/consumer-rules.pro b/android/core/nfc/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/android/core/nfc/proguard-rules.pro b/android/core/nfc/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/android/core/nfc/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/android/core/nfc/src/main/AndroidManifest.xml b/android/core/nfc/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..ae4c1e29
--- /dev/null
+++ b/android/core/nfc/src/main/AndroidManifest.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/core/nfc/src/main/kotlin/com/sixkids/core/nfc/HCEService.kt b/android/core/nfc/src/main/kotlin/com/sixkids/core/nfc/HCEService.kt
new file mode 100644
index 00000000..c0726faf
--- /dev/null
+++ b/android/core/nfc/src/main/kotlin/com/sixkids/core/nfc/HCEService.kt
@@ -0,0 +1,38 @@
+package com.sixkids.core.nfc
+
+import android.nfc.cardemulation.HostApduService
+import android.os.Bundle
+
+class HCEService : HostApduService() {
+ override fun processCommandApdu(commandApdu: ByteArray?, extras: Bundle?): ByteArray {
+ if (commandApdu == null) {
+ return FAIL_SW
+ }
+
+ // Check if APDU matches the SELECT APDU defined above
+ return if (commandApdu contentEquals SELECT_APDU) {
+ // Data to be sent to B screen
+ val newData = data.toByteArray()
+ setData("")
+ newData + SUCCESS_SW
+ } else {
+ FAIL_SW
+ }
+ }
+
+ override fun onDeactivated(p0: Int) {
+ }
+
+ companion object {
+ val SELECT_APDU = byteArrayOf(0x00.toByte(), 0xA4.toByte(), 0x04.toByte(), 0x00.toByte(), 0x07.toByte(), 0xF0.toByte(), 0x01.toByte(), 0x02.toByte(), 0x03.toByte(), 0x04.toByte(), 0x05.toByte(), 0x06.toByte())
+ val SUCCESS_SW = byteArrayOf(0x90.toByte(), 0x00.toByte()) // Status word (SW) for success
+ val FAIL_SW = byteArrayOf(0x6F.toByte(), 0x00.toByte())
+
+
+ private var data: String = ""
+
+ fun setData(data: String) {
+ this.data = data
+ }
+ }
+}
diff --git a/android/core/nfc/src/main/res/values/strings.xml b/android/core/nfc/src/main/res/values/strings.xml
new file mode 100644
index 00000000..e0da84ec
--- /dev/null
+++ b/android/core/nfc/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ Ulban
+
diff --git a/android/core/nfc/src/main/res/xml/apdu_service.xml b/android/core/nfc/src/main/res/xml/apdu_service.xml
new file mode 100644
index 00000000..1500f8f9
--- /dev/null
+++ b/android/core/nfc/src/main/res/xml/apdu_service.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/android/core/ui/src/main/java/com/sixkids/ui/extension/Result.kt b/android/core/ui/src/main/java/com/sixkids/ui/extension/Result.kt
new file mode 100644
index 00000000..cba8c684
--- /dev/null
+++ b/android/core/ui/src/main/java/com/sixkids/ui/extension/Result.kt
@@ -0,0 +1,7 @@
+package com.sixkids.ui.extension
+
+inline fun Result.flatMap(block: (T) -> (Result)): Result {
+ return this.mapCatching {
+ block(it).getOrThrow()
+ }
+}
diff --git a/android/core/ui/src/main/java/com/sixkids/ui/util/Date.kt b/android/core/ui/src/main/java/com/sixkids/ui/util/Date.kt
index 10b25413..5d5fe69c 100644
--- a/android/core/ui/src/main/java/com/sixkids/ui/util/Date.kt
+++ b/android/core/ui/src/main/java/com/sixkids/ui/util/Date.kt
@@ -10,6 +10,11 @@ fun LocalDateTime.formatToMonthDayTime(): String {
return this.format(formatter)
}
+fun LocalDateTime.formatToMonthDayTimeKorean(): String {
+ val formatter = DateTimeFormatter.ofPattern("M월 d일 a h시 m분")
+ return this.format(formatter)
+}
+
fun LocalDate.formatToDayMonthYear(): String {
val formatter = DateTimeFormatter.ofPattern("MM/dd/yyyy")
return this.format(formatter)
@@ -19,3 +24,8 @@ fun LocalTime.formatToHourMinute(): String {
val formatter = DateTimeFormatter.ofPattern("HH:mm")
return this.format(formatter)
}
+
+fun LocalDateTime.formatToMonthDayKorean(): String {
+ val formatter = DateTimeFormatter.ofPattern("M월 d일")
+ return this.format(formatter)
+}
\ No newline at end of file
diff --git a/android/core/ui/src/main/java/com/sixkids/ui/util/Int.kt b/android/core/ui/src/main/java/com/sixkids/ui/util/Int.kt
new file mode 100644
index 00000000..51e53a3b
--- /dev/null
+++ b/android/core/ui/src/main/java/com/sixkids/ui/util/Int.kt
@@ -0,0 +1,5 @@
+package com.sixkids.ui.util
+
+fun Int.formatPoint(): String {
+ return String.format("%,d POINT", this)
+}
diff --git a/android/data/src/main/java/com/sixkids/data/api/ChallengeService.kt b/android/data/src/main/java/com/sixkids/data/api/ChallengeService.kt
index bf0cfaed..ebf683a3 100644
--- a/android/data/src/main/java/com/sixkids/data/api/ChallengeService.kt
+++ b/android/data/src/main/java/com/sixkids/data/api/ChallengeService.kt
@@ -1,13 +1,18 @@
package com.sixkids.data.api
import com.sixkids.data.model.request.ChallengeCreateRequest
+import com.sixkids.data.model.response.ChallengeDetailResponse
import com.sixkids.data.model.response.ChallengeHistoryResponse
+import com.sixkids.data.model.response.ChallengeSimpleResponse
+import com.sixkids.data.model.response.RunningChallengeByStudentResponse
import com.sixkids.data.model.response.RunningChallengeResponse
import com.sixkids.data.network.ApiResponse
import com.sixkids.data.network.ApiResult
import retrofit2.http.Body
import retrofit2.http.GET
+import retrofit2.http.PATCH
import retrofit2.http.POST
+import retrofit2.http.Path
import retrofit2.http.Query
interface ChallengeService {
@@ -31,5 +36,25 @@ interface ChallengeService {
@Query("organizationId") organizationId: Int,
): ApiResult>
-
+ @GET("challenges/{id}/simple")
+ suspend fun getChallengeSimple(
+ @Path("id") challengeId: Int
+ ): ApiResult>
+
+ @GET("challenges/{id}")
+ suspend fun getChallengeDetail(
+ @Path("id") challengeId: Long,
+ @Query("groupId") memberId: Long?,
+ ): ApiResult>
+
+ @PATCH("challenges/reports/{id}")
+ suspend fun gradingChallenge(
+ @Path("id") reportId: Long,
+ @Query("reportType") acceptStatus: String,
+ ): ApiResult>
+
+ @GET("challenges/running/member")
+ suspend fun getRunningChallengeByStudent(
+ @Query("organizationId") organizationId: Int,
+ ): ApiResult>
}
diff --git a/android/data/src/main/java/com/sixkids/data/api/ChatFilterService.kt b/android/data/src/main/java/com/sixkids/data/api/ChatFilterService.kt
new file mode 100644
index 00000000..f51760d7
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/api/ChatFilterService.kt
@@ -0,0 +1,41 @@
+package com.sixkids.data.api
+
+import com.sixkids.data.model.request.ChatFilterRequest
+import com.sixkids.data.model.response.ChattingFilterListResponse
+import com.sixkids.data.model.response.ChattingFilterResponse
+import com.sixkids.data.network.ApiResponse
+import com.sixkids.data.network.ApiResult
+import retrofit2.http.Body
+import retrofit2.http.DELETE
+import retrofit2.http.GET
+import retrofit2.http.PATCH
+import retrofit2.http.POST
+import retrofit2.http.Path
+import retrofit2.http.Query
+
+interface ChatFilterService {
+ @GET("filters")
+ suspend fun getChatFilters(
+ @Query("organizationId") organizationId: Int,
+ @Query("page") page: Int,
+ @Query("size") size: Int,
+ ): ApiResult>
+
+ @DELETE("filters/{id}")
+ suspend fun deleteChatFilter(
+ @Path("id") id: Long,
+ ): ApiResult>
+
+ @POST("filters/{organizationId}")
+ suspend fun createChatFilter(
+ @Path("organizationId") organizationId: Long,
+ @Body badWord: ChatFilterRequest,
+ ): ApiResult>
+
+ @PATCH("filters/{organizationId}/{id}")
+ suspend fun updateChatFilter(
+ @Path("organizationId") organizationId: Long,
+ @Path("id") id: Long,
+ @Body badWord: ChatFilterRequest,
+ ): ApiResult>
+}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/api/ChattingService.kt b/android/data/src/main/java/com/sixkids/data/api/ChattingService.kt
new file mode 100644
index 00000000..b6998686
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/api/ChattingService.kt
@@ -0,0 +1,16 @@
+package com.sixkids.data.api
+
+import com.sixkids.data.model.response.ChatHistoryResponse
+import com.sixkids.data.network.ApiResponse
+import com.sixkids.data.network.ApiResult
+import retrofit2.http.GET
+import retrofit2.http.Query
+
+interface ChattingService {
+ @GET("chats")
+ suspend fun getChatList(
+ @Query("roomId") roomId: Long,
+ @Query("page") page: Int,
+ @Query("size") size: Int
+ ): ApiResult>
+}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/api/CommentService.kt b/android/data/src/main/java/com/sixkids/data/api/CommentService.kt
new file mode 100644
index 00000000..4f786b86
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/api/CommentService.kt
@@ -0,0 +1,34 @@
+package com.sixkids.data.api
+
+import com.sixkids.data.model.request.NewCommentRequest
+import com.sixkids.data.model.request.UpdateCommentRequest
+import com.sixkids.data.network.ApiResponse
+import com.sixkids.data.network.ApiResult
+import retrofit2.http.Body
+import retrofit2.http.DELETE
+import retrofit2.http.PATCH
+import retrofit2.http.POST
+import retrofit2.http.Path
+
+interface CommentService {
+ @POST("comments")
+ suspend fun createComment(
+ @Body request: NewCommentRequest
+ ): ApiResult>
+
+ @DELETE("comments/{id}")
+ suspend fun deleteComment(
+ @Path("id") id: Long
+ ): ApiResult>
+
+ @PATCH("comments/{id}")
+ suspend fun updateComment(
+ @Path("id") id: Long,
+ @Body request: UpdateCommentRequest
+ ): ApiResult>
+
+ @POST("comments/{id}/report")
+ suspend fun reportComment(
+ @Path("id") id: Long
+ ): ApiResult>
+}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/api/GroupService.kt b/android/data/src/main/java/com/sixkids/data/api/GroupService.kt
new file mode 100644
index 00000000..6444911b
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/api/GroupService.kt
@@ -0,0 +1,48 @@
+package com.sixkids.data.api
+
+import com.sixkids.data.model.response.GroupMatchingRoomResponse
+import com.sixkids.data.model.response.GroupMatchingSuccessResponse
+import com.sixkids.data.network.ApiResponse
+import com.sixkids.data.network.ApiResult
+import retrofit2.http.DELETE
+import retrofit2.http.GET
+import retrofit2.http.POST
+import retrofit2.http.Query
+
+interface GroupService {
+ @POST("challenges/groups/matchingroom")
+ suspend fun createMatchingRoom(
+ @Query("challengeId") challengeId: Long,
+ ): ApiResult>
+
+ @GET("challenges/groups/invite")
+ suspend fun inviteFriend(
+ @Query("key") key: String,
+ @Query("memberId") memberId: Long,
+ ): ApiResult>
+
+ @DELETE("challenges/groups/matching")
+ suspend fun deportFriend(
+ @Query("key") key: String,
+ @Query("memberId") memberId: Long,
+ ): ApiResult>
+
+ @POST("challenges/groups/join")
+ suspend fun joinGroup(
+ @Query("key") key: String,
+ @Query("joinStatus") joinStatus : Boolean,
+ ): ApiResult>
+
+ @POST("challenges/groups")
+ suspend fun createGroup(
+ @Query("key") key: String,
+ ): ApiResult>
+
+ @GET("challenges/groups/matching")
+ suspend fun getMatchingGroup(
+ @Query("organizationId") organizationId: Long,
+ @Query("minCount") minCount: Int,
+ @Query("matchingType") matchingType: String,
+ @Query("members") members: List,
+ ): ApiResult>>
+}
diff --git a/android/data/src/main/java/com/sixkids/data/api/MemberOrgService.kt b/android/data/src/main/java/com/sixkids/data/api/MemberOrgService.kt
new file mode 100644
index 00000000..956bdd6c
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/api/MemberOrgService.kt
@@ -0,0 +1,47 @@
+package com.sixkids.data.api
+
+import com.sixkids.data.model.request.GreetingRequest
+import com.sixkids.data.model.response.StudentDetailResponse
+import com.sixkids.data.model.response.StudentHomeResponse
+import com.sixkids.data.model.response.StudentRelationDetailResponse
+import com.sixkids.data.model.response.StudentWithRelationScoreResponse
+import com.sixkids.data.network.ApiResponse
+import com.sixkids.data.network.ApiResult
+import retrofit2.http.Body
+import retrofit2.http.GET
+import retrofit2.http.POST
+import retrofit2.http.Path
+import retrofit2.http.Query
+
+interface MemberOrgService {
+ @GET("organizations/{id}/home")
+ suspend fun getStudentHomeInfo(
+ @Path("id") organizationId: Long
+ ): ApiResult>
+
+ @GET("organizations/{id}")
+ suspend fun getMemberDetail(
+ @Path("id") organizationId: Long,
+ @Query("memberId") memberId: Long
+ ): ApiResult>
+
+ @GET("organizations/{id}/relations")
+ suspend fun getRelationSimple(
+ @Path("id") organizationId: Long,
+ @Query("memberId") memberId: Int,
+ @Query("limit") relationType: Int? = null
+ ): ApiResult>>
+
+ @GET("organizations/{id}/relation")
+ suspend fun getRelationDetail(
+ @Path("id") organizationId: Long,
+ @Query("sourceMemberId") sourceMemberId: Int,
+ @Query("targetMemberId") targetMemberId: Int
+ ): ApiResult>
+
+ @POST("organizations/tag")
+ suspend fun tagGreeting(
+ @Body greetingRequest: GreetingRequest
+ ): ApiResult>
+
+}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/api/MemberService.kt b/android/data/src/main/java/com/sixkids/data/api/MemberService.kt
index d13f9b18..9558279a 100644
--- a/android/data/src/main/java/com/sixkids/data/api/MemberService.kt
+++ b/android/data/src/main/java/com/sixkids/data/api/MemberService.kt
@@ -3,6 +3,7 @@ package com.sixkids.data.api
import com.sixkids.data.model.request.FcmRequest
import com.sixkids.data.model.response.ApiResponse
import com.sixkids.data.model.response.MemberInfoResponse
+import com.sixkids.data.model.response.MemberSimpleInfoResponse
import com.sixkids.data.model.response.SignInResponse
import com.sixkids.data.model.response.UpdateProfilePhotoResponse
import com.sixkids.data.network.ApiResult
@@ -15,11 +16,17 @@ import retrofit2.http.PATCH
import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.PartMap
+import retrofit2.http.Path
interface MemberService {
@GET("members/")
suspend fun getMemberInfo(): ApiResult>
+ @GET("members/{id}")
+ suspend fun getMemberInfoById(
+ @Path("id") id: Long
+ ): ApiResult>
+
@Multipart
@PATCH("members/photo")
suspend fun updateMemberProfilePhoto(
diff --git a/android/data/src/main/java/com/sixkids/data/api/OrganizationService.kt b/android/data/src/main/java/com/sixkids/data/api/OrganizationService.kt
index 3921e267..dd814144 100644
--- a/android/data/src/main/java/com/sixkids/data/api/OrganizationService.kt
+++ b/android/data/src/main/java/com/sixkids/data/api/OrganizationService.kt
@@ -1,12 +1,20 @@
package com.sixkids.data.api
+import com.sixkids.data.model.request.JoinOrganizationRequest
import com.sixkids.data.model.request.NewOrganizationRequest
import com.sixkids.data.model.response.ApiResponse
+import com.sixkids.data.model.response.ClassSummaryResponse
+import com.sixkids.data.model.response.OrganizationInviteCodeResponse
+import com.sixkids.data.model.response.OrganizationNameResponse
+import com.sixkids.data.model.response.OrganizationMemberResponse
import com.sixkids.data.model.response.OrganizationResponse
+import com.sixkids.data.model.response.RankResponse
import com.sixkids.data.network.ApiResult
import retrofit2.http.Body
import retrofit2.http.GET
+import retrofit2.http.PATCH
import retrofit2.http.POST
+import retrofit2.http.Path
interface OrganizationService {
@GET("organizations")
@@ -14,4 +22,36 @@ interface OrganizationService {
@POST("organizations")
suspend fun newOrganization(@Body newOrganizationRequest: NewOrganizationRequest): ApiResult>
+
+ @POST("organizations/{id}/join")
+ suspend fun joinOrganization(
+ @Path(value = "id") orgId: Int,
+ @Body joinOrganizationRequest: JoinOrganizationRequest
+ ): ApiResult>
+
+ @GET("organizations/{id}/summary")
+ suspend fun getOrganizationSummary(
+ @Path(value = "id") organizationId: Int
+ ): ApiResult>
+
+ @PATCH("organizations/{id}")
+ suspend fun updateOrganization(
+ @Path(value = "id") organizationId: Int,
+ @Body newOrganizationRequest: NewOrganizationRequest
+ ): ApiResult>
+
+ @GET("organizations/{id}/code")
+ suspend fun getOrganizationInviteCode(
+ @Path(value = "id") organizationId: Int
+ ): ApiResult>
+
+ @GET("organizations/{id}/members")
+ suspend fun getOrganizationMembers(
+ @Path(value = "id") orgId: Int
+ ): ApiResult>>
+
+ @GET("organizations/{id}/rank")
+ suspend fun getOrganizationRank(
+ @Path(value = "id") orgId: Int
+ ): ApiResult>>
}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/api/PostService.kt b/android/data/src/main/java/com/sixkids/data/api/PostService.kt
new file mode 100644
index 00000000..d6248997
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/api/PostService.kt
@@ -0,0 +1,59 @@
+package com.sixkids.data.api
+
+import com.sixkids.data.model.request.NewPostRequest
+import com.sixkids.data.model.response.PostDetailResponse
+import com.sixkids.data.model.response.PostListResponse
+import com.sixkids.data.network.ApiResponse
+import com.sixkids.data.network.ApiResult
+import okhttp3.MultipartBody
+import retrofit2.http.DELETE
+import retrofit2.http.GET
+import retrofit2.http.Multipart
+import retrofit2.http.PATCH
+import retrofit2.http.POST
+import retrofit2.http.Part
+import retrofit2.http.Path
+import retrofit2.http.Query
+
+interface PostService {
+
+ @GET("boards")
+ suspend fun getPosts(
+ @Query("organizationId") organizationId: Int,
+ @Query("memberId") memberId: Int? = null,
+ @Query("page") page: Int,
+ @Query("size") size: Int,
+ @Query("postCategory") postCategory: String,
+ ): ApiResult>
+
+ @Multipart
+ @POST("boards")
+ suspend fun createPost(
+ @Query("organizationId") organizationId: Long,
+ @Part("request") postRequestBody: NewPostRequest,
+ @Part file: MultipartBody.Part?,
+ ): ApiResult>
+
+ @GET("boards/{id}")
+ suspend fun getPostDetail(
+ @Path("id") postId: Long,
+ ): ApiResult>
+
+ @DELETE("boards/{id}")
+ suspend fun deletePost(
+ @Path("id") postId: Long,
+ ): ApiResult>
+
+ @Multipart
+ @PATCH("boards/{id}")
+ suspend fun updatePost(
+ @Path("id") postId: Long,
+ @Part("request") postRequestBody: NewPostRequest,
+ @Part file: MultipartBody.Part?,
+ ): ApiResult>
+
+ @POST("boards/{id}/report")
+ suspend fun reportPost(
+ @Path("id") postId: Long,
+ ): ApiResult>
+}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/api/RelayService.kt b/android/data/src/main/java/com/sixkids/data/api/RelayService.kt
new file mode 100644
index 00000000..5f0b7791
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/api/RelayService.kt
@@ -0,0 +1,59 @@
+package com.sixkids.data.api
+
+import com.sixkids.data.model.request.ReceiveRelayRequest
+import com.sixkids.data.model.request.RelayCreateRequest
+import com.sixkids.data.model.response.ReceiveRelayResponse
+import com.sixkids.data.model.response.RelayDetailResponse
+import com.sixkids.data.model.response.RelayHistoryResponse
+import com.sixkids.data.model.response.RunningRelayResponse
+import com.sixkids.data.model.response.SendRelayResponse
+import com.sixkids.data.network.ApiResponse
+import com.sixkids.data.network.ApiResult
+import retrofit2.http.Body
+import retrofit2.http.GET
+import retrofit2.http.POST
+import retrofit2.http.Path
+import retrofit2.http.Query
+
+interface RelayService {
+
+ @GET("relays")
+ suspend fun getRelayHistory(
+ @Query("orgId") organizationId: Int,
+ @Query("memberId") memberId: Int? = null,
+ @Query("page") page: Int,
+ @Query("size") size: Int,
+ ): ApiResult>
+
+ @GET("relays/running")
+ suspend fun getRunningRelay(
+ @Query("organizationId") organizationId: Long
+ ): ApiResult>
+
+ @GET("relays/{id}")
+ suspend fun getRelayDetail(
+ @Path("id") relayId: Long
+ ): ApiResult>
+
+ @POST("relays")
+ suspend fun createRelay(
+ @Body relayCreateRequest: RelayCreateRequest
+ ): ApiResult>
+
+ @GET("relays/{id}/question")
+ suspend fun getRelayQuestion(
+ @Path("id") relayId: Long
+ ): ApiResult>
+
+ @POST("relays/{id}/receive")
+ suspend fun receiveRelay(
+ @Path("id") relayId: Int,
+ @Body receiveRelayRequest: ReceiveRelayRequest
+ ): ApiResult>
+
+ @POST("relays/{id}/send")
+ suspend fun sendRelay(
+ @Path("id") relayId: Int,
+ ): ApiResult>
+
+}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/di/DataSourceModule.kt b/android/data/src/main/java/com/sixkids/data/di/DataSourceModule.kt
index 894415ec..24464180 100644
--- a/android/data/src/main/java/com/sixkids/data/di/DataSourceModule.kt
+++ b/android/data/src/main/java/com/sixkids/data/di/DataSourceModule.kt
@@ -1,15 +1,27 @@
package com.sixkids.data.di
+import com.sixkids.data.repository.challenge.remote.ChallengeRemoteDataSource
+import com.sixkids.data.repository.challenge.remote.ChallengeRemoteDataSourceImpl
+import com.sixkids.data.repository.chatting.remote.ChattingRemoteDataSource
+import com.sixkids.data.repository.chatting.remote.ChattingRemoteDataSourceImpl
+import com.sixkids.data.repository.chattingfilter.remote.ChattingFilterRemoteDataSource
+import com.sixkids.data.repository.chattingfilter.remote.ChattingFilterRemoteDataSourceImpl
+import com.sixkids.data.repository.comment.remote.CommentRemoteDataSource
+import com.sixkids.data.repository.comment.remote.CommentRemoteDataSourceImpl
+import com.sixkids.data.repository.group.remote.GroupDataSource
+import com.sixkids.data.repository.group.remote.GroupDataSourceImpl
+import com.sixkids.data.repository.organization.local.OrganizationLocalDataSource
+import com.sixkids.data.repository.organization.local.OrganizationLocalDataSourceImpl
import com.sixkids.data.repository.organization.remote.OrganizationRemoteDataSource
import com.sixkids.data.repository.organization.remote.OrganizationRemoteDataSourceImpl
+import com.sixkids.data.repository.post.remote.PostRemoteDataSource
+import com.sixkids.data.repository.post.remote.PostRemoteDataSourceImpl
+import com.sixkids.data.repository.relay.remote.RelayRemoteDataSource
+import com.sixkids.data.repository.relay.remote.RelayRemoteDataSourceImpl
import com.sixkids.data.repository.user.local.UserLocalDataSource
import com.sixkids.data.repository.user.local.UserLocalDataSourceImpl
import com.sixkids.data.repository.user.remote.UserRemoteDataSource
import com.sixkids.data.repository.user.remote.UserRemoteDataSourceImpl
-import com.sixkids.data.repository.challenge.remote.ChallengeRemoteDataSource
-import com.sixkids.data.repository.challenge.remote.ChallengeRemoteDataSourceImpl
-import com.sixkids.data.repository.organization.local.OrganizationLocalDataSource
-import com.sixkids.data.repository.organization.local.OrganizationLocalDataSourceImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@@ -32,7 +44,7 @@ abstract class DataSourceModule {
abstract fun bindChallengeDataSource(
challengeRemoteDataSource: ChallengeRemoteDataSourceImpl
): ChallengeRemoteDataSource
-
+
@Binds
abstract fun bindOrganizationRemoteDataSource(
organizationRemoteDataSource: OrganizationRemoteDataSourceImpl
@@ -43,4 +55,33 @@ abstract class DataSourceModule {
organizationLocalDataSource: OrganizationLocalDataSourceImpl
): OrganizationLocalDataSource
+ @Binds
+ abstract fun bindPostRemoteDataSource(
+ postRemoteDataSource: PostRemoteDataSourceImpl
+ ): PostRemoteDataSource
+
+ @Binds
+ abstract fun bindCommentRemoteDataSource(
+ commentRemoteDataSource: CommentRemoteDataSourceImpl
+ ): CommentRemoteDataSource
+
+ @Binds
+ abstract fun bindChattingRemoteDataSource(
+ chattingRemoteDataSource: ChattingRemoteDataSourceImpl
+ ): ChattingRemoteDataSource
+
+ @Binds
+ abstract fun bindRelayRemoteDataSource(
+ relayRemoteDataSource: RelayRemoteDataSourceImpl
+ ): RelayRemoteDataSource
+
+ @Binds
+ abstract fun bindChattingFilterRemoteDataSource(
+ chattingFilterRemoteDataSource: ChattingFilterRemoteDataSourceImpl
+ ): ChattingFilterRemoteDataSource
+
+ @Binds
+ abstract fun bindGroupDataSource(
+ groupDataSource: GroupDataSourceImpl
+ ): GroupDataSource
}
diff --git a/android/data/src/main/java/com/sixkids/data/di/RepositoryModule.kt b/android/data/src/main/java/com/sixkids/data/di/RepositoryModule.kt
index fc9fd0da..63e148ae 100644
--- a/android/data/src/main/java/com/sixkids/data/di/RepositoryModule.kt
+++ b/android/data/src/main/java/com/sixkids/data/di/RepositoryModule.kt
@@ -2,10 +2,22 @@ package com.sixkids.data.di
import com.sixkids.data.repository.TokenRepositoryImpl
import com.sixkids.data.repository.challenge.ChallengeRepositoryImpl
+import com.sixkids.data.repository.chatting.ChattingRepositoryImpl
+import com.sixkids.data.repository.chattingfilter.ChattingFilterRepositoryImpl
+import com.sixkids.data.repository.comment.CommentRepositoryImpl
+import com.sixkids.data.repository.group.GroupRepositoryImpl
import com.sixkids.data.repository.organization.OrganizationRepositoryImpl
-import com.sixkids.domain.repository.ChallengeRepository
+import com.sixkids.data.repository.post.PostRepositoryImpl
+import com.sixkids.data.repository.relay.RelayRepositoryImpl
import com.sixkids.data.repository.user.UserRepositoryImpl
+import com.sixkids.domain.repository.ChallengeRepository
+import com.sixkids.domain.repository.ChattingFilterRepository
+import com.sixkids.domain.repository.ChattingRepository
+import com.sixkids.domain.repository.CommentRepository
+import com.sixkids.domain.repository.GroupRepository
import com.sixkids.domain.repository.OrganizationRepository
+import com.sixkids.domain.repository.PostRepository
+import com.sixkids.domain.repository.RelayRepository
import com.sixkids.domain.repository.TokenRepository
import com.sixkids.domain.repository.UserRepository
import dagger.Binds
@@ -41,4 +53,41 @@ abstract class RepositoryModule {
abstract fun bindOrganizationRepository(
organizationRepository: OrganizationRepositoryImpl
): OrganizationRepository
+
+ @Singleton
+ @Binds
+ abstract fun bindPostRepository(
+ postRepository: PostRepositoryImpl
+ ): PostRepository
+
+ @Singleton
+ @Binds
+ abstract fun bindCommentRepository(
+ commentRepository: CommentRepositoryImpl
+ ): CommentRepository
+
+ @Singleton
+ @Binds
+ abstract fun bindChattingRepository(
+ chattingRepository: ChattingRepositoryImpl
+ ): ChattingRepository
+
+ @Singleton
+ @Binds
+ abstract fun bindRelayRepository(
+ relayRepository: RelayRepositoryImpl
+ ): RelayRepository
+
+ @Singleton
+ @Binds
+ abstract fun bindChattingFilterRepository(
+ chattingFilterRepository: ChattingFilterRepositoryImpl
+ ): ChattingFilterRepository
+
+ @Singleton
+ @Binds
+ abstract fun bindGroupRepository(
+ groupRepository: GroupRepositoryImpl
+ ): GroupRepository
+
}
diff --git a/android/data/src/main/java/com/sixkids/data/di/ServiceModule.kt b/android/data/src/main/java/com/sixkids/data/di/ServiceModule.kt
index a8f991ab..6136820b 100644
--- a/android/data/src/main/java/com/sixkids/data/di/ServiceModule.kt
+++ b/android/data/src/main/java/com/sixkids/data/di/ServiceModule.kt
@@ -1,8 +1,15 @@
package com.sixkids.data.di
import com.sixkids.data.api.ChallengeService
+import com.sixkids.data.api.ChatFilterService
+import com.sixkids.data.api.CommentService
+import com.sixkids.data.api.ChattingService
+import com.sixkids.data.api.GroupService
+import com.sixkids.data.api.MemberOrgService
import com.sixkids.data.api.MemberService
import com.sixkids.data.api.OrganizationService
+import com.sixkids.data.api.PostService
+import com.sixkids.data.api.RelayService
import com.sixkids.data.api.SignInService
import com.sixkids.data.api.TokenService
import dagger.Module
@@ -56,4 +63,60 @@ object ServiceModule {
return retrofit.create(OrganizationService::class.java)
}
+ @Singleton
+ @Provides
+ fun providePostService(
+ @NetworkModule.AuthRetrofit retrofit: Retrofit
+ ): PostService {
+ return retrofit.create(PostService::class.java)
+ }
+
+ @Singleton
+ @Provides
+ fun provideCommentService(
+ @NetworkModule.AuthRetrofit retrofit: Retrofit
+ ): CommentService {
+ return retrofit.create(CommentService::class.java)
+ }
+
+ @Singleton
+ @Provides
+ fun provideChattingService(
+ @NetworkModule.AuthRetrofit retrofit: Retrofit
+ ): ChattingService {
+ return retrofit.create(ChattingService::class.java)
+ }
+
+ @Singleton
+ @Provides
+ fun provideRelayService(
+ @NetworkModule.AuthRetrofit retrofit: Retrofit
+ ): RelayService {
+ return retrofit.create(RelayService::class.java)
+ }
+
+ @Singleton
+ @Provides
+ fun provideMemberOrgService(
+ @NetworkModule.AuthRetrofit retrofit: Retrofit
+ ): MemberOrgService {
+ return retrofit.create(MemberOrgService::class.java)
+ }
+
+ @Singleton
+ @Provides
+ fun provideChatFilterService(
+ @NetworkModule.AuthRetrofit retrofit: Retrofit
+ ): ChatFilterService {
+ return retrofit.create(ChatFilterService::class.java)
+ }
+
+ @Singleton
+ @Provides
+ fun provideGroupService(
+ @NetworkModule.AuthRetrofit retrofit: Retrofit
+ ): GroupService {
+ return retrofit.create(GroupService::class.java)
+ }
+
}
diff --git a/android/data/src/main/java/com/sixkids/data/model/request/ChallengeCreateRequest.kt b/android/data/src/main/java/com/sixkids/data/model/request/ChallengeCreateRequest.kt
index efd66bac..32b12f97 100644
--- a/android/data/src/main/java/com/sixkids/data/model/request/ChallengeCreateRequest.kt
+++ b/android/data/src/main/java/com/sixkids/data/model/request/ChallengeCreateRequest.kt
@@ -9,12 +9,12 @@ data class ChallengeCreateRequest(
val startTime: LocalDateTime,
val endTime: LocalDateTime,
val minCount: Int,
- val reword: Int,
+ val reward: Int,
val groups: List
)
data class GroupRequest(
val headCount: Int,
- val leaderId: Int,
+ val leaderId: Long,
val students: List
)
diff --git a/android/data/src/main/java/com/sixkids/data/model/request/ChatFilterRequest.kt b/android/data/src/main/java/com/sixkids/data/model/request/ChatFilterRequest.kt
new file mode 100644
index 00000000..0f905fd3
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/request/ChatFilterRequest.kt
@@ -0,0 +1,5 @@
+package com.sixkids.data.model.request
+
+data class ChatFilterRequest(
+ val badWord: String,
+)
diff --git a/android/data/src/main/java/com/sixkids/data/model/request/CommentRequest.kt b/android/data/src/main/java/com/sixkids/data/model/request/CommentRequest.kt
new file mode 100644
index 00000000..7fde717d
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/request/CommentRequest.kt
@@ -0,0 +1,13 @@
+package com.sixkids.data.model.request
+
+data class NewCommentRequest(
+ val boardId: Long,
+ val content: String,
+ val parentId: Long,
+)
+
+
+data class UpdateCommentRequest(
+ val id: Long,
+ val content: String,
+)
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/model/request/GreetingRequest.kt b/android/data/src/main/java/com/sixkids/data/model/request/GreetingRequest.kt
new file mode 100644
index 00000000..1e8d2738
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/request/GreetingRequest.kt
@@ -0,0 +1,6 @@
+package com.sixkids.data.model.request
+
+data class GreetingRequest(
+ val organizationId: Long,
+ val memberId: Long
+)
diff --git a/android/data/src/main/java/com/sixkids/data/model/request/JoinOrganizationRequest.kt b/android/data/src/main/java/com/sixkids/data/model/request/JoinOrganizationRequest.kt
new file mode 100644
index 00000000..a1c95f37
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/request/JoinOrganizationRequest.kt
@@ -0,0 +1,5 @@
+package com.sixkids.data.model.request
+
+data class JoinOrganizationRequest(
+ val code: String
+)
diff --git a/android/data/src/main/java/com/sixkids/data/model/request/NewPostRequest.kt b/android/data/src/main/java/com/sixkids/data/model/request/NewPostRequest.kt
new file mode 100644
index 00000000..7331390d
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/request/NewPostRequest.kt
@@ -0,0 +1,8 @@
+package com.sixkids.data.model.request
+
+data class NewPostRequest(
+ val title: String,
+ val content: String,
+ val secretStatus: Boolean,
+ val postCategory: String
+)
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/model/request/ReceiveRelayRequest.kt b/android/data/src/main/java/com/sixkids/data/model/request/ReceiveRelayRequest.kt
new file mode 100644
index 00000000..340b6778
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/request/ReceiveRelayRequest.kt
@@ -0,0 +1,6 @@
+package com.sixkids.data.model.request
+
+data class ReceiveRelayRequest(
+ val senderId: Long,
+ val question: String
+)
diff --git a/android/data/src/main/java/com/sixkids/data/model/request/RelayCreateRequest.kt b/android/data/src/main/java/com/sixkids/data/model/request/RelayCreateRequest.kt
new file mode 100644
index 00000000..ed211845
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/request/RelayCreateRequest.kt
@@ -0,0 +1,6 @@
+package com.sixkids.data.model.request
+
+data class RelayCreateRequest(
+ val organizationId: Int,
+ val question: String
+)
diff --git a/android/data/src/main/java/com/sixkids/data/model/response/ChallengeDetailResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/ChallengeDetailResponse.kt
new file mode 100644
index 00000000..040728b8
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/ChallengeDetailResponse.kt
@@ -0,0 +1,21 @@
+package com.sixkids.data.model.response
+
+import com.sixkids.model.ChallengeDetail
+import com.squareup.moshi.Json
+
+data class ChallengeDetailResponse(
+ @Json(name = "challengeSimpleDTO")
+ val challenge: ChallengeResponse,
+ val reports: List
+)
+
+internal fun ChallengeDetailResponse.toModel() = ChallengeDetail(
+ id = challenge.id,
+ title = challenge.title,
+ content = challenge.content,
+ startTime = challenge.startTime,
+ endTime = challenge.endTime,
+ headCount = reports.fold(0) { headCount, report -> headCount + report.group.headCount },
+ teamCount = reports.size,
+ reportList = reports.map { it.toModel() }
+)
diff --git a/android/data/src/main/java/com/sixkids/data/model/response/ChallengeHistoryResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/ChallengeHistoryResponse.kt
index 4975943c..602fa0c9 100644
--- a/android/data/src/main/java/com/sixkids/data/model/response/ChallengeHistoryResponse.kt
+++ b/android/data/src/main/java/com/sixkids/data/model/response/ChallengeHistoryResponse.kt
@@ -7,11 +7,12 @@ data class ChallengeHistoryResponse(
val page: Int,
val size: Int,
val last: Boolean,
+ val totalCount : Int,
val challenges: List
)
data class ChallengeResponse(
- val id: Int = 0,
+ val id: Long = 0,
val title: String = "",
val content: String = "",
val headCount: Int = 0,
@@ -19,11 +20,12 @@ data class ChallengeResponse(
val endTime: LocalDateTime = LocalDateTime.now(),
)
-internal fun ChallengeResponse.toModel() = Challenge(
+internal fun ChallengeResponse.toModel(totalCount: Int = 0) = Challenge(
id = id,
title = title,
content = content,
headCount = headCount,
startTime = startTime,
- endTime = endTime
+ endTime = endTime,
+ totalCount = totalCount
)
diff --git a/android/data/src/main/java/com/sixkids/data/model/response/ChallengeSimpleResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/ChallengeSimpleResponse.kt
new file mode 100644
index 00000000..fd967e56
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/ChallengeSimpleResponse.kt
@@ -0,0 +1,24 @@
+package com.sixkids.data.model.response
+
+import com.sixkids.model.Challenge
+import java.time.LocalDateTime
+
+data class ChallengeSimpleResponse(
+ val id: Long,
+ val title: String,
+ val content: String,
+ val startTime: LocalDateTime,
+ val endTime: LocalDateTime,
+ val reward: Int,
+)
+
+fun ChallengeSimpleResponse.toModel(): Challenge {
+ return Challenge(
+ id = id,
+ title = title,
+ content = content,
+ startTime = startTime,
+ endTime = endTime,
+ reward = reward
+ )
+}
diff --git a/android/data/src/main/java/com/sixkids/data/model/response/ChatHistoryResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/ChatHistoryResponse.kt
new file mode 100644
index 00000000..c89a7d0b
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/ChatHistoryResponse.kt
@@ -0,0 +1,29 @@
+package com.sixkids.data.model.response
+
+import com.sixkids.model.Chat
+
+data class ChatHistoryResponse(
+ val roomId: Long,
+ val hasNext: Boolean,
+ val messages: List
+)
+
+data class MessageResponse(
+ val memberId: Long,
+ val memberName: String,
+ val memberImageUrl: String?,
+ val content: String,
+ val sendDateTime: Long,
+ val readCount: Int
+)
+
+internal fun MessageResponse.toModel() : Chat {
+ return Chat(
+ memberId = memberId,
+ memberName = memberName,
+ memberImageUrl = memberImageUrl?:"https://ulvanbucket.s3.ap-northeast-2.amazonaws.com/5001fcad-52b8-42ba-a587-c72797a3a36f_profile.jpg",
+ content = content,
+ sendDateTime = sendDateTime,
+ readCount = readCount
+ )
+}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/model/response/ChattingFilterListResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/ChattingFilterListResponse.kt
new file mode 100644
index 00000000..7028d84a
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/ChattingFilterListResponse.kt
@@ -0,0 +1,20 @@
+package com.sixkids.data.model.response
+
+import com.sixkids.model.ChatFilterWord
+
+data class ChattingFilterListResponse(
+ val hasNext: Boolean,
+ val words: List,
+)
+
+data class ChattingFilterResponse(
+ val id: Long,
+ val badWord: String,
+)
+
+fun ChattingFilterResponse.toModel(): ChatFilterWord {
+ return ChatFilterWord(
+ id = id,
+ badWord = badWord,
+ )
+}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/model/response/ClassSummaryResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/ClassSummaryResponse.kt
new file mode 100644
index 00000000..8dfefefc
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/ClassSummaryResponse.kt
@@ -0,0 +1,30 @@
+package com.sixkids.data.model.response
+
+import com.sixkids.model.ClassSummary
+import com.sixkids.model.MemberSimpleClassSummary
+
+data class ClassSummaryResponse(
+ val challengeCounts: List,
+ val relayCounts: List,
+ val postCounts: List,
+)
+
+data class ClassSummaryMemberResponse(
+ val member: MemberSimpleInfoResponse,
+ val count: Int
+)
+
+fun ClassSummaryMemberResponse.toModel(): MemberSimpleClassSummary {
+ return MemberSimpleClassSummary(
+ member.toModel(),
+ count
+ )
+}
+
+internal fun ClassSummaryResponse.toModel(): ClassSummary {
+ return ClassSummary(
+ challengeCounts.map { it.toModel() },
+ relayCounts.map { it.toModel() },
+ postCounts.map { it.toModel() }
+ )
+}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/model/response/GroupMatchingRoomResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/GroupMatchingRoomResponse.kt
new file mode 100644
index 00000000..ff44d792
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/GroupMatchingRoomResponse.kt
@@ -0,0 +1,14 @@
+package com.sixkids.data.model.response
+
+import com.sixkids.model.MatchingRoom
+
+data class GroupMatchingRoomResponse(
+ val dataKey: String,
+ val minCount: Int
+)
+
+internal fun GroupMatchingRoomResponse.toModel(): MatchingRoom =
+ MatchingRoom(
+ dataKey = dataKey,
+ minCount = minCount
+ )
diff --git a/android/data/src/main/java/com/sixkids/data/model/response/GroupMatchingSuccessResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/GroupMatchingSuccessResponse.kt
new file mode 100644
index 00000000..faba86d8
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/GroupMatchingSuccessResponse.kt
@@ -0,0 +1,14 @@
+package com.sixkids.data.model.response
+
+import com.sixkids.model.ChallengeGroup
+
+data class GroupMatchingSuccessResponse(
+ val members: List,
+ val headCount: Int
+)
+
+internal fun GroupMatchingSuccessResponse.toModel(): ChallengeGroup =
+ ChallengeGroup(
+ headCount = headCount,
+ memberList = members.map { it.toModel() }
+ )
diff --git a/android/data/src/main/java/com/sixkids/data/model/response/GroupResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/GroupResponse.kt
new file mode 100644
index 00000000..98260370
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/GroupResponse.kt
@@ -0,0 +1,16 @@
+package com.sixkids.data.model.response
+
+import com.sixkids.model.Group
+
+data class GroupResponse(
+ val id: Long,
+ val headCount: Int,
+ val leaderId: Long,
+ val students: List,
+)
+
+internal fun GroupResponse.toModel() = Group(
+ id = id,
+ leaderId = leaderId,
+ studentList = students.map { it.toModel() }
+)
diff --git a/android/data/src/main/java/com/sixkids/data/model/response/MemberInfoResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/MemberInfoResponse.kt
index ac7a4385..2b3c3375 100644
--- a/android/data/src/main/java/com/sixkids/data/model/response/MemberInfoResponse.kt
+++ b/android/data/src/main/java/com/sixkids/data/model/response/MemberInfoResponse.kt
@@ -1,5 +1,6 @@
package com.sixkids.data.model.response
+import com.sixkids.model.MemberSimple
import com.sixkids.model.UserInfo
data class MemberInfoResponse(
@@ -10,6 +11,12 @@ data class MemberInfoResponse(
val role: String
)
+data class MemberSimpleInfoResponse(
+ val id: Long,
+ val name: String,
+ val photo: String?
+)
+
internal fun MemberInfoResponse.toModel(): UserInfo {
return UserInfo(
id = id,
@@ -18,4 +25,12 @@ internal fun MemberInfoResponse.toModel(): UserInfo {
photo = photo,
role = role
)
+}
+
+internal fun MemberSimpleInfoResponse.toModel(): MemberSimple {
+ return MemberSimple(
+ id = id,
+ name = name,
+ photo = photo?: ""
+ )
}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/model/response/OrganizationInviteCodeResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/OrganizationInviteCodeResponse.kt
new file mode 100644
index 00000000..c5d435cf
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/OrganizationInviteCodeResponse.kt
@@ -0,0 +1,5 @@
+package com.sixkids.data.model.response
+
+data class OrganizationInviteCodeResponse(
+ val code: String
+)
diff --git a/android/data/src/main/java/com/sixkids/data/model/response/OrganizationMemberResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/OrganizationMemberResponse.kt
new file mode 100644
index 00000000..2ff5854f
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/OrganizationMemberResponse.kt
@@ -0,0 +1,15 @@
+package com.sixkids.data.model.response
+
+import com.sixkids.model.MemberSimple
+
+data class OrganizationMemberResponse(
+ val id: Int,
+ val name: String,
+ val photo: String
+)
+
+internal fun OrganizationMemberResponse.toModel() = MemberSimple(
+ id = id.toLong(),
+ name = name,
+ photo = photo
+)
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/model/response/OrganizationNameResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/OrganizationNameResponse.kt
new file mode 100644
index 00000000..c1c90717
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/OrganizationNameResponse.kt
@@ -0,0 +1,5 @@
+package com.sixkids.data.model.response
+
+data class OrganizationNameResponse(
+ val name: String
+)
diff --git a/android/data/src/main/java/com/sixkids/data/model/response/PostDetailResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/PostDetailResponse.kt
new file mode 100644
index 00000000..2cbfeeda
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/PostDetailResponse.kt
@@ -0,0 +1,73 @@
+package com.sixkids.data.model.response
+
+import com.sixkids.model.Comment
+import com.sixkids.model.PostDetail
+import com.sixkids.model.Recomment
+import com.squareup.moshi.Json
+import java.time.LocalDateTime
+
+data class PostDetailResponse(
+ @Json(name = "member")
+ val writer: MemberSimpleInfoResponse,
+ val createTime: LocalDateTime,
+ val updateTime: LocalDateTime?,
+ val title: String,
+ val content: String,
+ @Json(name = "path")
+ val photoUrl: String?,
+ val comments: List,
+)
+
+data class CommentResponse(
+ val id: Long,
+ @Json(name = "member")
+ val writer: MemberSimpleInfoResponse,
+ val content: String,
+ val createTime: LocalDateTime,
+ val updateTime: LocalDateTime?,
+ @Json(name = "children")
+ val recommentList: List,
+)
+
+data class RecommentResponse(
+ val id: Long,
+ @Json(name = "member")
+ val writer: MemberSimpleInfoResponse,
+ val content: String,
+ val createTime: LocalDateTime,
+ val updateTime: LocalDateTime?,
+ val parentId: Long,
+)
+
+fun PostDetailResponse.toModel(): PostDetail {
+ return PostDetail(
+ createTime = createTime,
+ title = title,
+ content = content,
+ imageUri = photoUrl ?: "",
+ comments = comments.map { it.toModel() },
+ writeMember = writer.toModel()
+ )
+}
+
+fun CommentResponse.toModel(): Comment{
+ return Comment(
+ id = id,
+ content = content,
+ createTime = createTime,
+ updateTime = updateTime,
+ member = writer.toModel(),
+ recomments = recommentList.map { it.toModel() }
+ )
+}
+
+fun RecommentResponse.toModel(): Recomment {
+ return Recomment(
+ id = id,
+ content = content,
+ createTime = createTime,
+ updateTime = updateTime,
+ parentId = parentId,
+ member = writer.toModel()
+ )
+}
diff --git a/android/data/src/main/java/com/sixkids/data/model/response/PostListResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/PostListResponse.kt
new file mode 100644
index 00000000..bce48f60
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/PostListResponse.kt
@@ -0,0 +1,31 @@
+package com.sixkids.data.model.response
+
+import com.sixkids.model.Post
+import com.squareup.moshi.Json
+import java.time.LocalDateTime
+
+data class PostListResponse(
+ val page: Int,
+ val size: Int,
+ val hasNextPage: Boolean,
+ val posts: List
+)
+
+data class PostResponse(
+ val id: Long,
+ val title: String,
+ val author: String,
+ @Json(name = "CommentCount")
+ val commentCount: Int,
+ val createTime: LocalDateTime = LocalDateTime.now(),
+)
+
+fun PostResponse.toModel(): Post {
+ return Post(
+ id = id,
+ title = title,
+ writer = author,
+ commentCount = commentCount,
+ time = createTime
+ )
+}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/model/response/RankResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/RankResponse.kt
new file mode 100644
index 00000000..151ea29a
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/RankResponse.kt
@@ -0,0 +1,18 @@
+package com.sixkids.data.model.response
+
+import com.sixkids.model.MemberRankItem
+
+data class RankResponse(
+ val name: String,
+ val point: Int,
+)
+
+fun List.toModel(): List {
+ return this.mapIndexed { index, rankResponse ->
+ MemberRankItem(
+ rank = index + 1,
+ name = rankResponse.name,
+ exp = rankResponse.point,
+ )
+ }
+}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/model/response/ReceiveRelayResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/ReceiveRelayResponse.kt
new file mode 100644
index 00000000..838c7573
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/ReceiveRelayResponse.kt
@@ -0,0 +1,15 @@
+package com.sixkids.data.model.response
+
+data class ReceiveRelayResponse(
+ val senderName: String,
+ val question: String,
+ val lastStatus: Boolean,
+ val demerit: Int,
+)
+
+internal fun ReceiveRelayResponse.toModel() = com.sixkids.model.RelayReceive(
+ senderName = senderName,
+ question = question,
+ lastStatus = lastStatus,
+ demerit = demerit,
+)
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/model/response/RelayDetailResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/RelayDetailResponse.kt
new file mode 100644
index 00000000..939259f9
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/RelayDetailResponse.kt
@@ -0,0 +1,17 @@
+package com.sixkids.data.model.response
+
+import com.sixkids.model.RelayDetail
+
+data class RelayDetailResponse(
+ val relaySimple: RelayResponse,
+ val runners: List
+)
+
+internal fun RelayDetailResponse.toModel() = RelayDetail(
+ id = relaySimple.id,
+ startTime = relaySimple.startTime,
+ endTime = relaySimple.endTime,
+ lastTurn = relaySimple.lastTurn,
+ lastMemberName = relaySimple.lastMemberName,
+ runnerList = runners.map { it.toModel() }
+)
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/model/response/RelayHistoryResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/RelayHistoryResponse.kt
new file mode 100644
index 00000000..4b060b33
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/RelayHistoryResponse.kt
@@ -0,0 +1,29 @@
+package com.sixkids.data.model.response
+
+import com.sixkids.model.Relay
+import java.time.LocalDateTime
+
+data class RelayHistoryResponse(
+ val page: Int,
+ val size: Int,
+ val last: Boolean,
+ val totalCount: Int,
+ val relays: List
+)
+
+data class RelayResponse(
+ val id : Long,
+ val startTime : LocalDateTime = LocalDateTime.now(),
+ val endTime : LocalDateTime = LocalDateTime.now(),
+ val lastTurn : Int,
+ val lastMemberName : String
+)
+
+internal fun RelayResponse.toModel(totalCount: Int) = Relay(
+ id = id,
+ startTime = startTime,
+ endTime = endTime,
+ lastTurn = lastTurn,
+ lastMemberName = lastMemberName,
+ totalCount = totalCount
+)
diff --git a/android/data/src/main/java/com/sixkids/data/model/response/ReportResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/ReportResponse.kt
new file mode 100644
index 00000000..0f15130e
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/ReportResponse.kt
@@ -0,0 +1,25 @@
+package com.sixkids.data.model.response
+
+import com.sixkids.model.AcceptStatus
+import com.sixkids.model.Report
+import java.time.LocalDateTime
+
+data class ReportResponse(
+ val id: Long,
+ val group: GroupResponse,
+ val startTime: LocalDateTime,
+ val endTime: LocalDateTime,
+ val file: String,
+ val content: String,
+ val acceptStatus: String,
+)
+
+internal fun ReportResponse.toModel() = Report(
+ id = id,
+ group = group.toModel(),
+ startTime = startTime,
+ endTime = endTime,
+ file = file,
+ content = content,
+ acceptStatus = AcceptStatus.valueOf(acceptStatus),
+)
diff --git a/android/data/src/main/java/com/sixkids/data/model/response/RunnerResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/RunnerResponse.kt
new file mode 100644
index 00000000..019161eb
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/RunnerResponse.kt
@@ -0,0 +1,37 @@
+package com.sixkids.data.model.response
+
+import com.sixkids.model.MemberSimple
+import com.sixkids.model.RelayQuestion
+import java.time.LocalDateTime
+
+data class RunnerResponse(
+ val id: Long,
+ val turn: Int,
+ val member: MemberSimpleResponse,
+ val time: LocalDateTime = LocalDateTime.now(),
+ val question: String,
+ val endStatus: Boolean
+)
+
+data class MemberSimpleResponse(
+ val id: Long,
+ val name: String,
+ val photo: String,
+)
+
+internal fun RunnerResponse.toModel() = RelayQuestion(
+ id = id,
+ memberId = member.id,
+ memberName = member.name,
+ memberPhoto = member.photo,
+ time = time,
+ question = question,
+ turn = turn,
+ endStatus = endStatus,
+)
+
+internal fun MemberSimpleResponse.toModel() = MemberSimple(
+ id = id,
+ name = name,
+ photo = photo,
+)
diff --git a/android/data/src/main/java/com/sixkids/data/model/response/RunningChallengeByStudentResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/RunningChallengeByStudentResponse.kt
new file mode 100644
index 00000000..4d759292
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/RunningChallengeByStudentResponse.kt
@@ -0,0 +1,27 @@
+package com.sixkids.data.model.response
+
+import com.sixkids.model.GroupType
+import com.sixkids.model.RunningChallengeByStudent
+import com.squareup.moshi.Json
+import java.time.LocalDateTime
+
+data class RunningChallengeByStudentResponse(
+ @Json(name = "challengeSimpleDTO")
+ val challenge: ChallengeResponse,
+ val leaderStatus: Boolean?,
+ @Json(name = "memberNames")
+ val memberList: List?,
+ val type: String,
+ val createTime: LocalDateTime?,
+ val endStatus: Boolean?
+)
+
+internal fun RunningChallengeByStudentResponse.toModel() =
+ RunningChallengeByStudent(
+ challenge = challenge.toModel(),
+ leaderStatus = leaderStatus,
+ memberNames = memberList?.map { it.toModel() } ?: emptyList(),
+ type = GroupType.valueOf(type),
+ createTime = createTime,
+ endStatus = endStatus
+ )
diff --git a/android/data/src/main/java/com/sixkids/data/model/response/RunningRelayResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/RunningRelayResponse.kt
new file mode 100644
index 00000000..85699e90
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/RunningRelayResponse.kt
@@ -0,0 +1,22 @@
+package com.sixkids.data.model.response
+
+import com.sixkids.model.RunningRelay
+import java.time.LocalDateTime
+
+data class RunningRelayResponse(
+ val id: Long,
+ val startTime: LocalDateTime = LocalDateTime.now(),
+ val currentMemberName : String,
+ val currentTurn : Int,
+ val totalTurn : Int,
+ val myTurnStatus : Boolean
+)
+
+internal fun RunningRelayResponse.toModel() = RunningRelay(
+ id = id,
+ startTime = startTime,
+ curMemberNickname = currentMemberName,
+ doneMemberCount = currentTurn,
+ totalMemberCount = totalTurn,
+ myTurnStatus = myTurnStatus
+)
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/model/response/SendRelayResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/SendRelayResponse.kt
new file mode 100644
index 00000000..2976a8c0
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/SendRelayResponse.kt
@@ -0,0 +1,13 @@
+package com.sixkids.data.model.response
+
+import com.sixkids.model.RelaySend
+
+data class SendRelayResponse(
+ val prevMemberName: String,
+ val prevQuestion: String
+)
+
+internal fun SendRelayResponse.toModel() = RelaySend(
+ prevMemberName = prevMemberName,
+ prevQuestion = prevQuestion
+)
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/model/response/StudentDetailResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/StudentDetailResponse.kt
new file mode 100644
index 00000000..0028082b
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/StudentDetailResponse.kt
@@ -0,0 +1,27 @@
+package com.sixkids.data.model.response
+
+import com.sixkids.model.MemberDetail
+
+data class StudentDetailResponse(
+ val name: String = "",
+ val photo: String = "",
+ val isolationPoint: Double = 0.0,
+ val isolationRank: Int = -1,
+ val exp: Int = -1,
+ val challengeCount: Int = -1,
+ val relayCount: Int = -1,
+ val postCount: Int = -1,
+)
+
+internal fun StudentDetailResponse.toModel(): MemberDetail {
+ return MemberDetail(
+ name = this.name,
+ photo = this.photo,
+ isolationPoint = this.isolationPoint,
+ isolationRank = this.isolationRank,
+ exp = this.exp,
+ challengeCount = this.challengeCount,
+ relayCount = this.relayCount,
+ postCount = this.postCount,
+ )
+}
diff --git a/android/data/src/main/java/com/sixkids/data/model/response/StudentHomeResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/StudentHomeResponse.kt
new file mode 100644
index 00000000..b27d861f
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/StudentHomeResponse.kt
@@ -0,0 +1,23 @@
+package com.sixkids.data.model.response
+
+import com.sixkids.model.StudentHomeInfo
+import com.squareup.moshi.Json
+
+data class StudentHomeResponse(
+ val name: String,
+ val photo: String,
+ @Json(name = "organizationName")
+ val className: String,
+ val exp: Int,
+ val notifyCount: Int,
+ val relations: List,
+)
+
+internal fun StudentHomeResponse.toModel() = StudentHomeInfo(
+ name = name,
+ photo = photo,
+ className = className,
+ exp = exp,
+ notifyCount = notifyCount,
+ relations = relations.map { it.toModel() },
+)
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/model/response/StudentRelationDetailResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/StudentRelationDetailResponse.kt
new file mode 100644
index 00000000..298e2c0f
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/StudentRelationDetailResponse.kt
@@ -0,0 +1,23 @@
+package com.sixkids.data.model.response
+
+import com.sixkids.model.StudentRelation
+
+data class StudentRelationDetailResponse(
+ val memberId: Long = -1,
+ val memberName: String = "",
+ val relationPoint: Int = 0,
+ val tagGreetingCount: Int = 0,
+ val groupCount: Int = 0,
+ val receiveCount: Int = 0,
+ val sendCount: Int = 0,
+)
+
+internal fun StudentRelationDetailResponse.toModel() = StudentRelation(
+ id = memberId,
+ name = memberName,
+ relationPoint = relationPoint,
+ tagGreetingCount = tagGreetingCount,
+ groupCount = groupCount,
+ receiveCount = receiveCount,
+ sendCount = sendCount,
+)
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/model/response/StudentResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/StudentResponse.kt
new file mode 100644
index 00000000..4dd45dab
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/StudentResponse.kt
@@ -0,0 +1,28 @@
+package com.sixkids.data.model.response
+
+import com.sixkids.model.MemberSimple
+import com.sixkids.model.MemberSimpleWithScore
+import com.squareup.moshi.Json
+
+data class StudentResponse(
+ val id: Long,
+ val name: String,
+ val photo: String,
+)
+
+data class StudentWithRelationScoreResponse(
+ @Json(name = "member")
+ val student: StudentResponse,
+ val relationPoint: Int,
+)
+
+internal fun StudentResponse.toModel() = MemberSimple(
+ id = id,
+ name = name,
+ photo = photo,
+)
+
+internal fun StudentWithRelationScoreResponse.toModel() = MemberSimpleWithScore(
+ memberSimple = student.toModel(),
+ relationPoint = relationPoint,
+)
diff --git a/android/data/src/main/java/com/sixkids/data/repository/challenge/ChallengeRepositoryImpl.kt b/android/data/src/main/java/com/sixkids/data/repository/challenge/ChallengeRepositoryImpl.kt
index a30d019a..69b90ca2 100644
--- a/android/data/src/main/java/com/sixkids/data/repository/challenge/ChallengeRepositoryImpl.kt
+++ b/android/data/src/main/java/com/sixkids/data/repository/challenge/ChallengeRepositoryImpl.kt
@@ -1,15 +1,20 @@
package com.sixkids.data.repository.challenge
+import android.util.Log
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import com.sixkids.data.api.ChallengeService
+import com.sixkids.data.model.response.toModel
import com.sixkids.data.repository.challenge.remote.ChallengeHistoryPagingSource
import com.sixkids.data.repository.challenge.remote.ChallengeHistoryPagingSource.Companion.DEFAULT_SIZE
import com.sixkids.data.repository.challenge.remote.ChallengeRemoteDataSourceImpl
import com.sixkids.domain.repository.ChallengeRepository
+import com.sixkids.model.AcceptStatus
import com.sixkids.model.Challenge
+import com.sixkids.model.ChallengeDetail
import com.sixkids.model.GroupSimple
+import com.sixkids.model.RunningChallengeByStudent
import kotlinx.coroutines.flow.Flow
import java.time.LocalDateTime
import javax.inject.Inject
@@ -36,24 +41,41 @@ class ChallengeRepositoryImpl @Inject constructor(
override suspend fun getRunningChallenge(organizationId: Int) =
challengeRemoteDataSourceImpl.getRunningChallenges(organizationId)
+ override suspend fun getRunningChallengesByStudent(
+ organizationId: Int
+ ): RunningChallengeByStudent {
+ val a = challengeRemoteDataSourceImpl.getRunningChallengesByStudent(organizationId)
+ Log.d("ttt", "getRunningChallengesByStudent: $a")
+ Log.d("ttt", "getRunningChallengesByStudent: ${a.toModel()}")
+ return a.toModel()
+ }
+
override suspend fun createChallenge(
organizationId: Int,
title: String,
content: String,
startTime: LocalDateTime,
endTime: LocalDateTime,
- reword: Int,
+ reward: Int,
minCount: Int,
groups: List
- ) = challengeRemoteDataSourceImpl.createChallenge(
+ ): Long = challengeRemoteDataSourceImpl.createChallenge(
organizationId,
title,
content,
startTime,
endTime,
- reword,
+ reward,
minCount,
groups
)
+ override suspend fun getChallengeSimple(challengeId: Int): Challenge =
+ challengeRemoteDataSourceImpl.getChallengeSimple(challengeId)
+
+ override suspend fun getChallengeDetail(challengeId: Long, groupId: Long?): ChallengeDetail =
+ challengeRemoteDataSourceImpl.getChallengeDetail(challengeId, groupId)
+
+ override suspend fun gradingChallenge(reportId: Long, acceptStatus: AcceptStatus) =
+ challengeRemoteDataSourceImpl.gradingChallenge(reportId, acceptStatus)
}
diff --git a/android/data/src/main/java/com/sixkids/data/repository/challenge/remote/ChallengeHistoryPagingSource.kt b/android/data/src/main/java/com/sixkids/data/repository/challenge/remote/ChallengeHistoryPagingSource.kt
index 85277db1..ac10c0ad 100644
--- a/android/data/src/main/java/com/sixkids/data/repository/challenge/remote/ChallengeHistoryPagingSource.kt
+++ b/android/data/src/main/java/com/sixkids/data/repository/challenge/remote/ChallengeHistoryPagingSource.kt
@@ -1,5 +1,6 @@
package com.sixkids.data.repository.challenge.remote
+import android.util.Log
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.sixkids.data.api.ChallengeService
@@ -29,7 +30,9 @@ class ChallengeHistoryPagingSource @Inject constructor(
page,
DEFAULT_SIZE
)
- val challengeHistory = response.getOrThrow().data.challenges.map { it.toModel() }
+ val challengeHistory = response.getOrThrow().data.let { challengeResponse ->
+ challengeResponse.challenges.map { it.toModel(challengeResponse.totalCount) }
+ }
LoadResult.Page(
data = challengeHistory,
diff --git a/android/data/src/main/java/com/sixkids/data/repository/challenge/remote/ChallengeRemoteDataSource.kt b/android/data/src/main/java/com/sixkids/data/repository/challenge/remote/ChallengeRemoteDataSource.kt
index e7e0a7b1..f6868748 100644
--- a/android/data/src/main/java/com/sixkids/data/repository/challenge/remote/ChallengeRemoteDataSource.kt
+++ b/android/data/src/main/java/com/sixkids/data/repository/challenge/remote/ChallengeRemoteDataSource.kt
@@ -1,5 +1,9 @@
package com.sixkids.data.repository.challenge.remote
+import com.sixkids.data.model.response.RunningChallengeByStudentResponse
+import com.sixkids.model.AcceptStatus
+import com.sixkids.model.Challenge
+import com.sixkids.model.ChallengeDetail
import com.sixkids.model.GroupSimple
import com.sixkids.model.RunningChallenge
import java.time.LocalDateTime
@@ -7,14 +11,27 @@ import java.time.LocalDateTime
interface ChallengeRemoteDataSource {
suspend fun getRunningChallenges(organizationId: Int): RunningChallenge
+
+ suspend fun getRunningChallengesByStudent(organizationId: Int): RunningChallengeByStudentResponse
+
suspend fun createChallenge(
organizationId: Int,
title: String,
content: String,
startTime: LocalDateTime,
endTime: LocalDateTime,
- reword: Int,
+ reward: Int,
minCount: Int,
groups: List
): Long
+
+ suspend fun getChallengeDetail(challengeId: Long, groupId: Long?): ChallengeDetail
+
+ suspend fun gradingChallenge(reportId: Long, acceptStatus: AcceptStatus)
+
+ suspend fun getChallengeSimple(
+ challengeId: Int
+ ): Challenge
+
+ suspend fun connectSse()
}
diff --git a/android/data/src/main/java/com/sixkids/data/repository/challenge/remote/ChallengeRemoteDataSourceImpl.kt b/android/data/src/main/java/com/sixkids/data/repository/challenge/remote/ChallengeRemoteDataSourceImpl.kt
index 049e326f..a4461fac 100644
--- a/android/data/src/main/java/com/sixkids/data/repository/challenge/remote/ChallengeRemoteDataSourceImpl.kt
+++ b/android/data/src/main/java/com/sixkids/data/repository/challenge/remote/ChallengeRemoteDataSourceImpl.kt
@@ -3,7 +3,11 @@ package com.sixkids.data.repository.challenge.remote
import com.sixkids.data.api.ChallengeService
import com.sixkids.data.model.request.ChallengeCreateRequest
import com.sixkids.data.model.request.GroupRequest
+import com.sixkids.data.model.response.RunningChallengeByStudentResponse
import com.sixkids.data.model.response.toModel
+import com.sixkids.model.AcceptStatus
+import com.sixkids.model.Challenge
+import com.sixkids.model.ChallengeDetail
import com.sixkids.model.GroupSimple
import java.time.LocalDateTime
import javax.inject.Inject
@@ -14,23 +18,29 @@ class ChallengeRemoteDataSourceImpl @Inject constructor(
override suspend fun getRunningChallenges(organizationId: Int) =
challengeService.getRunningChallenge(organizationId).getOrThrow().data.toModel()
+ override suspend fun getRunningChallengesByStudent(
+ organizationId: Int
+ ): RunningChallengeByStudentResponse =
+ challengeService.getRunningChallengeByStudent(organizationId).getOrThrow().data
+
+
override suspend fun createChallenge(
organizationId: Int,
title: String,
content: String,
startTime: LocalDateTime,
endTime: LocalDateTime,
- reword: Int,
+ reward: Int,
minCount: Int,
groups: List,
- ) = challengeService.createChallenge(
+ ): Long = challengeService.createChallenge(
ChallengeCreateRequest(
organizationId = organizationId,
title = title,
content = content,
startTime = startTime,
endTime = endTime,
- reword = reword,
+ reward = reward,
minCount = minCount,
groups = groups.map {
GroupRequest(
@@ -41,4 +51,17 @@ class ChallengeRemoteDataSourceImpl @Inject constructor(
}
)
).getOrThrow().data
+
+ override suspend fun getChallengeSimple(challengeId: Int): Challenge =
+ challengeService.getChallengeSimple(challengeId).getOrThrow().data.toModel()
+
+ override suspend fun connectSse() {
+
+ }
+
+ override suspend fun getChallengeDetail(challengeId: Long, groupId: Long?): ChallengeDetail =
+ challengeService.getChallengeDetail(challengeId, groupId).getOrThrow().data.toModel()
+
+ override suspend fun gradingChallenge(reportId: Long, acceptStatus: AcceptStatus) =
+ challengeService.gradingChallenge(reportId, acceptStatus.name).getOrThrow().data
}
diff --git a/android/data/src/main/java/com/sixkids/data/repository/chatting/ChattingRepositoryImpl.kt b/android/data/src/main/java/com/sixkids/data/repository/chatting/ChattingRepositoryImpl.kt
new file mode 100644
index 00000000..2f12b747
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/chatting/ChattingRepositoryImpl.kt
@@ -0,0 +1,29 @@
+package com.sixkids.data.repository.chatting
+
+import androidx.paging.Pager
+import androidx.paging.PagingConfig
+import androidx.paging.PagingData
+import com.sixkids.data.api.ChattingService
+import com.sixkids.data.repository.chatting.remote.ChatHistoryPagingSource
+import com.sixkids.data.repository.chatting.remote.ChattingRemoteDataSource
+import com.sixkids.domain.repository.ChattingRepository
+import com.sixkids.model.Chat
+import kotlinx.coroutines.flow.Flow
+import javax.inject.Inject
+
+class ChattingRepositoryImpl @Inject constructor(
+ private val chattingService: ChattingService,
+ private val chattingRemoteDataSource: ChattingRemoteDataSource
+) : ChattingRepository {
+ override suspend fun getChattingList(roomId: Long): Flow> =
+ Pager(
+ config = PagingConfig(ChatHistoryPagingSource.DEFAULT_SIZE),
+ pagingSourceFactory = {
+ ChatHistoryPagingSource(
+ chattingService,
+ roomId
+ )
+ }
+ ).flow
+
+}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/repository/chatting/remote/ChatHistoryPagingSource.kt b/android/data/src/main/java/com/sixkids/data/repository/chatting/remote/ChatHistoryPagingSource.kt
new file mode 100644
index 00000000..0bc535f5
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/chatting/remote/ChatHistoryPagingSource.kt
@@ -0,0 +1,49 @@
+package com.sixkids.data.repository.chatting.remote
+
+import android.util.Log
+import androidx.paging.PagingSource
+import androidx.paging.PagingState
+import com.sixkids.data.api.ChattingService
+import com.sixkids.data.model.response.toModel
+import com.sixkids.data.repository.challenge.remote.ChallengeHistoryPagingSource
+import com.sixkids.model.Chat
+import javax.inject.Inject
+
+private const val TAG = "D107"
+class ChatHistoryPagingSource @Inject constructor(
+ private val chattingService: ChattingService,
+ private val roomId: Long,
+) : PagingSource() {
+ override fun getRefreshKey(state: PagingState): Int? {
+ return state.anchorPosition?.let { anchorPosition ->
+ state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
+ ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
+ }
+ }
+
+ override suspend fun load(params: LoadParams): LoadResult {
+ val page = params.key ?: 0
+ return try {
+ val response = chattingService.getChatList(
+ roomId,
+ page,
+ DEFAULT_SIZE
+ )
+ val chatHistory = response.getOrThrow().data.messages.map { it.toModel() }
+ Log.d(TAG, "load: ${page} -> ${chatHistory.map { it.content }}")
+ LoadResult.Page(
+ data = chatHistory,
+ prevKey = if (chatHistory.isEmpty()) null else page.plus(1),
+ nextKey = null
+ )
+ } catch (e: Exception) {
+ Log.d(TAG, "load: ${e.message}")
+ LoadResult.Error(e)
+ }
+ }
+
+ companion object {
+ const val DEFAULT_SIZE = 500
+ }
+
+}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/repository/chatting/remote/ChattingRemoteDataSource.kt b/android/data/src/main/java/com/sixkids/data/repository/chatting/remote/ChattingRemoteDataSource.kt
new file mode 100644
index 00000000..43e7ed6f
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/chatting/remote/ChattingRemoteDataSource.kt
@@ -0,0 +1,6 @@
+package com.sixkids.data.repository.chatting.remote
+
+import com.sixkids.model.Chat
+
+interface ChattingRemoteDataSource {
+}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/repository/chatting/remote/ChattingRemoteDataSourceImpl.kt b/android/data/src/main/java/com/sixkids/data/repository/chatting/remote/ChattingRemoteDataSourceImpl.kt
new file mode 100644
index 00000000..27729d26
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/chatting/remote/ChattingRemoteDataSourceImpl.kt
@@ -0,0 +1,10 @@
+package com.sixkids.data.repository.chatting.remote
+
+import com.sixkids.data.api.ChattingService
+import javax.inject.Inject
+
+class ChattingRemoteDataSourceImpl @Inject constructor(
+ private val chattingService: ChattingService
+) : ChattingRemoteDataSource{
+
+}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/repository/chattingfilter/ChattingFilterRepositoryImpl.kt b/android/data/src/main/java/com/sixkids/data/repository/chattingfilter/ChattingFilterRepositoryImpl.kt
new file mode 100644
index 00000000..6fc1b9ac
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/chattingfilter/ChattingFilterRepositoryImpl.kt
@@ -0,0 +1,54 @@
+package com.sixkids.data.repository.chattingfilter
+
+import androidx.paging.Pager
+import androidx.paging.PagingConfig
+import androidx.paging.PagingData
+import androidx.paging.map
+import com.sixkids.data.api.ChatFilterService
+import com.sixkids.data.model.response.toModel
+import com.sixkids.data.repository.chattingfilter.remote.ChattingFilterPagingSource
+import com.sixkids.data.repository.chattingfilter.remote.ChattingFilterRemoteDataSource
+import com.sixkids.domain.repository.ChattingFilterRepository
+import com.sixkids.model.ChatFilterWord
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import javax.inject.Inject
+
+class ChattingFilterRepositoryImpl @Inject constructor(
+ private val chattingFilterRemoteDataSource: ChattingFilterRemoteDataSource,
+ private val chatFilterService: ChatFilterService
+) : ChattingFilterRepository {
+ override suspend fun getChattingFilters(
+ organizationId: Int
+ ): Flow> {
+ return Pager(
+ config = PagingConfig(ChattingFilterPagingSource.DEFAULT_SIZE),
+ pagingSourceFactory = {
+ ChattingFilterPagingSource(
+ chatFilterService,
+ organizationId
+ )
+ }
+ ).flow.map {pagingData ->
+ pagingData.map {
+ chattingFilterResponse -> chattingFilterResponse.toModel()
+ }
+ }
+ }
+
+ override suspend fun deleteChatFilter(id: Long): Boolean {
+ return chattingFilterRemoteDataSource.deleteChatFilter(id)
+ }
+
+ override suspend fun createChatFilter(organizationId: Long, badWord: String): Long {
+ return chattingFilterRemoteDataSource.createChatFilter(organizationId, badWord)
+ }
+
+ override suspend fun updateChatFilter(
+ organizationId: Long,
+ id: Long,
+ badWord: String
+ ): Boolean {
+ return chattingFilterRemoteDataSource.updateChatFilter(organizationId, id, badWord)
+ }
+}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/repository/chattingfilter/remote/ChattingFilterPagingSource.kt b/android/data/src/main/java/com/sixkids/data/repository/chattingfilter/remote/ChattingFilterPagingSource.kt
new file mode 100644
index 00000000..2b11222e
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/chattingfilter/remote/ChattingFilterPagingSource.kt
@@ -0,0 +1,44 @@
+package com.sixkids.data.repository.chattingfilter.remote
+
+import androidx.paging.PagingSource
+import androidx.paging.PagingState
+import com.sixkids.data.api.ChatFilterService
+import com.sixkids.data.api.ChattingService
+import com.sixkids.data.model.response.ChattingFilterListResponse
+import com.sixkids.data.model.response.ChattingFilterResponse
+import javax.inject.Inject
+
+class ChattingFilterPagingSource @Inject constructor(
+ private val chatFilterService: ChatFilterService,
+ private val organizationId: Int,
+ ): PagingSource() {
+ override fun getRefreshKey(state: PagingState): Int? {
+ return state.anchorPosition?.let { anchorPosition ->
+ state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
+ ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
+ }
+ }
+
+ override suspend fun load(params: LoadParams): LoadResult {
+ val page = params.key ?: 0
+ return try {
+ val response = chatFilterService.getChatFilters(
+ organizationId,
+ page,
+ DEFAULT_SIZE
+ ).getOrThrow().data.words
+
+ LoadResult.Page(
+ data = response,
+ prevKey = if (page == 0) null else page.minus(1),
+ nextKey = if (response.isEmpty()) null else page.plus(1)
+ )
+ } catch (e: Exception) {
+ LoadResult.Error(e)
+ }
+ }
+
+ companion object {
+ const val DEFAULT_SIZE = 30
+ }
+}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/repository/chattingfilter/remote/ChattingFilterRemoteDataSource.kt b/android/data/src/main/java/com/sixkids/data/repository/chattingfilter/remote/ChattingFilterRemoteDataSource.kt
new file mode 100644
index 00000000..f0d5c449
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/chattingfilter/remote/ChattingFilterRemoteDataSource.kt
@@ -0,0 +1,22 @@
+package com.sixkids.data.repository.chattingfilter.remote
+
+import com.sixkids.data.model.response.ChattingFilterListResponse
+
+interface ChattingFilterRemoteDataSource {
+
+ suspend fun deleteChatFilter(
+ id: Long
+ ): Boolean
+
+ suspend fun createChatFilter(
+ organizationId: Long,
+ badWord: String,
+ ): Long
+
+ suspend fun updateChatFilter(
+ organizationId: Long,
+ id: Long,
+ badWord: String,
+ ): Boolean
+
+}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/repository/chattingfilter/remote/ChattingFilterRemoteDataSourceImpl.kt b/android/data/src/main/java/com/sixkids/data/repository/chattingfilter/remote/ChattingFilterRemoteDataSourceImpl.kt
new file mode 100644
index 00000000..74102695
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/chattingfilter/remote/ChattingFilterRemoteDataSourceImpl.kt
@@ -0,0 +1,33 @@
+package com.sixkids.data.repository.chattingfilter.remote
+
+import com.sixkids.data.api.ChatFilterService
+import com.sixkids.data.model.request.ChatFilterRequest
+import com.sixkids.data.model.response.ChattingFilterListResponse
+import javax.inject.Inject
+
+class ChattingFilterRemoteDataSourceImpl @Inject constructor(
+ private val chattingFilterService: ChatFilterService
+) : ChattingFilterRemoteDataSource {
+
+ override suspend fun deleteChatFilter(id: Long): Boolean {
+ return chattingFilterService.deleteChatFilter(id).getOrThrow().data
+ }
+
+ override suspend fun createChatFilter(organizationId: Long, badWord: String): Long {
+ return chattingFilterService.createChatFilter(organizationId, ChatFilterRequest(badWord))
+ .getOrThrow().data
+ }
+
+ override suspend fun updateChatFilter(
+ organizationId: Long,
+ id: Long,
+ badWord: String
+ ): Boolean {
+ return chattingFilterService.updateChatFilter(
+ id,
+ organizationId,
+ ChatFilterRequest(badWord)
+ ).getOrThrow().data
+ }
+
+}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/repository/comment/CommentRepositoryImpl.kt b/android/data/src/main/java/com/sixkids/data/repository/comment/CommentRepositoryImpl.kt
new file mode 100644
index 00000000..340c4d8f
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/comment/CommentRepositoryImpl.kt
@@ -0,0 +1,25 @@
+package com.sixkids.data.repository.comment
+
+import com.sixkids.data.repository.comment.remote.CommentRemoteDataSource
+import com.sixkids.domain.repository.CommentRepository
+import javax.inject.Inject
+
+class CommentRepositoryImpl @Inject constructor(
+ private val commentRemoteDataSource: CommentRemoteDataSource
+) : CommentRepository {
+ override suspend fun createComment(postId: Long, content: String, parentId: Long): Long {
+ return commentRemoteDataSource.createComment(postId, content, parentId)
+ }
+
+ override suspend fun deleteComment(id: Long): Boolean {
+ return commentRemoteDataSource.deleteComment(id)
+ }
+
+ override suspend fun updateComment(id: Long, content: String): Long {
+ return commentRemoteDataSource.updateComment(id, content)
+ }
+
+ override suspend fun reportComment(id: Long): Boolean {
+ return commentRemoteDataSource.reportComment(id)
+ }
+}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/repository/comment/remote/CommentRemoteDataSource.kt b/android/data/src/main/java/com/sixkids/data/repository/comment/remote/CommentRemoteDataSource.kt
new file mode 100644
index 00000000..3e2276ce
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/comment/remote/CommentRemoteDataSource.kt
@@ -0,0 +1,11 @@
+package com.sixkids.data.repository.comment.remote
+
+interface CommentRemoteDataSource {
+ suspend fun createComment(postId: Long, content: String, parentId: Long): Long
+
+ suspend fun deleteComment(id: Long): Boolean
+
+ suspend fun updateComment(id: Long, content: String): Long
+
+ suspend fun reportComment(id: Long): Boolean
+}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/repository/comment/remote/CommentRemoteDataSourceImpl.kt b/android/data/src/main/java/com/sixkids/data/repository/comment/remote/CommentRemoteDataSourceImpl.kt
new file mode 100644
index 00000000..28387f5d
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/comment/remote/CommentRemoteDataSourceImpl.kt
@@ -0,0 +1,26 @@
+package com.sixkids.data.repository.comment.remote
+
+import com.sixkids.data.api.CommentService
+import com.sixkids.data.model.request.NewCommentRequest
+import com.sixkids.data.model.request.UpdateCommentRequest
+import javax.inject.Inject
+
+class CommentRemoteDataSourceImpl @Inject constructor(
+ private val commentService: CommentService
+) : CommentRemoteDataSource{
+ override suspend fun createComment(postId: Long, content: String, parentId: Long): Long {
+ return commentService.createComment(NewCommentRequest(postId, content, parentId)).getOrThrow().data
+ }
+
+ override suspend fun deleteComment(id: Long): Boolean {
+ return commentService.deleteComment(id).getOrThrow().data
+ }
+
+ override suspend fun updateComment(id: Long, content: String): Long {
+ return commentService.updateComment(id, UpdateCommentRequest(id, content)).getOrThrow().data
+ }
+
+ override suspend fun reportComment(id: Long): Boolean {
+ return commentService.reportComment(id).getOrThrow().data
+ }
+}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/repository/group/GroupRepositoryImpl.kt b/android/data/src/main/java/com/sixkids/data/repository/group/GroupRepositoryImpl.kt
new file mode 100644
index 00000000..953ee2b9
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/group/GroupRepositoryImpl.kt
@@ -0,0 +1,32 @@
+package com.sixkids.data.repository.group
+
+import com.sixkids.data.model.response.toModel
+import com.sixkids.data.repository.group.remote.GroupDataSource
+import com.sixkids.domain.repository.GroupRepository
+import com.sixkids.model.MatchingRoom
+import javax.inject.Inject
+
+class GroupRepositoryImpl @Inject constructor(
+ private val groupDataSource: GroupDataSource
+) : GroupRepository {
+ override suspend fun createMatchingRoom(challengeId: Long): MatchingRoom =
+ groupDataSource.createMatchingRoom(challengeId).toModel()
+
+ override suspend fun inviteFriend(key: String, memberId: Long) =
+ groupDataSource.inviteFriend(key, memberId)
+
+ override suspend fun deportFriend(key: String, memberId: Long) =
+ groupDataSource.deportFriend(key, memberId)
+
+ override suspend fun joinGroup(key: String, joinStatus: Boolean) =
+ groupDataSource.joinGroup(key, joinStatus)
+
+ override suspend fun createGroup(key: String): Long = groupDataSource.createGroup(key)
+
+ override suspend fun getMatchingGroup(
+ organizationId: Long,
+ minCount: Int,
+ matchingType: String,
+ members: List
+ ) = groupDataSource.getMatchingGroup(organizationId, minCount, matchingType, members).map { it.toModel() }
+}
diff --git a/android/data/src/main/java/com/sixkids/data/repository/group/remote/GroupDataSource.kt b/android/data/src/main/java/com/sixkids/data/repository/group/remote/GroupDataSource.kt
new file mode 100644
index 00000000..c33e810d
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/group/remote/GroupDataSource.kt
@@ -0,0 +1,23 @@
+package com.sixkids.data.repository.group.remote
+
+import com.sixkids.data.model.response.GroupMatchingRoomResponse
+import com.sixkids.data.model.response.GroupMatchingSuccessResponse
+
+interface GroupDataSource {
+ suspend fun createMatchingRoom(challengeId: Long): GroupMatchingRoomResponse
+
+ suspend fun inviteFriend(key: String, memberId: Long)
+
+ suspend fun deportFriend(key: String, memberId: Long)
+
+ suspend fun joinGroup(key: String, joinStatus: Boolean)
+
+ suspend fun createGroup(key: String): Long
+
+ suspend fun getMatchingGroup(
+ organizationId: Long,
+ minCount: Int,
+ matchingType: String,
+ members: List
+ ): List
+}
diff --git a/android/data/src/main/java/com/sixkids/data/repository/group/remote/GroupDataSourceImpl.kt b/android/data/src/main/java/com/sixkids/data/repository/group/remote/GroupDataSourceImpl.kt
new file mode 100644
index 00000000..d5d05239
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/group/remote/GroupDataSourceImpl.kt
@@ -0,0 +1,33 @@
+package com.sixkids.data.repository.group.remote
+
+import com.sixkids.data.api.GroupService
+import com.sixkids.data.model.response.GroupMatchingRoomResponse
+import com.sixkids.data.model.response.GroupMatchingSuccessResponse
+import javax.inject.Inject
+
+class GroupDataSourceImpl @Inject constructor(
+ private val groupService: GroupService
+) : GroupDataSource {
+ override suspend fun createMatchingRoom(challengeId: Long): GroupMatchingRoomResponse =
+ groupService.createMatchingRoom(challengeId).getOrThrow().data
+
+ override suspend fun inviteFriend(key: String, memberId: Long) =
+ groupService.inviteFriend(key, memberId).getOrThrow().data
+
+ override suspend fun deportFriend(key: String, memberId: Long) =
+ groupService.deportFriend(key, memberId).getOrThrow().data
+
+ override suspend fun joinGroup(key: String, joinStatus: Boolean) =
+ groupService.joinGroup(key, joinStatus).getOrThrow().data
+
+ override suspend fun createGroup(key: String): Long =
+ groupService.createGroup(key).getOrThrow().data
+
+ override suspend fun getMatchingGroup(
+ organizationId: Long,
+ minCount: Int,
+ matchingType: String,
+ members: List
+ ): List =
+ groupService.getMatchingGroup(organizationId, minCount, matchingType, members).getOrThrow().data
+}
diff --git a/android/data/src/main/java/com/sixkids/data/repository/organization/OrganizationRepositoryImpl.kt b/android/data/src/main/java/com/sixkids/data/repository/organization/OrganizationRepositoryImpl.kt
index ccd469ad..dd6a6e75 100644
--- a/android/data/src/main/java/com/sixkids/data/repository/organization/OrganizationRepositoryImpl.kt
+++ b/android/data/src/main/java/com/sixkids/data/repository/organization/OrganizationRepositoryImpl.kt
@@ -1,9 +1,16 @@
package com.sixkids.data.repository.organization
+import com.sixkids.data.model.response.toModel
import com.sixkids.data.repository.organization.local.OrganizationLocalDataSource
import com.sixkids.data.repository.organization.remote.OrganizationRemoteDataSource
import com.sixkids.domain.repository.OrganizationRepository
+import com.sixkids.model.ClassSummary
+import com.sixkids.model.MemberDetail
+import com.sixkids.model.MemberRankItem
+import com.sixkids.model.MemberSimple
+import com.sixkids.model.MemberSimpleWithScore
import com.sixkids.model.Organization
+import com.sixkids.model.StudentRelation
import javax.inject.Inject
class OrganizationRepositoryImpl @Inject constructor(
@@ -25,4 +32,60 @@ class OrganizationRepositoryImpl @Inject constructor(
override suspend fun newOrganization(name: String): Long {
return organizationRemoteDataSource.newOrganization(name)
}
-}
\ No newline at end of file
+
+ override suspend fun joinOrganization(orgId: Int, code: String): Long {
+ return organizationRemoteDataSource.joinOrganization(orgId, code)
+ }
+
+ override suspend fun getOrganizationSummary(organizationId: Int): ClassSummary {
+ return organizationRemoteDataSource.getOrganizationSummary(organizationId)
+ }
+
+ override suspend fun updateOrganization(organizationId: Int, name: String): String {
+ return organizationRemoteDataSource.updateOrganization(organizationId, name)
+ }
+
+ override suspend fun getOrganizationInviteCode(organizationId: Int): String {
+ return organizationRemoteDataSource.getOrganizationInviteCode(organizationId)
+ }
+
+ override suspend fun saveSelectedOrganizationName(organizationName: String) {
+ organizationLocalDataSource.saveSelectedOrganizationName(organizationName)
+ }
+
+ override suspend fun loadSelectedOrganizationName(): String {
+ return organizationLocalDataSource.getSelectedOrganizationName()
+ }
+
+ override suspend fun getOrganizationMembers(orgId: Int): List {
+ return organizationRemoteDataSource.getOrganizationMembers(orgId)
+ }
+
+ override suspend fun getStudentDetail(orgId: Long, studentId: Long): MemberDetail {
+ return organizationRemoteDataSource.getStudentDetail(orgId, studentId)
+ }
+
+ override suspend fun getStudentRelation(
+ orgId: Long,
+ studentId: Long,
+ limit: Int?
+ ): List {
+ return organizationRemoteDataSource.getStudentRelation(orgId, studentId, limit)
+ }
+
+ override suspend fun getStudentRelationDetail(
+ orgId: Long,
+ sourceStudentId: Long,
+ targetStudentId: Long
+ ): StudentRelation {
+ return organizationRemoteDataSource.getStudentRelationDetail(orgId, sourceStudentId, targetStudentId)
+ }
+
+ override suspend fun getOrganizationRank(orgId: Int): List {
+ return organizationRemoteDataSource.getOrganizationRank(orgId).toModel()
+ }
+
+ override suspend fun tagGreeting(orgId: Long, memberId: Long): Int {
+ return organizationRemoteDataSource.tagGreeting(orgId, memberId)
+ }
+}
diff --git a/android/data/src/main/java/com/sixkids/data/repository/organization/local/OrganizationLocalDataSource.kt b/android/data/src/main/java/com/sixkids/data/repository/organization/local/OrganizationLocalDataSource.kt
index 4d833d04..2298ee06 100644
--- a/android/data/src/main/java/com/sixkids/data/repository/organization/local/OrganizationLocalDataSource.kt
+++ b/android/data/src/main/java/com/sixkids/data/repository/organization/local/OrganizationLocalDataSource.kt
@@ -3,4 +3,6 @@ package com.sixkids.data.repository.organization.local
interface OrganizationLocalDataSource {
suspend fun getSelectedOrganizationId(): Int
suspend fun saveSelectedOrganizationId(organizationId: Int)
+ suspend fun getSelectedOrganizationName(): String
+ suspend fun saveSelectedOrganizationName(organizationName: String)
}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/repository/organization/local/OrganizationLocalDataSourceImpl.kt b/android/data/src/main/java/com/sixkids/data/repository/organization/local/OrganizationLocalDataSourceImpl.kt
index 7be579e7..62cff7b0 100644
--- a/android/data/src/main/java/com/sixkids/data/repository/organization/local/OrganizationLocalDataSourceImpl.kt
+++ b/android/data/src/main/java/com/sixkids/data/repository/organization/local/OrganizationLocalDataSourceImpl.kt
@@ -4,6 +4,7 @@ import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
+import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import javax.inject.Inject
@@ -23,8 +24,21 @@ class OrganizationLocalDataSourceImpl @Inject constructor(
}
}
+ override suspend fun getSelectedOrganizationName(): String {
+ return dataStore.data.map { preference ->
+ preference[SELECTED_ORGANIZATION_NAME_KEY] ?: ""
+ }.first()
+ }
+
+ override suspend fun saveSelectedOrganizationName(organizationName: String) {
+ dataStore.edit { preference ->
+ preference[SELECTED_ORGANIZATION_NAME_KEY] = organizationName
+ }
+ }
+
companion object{
private val SELECTED_ORGANIZATION_ID_KEY = intPreferencesKey("selected_organization_id")
+ private val SELECTED_ORGANIZATION_NAME_KEY = stringPreferencesKey("selected_organization_name")
}
}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/repository/organization/remote/OrganizationRemoteDataSource.kt b/android/data/src/main/java/com/sixkids/data/repository/organization/remote/OrganizationRemoteDataSource.kt
index 3f0d8680..a25d5bc3 100644
--- a/android/data/src/main/java/com/sixkids/data/repository/organization/remote/OrganizationRemoteDataSource.kt
+++ b/android/data/src/main/java/com/sixkids/data/repository/organization/remote/OrganizationRemoteDataSource.kt
@@ -1,9 +1,36 @@
package com.sixkids.data.repository.organization.remote
+import com.sixkids.model.ClassSummary
+import com.sixkids.data.model.response.ClassSummaryResponse
+import com.sixkids.data.model.response.RankResponse
+import com.sixkids.model.MemberDetail
+import com.sixkids.model.MemberSimple
+import com.sixkids.model.MemberSimpleWithScore
import com.sixkids.model.Organization
+import com.sixkids.model.StudentRelation
interface OrganizationRemoteDataSource {
suspend fun getClassList(): List
suspend fun newOrganization(name: String): Long
+
+ suspend fun joinOrganization(orgId: Int, code: String): Long
+
+ suspend fun getOrganizationSummary(organizationId: Int): ClassSummary
+
+ suspend fun updateOrganization(organizationId: Int, name: String): String
+
+ suspend fun getOrganizationInviteCode(organizationId: Int): String
+
+ suspend fun getOrganizationMembers(orgId: Int): List
+
+ suspend fun getStudentDetail(orgId: Long, studentId: Long): MemberDetail
+
+ suspend fun getStudentRelation(orgId: Long, studentId: Long, limit: Int?): List
+
+ suspend fun getStudentRelationDetail(orgId: Long, sourceStudentId: Long, targetStudentId: Long): StudentRelation
+
+ suspend fun getOrganizationRank(orgId: Int): List
+
+ suspend fun tagGreeting(orgId: Long, memberId: Long): Int
}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/repository/organization/remote/OrganizationRemoteDataSourceImpl.kt b/android/data/src/main/java/com/sixkids/data/repository/organization/remote/OrganizationRemoteDataSourceImpl.kt
index 5cf2dcc4..084e9b54 100644
--- a/android/data/src/main/java/com/sixkids/data/repository/organization/remote/OrganizationRemoteDataSourceImpl.kt
+++ b/android/data/src/main/java/com/sixkids/data/repository/organization/remote/OrganizationRemoteDataSourceImpl.kt
@@ -1,14 +1,25 @@
package com.sixkids.data.repository.organization.remote
+import com.sixkids.data.api.MemberOrgService
import com.sixkids.data.api.OrganizationService
+import com.sixkids.data.model.request.GreetingRequest
+import com.sixkids.data.model.request.JoinOrganizationRequest
import com.sixkids.data.model.request.NewOrganizationRequest
+import com.sixkids.data.model.response.ClassSummaryResponse
+import com.sixkids.data.model.response.RankResponse
import com.sixkids.data.model.response.toModel
+import com.sixkids.model.ClassSummary
+import com.sixkids.model.MemberDetail
+import com.sixkids.model.MemberSimple
+import com.sixkids.model.MemberSimpleWithScore
import com.sixkids.model.Organization
+import com.sixkids.model.StudentRelation
import javax.inject.Inject
class OrganizationRemoteDataSourceImpl @Inject constructor(
- private val organizationService: OrganizationService
-) : OrganizationRemoteDataSource{
+ private val organizationService: OrganizationService,
+ private val memberOrgService: MemberOrgService
+) : OrganizationRemoteDataSource {
override suspend fun getClassList(): List {
return organizationService.getOrganizationList().getOrThrow().data.map { it.toModel() }
}
@@ -16,4 +27,54 @@ class OrganizationRemoteDataSourceImpl @Inject constructor(
override suspend fun newOrganization(name: String): Long {
return organizationService.newOrganization(NewOrganizationRequest(name)).getOrThrow().data
}
+
+ override suspend fun joinOrganization(orgId: Int, code: String): Long {
+ return organizationService.joinOrganization(orgId, JoinOrganizationRequest(code))
+ .getOrThrow().data
+ }
+
+ override suspend fun getOrganizationSummary(organizationId: Int): ClassSummary {
+ return organizationService.getOrganizationSummary(organizationId).getOrThrow().data.toModel()
+ }
+
+ override suspend fun updateOrganization(organizationId: Int, name: String): String {
+ return organizationService.updateOrganization(organizationId, NewOrganizationRequest(name))
+ .getOrThrow().data.name
+ }
+
+ override suspend fun getOrganizationInviteCode(organizationId: Int): String {
+ return organizationService.getOrganizationInviteCode(organizationId).getOrThrow().data.code
+ }
+
+ override suspend fun getOrganizationMembers(orgId: Int): List {
+ return organizationService.getOrganizationMembers(orgId).getOrThrow().data.map { it.toModel() }
+ }
+
+ override suspend fun getStudentDetail(orgId: Long, studentId: Long): MemberDetail {
+ return memberOrgService.getMemberDetail(orgId, studentId).getOrThrow().data.toModel()
+ }
+
+ override suspend fun getStudentRelation(
+ orgId: Long,
+ studentId: Long,
+ limit: Int?
+ ): List {
+ return memberOrgService.getRelationSimple(orgId, studentId.toInt(), limit).getOrThrow().data.map { it.toModel() }
+ }
+
+ override suspend fun getStudentRelationDetail(
+ orgId: Long,
+ sourceStudentId: Long,
+ targetStudentId: Long
+ ): StudentRelation {
+ return memberOrgService.getRelationDetail(orgId, sourceStudentId.toInt(), targetStudentId.toInt()).getOrThrow().data.toModel()
+ }
+
+ override suspend fun getOrganizationRank(orgId: Int): List {
+ return organizationService.getOrganizationRank(orgId).getOrThrow().data
+ }
+
+ override suspend fun tagGreeting(orgId: Long, memberId: Long): Int {
+ return memberOrgService.tagGreeting(GreetingRequest(orgId, memberId)).getOrThrow().data
+ }
}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/repository/post/PostRepositoryImpl.kt b/android/data/src/main/java/com/sixkids/data/repository/post/PostRepositoryImpl.kt
new file mode 100644
index 00000000..24fcf2fd
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/post/PostRepositoryImpl.kt
@@ -0,0 +1,92 @@
+package com.sixkids.data.repository.post
+
+import androidx.paging.Pager
+import androidx.paging.PagingConfig
+import androidx.paging.PagingData
+import androidx.paging.map
+import com.sixkids.data.api.PostService
+import com.sixkids.data.model.response.toModel
+import com.sixkids.data.repository.post.remote.PostListPagingSource
+import com.sixkids.data.repository.post.remote.PostRemoteDataSource
+import com.sixkids.domain.repository.PostRepository
+import com.sixkids.model.Post
+import com.sixkids.model.PostDetail
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import java.io.File
+import javax.inject.Inject
+
+class PostRepositoryImpl @Inject constructor(
+ private val postRemoteDataSource: PostRemoteDataSource,
+ private val postService: PostService
+) : PostRepository {
+ override suspend fun getPosts(
+ organizationId: Int,
+ memberId: Int?,
+ postCategory: String
+ ): Flow> {
+ return Pager(
+ config = PagingConfig(PostListPagingSource.DEFAULT_SIZE),
+ pagingSourceFactory = {
+ PostListPagingSource(
+ postService,
+ organizationId,
+ memberId,
+ postCategory
+ )
+ }
+ ).flow.map { pagingData ->
+ pagingData.map {
+ postResponse -> postResponse.toModel()
+ }
+ }
+ }
+
+ override suspend fun createPost(
+ organizationId: Long,
+ title: String,
+ content: String,
+ secretStatus: Boolean,
+ postCategory: String,
+ file: File?
+ ): Long {
+ return postRemoteDataSource.createPost(
+ organizationId,
+ title,
+ content,
+ secretStatus,
+ postCategory,
+ file
+ )
+ }
+
+ override suspend fun getPostDetail(postId: Long): PostDetail {
+ return postRemoteDataSource.getPostDetail(postId).toModel()
+ }
+
+ override suspend fun deletePost(postId: Long): Boolean {
+ return postRemoteDataSource.deletePost(postId)
+ }
+
+ override suspend fun updatePost(
+ postId: Long,
+ title: String,
+ content: String,
+ secretStatus: Boolean,
+ postCategory: String,
+ file: File?
+ ): Long {
+ return postRemoteDataSource.updatePost(
+ postId,
+ title,
+ content,
+ secretStatus,
+ postCategory,
+ file
+ )
+ }
+
+ override suspend fun reportPost(postId: Long): Boolean {
+ return postRemoteDataSource.reportPost(postId)
+ }
+}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/repository/post/remote/PostListPagingSource.kt b/android/data/src/main/java/com/sixkids/data/repository/post/remote/PostListPagingSource.kt
new file mode 100644
index 00000000..5edd409d
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/post/remote/PostListPagingSource.kt
@@ -0,0 +1,46 @@
+package com.sixkids.data.repository.post.remote
+
+import androidx.paging.PagingSource
+import androidx.paging.PagingState
+import com.sixkids.data.api.PostService
+import com.sixkids.data.model.response.PostResponse
+import javax.inject.Inject
+
+class PostListPagingSource @Inject constructor(
+ private val postService: PostService,
+ private val organizationId: Int,
+ private val memberId: Int? = null,
+ private val category: String,
+) : PagingSource() {
+ override fun getRefreshKey(state: PagingState): Int? {
+ return state.anchorPosition?.let { anchorPosition ->
+ state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
+ ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
+ }
+ }
+
+ override suspend fun load(params: LoadParams): LoadResult {
+ val page = params.key ?: 0
+ return try {
+ val response = postService.getPosts(
+ organizationId,
+ memberId,
+ page,
+ DEFAULT_SIZE,
+ category
+ ).getOrThrow().data.posts
+
+ LoadResult.Page(
+ data = response,
+ prevKey = if (page == 0) null else page.minus(1),
+ nextKey = if (response.isEmpty()) null else page.plus(1)
+ )
+ } catch (e: Exception) {
+ LoadResult.Error(e)
+ }
+ }
+
+ companion object {
+ const val DEFAULT_SIZE = 10
+ }
+}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/repository/post/remote/PostRemoteDataSource.kt b/android/data/src/main/java/com/sixkids/data/repository/post/remote/PostRemoteDataSource.kt
new file mode 100644
index 00000000..1aeed962
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/post/remote/PostRemoteDataSource.kt
@@ -0,0 +1,38 @@
+package com.sixkids.data.repository.post.remote
+
+import com.sixkids.data.model.response.PostDetailResponse
+import java.io.File
+
+interface PostRemoteDataSource {
+
+ suspend fun createPost(
+ organizationId: Long,
+ title: String,
+ content: String,
+ secretStatus: Boolean,
+ postCategory: String,
+ file: File?,
+ ): Long
+
+ suspend fun getPostDetail(
+ postId: Long,
+ ): PostDetailResponse
+
+ suspend fun deletePost(
+ postId: Long,
+ ): Boolean
+
+ suspend fun updatePost(
+ postId: Long,
+ title: String,
+ content: String,
+ secretStatus: Boolean,
+ postCategory: String,
+ file: File?,
+ ): Long
+
+ suspend fun reportPost(
+ postId: Long,
+ ): Boolean
+
+}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/repository/post/remote/PostRemoteDataSourceImpl.kt b/android/data/src/main/java/com/sixkids/data/repository/post/remote/PostRemoteDataSourceImpl.kt
new file mode 100644
index 00000000..f276385b
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/post/remote/PostRemoteDataSourceImpl.kt
@@ -0,0 +1,72 @@
+package com.sixkids.data.repository.post.remote
+
+import com.sixkids.data.api.PostService
+import com.sixkids.data.model.request.NewPostRequest
+import com.sixkids.data.model.response.PostDetailResponse
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
+import okhttp3.MultipartBody
+import okhttp3.RequestBody.Companion.asRequestBody
+import java.io.File
+import javax.inject.Inject
+
+class PostRemoteDataSourceImpl @Inject constructor(
+ private val postService: PostService,
+): PostRemoteDataSource {
+ override suspend fun createPost(
+ organizationId: Long,
+ title: String,
+ content: String,
+ secretStatus: Boolean,
+ postCategory: String,
+ file: File?
+ ): Long {
+ val image = file?.asRequestBody("image/jpg".toMediaTypeOrNull())
+ val multipartImage = image?.let {
+ MultipartBody.Part.createFormData("file", file.name, it)
+ }
+
+ val request = NewPostRequest(
+ title = title,
+ content = content,
+ secretStatus = secretStatus,
+ postCategory = postCategory,
+ )
+
+ return postService.createPost(organizationId, request, multipartImage).getOrThrow().data
+ }
+
+ override suspend fun getPostDetail(postId: Long): PostDetailResponse {
+ return postService.getPostDetail(postId).getOrThrow().data
+ }
+
+ override suspend fun deletePost(postId: Long): Boolean {
+ return postService.deletePost(postId).getOrThrow().data
+ }
+
+ override suspend fun updatePost(
+ postId: Long,
+ title: String,
+ content: String,
+ secretStatus: Boolean,
+ postCategory: String,
+ file: File?
+ ): Long {
+ val image = file?.asRequestBody("image/jpg".toMediaTypeOrNull())
+ val multipartImage = image?.let {
+ MultipartBody.Part.createFormData("file", file.name, it)
+ }
+
+ val request = NewPostRequest(
+ title = title,
+ content = content,
+ secretStatus = secretStatus,
+ postCategory = postCategory,
+ )
+
+ return postService.updatePost(postId, request, multipartImage).getOrThrow().data
+ }
+
+ override suspend fun reportPost(postId: Long): Boolean {
+ return postService.reportPost(postId).getOrThrow().data
+ }
+}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/repository/relay/RelayRepositoryImpl.kt b/android/data/src/main/java/com/sixkids/data/repository/relay/RelayRepositoryImpl.kt
new file mode 100644
index 00000000..2db214f0
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/relay/RelayRepositoryImpl.kt
@@ -0,0 +1,67 @@
+package com.sixkids.data.repository.relay
+
+import androidx.paging.Pager
+import androidx.paging.PagingConfig
+import androidx.paging.PagingData
+import com.sixkids.data.api.RelayService
+import com.sixkids.data.repository.relay.remote.RelayHistoryPagingSource
+import com.sixkids.data.repository.relay.remote.RelayRemoteDataSource
+import com.sixkids.domain.repository.RelayRepository
+import com.sixkids.model.Relay
+import com.sixkids.model.RelayDetail
+import com.sixkids.model.RelayReceive
+import com.sixkids.model.RelaySend
+import com.sixkids.model.RunningRelay
+import kotlinx.coroutines.flow.Flow
+import javax.inject.Inject
+
+class RelayRepositoryImpl @Inject constructor(
+ private val relayService: RelayService,
+ private val relayRemoteDataSource: RelayRemoteDataSource
+) : RelayRepository{
+
+ override suspend fun getRelayHistory(
+ organizationId: Int,
+ memberId: Int?,
+ ): Flow> =
+ Pager(
+ config = PagingConfig(RelayHistoryPagingSource.DEFAULT_SIZE),
+ pagingSourceFactory = {
+ RelayHistoryPagingSource(
+ relayService,
+ organizationId,
+ memberId,
+ )
+ }
+ ).flow
+
+ override suspend fun getRunningRelay(organizationId: Long): RunningRelay {
+ return relayRemoteDataSource.getRunningRelay(organizationId)
+ }
+
+ override suspend fun getRelayDetail(relayId: Long): RelayDetail {
+ return relayRemoteDataSource.getRelayDetail(relayId)
+ }
+
+ override suspend fun createRelay(organizationId: Int, question: String): Long {
+ return relayRemoteDataSource.createRelay(organizationId, question)
+ }
+
+ override suspend fun getRelayQuestion(relayId: Long): String {
+ return relayRemoteDataSource.getRelayQuestion(relayId)
+ }
+
+ override suspend fun receiveRelay(
+ relayId: Int,
+ senderId: Long,
+ question: String
+ ): RelayReceive {
+ return relayRemoteDataSource.receiveRelay(relayId, senderId, question)
+ }
+
+ override suspend fun sendRelay(relayId: Int): RelaySend {
+ return relayRemoteDataSource.sendRelay(relayId)
+ }
+
+
+}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/repository/relay/remote/RelayHistoryPagingSource.kt b/android/data/src/main/java/com/sixkids/data/repository/relay/remote/RelayHistoryPagingSource.kt
new file mode 100644
index 00000000..e9e67f46
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/relay/remote/RelayHistoryPagingSource.kt
@@ -0,0 +1,51 @@
+package com.sixkids.data.repository.relay.remote
+
+import androidx.paging.PagingSource
+import androidx.paging.PagingState
+import com.sixkids.data.api.RelayService
+import com.sixkids.data.model.response.toModel
+import com.sixkids.model.Relay
+import javax.inject.Inject
+
+class RelayHistoryPagingSource @Inject constructor(
+ private val relayService: RelayService,
+ private val organizationId: Int,
+ private val memberId: Int? = null,
+) : PagingSource() {
+ override fun getRefreshKey(state: PagingState): Int? {
+ return state.anchorPosition?.let { anchorPosition ->
+ state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
+ ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
+ }
+ }
+
+ override suspend fun load(params: LoadParams): LoadResult {
+ val page = params.key ?: 0
+ return try {
+
+ val response = relayService.getRelayHistory(
+ organizationId,
+ memberId,
+ page,
+ DEFAULT_SIZE
+ )
+ val challengeHistory = response.getOrThrow().data.let {relayResponse ->
+ relayResponse.relays.map{it.toModel(relayResponse.totalCount)}
+ }
+
+
+ LoadResult.Page(
+ data = challengeHistory,
+ prevKey = if (page == 0) null else page.minus(1),
+ nextKey = if (challengeHistory.isEmpty()) null else page.plus(1)
+ )
+ } catch (e: Exception) {
+ LoadResult.Error(e)
+ }
+ }
+
+ companion object {
+ const val DEFAULT_SIZE = 10
+ }
+
+}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/repository/relay/remote/RelayRemoteDataSource.kt b/android/data/src/main/java/com/sixkids/data/repository/relay/remote/RelayRemoteDataSource.kt
new file mode 100644
index 00000000..0c02b141
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/relay/remote/RelayRemoteDataSource.kt
@@ -0,0 +1,20 @@
+package com.sixkids.data.repository.relay.remote
+
+import com.sixkids.model.RelayDetail
+import com.sixkids.model.RelayReceive
+import com.sixkids.model.RelaySend
+import com.sixkids.model.RunningRelay
+
+interface RelayRemoteDataSource {
+ suspend fun getRunningRelay(organizationId: Long) : RunningRelay
+
+ suspend fun getRelayDetail(relayId: Long) : RelayDetail
+
+ suspend fun createRelay(organizationId: Int, question: String) : Long
+
+ suspend fun getRelayQuestion(relayId: Long) : String
+
+ suspend fun receiveRelay(relayId: Int, senderId: Long, question: String) : RelayReceive
+
+ suspend fun sendRelay(relayId: Int) : RelaySend
+}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/repository/relay/remote/RelayRemoteDataSourceImpl.kt b/android/data/src/main/java/com/sixkids/data/repository/relay/remote/RelayRemoteDataSourceImpl.kt
new file mode 100644
index 00000000..84438efd
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/relay/remote/RelayRemoteDataSourceImpl.kt
@@ -0,0 +1,43 @@
+package com.sixkids.data.repository.relay.remote
+
+import com.sixkids.data.api.RelayService
+import com.sixkids.data.model.request.ReceiveRelayRequest
+import com.sixkids.data.model.request.RelayCreateRequest
+import com.sixkids.data.model.response.toModel
+import com.sixkids.model.RelayDetail
+import com.sixkids.model.RelayReceive
+import com.sixkids.model.RelaySend
+import com.sixkids.model.RunningRelay
+import javax.inject.Inject
+
+class RelayRemoteDataSourceImpl @Inject constructor(
+ private val relayService: RelayService
+) : RelayRemoteDataSource{
+ override suspend fun getRunningRelay(organizationId: Long) : RunningRelay =
+ relayService.getRunningRelay(organizationId).getOrThrow().data.toModel()
+
+ override suspend fun getRelayDetail(relayId: Long): RelayDetail {
+ return relayService.getRelayDetail(relayId).getOrThrow().data.toModel()
+ }
+
+ override suspend fun createRelay(organizationId: Int, question: String): Long {
+ return relayService.createRelay(RelayCreateRequest(organizationId, question)).getOrThrow().data
+ }
+
+ override suspend fun getRelayQuestion(relayId: Long): String {
+ return relayService.getRelayQuestion(relayId).getOrThrow().data
+ }
+
+ override suspend fun receiveRelay(
+ relayId: Int,
+ senderId: Long,
+ question: String
+ ): RelayReceive {
+ return relayService.receiveRelay(relayId, ReceiveRelayRequest(senderId, question)).getOrThrow().data.toModel()
+ }
+
+ override suspend fun sendRelay(relayId: Int): RelaySend {
+ return relayService.sendRelay(relayId).getOrThrow().data.toModel()
+ }
+
+}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/repository/user/UserRepositoryImpl.kt b/android/data/src/main/java/com/sixkids/data/repository/user/UserRepositoryImpl.kt
index ab04a37c..f5054b0a 100644
--- a/android/data/src/main/java/com/sixkids/data/repository/user/UserRepositoryImpl.kt
+++ b/android/data/src/main/java/com/sixkids/data/repository/user/UserRepositoryImpl.kt
@@ -1,10 +1,13 @@
package com.sixkids.data.repository.user
+import com.sixkids.data.model.response.toModel
import com.sixkids.data.repository.user.local.UserLocalDataSource
import com.sixkids.data.repository.user.remote.UserRemoteDataSource
import com.sixkids.domain.repository.TokenRepository
import com.sixkids.domain.repository.UserRepository
import com.sixkids.model.JwtToken
+import com.sixkids.model.MemberSimple
+import com.sixkids.model.StudentHomeInfo
import com.sixkids.model.UserInfo
import java.io.File
import javax.inject.Inject
@@ -56,6 +59,10 @@ class UserRepositoryImpl @Inject constructor(
return userRemoteDataSource.getMemberInfo()
}
+ override suspend fun getMemberSimpleInfo(id: Long): MemberSimple {
+ return userRemoteDataSource.getMemberSimple(id)
+ }
+
override suspend fun updateMemberProfilePhoto(file: File?, defaultImage: Int): String {
return userRemoteDataSource.updateMemberProfilePhoto(file, defaultImage)
}
@@ -79,4 +86,12 @@ class UserRepositoryImpl @Inject constructor(
throw e
}
}
+
+ override suspend fun loadUserInfo(): UserInfo {
+ return userLocalDataSource.getUserInfo()
+ }
+
+ override suspend fun getStudentHomeInfo(organizationId: Long): StudentHomeInfo {
+ return userRemoteDataSource.getStudentHomeInfo(organizationId).toModel()
+ }
}
diff --git a/android/data/src/main/java/com/sixkids/data/repository/user/remote/UserRemoteDataSource.kt b/android/data/src/main/java/com/sixkids/data/repository/user/remote/UserRemoteDataSource.kt
index f09780ca..a4dff1ad 100644
--- a/android/data/src/main/java/com/sixkids/data/repository/user/remote/UserRemoteDataSource.kt
+++ b/android/data/src/main/java/com/sixkids/data/repository/user/remote/UserRemoteDataSource.kt
@@ -1,6 +1,8 @@
package com.sixkids.data.repository.user.remote
+import com.sixkids.data.model.response.StudentHomeResponse
import com.sixkids.model.JwtToken
+import com.sixkids.model.MemberSimple
import com.sixkids.model.UserInfo
import java.io.File
@@ -11,9 +13,13 @@ interface UserRemoteDataSource {
suspend fun getMemberInfo(): UserInfo
+ suspend fun getMemberSimple(id: Long): MemberSimple
+
suspend fun updateMemberProfilePhoto(file: File?, defaultImage: Int): String
suspend fun autoSignIn(): JwtToken
suspend fun updateFCMToken(fcmToken: String)
+
+ suspend fun getStudentHomeInfo(organizationId: Long) : StudentHomeResponse
}
diff --git a/android/data/src/main/java/com/sixkids/data/repository/user/remote/UserRemoteDataSourceImpl.kt b/android/data/src/main/java/com/sixkids/data/repository/user/remote/UserRemoteDataSourceImpl.kt
index b6219ed6..af989e6b 100644
--- a/android/data/src/main/java/com/sixkids/data/repository/user/remote/UserRemoteDataSourceImpl.kt
+++ b/android/data/src/main/java/com/sixkids/data/repository/user/remote/UserRemoteDataSourceImpl.kt
@@ -1,5 +1,6 @@
package com.sixkids.data.repository.user.remote
+import com.sixkids.data.api.MemberOrgService
import com.sixkids.data.api.MemberService
import com.sixkids.data.api.SignInService
import com.sixkids.data.model.request.FcmRequest
@@ -7,6 +8,7 @@ import com.sixkids.data.model.request.SignInRequest
import com.sixkids.data.model.response.toModel
import com.sixkids.data.repository.user.local.UserLocalDataSource
import com.sixkids.model.JwtToken
+import com.sixkids.model.MemberSimple
import com.sixkids.model.UserInfo
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
@@ -18,7 +20,8 @@ import javax.inject.Inject
class UserRemoteDataSourceImpl @Inject constructor(
private val signInService: SignInService,
private val memberService: MemberService,
- private val userLocalDataSource: UserLocalDataSource
+ private val userLocalDataSource: UserLocalDataSource,
+ private val memberOrgService: MemberOrgService
) : UserRemoteDataSource{
override suspend fun signIn(idToken: String): JwtToken {
val response = signInService.signIn(SignInRequest(idToken))
@@ -75,6 +78,9 @@ class UserRemoteDataSourceImpl @Inject constructor(
return response.getOrThrow().data.toModel()
}
+ override suspend fun getMemberSimple(id: Long): MemberSimple =
+ memberService.getMemberInfoById(id).getOrThrow().data.toModel()
+
override suspend fun updateMemberProfilePhoto(file: File?, defaultImage: Int): String {
val data = HashMap()
@@ -107,4 +113,5 @@ class UserRemoteDataSourceImpl @Inject constructor(
return response.getOrThrow().data.toModel()
}
+ override suspend fun getStudentHomeInfo(organizationId: Long) = memberOrgService.getStudentHomeInfo(organizationId).getOrThrow().data
}
diff --git a/android/domain/src/main/java/com/sixkids/domain/repository/ChallengeRepository.kt b/android/domain/src/main/java/com/sixkids/domain/repository/ChallengeRepository.kt
index dd64264d..9df6f72b 100644
--- a/android/domain/src/main/java/com/sixkids/domain/repository/ChallengeRepository.kt
+++ b/android/domain/src/main/java/com/sixkids/domain/repository/ChallengeRepository.kt
@@ -1,9 +1,12 @@
package com.sixkids.domain.repository
import androidx.paging.PagingData
+import com.sixkids.model.AcceptStatus
import com.sixkids.model.Challenge
+import com.sixkids.model.ChallengeDetail
import com.sixkids.model.GroupSimple
import com.sixkids.model.RunningChallenge
+import com.sixkids.model.RunningChallengeByStudent
import kotlinx.coroutines.flow.Flow
import java.time.LocalDateTime
@@ -15,14 +18,24 @@ interface ChallengeRepository {
suspend fun getRunningChallenge(organizationId: Int): RunningChallenge
+ suspend fun getRunningChallengesByStudent(organizationId: Int): RunningChallengeByStudent
+
suspend fun createChallenge(
organizationId: Int,
title: String,
content: String,
startTime: LocalDateTime,
endTime: LocalDateTime,
- reword: Int,
+ reward: Int,
minCount: Int,
groups: List
): Long
+
+ suspend fun getChallengeSimple(
+ challengeId: Int
+ ): Challenge
+
+ suspend fun getChallengeDetail(challengeId: Long, groupId: Long?): ChallengeDetail
+
+ suspend fun gradingChallenge(reportId: Long, acceptStatus: AcceptStatus)
}
diff --git a/android/domain/src/main/java/com/sixkids/domain/repository/ChattingFilterRepository.kt b/android/domain/src/main/java/com/sixkids/domain/repository/ChattingFilterRepository.kt
new file mode 100644
index 00000000..5a79ebd7
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/repository/ChattingFilterRepository.kt
@@ -0,0 +1,27 @@
+package com.sixkids.domain.repository
+
+import androidx.paging.PagingData
+import com.sixkids.model.ChatFilterWord
+import kotlinx.coroutines.flow.Flow
+
+interface ChattingFilterRepository {
+
+ suspend fun getChattingFilters(
+ organizationId: Int
+ ): Flow>
+
+ suspend fun deleteChatFilter(
+ id: Long
+ ): Boolean
+
+ suspend fun createChatFilter(
+ organizationId: Long,
+ badWord: String,
+ ): Long
+
+ suspend fun updateChatFilter(
+ organizationId: Long,
+ id: Long,
+ badWord: String,
+ ): Boolean
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/repository/ChattingRepository.kt b/android/domain/src/main/java/com/sixkids/domain/repository/ChattingRepository.kt
new file mode 100644
index 00000000..4da8db8d
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/repository/ChattingRepository.kt
@@ -0,0 +1,9 @@
+package com.sixkids.domain.repository
+
+import androidx.paging.PagingData
+import com.sixkids.model.Chat
+import kotlinx.coroutines.flow.Flow
+
+interface ChattingRepository {
+ suspend fun getChattingList(roomId: Long): Flow>
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/repository/CommentRepository.kt b/android/domain/src/main/java/com/sixkids/domain/repository/CommentRepository.kt
new file mode 100644
index 00000000..da95ef4d
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/repository/CommentRepository.kt
@@ -0,0 +1,11 @@
+package com.sixkids.domain.repository
+
+interface CommentRepository {
+ suspend fun createComment(postId: Long, content: String, parentId: Long): Long
+
+ suspend fun deleteComment(id: Long): Boolean
+
+ suspend fun updateComment(id: Long, content: String): Long
+
+ suspend fun reportComment(id: Long): Boolean
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/repository/GroupRepository.kt b/android/domain/src/main/java/com/sixkids/domain/repository/GroupRepository.kt
new file mode 100644
index 00000000..6033e4f4
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/repository/GroupRepository.kt
@@ -0,0 +1,23 @@
+package com.sixkids.domain.repository
+
+import com.sixkids.model.ChallengeGroup
+import com.sixkids.model.MatchingRoom
+
+interface GroupRepository {
+ suspend fun createMatchingRoom(challengeId: Long): MatchingRoom
+
+ suspend fun inviteFriend(key: String, memberId: Long)
+
+ suspend fun deportFriend(key: String, memberId: Long)
+
+ suspend fun joinGroup(key: String, joinStatus: Boolean)
+
+ suspend fun createGroup(key: String): Long
+
+ suspend fun getMatchingGroup(
+ organizationId: Long,
+ minCount: Int,
+ matchingType: String,
+ members: List
+ ): List
+}
diff --git a/android/domain/src/main/java/com/sixkids/domain/repository/OrganizationRepository.kt b/android/domain/src/main/java/com/sixkids/domain/repository/OrganizationRepository.kt
index cf619c6b..c0d2ffe7 100644
--- a/android/domain/src/main/java/com/sixkids/domain/repository/OrganizationRepository.kt
+++ b/android/domain/src/main/java/com/sixkids/domain/repository/OrganizationRepository.kt
@@ -1,6 +1,12 @@
package com.sixkids.domain.repository
+import com.sixkids.model.MemberDetail
+import com.sixkids.model.ClassSummary
+import com.sixkids.model.MemberRankItem
+import com.sixkids.model.MemberSimple
+import com.sixkids.model.MemberSimpleWithScore
import com.sixkids.model.Organization
+import com.sixkids.model.StudentRelation
interface OrganizationRepository {
suspend fun getClassList(): List
@@ -9,4 +15,28 @@ interface OrganizationRepository {
suspend fun getSelectedOrganizationId(): Int
suspend fun newOrganization(name: String): Long
+
+ suspend fun joinOrganization(orgId: Int, code: String): Long
+
+ suspend fun getOrganizationSummary(organizationId: Int): ClassSummary
+
+ suspend fun updateOrganization(organizationId: Int, name: String): String
+
+ suspend fun getOrganizationInviteCode(organizationId: Int): String
+
+ suspend fun saveSelectedOrganizationName(organizationName: String)
+
+ suspend fun loadSelectedOrganizationName(): String
+
+ suspend fun getOrganizationMembers(orgId: Int): List
+
+ suspend fun getStudentDetail(orgId: Long, studentId: Long): MemberDetail
+
+ suspend fun getStudentRelation(orgId: Long, studentId: Long, limit: Int?): List
+
+ suspend fun getStudentRelationDetail(orgId: Long, sourceStudentId: Long, targetStudentId: Long): StudentRelation
+
+ suspend fun getOrganizationRank(orgId: Int): List
+
+ suspend fun tagGreeting(orgId: Long, memberId: Long): Int
}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/repository/PostRepository.kt b/android/domain/src/main/java/com/sixkids/domain/repository/PostRepository.kt
new file mode 100644
index 00000000..e54b5269
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/repository/PostRepository.kt
@@ -0,0 +1,47 @@
+package com.sixkids.domain.repository
+
+import androidx.paging.PagingData
+import com.sixkids.model.Post
+import com.sixkids.model.PostDetail
+import kotlinx.coroutines.flow.Flow
+import java.io.File
+
+interface PostRepository {
+
+ suspend fun getPosts(
+ organizationId: Int,
+ memberId: Int? = null,
+ postCategory: String,
+ ): Flow>
+
+ suspend fun createPost(
+ organizationId: Long,
+ title: String,
+ content: String,
+ secretStatus: Boolean,
+ postCategory: String,
+ file: File?,
+ ): Long
+
+ suspend fun getPostDetail(
+ postId: Long,
+ ): PostDetail
+
+ suspend fun deletePost(
+ postId: Long,
+ ): Boolean
+
+ suspend fun updatePost(
+ postId: Long,
+ title: String,
+ content: String,
+ secretStatus: Boolean,
+ postCategory: String,
+ file: File?,
+ ): Long
+
+ suspend fun reportPost(
+ postId: Long,
+ ): Boolean
+
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/repository/RelayRepository.kt b/android/domain/src/main/java/com/sixkids/domain/repository/RelayRepository.kt
new file mode 100644
index 00000000..90c7a4ae
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/repository/RelayRepository.kt
@@ -0,0 +1,28 @@
+package com.sixkids.domain.repository
+
+import androidx.paging.PagingData
+import com.sixkids.model.Relay
+import com.sixkids.model.RelayDetail
+import com.sixkids.model.RelayReceive
+import com.sixkids.model.RelaySend
+import com.sixkids.model.RunningRelay
+import kotlinx.coroutines.flow.Flow
+
+interface RelayRepository {
+ suspend fun getRelayHistory(
+ organizationId: Int,
+ memberId: Int?
+ ): Flow>
+
+ suspend fun getRunningRelay(organizationId: Long): RunningRelay
+
+ suspend fun getRelayDetail(relayId: Long): RelayDetail
+
+ suspend fun createRelay(organizationId: Int, question: String): Long
+
+ suspend fun getRelayQuestion(relayId: Long): String
+
+ suspend fun receiveRelay(relayId: Int, senderId: Long, question: String) : RelayReceive
+
+ suspend fun sendRelay(relayId: Int): RelaySend
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/repository/UserRepository.kt b/android/domain/src/main/java/com/sixkids/domain/repository/UserRepository.kt
index 419313ee..23418399 100644
--- a/android/domain/src/main/java/com/sixkids/domain/repository/UserRepository.kt
+++ b/android/domain/src/main/java/com/sixkids/domain/repository/UserRepository.kt
@@ -1,6 +1,8 @@
package com.sixkids.domain.repository
import com.sixkids.model.JwtToken
+import com.sixkids.model.MemberSimple
+import com.sixkids.model.StudentHomeInfo
import com.sixkids.model.UserInfo
import java.io.File
@@ -13,6 +15,8 @@ interface UserRepository {
suspend fun getMemberInfo(): UserInfo
+ suspend fun getMemberSimpleInfo(id: Long): MemberSimple
+
suspend fun updateMemberProfilePhoto(file: File?, defaultImage: Int): String
suspend fun signOut() : Boolean
@@ -20,4 +24,8 @@ interface UserRepository {
suspend fun updateFCMToken(fcmToken: String)
suspend fun autoSignIn(): JwtToken
+
+ suspend fun loadUserInfo(): UserInfo
+
+ suspend fun getStudentHomeInfo(organizationId: Long): StudentHomeInfo
}
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/challenge/CreateChallengeUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/challenge/CreateChallengeUseCase.kt
index 26d6a674..242f9455 100644
--- a/android/domain/src/main/java/com/sixkids/domain/usecase/challenge/CreateChallengeUseCase.kt
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/challenge/CreateChallengeUseCase.kt
@@ -14,7 +14,7 @@ class CreateChallengeUseCase @Inject constructor(
content: String,
startTime: LocalDateTime,
endTime: LocalDateTime,
- reword: Int,
+ reward: Int,
minCount: Int,
groups: List = emptyList(),
) = runCatching {
@@ -24,7 +24,7 @@ class CreateChallengeUseCase @Inject constructor(
content = content,
startTime = startTime,
endTime = endTime,
- reword = reword,
+ reward = reward,
minCount = minCount,
groups = groups
)
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/challenge/GetChallengeDetailUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/challenge/GetChallengeDetailUseCase.kt
new file mode 100644
index 00000000..0d08b0e2
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/challenge/GetChallengeDetailUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.challenge
+
+import com.sixkids.domain.repository.ChallengeRepository
+import javax.inject.Inject
+
+class GetChallengeDetailUseCase @Inject constructor(
+ private val challengeRepository: ChallengeRepository
+) {
+ suspend operator fun invoke(challengeId: Long, groupId: Long?) = runCatching {
+ challengeRepository.getChallengeDetail(challengeId, groupId)
+ }
+}
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/challenge/GetChallengeSimpleUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/challenge/GetChallengeSimpleUseCase.kt
new file mode 100644
index 00000000..581333ad
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/challenge/GetChallengeSimpleUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.challenge
+
+import com.sixkids.domain.repository.ChallengeRepository
+import javax.inject.Inject
+
+class GetChallengeSimpleUseCase @Inject constructor(
+ private val challengeRepository: ChallengeRepository
+) {
+ suspend operator fun invoke(challengeId: Int) = runCatching {
+ challengeRepository.getChallengeSimple(challengeId)
+ }
+}
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/challenge/GetRunningChallengeByStudentUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/challenge/GetRunningChallengeByStudentUseCase.kt
new file mode 100644
index 00000000..f69c3dce
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/challenge/GetRunningChallengeByStudentUseCase.kt
@@ -0,0 +1,13 @@
+package com.sixkids.domain.usecase.challenge
+
+import com.sixkids.domain.repository.ChallengeRepository
+import javax.inject.Inject
+
+class GetRunningChallengeByStudentUseCase @Inject constructor(
+ private val challengeRepository: ChallengeRepository
+){
+ suspend operator fun invoke(organizationId: Int) =
+ runCatching {
+ challengeRepository.getRunningChallengesByStudent(organizationId)
+ }
+}
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/challenge/GradingReportUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/challenge/GradingReportUseCase.kt
new file mode 100644
index 00000000..a78d165e
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/challenge/GradingReportUseCase.kt
@@ -0,0 +1,16 @@
+package com.sixkids.domain.usecase.challenge
+
+import com.sixkids.domain.repository.ChallengeRepository
+import com.sixkids.model.AcceptStatus
+import javax.inject.Inject
+
+class GradingReportUseCase @Inject constructor(
+ private val challengeRepository: ChallengeRepository
+) {
+ suspend operator fun invoke(reportId: Long, acceptStatus: AcceptStatus) =
+ runCatching {
+ challengeRepository.gradingChallenge(
+ reportId, acceptStatus
+ )
+ }
+}
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/chatting/CreateNewChattingFilterWordUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/chatting/CreateNewChattingFilterWordUseCase.kt
new file mode 100644
index 00000000..0b3cacea
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/chatting/CreateNewChattingFilterWordUseCase.kt
@@ -0,0 +1,16 @@
+package com.sixkids.domain.usecase.chatting
+
+import com.sixkids.domain.repository.ChattingFilterRepository
+import javax.inject.Inject
+
+class CreateNewChattingFilterWordUseCase @Inject constructor(
+ private val chattingFilterRepository: ChattingFilterRepository
+)
+{
+ suspend operator fun invoke(
+ organizationId: Long,
+ badWord: String
+ ) = runCatching {
+ chattingFilterRepository.createChatFilter(organizationId, badWord)
+ }
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/chatting/DeleteChattingFilterWordUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/chatting/DeleteChattingFilterWordUseCase.kt
new file mode 100644
index 00000000..59ffbf95
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/chatting/DeleteChattingFilterWordUseCase.kt
@@ -0,0 +1,14 @@
+package com.sixkids.domain.usecase.chatting
+
+import com.sixkids.domain.repository.ChattingFilterRepository
+import javax.inject.Inject
+
+class DeleteChattingFilterWordUseCase @Inject constructor(
+ private val chattingFilterRepository: ChattingFilterRepository
+){
+ suspend operator fun invoke(
+ chattingFilterId: Long
+ ) = chattingFilterRepository.deleteChatFilter(
+ chattingFilterId
+ )
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/chatting/GetChattingFilterWordsUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/chatting/GetChattingFilterWordsUseCase.kt
new file mode 100644
index 00000000..7354278c
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/chatting/GetChattingFilterWordsUseCase.kt
@@ -0,0 +1,14 @@
+package com.sixkids.domain.usecase.chatting
+
+import com.sixkids.domain.repository.ChattingFilterRepository
+import javax.inject.Inject
+
+class GetChattingFilterWordsUseCase @Inject constructor(
+ private val chattingFilterRepository: ChattingFilterRepository
+) {
+ suspend operator fun invoke(
+ organizationId: Int
+ ) = chattingFilterRepository.getChattingFilters(
+ organizationId
+ )
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/chatting/GetChattingHistoryUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/chatting/GetChattingHistoryUseCase.kt
new file mode 100644
index 00000000..4057439a
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/chatting/GetChattingHistoryUseCase.kt
@@ -0,0 +1,11 @@
+package com.sixkids.domain.usecase.chatting
+
+import com.sixkids.domain.repository.ChattingRepository
+import javax.inject.Inject
+
+class GetChattingHistoryUseCase @Inject constructor(
+ private val chattingRepository: ChattingRepository
+){
+ suspend operator fun invoke(roomId: Long) = chattingRepository.getChattingList(roomId)
+
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/comment/DeleteCommentUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/comment/DeleteCommentUseCase.kt
new file mode 100644
index 00000000..d3c8272b
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/comment/DeleteCommentUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.comment
+
+import com.sixkids.domain.repository.CommentRepository
+import javax.inject.Inject
+
+class DeleteCommentUseCase @Inject constructor(
+ private val commentRepository: CommentRepository
+){
+ suspend operator fun invoke(commentId: Long) = runCatching {
+ commentRepository.deleteComment(commentId)
+ }
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/comment/NewCommentUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/comment/NewCommentUseCase.kt
new file mode 100644
index 00000000..9806c477
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/comment/NewCommentUseCase.kt
@@ -0,0 +1,14 @@
+package com.sixkids.domain.usecase.comment
+
+import com.sixkids.domain.repository.CommentRepository
+import javax.inject.Inject
+
+class NewCommentUseCase @Inject constructor(
+ private val commentRepository: CommentRepository
+){
+ suspend operator fun invoke(
+ postId: Long, content: String
+ ) = runCatching {
+ commentRepository.createComment(postId, content, 0L)
+ }
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/comment/NewRecommentUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/comment/NewRecommentUseCase.kt
new file mode 100644
index 00000000..51259c73
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/comment/NewRecommentUseCase.kt
@@ -0,0 +1,14 @@
+package com.sixkids.domain.usecase.comment
+
+import com.sixkids.domain.repository.CommentRepository
+import javax.inject.Inject
+
+class NewRecommentUseCase @Inject constructor(
+ private val commentRepository: CommentRepository
+){
+ suspend operator fun invoke(
+ postId: Long, content: String, parentId: Long
+ ) = runCatching {
+ commentRepository.createComment(postId, content, parentId)
+ }
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/comment/ReportCommentUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/comment/ReportCommentUseCase.kt
new file mode 100644
index 00000000..46ccda3c
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/comment/ReportCommentUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.comment
+
+import com.sixkids.domain.repository.CommentRepository
+import javax.inject.Inject
+
+class ReportCommentUseCase @Inject constructor(
+ private val commentRepository: CommentRepository
+){
+ suspend operator fun invoke(commentId: Long) = runCatching {
+ commentRepository.reportComment(commentId)
+ }
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/comment/UpdateCommentUsecase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/comment/UpdateCommentUsecase.kt
new file mode 100644
index 00000000..b3c8a29d
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/comment/UpdateCommentUsecase.kt
@@ -0,0 +1,14 @@
+package com.sixkids.domain.usecase.comment
+
+import com.sixkids.domain.repository.CommentRepository
+import javax.inject.Inject
+
+class UpdateCommentUsecase @Inject constructor(
+ private val commentRepository: CommentRepository
+){
+ suspend operator fun invoke(
+ commentId: Long, content: String
+ ) = runCatching {
+ commentRepository.updateComment(commentId, content)
+ }
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/group/CreateGroupMatchingRoomUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/group/CreateGroupMatchingRoomUseCase.kt
new file mode 100644
index 00000000..8bc4e2b9
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/group/CreateGroupMatchingRoomUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.group
+
+import com.sixkids.domain.repository.GroupRepository
+import javax.inject.Inject
+
+class CreateGroupMatchingRoomUseCase @Inject constructor(
+ private val groupRepository: GroupRepository
+) {
+ suspend operator fun invoke(challengeId: Long) = runCatching {
+ groupRepository.createMatchingRoom(challengeId)
+ }
+}
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/group/CreateGroupUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/group/CreateGroupUseCase.kt
new file mode 100644
index 00000000..9b978867
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/group/CreateGroupUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.group
+
+import com.sixkids.domain.repository.GroupRepository
+import javax.inject.Inject
+
+class CreateGroupUseCase @Inject constructor(
+ private val groupRepository: GroupRepository
+) {
+ suspend operator fun invoke(key: String) = runCatching {
+ groupRepository.createGroup(key)
+ }
+}
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/group/DeportFriendUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/group/DeportFriendUseCase.kt
new file mode 100644
index 00000000..32ab6b78
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/group/DeportFriendUseCase.kt
@@ -0,0 +1,15 @@
+package com.sixkids.domain.usecase.group
+
+import com.sixkids.domain.repository.GroupRepository
+import javax.inject.Inject
+
+class DeportFriendUseCase @Inject constructor(
+ private val groupRepository: GroupRepository
+) {
+ suspend operator fun invoke(key: String, memberId: Long) = runCatching {
+ groupRepository.deportFriend(
+ key, memberId
+ )
+ }
+
+}
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/group/GetMatchingGroupUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/group/GetMatchingGroupUseCase.kt
new file mode 100644
index 00000000..c31a14df
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/group/GetMatchingGroupUseCase.kt
@@ -0,0 +1,17 @@
+package com.sixkids.domain.usecase.group
+
+import com.sixkids.domain.repository.GroupRepository
+import javax.inject.Inject
+
+class GetMatchingGroupUseCase @Inject constructor(
+ private val groupRepository: GroupRepository
+) {
+ suspend operator fun invoke(
+ organizationId: Long,
+ minCount: Int,
+ matchingType: String,
+ members: List
+ ) = runCatching {
+ groupRepository.getMatchingGroup(organizationId, minCount, matchingType, members)
+ }
+}
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/group/InviteFriendUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/group/InviteFriendUseCase.kt
new file mode 100644
index 00000000..97693c7b
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/group/InviteFriendUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.group
+
+import com.sixkids.domain.repository.GroupRepository
+import javax.inject.Inject
+
+class InviteFriendUseCase @Inject constructor(
+ private val groupRepository: GroupRepository
+) {
+ suspend operator fun invoke(key: String, memberId: Long) = runCatching {
+ groupRepository.inviteFriend(key, memberId)
+ }
+}
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/group/JoinGroupUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/group/JoinGroupUseCase.kt
new file mode 100644
index 00000000..66d63d5e
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/group/JoinGroupUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.group
+
+import com.sixkids.domain.repository.GroupRepository
+import javax.inject.Inject
+
+class JoinGroupUseCase @Inject constructor(
+ private val groupRepository: GroupRepository
+) {
+ suspend operator fun invoke(key: String, joinStatus: Boolean) = runCatching {
+ groupRepository.joinGroup(key, joinStatus)
+ }
+}
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/organization/GetClassRankUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/organization/GetClassRankUseCase.kt
new file mode 100644
index 00000000..b7e5dbc8
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/organization/GetClassRankUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.organization
+
+import com.sixkids.domain.repository.OrganizationRepository
+import javax.inject.Inject
+
+class GetClassRankUseCase @Inject constructor(
+ private val organizationRepository: OrganizationRepository
+) {
+ suspend operator fun invoke(organizationId: Int) = runCatching {
+ organizationRepository.getOrganizationRank(organizationId)
+ }
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/organization/GetMemberRelationUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/organization/GetMemberRelationUseCase.kt
new file mode 100644
index 00000000..aceb89e6
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/organization/GetMemberRelationUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.organization
+
+import com.sixkids.domain.repository.OrganizationRepository
+import javax.inject.Inject
+
+class GetMemberRelationUseCase @Inject constructor(
+ private val organizationRepository: OrganizationRepository
+){
+ suspend operator fun invoke(orgId: Long, studentId: Long, limit: Int?) = runCatching {
+ organizationRepository.getStudentRelation(orgId, studentId, limit)
+ }
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/organization/GetOrganizaionInviteCodeUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/organization/GetOrganizaionInviteCodeUseCase.kt
new file mode 100644
index 00000000..cb2bf6bf
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/organization/GetOrganizaionInviteCodeUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.organization
+
+import com.sixkids.domain.repository.OrganizationRepository
+import javax.inject.Inject
+
+class GetOrganizaionInviteCodeUseCase @Inject constructor(
+ private val organizationRepository: OrganizationRepository
+) {
+ suspend operator fun invoke(organizationId: Int) = runCatching{
+ organizationRepository.getOrganizationInviteCode(organizationId)
+ }
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/organization/GetOrganizationMemberUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/organization/GetOrganizationMemberUseCase.kt
new file mode 100644
index 00000000..3fbd4103
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/organization/GetOrganizationMemberUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.organization
+
+import com.sixkids.domain.repository.OrganizationRepository
+import javax.inject.Inject
+
+class GetOrganizationMemberUseCase @Inject constructor(
+ private val organizationRepository: OrganizationRepository
+){
+ suspend operator fun invoke(orgId: Int) = runCatching {
+ organizationRepository.getOrganizationMembers(orgId)
+ }
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/organization/GetOrganizationSummaryUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/organization/GetOrganizationSummaryUseCase.kt
new file mode 100644
index 00000000..ed307ad2
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/organization/GetOrganizationSummaryUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.organization
+
+import com.sixkids.domain.repository.OrganizationRepository
+import javax.inject.Inject
+
+class GetOrganizationSummaryUseCase @Inject constructor(
+ private val organizationRepository: OrganizationRepository
+){
+ suspend operator fun invoke(organizationId: Int) = runCatching {
+ organizationRepository.getOrganizationSummary(organizationId)
+ }
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/organization/GetSelectedOrganizationIdUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/organization/GetSelectedOrganizationIdUseCase.kt
index f319e966..063c4209 100644
--- a/android/domain/src/main/java/com/sixkids/domain/usecase/organization/GetSelectedOrganizationIdUseCase.kt
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/organization/GetSelectedOrganizationIdUseCase.kt
@@ -5,8 +5,8 @@ import javax.inject.Inject
class GetSelectedOrganizationIdUseCase @Inject constructor(
private val organizationRepository: OrganizationRepository
-){
- suspend operator fun invoke(): Int {
- return organizationRepository.getSelectedOrganizationId()
+) {
+ suspend operator fun invoke() = runCatching {
+ organizationRepository.getSelectedOrganizationId()
}
-}
\ No newline at end of file
+}
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/organization/GetStudentDetailUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/organization/GetStudentDetailUseCase.kt
new file mode 100644
index 00000000..c5171218
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/organization/GetStudentDetailUseCase.kt
@@ -0,0 +1,13 @@
+package com.sixkids.domain.usecase.organization
+
+import com.sixkids.domain.repository.OrganizationRepository
+import javax.inject.Inject
+
+class GetStudentDetailUseCase @Inject constructor(
+ private val organizationRepository: OrganizationRepository
+) {
+ suspend operator fun invoke(orgId: Long, studentId: Long) = runCatching {
+ organizationRepository.getStudentDetail(orgId, studentId)
+ }
+
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/organization/GetStudentRelationUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/organization/GetStudentRelationUseCase.kt
new file mode 100644
index 00000000..0a931e31
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/organization/GetStudentRelationUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.organization
+
+import com.sixkids.domain.repository.OrganizationRepository
+import javax.inject.Inject
+
+class GetStudentRelationUseCase @Inject constructor(
+ private val organizationRepository: OrganizationRepository
+){
+ suspend operator fun invoke(orgId: Long, sourceMemberId: Long, targetMemberId: Long) = runCatching {
+ organizationRepository.getStudentRelationDetail(orgId, sourceMemberId, targetMemberId)
+ }
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/organization/GreetingUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/organization/GreetingUseCase.kt
new file mode 100644
index 00000000..92238c9c
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/organization/GreetingUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.organization
+
+import com.sixkids.domain.repository.OrganizationRepository
+import javax.inject.Inject
+
+class GreetingUseCase @Inject constructor(
+ private val organizationRepository: OrganizationRepository
+){
+ suspend operator fun invoke(organizationId: Long, memberId: Long) = runCatching {
+ organizationRepository.tagGreeting(organizationId, memberId)
+ }
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/organization/JoinOrganizationUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/organization/JoinOrganizationUseCase.kt
new file mode 100644
index 00000000..1a2d1618
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/organization/JoinOrganizationUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.organization
+
+import com.sixkids.domain.repository.OrganizationRepository
+import javax.inject.Inject
+
+class JoinOrganizationUseCase @Inject constructor(
+ private val organizationRepository: OrganizationRepository
+){
+ suspend operator fun invoke(orgId: Int, code: String) = runCatching {
+ organizationRepository.joinOrganization(orgId, code)
+ }
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/organization/LoadSelectedOrganizationNameUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/organization/LoadSelectedOrganizationNameUseCase.kt
new file mode 100644
index 00000000..56ac01e8
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/organization/LoadSelectedOrganizationNameUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.organization
+
+import com.sixkids.domain.repository.OrganizationRepository
+import javax.inject.Inject
+
+class LoadSelectedOrganizationNameUseCase @Inject constructor(
+ private val organizationRepository: OrganizationRepository
+){
+ suspend operator fun invoke() = runCatching {
+ organizationRepository.loadSelectedOrganizationName()
+ }
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/organization/SaveSelectedOrganizationNameUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/organization/SaveSelectedOrganizationNameUseCase.kt
new file mode 100644
index 00000000..beaa8b52
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/organization/SaveSelectedOrganizationNameUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.organization
+
+import com.sixkids.domain.repository.OrganizationRepository
+import javax.inject.Inject
+
+class SaveSelectedOrganizationNameUseCase @Inject constructor(
+ private val organizationRepository: OrganizationRepository
+){
+ suspend operator fun invoke(organizationName: String) {
+ organizationRepository.saveSelectedOrganizationName(organizationName)
+ }
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/organization/UpdateClassNameUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/organization/UpdateClassNameUseCase.kt
new file mode 100644
index 00000000..de5b04ee
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/organization/UpdateClassNameUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.organization
+
+import com.sixkids.domain.repository.OrganizationRepository
+import javax.inject.Inject
+
+class UpdateClassNameUseCase @Inject constructor(
+ private val organizationRepository: OrganizationRepository
+) {
+ suspend operator fun invoke(organizationId: Int, name: String) = runCatching{
+ organizationRepository.updateOrganization(organizationId, name)
+ }
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/post/DeletePostUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/post/DeletePostUseCase.kt
new file mode 100644
index 00000000..566b2cd7
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/post/DeletePostUseCase.kt
@@ -0,0 +1,13 @@
+package com.sixkids.domain.usecase.post
+
+import com.sixkids.domain.repository.PostRepository
+import javax.inject.Inject
+
+class DeletePostUseCase @Inject constructor(
+ private val postRepository: PostRepository
+
+){
+ suspend operator fun invoke(postId: Long) = runCatching {
+ postRepository.deletePost(postId)
+ }
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/post/GetPostDetailUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/post/GetPostDetailUseCase.kt
new file mode 100644
index 00000000..029a73a9
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/post/GetPostDetailUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.post
+
+import com.sixkids.domain.repository.PostRepository
+import javax.inject.Inject
+
+class GetPostDetailUseCase @Inject constructor(
+ private val postRepository: PostRepository
+) {
+ suspend operator fun invoke(postId: Long) = runCatching {
+ postRepository.getPostDetail(postId)
+ }
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/post/GetPostListUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/post/GetPostListUseCase.kt
new file mode 100644
index 00000000..43d134d5
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/post/GetPostListUseCase.kt
@@ -0,0 +1,18 @@
+package com.sixkids.domain.usecase.post
+
+import com.sixkids.domain.repository.PostRepository
+import javax.inject.Inject
+
+class GetPostListUseCase @Inject constructor(
+ private val postRepository: PostRepository
+) {
+ suspend operator fun invoke(
+ organizationId: Int,
+ memberId: Int? = null,
+ postCategory: String
+ ) = postRepository.getPosts(
+ organizationId,
+ memberId,
+ postCategory
+ )
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/post/NewPostUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/post/NewPostUseCase.kt
new file mode 100644
index 00000000..ec12d853
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/post/NewPostUseCase.kt
@@ -0,0 +1,28 @@
+package com.sixkids.domain.usecase.post
+
+import com.sixkids.domain.repository.PostRepository
+import com.sixkids.model.Post
+import java.io.File
+import javax.inject.Inject
+
+class NewPostUseCase @Inject constructor(
+ private val postRepository: PostRepository
+){
+ suspend operator fun invoke(
+ organizationId: Long,
+ title: String,
+ content: String,
+ secretStatus: Boolean,
+ postCategory: String,
+ file: File?
+ ) = runCatching {
+ postRepository.createPost(
+ organizationId,
+ title,
+ content,
+ secretStatus,
+ postCategory,
+ file
+ )
+ }
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/post/ReportPostUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/post/ReportPostUseCase.kt
new file mode 100644
index 00000000..7151d311
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/post/ReportPostUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.post
+
+import com.sixkids.domain.repository.PostRepository
+import javax.inject.Inject
+
+class ReportPostUseCase @Inject constructor(
+ private val postRepository: PostRepository
+) {
+ suspend operator fun invoke(postId: Long) = runCatching {
+ postRepository.reportPost(postId)
+ }
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/post/UpdatePostUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/post/UpdatePostUseCase.kt
new file mode 100644
index 00000000..dcc3c559
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/post/UpdatePostUseCase.kt
@@ -0,0 +1,27 @@
+package com.sixkids.domain.usecase.post
+
+import com.sixkids.domain.repository.PostRepository
+import java.io.File
+import javax.inject.Inject
+
+class UpdatePostUseCase @Inject constructor(
+ private val postRepository: PostRepository
+) {
+ suspend operator fun invoke(
+ postId: Long,
+ title: String,
+ content: String,
+ secretStatus: Boolean,
+ postCategory: String,
+ file: File?
+ ) = runCatching {
+ postRepository.updatePost(
+ postId,
+ title,
+ content,
+ secretStatus,
+ postCategory,
+ file
+ )
+ }
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/relay/CreateRelayUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/relay/CreateRelayUseCase.kt
new file mode 100644
index 00000000..22b3dd58
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/relay/CreateRelayUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.relay
+
+import com.sixkids.domain.repository.RelayRepository
+import javax.inject.Inject
+
+class CreateRelayUseCase @Inject constructor(
+ private val relayRepository: RelayRepository
+){
+ suspend operator fun invoke(organizationId: Int, question: String) = runCatching {
+ relayRepository.createRelay(organizationId, question)
+ }
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/relay/GetRelayDetailUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/relay/GetRelayDetailUseCase.kt
new file mode 100644
index 00000000..444a91a6
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/relay/GetRelayDetailUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.relay
+
+import com.sixkids.domain.repository.RelayRepository
+import javax.inject.Inject
+
+class GetRelayDetailUseCase @Inject constructor(
+ private val relayRepository: RelayRepository
+){
+ suspend operator fun invoke(relayId: Long) = runCatching {
+ relayRepository.getRelayDetail(relayId)
+ }
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/relay/GetRelayHistoryUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/relay/GetRelayHistoryUseCase.kt
new file mode 100644
index 00000000..2a423d1d
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/relay/GetRelayHistoryUseCase.kt
@@ -0,0 +1,14 @@
+package com.sixkids.domain.usecase.relay
+
+import com.sixkids.domain.repository.RelayRepository
+import javax.inject.Inject
+
+class GetRelayHistoryUseCase @Inject constructor(
+ private val relayRepository: RelayRepository)
+{
+
+ suspend operator fun invoke(
+ organizationId: Int,
+ memberId: Int? = null
+ ) = relayRepository.getRelayHistory(organizationId, memberId)
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/relay/GetRelayQuestionUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/relay/GetRelayQuestionUseCase.kt
new file mode 100644
index 00000000..232d8b2e
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/relay/GetRelayQuestionUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.relay
+
+import com.sixkids.domain.repository.RelayRepository
+import javax.inject.Inject
+
+class GetRelayQuestionUseCase @Inject constructor(
+ private val relayRepository: RelayRepository
+){
+ suspend operator fun invoke(relayId: Long) = runCatching {
+ relayRepository.getRelayQuestion(relayId)
+ }
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/relay/GetRunningRelayUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/relay/GetRunningRelayUseCase.kt
new file mode 100644
index 00000000..a30f05da
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/relay/GetRunningRelayUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.relay
+
+import com.sixkids.domain.repository.RelayRepository
+import javax.inject.Inject
+
+class GetRunningRelayUseCase @Inject constructor(
+ private val relayRepository: RelayRepository
+){
+ suspend operator fun invoke(organizationId: Long) = runCatching {
+ relayRepository.getRunningRelay(organizationId)
+ }
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/relay/ReceiveRelayUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/relay/ReceiveRelayUseCase.kt
new file mode 100644
index 00000000..26e9e6c4
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/relay/ReceiveRelayUseCase.kt
@@ -0,0 +1,16 @@
+package com.sixkids.domain.usecase.relay
+
+import com.sixkids.domain.repository.RelayRepository
+import javax.inject.Inject
+
+class ReceiveRelayUseCase @Inject constructor(
+ private val relayRepository: RelayRepository
+){
+ suspend operator fun invoke(
+ relayId: Int,
+ senderId: Long,
+ question: String
+ ) = runCatching {
+ relayRepository.receiveRelay(relayId, senderId, question)
+ }
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/relay/SendRelayUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/relay/SendRelayUseCase.kt
new file mode 100644
index 00000000..a0e14e69
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/relay/SendRelayUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.relay
+
+import com.sixkids.domain.repository.RelayRepository
+import javax.inject.Inject
+
+class SendRelayUseCase @Inject constructor(
+ private val relayRepository: RelayRepository
+){
+ suspend operator fun invoke(relayId: Int) = runCatching {
+ relayRepository.sendRelay(relayId)
+ }
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/user/GetATKUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/user/GetATKUseCase.kt
new file mode 100644
index 00000000..f7bf9794
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/user/GetATKUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.user
+
+import com.sixkids.domain.repository.TokenRepository
+import javax.inject.Inject
+
+class GetATKUseCase @Inject constructor(
+ private val tokenRepository: TokenRepository
+){
+ suspend operator fun invoke() = runCatching {
+ tokenRepository.getAccessToken()
+ }
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/user/GetMemberSimpleUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/user/GetMemberSimpleUseCase.kt
new file mode 100644
index 00000000..f6fa833b
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/user/GetMemberSimpleUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.user
+
+import com.sixkids.domain.repository.UserRepository
+import javax.inject.Inject
+
+class GetMemberSimpleUseCase @Inject constructor(
+ private val userRepository: UserRepository
+){
+ suspend operator fun invoke(id: Long) = runCatching {
+ userRepository.getMemberSimpleInfo(id)
+ }
+}
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/user/GetStudentHomeInfoUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/user/GetStudentHomeInfoUseCase.kt
new file mode 100644
index 00000000..e8a97405
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/user/GetStudentHomeInfoUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.user
+
+import com.sixkids.domain.repository.UserRepository
+import javax.inject.Inject
+
+class GetStudentHomeInfoUseCase @Inject constructor(
+ private val userRepository: UserRepository
+) {
+ suspend operator fun invoke(organizationId: Long) = runCatching {
+ userRepository.getStudentHomeInfo(organizationId)
+ }
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/user/LoadUserInfoUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/user/LoadUserInfoUseCase.kt
new file mode 100644
index 00000000..8a60f426
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/user/LoadUserInfoUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.user
+
+import com.sixkids.domain.repository.UserRepository
+import javax.inject.Inject
+
+class LoadUserInfoUseCase @Inject constructor(
+ private val userRepository: UserRepository
+){
+ suspend operator fun invoke() = runCatching {
+ userRepository.loadUserInfo()
+ }
+}
\ No newline at end of file
diff --git a/android/feature/navigator/build.gradle.kts b/android/feature/navigator/build.gradle.kts
index 06cef812..4f4dc89b 100644
--- a/android/feature/navigator/build.gradle.kts
+++ b/android/feature/navigator/build.gradle.kts
@@ -14,6 +14,12 @@ dependencies {
implementation(projects.feature.signin)
implementation(projects.feature.teacher.managestudent)
implementation(projects.feature.teacher.main)
+ implementation(projects.feature.student.board)
+ implementation(projects.feature.student.home)
+ implementation(projects.feature.student.challenge)
+ implementation(projects.feature.student.relay)
+ implementation(projects.feature.student.main)
+ implementation(projects.feature.teacher.relay)
implementation(libs.permissions)
}
diff --git a/android/feature/navigator/src/main/java/com/sixkids/feature/navigator/MainNavigationTab.kt b/android/feature/navigator/src/main/java/com/sixkids/feature/navigator/MainNavigationTab.kt
index eec4204d..80b25269 100644
--- a/android/feature/navigator/src/main/java/com/sixkids/feature/navigator/MainNavigationTab.kt
+++ b/android/feature/navigator/src/main/java/com/sixkids/feature/navigator/MainNavigationTab.kt
@@ -8,6 +8,10 @@ import com.sixkids.designsystem.theme.Green
import com.sixkids.designsystem.theme.Purple
import com.sixkids.designsystem.theme.Red
import com.sixkids.navigator.R
+import com.sixkids.student.board.navigation.StudentBoardRoute
+import com.sixkids.student.home.navigation.StudentHomeRoute
+import com.sixkids.student.navigation.ChallengeRoute
+import com.sixkids.student.relay.navigation.RelayRoute
import com.sixkids.teacher.board.navigation.BoardRoute
import com.sixkids.teacher.home.navigation.HomeRoute
import com.sixkids.teacher.manageclass.navigation.ManageClassRoute
@@ -27,7 +31,7 @@ enum class MainNavigationTab(
route = HomeRoute.defaultRoute,
),
BOARD(
- iconId = R.drawable.board,
+ iconId = R.drawable.ic_board,
iconTint = Blue,
labelId = R.string.bottom_navigation_tab_label_board,
route = BoardRoute.defaultRoute,
@@ -43,6 +47,30 @@ enum class MainNavigationTab(
iconTint = Green,
labelId = R.string.bottom_navigation_tab_label_manage_class,
route = ManageClassRoute.defaultRoute,
+ ),
+ STUDENT_HOME(
+ iconId = R.drawable.home,
+ iconTint = Red,
+ labelId = R.string.bottom_navigation_tab_label_home,
+ route = StudentHomeRoute.defaultRoute,
+ ),
+ STUDENT_BOARD(
+ iconId = R.drawable.ic_board,
+ iconTint = Blue,
+ labelId = R.string.bottom_navigation_tab_label_board,
+ route = StudentBoardRoute.defaultRoute,
+ ),
+ STUDENT_RELAY(
+ iconId = R.drawable.folded_hands,
+ iconTint = Purple,
+ labelId = R.string.bottom_navigation_tab_label_relay,
+ route = RelayRoute.defaultRoute,
+ ),
+ STUDENT_CHALLENGE(
+ iconId = R.drawable.home_rocket,
+ iconTint = Green,
+ labelId = R.string.bottom_navigation_tab_label_challenge,
+ route = ChallengeRoute.defaultRoute,
)
;
@@ -55,4 +83,4 @@ enum class MainNavigationTab(
return entries.find { it.route == route }
}
}
-}
\ No newline at end of file
+}
diff --git a/android/feature/navigator/src/main/java/com/sixkids/feature/navigator/MainNavigator.kt b/android/feature/navigator/src/main/java/com/sixkids/feature/navigator/MainNavigator.kt
index 7d6e9bdc..bbe5c76c 100644
--- a/android/feature/navigator/src/main/java/com/sixkids/feature/navigator/MainNavigator.kt
+++ b/android/feature/navigator/src/main/java/com/sixkids/feature/navigator/MainNavigator.kt
@@ -1,6 +1,8 @@
package com.sixkids.feature.navigator
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.navigation.NavDestination
import androidx.navigation.NavHostController
@@ -12,10 +14,50 @@ import com.sixkids.feature.signin.navigation.SignInRoute
import com.sixkids.feature.signin.navigation.navigateSignIn
import com.sixkids.feature.signin.navigation.navigateSignUp
import com.sixkids.feature.signin.navigation.navigateSignUpPhoto
+import com.sixkids.model.GroupType
+import com.sixkids.model.MemberSimple
+import com.sixkids.student.board.navigation.StudentBoardRoute
+import com.sixkids.student.board.navigation.navigateStudentBoard
+import com.sixkids.student.board.navigation.navigateStudentBoardDetail
+import com.sixkids.student.board.navigation.navigateStudentBoardWrite
+import com.sixkids.student.home.navigation.StudentHomeRoute
+import com.sixkids.student.home.navigation.navigateStudentAnnounceDetail
+import com.sixkids.student.home.navigation.navigateStudentAnnounceList
+import com.sixkids.student.home.navigation.navigateStudentChatting
+import com.sixkids.student.home.navigation.navigateStudentGreetingReceiver
+import com.sixkids.student.home.navigation.navigateStudentGreetingSender
+import com.sixkids.student.home.navigation.navigateStudentHome
+import com.sixkids.student.main.navigation.navigateJoinOrganization
+import com.sixkids.student.main.navigation.navigateStudentOrganizationList
+import com.sixkids.student.main.navigation.navigateStudentProfile
+import com.sixkids.student.navigation.navigatePopupToStudentChallengeHistory
+import com.sixkids.student.navigation.navigateStudentChallengeHistory
+import com.sixkids.student.navigation.navigateStudentGroupCreate
+import com.sixkids.student.navigation.navigateStudentGroupJoin
+import com.sixkids.student.navigation.navigateStudentMatchedGroupCreate
+import com.sixkids.student.relay.navigation.RelayRoute
+import com.sixkids.student.relay.navigation.navigateStudentRelayAnswer
+import com.sixkids.student.relay.navigation.navigateStudentRelayCreate
+import com.sixkids.student.relay.navigation.navigateStudentRelayCreateResult
+import com.sixkids.student.relay.navigation.navigateStudentRelayDetail
+import com.sixkids.student.relay.navigation.navigateStudentRelayHistory
+import com.sixkids.student.relay.navigation.navigateStudentRelayJoin
+import com.sixkids.student.relay.navigation.navigateStudentRelayTaggingReceiver
+import com.sixkids.student.relay.navigation.navigateStudentRelayTaggingSender
+import com.sixkids.teacher.board.navigation.BoardRoute
+import com.sixkids.teacher.board.navigation.navigateAnnounce
+import com.sixkids.teacher.board.navigation.navigateAnnounceDetail
+import com.sixkids.teacher.board.navigation.navigateAnnounceWrite
import com.sixkids.teacher.board.navigation.navigateBoard
+import com.sixkids.teacher.board.navigation.navigateChatting
+import com.sixkids.teacher.board.navigation.navigatePost
+import com.sixkids.teacher.board.navigation.navigatePostWrite
+import com.sixkids.teacher.board.navigation.navigatePostDetail
+import com.sixkids.teacher.challenge.navigation.navigateChallengeCreatedResult
import com.sixkids.teacher.challenge.navigation.navigateChallengeDetail
import com.sixkids.teacher.challenge.navigation.navigateChallengeHistory
import com.sixkids.teacher.challenge.navigation.navigateCreateChallenge
+import com.sixkids.teacher.challenge.navigation.navigatePopupToHistory
import com.sixkids.teacher.home.navigation.HomeRoute
import com.sixkids.teacher.home.navigation.navigateHome
import com.sixkids.teacher.home.navigation.navigateRank
@@ -23,13 +65,21 @@ import com.sixkids.teacher.main.navigation.navigateNewOrganization
import com.sixkids.teacher.main.navigation.navigateProfile
import com.sixkids.teacher.main.navigation.navigateTeacherOrganizationList
import com.sixkids.teacher.manageclass.navigation.ManageClassRoute
+import com.sixkids.teacher.manageclass.navigation.navigateChattingFilter
+import com.sixkids.teacher.manageclass.navigation.navigateClassSetting
+import com.sixkids.teacher.manageclass.navigation.navigateInvite
import com.sixkids.teacher.manageclass.navigation.navigateManageClass
+import com.sixkids.teacher.manageclass.navigation.navigateStatistics
import com.sixkids.teacher.managestudent.navigation.ManageStudentRoute
import com.sixkids.teacher.managestudent.navigation.navigateManageStudent
+import com.sixkids.teacher.managestudent.navigation.navigateStudentDetail
+import com.sixkids.teacher.relay.navigation.navigateTeacherRelayDetail
+import com.sixkids.teacher.relay.navigation.navigateTeacherRelayHistory
class MainNavigator(
val navController: NavHostController,
) {
+ var bottomTabItems: State>? = null
val startDestination = SignInRoute.defaultRoute
private val currentDestination: NavDestination?
@Composable get() = navController
@@ -40,7 +90,7 @@ class MainNavigator(
?.let { MainNavigationTab.find(it) }
fun navigate(tab: MainNavigationTab) {
- val navOptions = navOptions {
+ val teacherNavOptions = navOptions {
popUpTo(HomeRoute.defaultRoute) {
saveState = true
}
@@ -48,30 +98,96 @@ class MainNavigator(
restoreState = true
}
+ val studentNavOptions = navOptions {
+ popUpTo(StudentHomeRoute.defaultRoute) {
+ saveState = true
+ }
+ launchSingleTop = true
+ restoreState = true
+ }
+
when (tab) {
- MainNavigationTab.HOME -> navController.navigateHome(navOptions)
- MainNavigationTab.BOARD -> navController.navigateBoard(navOptions)
- MainNavigationTab.MANAGE_STUDENT -> navController.navigateManageStudent(navOptions)
- MainNavigationTab.MANAGE_CLASS -> navController.navigateManageClass(navOptions)
+ // 선생님 바텀 네비게이션 탭
+ MainNavigationTab.HOME -> navController.navigateHome(teacherNavOptions)
+ MainNavigationTab.BOARD -> navController.navigateBoard(teacherNavOptions)
+ MainNavigationTab.MANAGE_STUDENT -> navController.navigateManageStudent(teacherNavOptions)
+ MainNavigationTab.MANAGE_CLASS -> navController.navigateManageClass(teacherNavOptions)
+ // 학생 바텀 네비게이션 탭
+ MainNavigationTab.STUDENT_HOME -> navController.navigateStudentHome(studentNavOptions)
+ MainNavigationTab.STUDENT_BOARD -> navController.navigateStudentBoard(studentNavOptions)
+ MainNavigationTab.STUDENT_RELAY -> navController.navigateStudentRelayHistory(studentNavOptions)
+ MainNavigationTab.STUDENT_CHALLENGE -> navController.navigateStudentChallengeHistory(studentNavOptions)
}
}
- fun popBackStack() {
- navController.popBackStack()
+ /**
+ * Board Navigation
+ */
+
+ fun navigateBoard() {
+ navController.navigate(BoardRoute.defaultRoute) {
+ popUpTo(navController.graph.id) {
+ inclusive = true
+ }
+ }
+ }
+
+ fun navigatePost() {
+ navController.navigatePost()
+ }
+
+ fun navigatePostWrite() {
+ navController.navigatePostWrite()
+ }
+
+ fun navigatePostDetail(postId: Long) {
+ navController.navigatePostDetail(postId)
+ }
+
+ fun navigateAnnounce() {
+ navController.navigateAnnounce()
+ }
+
+ fun navigateAnnounceWrite() {
+ navController.navigateAnnounceWrite()
+ }
+
+ fun navigateAnnounceDetail(announceId: Long) {
+ navController.navigateAnnounceDetail(announceId)
+ }
+
+ /**
+ * Student Main Navigation
+ */
+ fun navigateStudentOrganizationList(){
+ navController.navigateStudentOrganizationList()
+ }
+
+ fun navigateStudentProfile(){
+ navController.navigateStudentProfile()
+ }
+
+ fun navigateJoinOrganization() {
+ navController.navigateJoinOrganization()
+ }
+
+
+ /**
+ * Teacher Manage Student Navigation
+ */
+ fun navigateManageStudentDetail(studentId: Long) {
+ navController.navigateStudentDetail(studentId)
}
/**
* Home Navigation
*/
fun navigateHome() {
- navController.navigate(HomeRoute.defaultRoute){
- popUpTo(navController.graph.id){
- inclusive = true
- }
- }
+ bottomTabItems = teacherTab() // 바텀 네비게이션 탭 초기화
+ navController.navigate(HomeRoute.defaultRoute)
}
- fun navigateRank(){
+ fun navigateRank() {
navController.navigateRank()
}
@@ -86,56 +202,224 @@ class MainNavigator(
}
}
+ fun navigateChattingFilter() {
+ navController.navigateChattingFilter()
+ }
+
+ fun navigateClassInvite() {
+ navController.navigateInvite()
+ }
+
+ fun navigateClassSetting() {
+ navController.navigateClassSetting()
+ }
+
+ fun navigateClassStatistics() {
+ navController.navigateStatistics()
+ }
+
/**
* Manage Student Navigation
*/
fun navigateManageStudent() {
- navController.navigateManageStudent(navOptions{
- popUpTo(ManageStudentRoute.defaultRoute){
+ navController.navigateManageStudent(navOptions {
+ popUpTo(ManageStudentRoute.defaultRoute) {
+ inclusive = true
+ }
+ })
+ }
+
+ /**
+ * Student Home Navigation
+ */
+ fun navigateStudentHome() {
+ bottomTabItems = studentTab() // 바텀 네비게이션 탭 초기화
+ navController.navigateStudentHome(navOptions{
+ popUpTo(StudentHomeRoute.defaultRoute){
inclusive = true
}
})
}
+ fun navigateStudentAnnounceList() {
+ navController.navigateStudentAnnounceList()
+ }
+
+ fun navigateStudentAnnounceDetail(announceId: Long) {
+ navController.navigateStudentAnnounceDetail(announceId)
+ }
+
+ fun navigateStudentRank() {
+ navController.navigateRank()
+ }
+
+ fun navigateGreetingSender(){
+ navController.navigateStudentGreetingSender()
+ }
+
+ fun navigateGreetingReceiver(){
+ navController.navigateStudentGreetingReceiver()
+ }
+
+ /**
+ * Student Group Navigation
+ */
+ //TODO : memberId 추가
+
+ fun navigatePopupToStudentGroupHistory() {
+ navController.navigatePopupToStudentChallengeHistory()
+ }
+
+ fun navigateStudentGroupCreate(challengeId: Long, groupType: GroupType) {
+ navController.navigateStudentGroupCreate(
+ challengeId = challengeId,
+ groupType = groupType
+ )
+ }
+
+ fun navigateStudentMatchedGroupCreate(challengeId: Long, members: List) {
+ navController.navigateStudentMatchedGroupCreate(
+ challengeId = challengeId,
+ members = members
+ )
+ }
+
+ fun navigateStudentGroupJoin(memberId: Long) {
+ navController.navigateStudentGroupJoin()
+ }
+
/**
* SignIn Navigation
*/
- fun navigateSignIn(){
+ fun navigateSignIn() {
navController.navigateSignIn()
}
- fun navigateSignUp(){
+ fun navigateSignUp() {
navController.navigateSignUp()
}
- fun navigateSignUpPhoto(isTeacher: Boolean){
+ fun navigateSignUpPhoto(isTeacher: Boolean) {
navController.navigateSignUpPhoto(isTeacher)
}
+ /**
+ * Student Relay Navigation
+ */
+ fun navigateStudentRelayHistory() {
+ navController.navigate(RelayRoute.defaultRoute) {
+ popUpTo(RelayRoute.defaultRoute) {
+ inclusive = true
+ }
+ }
+ }
+
+ fun navigateStudentRelayDetail(relayId: Long) {
+ navController.navigateStudentRelayDetail(relayId)
+ }
+
+ fun navigateStudentRelayCreate() {
+ navController.navigateStudentRelayCreate()
+ }
+
+ fun navigateStudentRelayJoin() {
+ navController.navigateStudentRelayJoin()
+ }
+
+ fun navigateStudentRelayCreateResult() {
+ navController.navigateStudentRelayCreateResult()
+ }
+
+ fun navigateStudentRelayAnswer(relayId: Long) {
+ navController.navigateStudentRelayAnswer(relayId)
+ }
+
+ fun navigateStudentRelayTaggingSender(relayId: Long, question: String) {
+ navController.navigateStudentRelayTaggingSender(relayId, question)
+ }
+
+ fun navigateStudentRelayTaggingReceiver(relayId: Long) {
+ navController.navigateStudentRelayTaggingReceiver(relayId)
+ }
+
+ /**
+ * Challenge Navigation
+ */
fun navigateChallengeHistory() {
navController.navigateChallengeHistory()
}
- fun navigateChallengeDetail(challengeId: Int) {
- navController.navigateChallengeDetail(challengeId)
+ fun navigateChallengeDetail(challengeId: Long, groupId: Long?) {
+ navController.navigateChallengeDetail(challengeId, groupId)
+ }
+
+ fun navigatePopupToHistory() {
+ navController.navigatePopupToHistory()
}
fun navigateCreateChallenge() {
navController.navigateCreateChallenge()
}
- fun navigateTeacherOrganizationList(){
+ fun navigateChallengeCreatedResult(challengeId: Long, title: String) {
+ navController.navigateChallengeCreatedResult(challengeId, title)
+ }
+
+ /**
+ * Teacher Relay
+ */
+ fun navigateTeacherRelayHistory(){
+ navController.navigateTeacherRelayHistory()
+ }
+
+ fun navigateTeacherRelayDetail(relayId: Long){
+ navController.navigateTeacherRelayDetail(relayId)
+ }
+
+ fun navigateTeacherOrganizationList() {
+ bottomTabItems = teacherTab() // 바텀 네비게이션 탭 초기화
navController.navigateTeacherOrganizationList()
}
- fun navigateNewOrganization(){
+ fun navigateNewOrganization() {
navController.navigateNewOrganization()
}
- fun navigateProfile(){
+ fun navigateProfile() {
navController.navigateProfile()
}
+ fun popBackStack() {
+ navController.popBackStack()
+ }
+
+ fun navigateChatting() {
+ navController.navigateChatting()
+ }
+
+ /**
+ * Student Board Navigation
+ */
+ fun navigateStudentBoard() {
+ navController.navigate(StudentBoardRoute.defaultRoute) {
+ popUpTo(navController.graph.id) {
+ inclusive = true
+ }
+ }
+ }
+
+ fun navigateStudentBoardWrite() {
+ navController.navigateStudentBoardWrite()
+ }
+
+ fun navigateStudentBoardDetail(postId: Long) {
+ navController.navigateStudentBoardDetail(postId)
+ }
+
+ fun navigateStudentChatting() {
+ navController.navigateStudentChatting()
+ }
+
@Composable
fun shouldShowBottomBar(): Boolean {
val currentRoute = currentDestination?.route ?: return false
@@ -149,3 +433,25 @@ internal fun rememberMainNavigator(
): MainNavigator = remember(navController) {
MainNavigator(navController)
}
+
+internal fun teacherTab(): State>{
+ return mutableStateOf(
+ listOf(
+ MainNavigationTab.HOME,
+ MainNavigationTab.BOARD,
+ MainNavigationTab.MANAGE_STUDENT,
+ MainNavigationTab.MANAGE_CLASS
+ )
+ )
+}
+
+internal fun studentTab(): State>{
+ return mutableStateOf(
+ listOf(
+ MainNavigationTab.STUDENT_HOME,
+ MainNavigationTab.STUDENT_BOARD,
+ MainNavigationTab.STUDENT_RELAY,
+ MainNavigationTab.STUDENT_CHALLENGE
+ )
+ )
+}
diff --git a/android/feature/navigator/src/main/java/com/sixkids/feature/navigator/MainScreen.kt b/android/feature/navigator/src/main/java/com/sixkids/feature/navigator/MainScreen.kt
index 486cb815..8fb5464f 100644
--- a/android/feature/navigator/src/main/java/com/sixkids/feature/navigator/MainScreen.kt
+++ b/android/feature/navigator/src/main/java/com/sixkids/feature/navigator/MainScreen.kt
@@ -3,8 +3,7 @@ package com.sixkids.feature.navigator
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.EnterTransition
-import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideIn
@@ -37,12 +36,19 @@ import com.google.accompanist.permissions.rememberPermissionState
import com.sixkids.designsystem.component.snackbar.UlbanSnackbar
import com.sixkids.designsystem.theme.Cream
import com.sixkids.feature.signin.navigation.signInNavGraph
+import com.sixkids.student.board.navigation.studentBoardNavGraph
+import com.sixkids.student.home.navigation.studentHomeNavGraph
+import com.sixkids.student.main.navigation.studentOrganizationListNavGraph
+import com.sixkids.student.navigation.studentChallengeNavGraph
+import com.sixkids.student.navigation.studentGroupNavGraph
+import com.sixkids.student.relay.navigation.studentRelayNavGraph
import com.sixkids.teacher.board.navigation.boardNavGraph
import com.sixkids.teacher.challenge.navigation.challengeNavGraph
import com.sixkids.teacher.home.navigation.homeNavGraph
import com.sixkids.teacher.main.navigation.teacherOrganizationListNavGraph
import com.sixkids.teacher.manageclass.navigation.manageClassNavGraph
import com.sixkids.teacher.managestudent.navigation.manageStudentNavGraph
+import com.sixkids.teacher.relay.navigation.teacherRelayNavGraph
import com.sixkids.ui.extension.collectWithLifecycle
@Composable
@@ -62,7 +68,7 @@ fun MainScreen(
}
}
- if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
RequestNotificationPermission()
}
@@ -73,30 +79,44 @@ fun MainScreen(
modifier = Modifier,
selectedTab = navigator.currentTab ?: MainNavigationTab.HOME,
itemClick = navigator::navigate,
+ bottomTavItems = navigator.bottomTabItems?.value
)
}
) { innerPadding ->
NavHost(
navController = navigator.navController,
startDestination = navigator.startDestination,
- enterTransition = { EnterTransition.None },
- exitTransition = { ExitTransition.None },
+ enterTransition = { fadeIn(animationSpec = tween(0)) },
+ exitTransition = { fadeOut(animationSpec = tween(0)) },
)
{
homeNavGraph(
padding = innerPadding,
navigateToRank = navigator::navigateRank,
navigateToChallenge = navigator::navigateChallengeHistory,
+ navigateToRelay = navigator::navigateTeacherRelayHistory,
+ navigateToQuiz = { } ,
+ onShowSnackBar = viewModel::onShowSnackbar,
)
boardNavGraph(
padding = innerPadding,
+ navigateToPost = navigator::navigatePost,
+ navigateToPostDetail = navigator::navigatePostDetail,
+ onBackClick = navigator::popBackStack,
+ onShowSnackBar = viewModel::onShowSnackbar,
+ navigateToChatting = navigator::navigateChatting,
+ navigateToPostWrite = navigator::navigatePostWrite,
+ navigateToAnnounceDetail = navigator::navigateAnnounceDetail,
+ navigateToAnnounceWrite = navigator::navigateAnnounceWrite,
+ navigateToAnnounceList = navigator::navigateAnnounce,
)
challengeNavGraph(
- padding = innerPadding,
navigateChallengeDetail = navigator::navigateChallengeDetail,
navigateCreateChallenge = navigator::navigateCreateChallenge,
+ navigateChallengeCreatedResult = navigator::navigateChallengeCreatedResult,
+ navigateChallengeHistory = navigator::navigatePopupToHistory,
handleException = viewModel::handleException,
showSnackbar = viewModel::onShowSnackbar,
navigateUp = navigator::popBackStack,
@@ -104,10 +124,18 @@ fun MainScreen(
manageClassNavGraph(
padding = innerPadding,
+ onShowSnackBar = viewModel::onShowSnackbar,
+ navigateToClassSummary = navigator::navigateClassStatistics,
+ navigateToClassSetting = navigator::navigateClassSetting,
+ navigateToChattingFilter = navigator::navigateChattingFilter,
+ navigateToInvite = navigator::navigateClassInvite,
+ navigateBack = navigator::popBackStack,
)
manageStudentNavGraph(
padding = innerPadding,
+ navigateToStudentDetail = navigator::navigateManageStudentDetail,
+ handleException = viewModel::handleException,
)
signInNavGraph(
@@ -117,6 +145,7 @@ fun MainScreen(
onShowSnackBar = viewModel::onShowSnackbar,
onBackClick = navigator::popBackStack,
navigateToTeacherOrganizationList = navigator::navigateTeacherOrganizationList,
+ navigateToStudentOrganizationList = navigator::navigateStudentOrganizationList,
)
teacherOrganizationListNavGraph(
@@ -128,6 +157,72 @@ fun MainScreen(
navigateToSignIn = navigator::navigateSignIn,
)
+ studentHomeNavGraph(
+ padding = innerPadding,
+ onShowSnackbar = viewModel::onShowSnackbar,
+ navigateToStudentAnnounceList = navigator::navigateStudentAnnounceList,
+ navigateToStudentAnnounceDetail = navigator::navigateStudentAnnounceDetail,
+ navigateToTagHello = { },
+ navigateToRank = navigator::navigateRank,
+ navigateToChatting = navigator::navigateStudentChatting,
+ navigateBack = navigator::popBackStack,
+ navigateToGreetingSender = navigator::navigateGreetingSender,
+ navigateToGreetingReceiver = navigator::navigateGreetingReceiver,
+ onBackClick = navigator::popBackStack,
+ )
+
+ studentChallengeNavGraph(
+ navigateChallengeDetail = navigator::navigateChallengeDetail,
+ navigateToCreateGroup = navigator::navigateStudentGroupCreate,
+ navigateToMatchedGroupCreate = navigator::navigateStudentMatchedGroupCreate,
+ navigateToJoinGroup = navigator::navigateStudentGroupJoin,
+ handleException = viewModel::handleException,
+ )
+
+ studentGroupNavGraph(
+ navigateToChallengeHistory = navigator::navigatePopupToStudentGroupHistory,
+ handleException = viewModel::handleException,
+ )
+
+ studentOrganizationListNavGraph(
+ navigateToJoinOrganization = navigator::navigateJoinOrganization,
+ navigateToProfile = navigator::navigateStudentProfile,
+ navigateToHome = navigator::navigateStudentHome,
+ navigateToSignIn = navigator::navigateSignIn,
+ onShowSnackBar = viewModel::onShowSnackbar,
+ onBackClick = navigator::popBackStack,
+ )
+
+ studentRelayNavGraph(
+ padding = innerPadding,
+ navigateRelayHistory = navigator::navigateStudentRelayHistory,
+ navigateRelayDetail = navigator::navigateStudentRelayDetail,
+ navigateCreateRelay = navigator::navigateStudentRelayCreate,
+ navigateCreateRelayResult = navigator::navigateStudentRelayCreateResult,
+ navigateJoinRelay = navigator::navigateStudentRelayJoin,
+ navigateAnswerRelay = navigator::navigateStudentRelayAnswer,
+ navigateTaggingSender = navigator::navigateStudentRelayTaggingSender,
+ navigateTaggingReceiver = navigator::navigateStudentRelayTaggingReceiver,
+ onShowSnackBar = viewModel::onShowSnackbar,
+ onBackClick = navigator::popBackStack,
+ handleException = viewModel::handleException
+ )
+
+ studentBoardNavGraph(
+ padding = innerPadding,
+ onShowSnackBar = viewModel::onShowSnackbar,
+ navigateToStudentBoardDetail = navigator::navigateStudentBoardDetail,
+ navigateToStudentBoardWrite = navigator::navigateStudentBoardWrite,
+ navigateBack = navigator::popBackStack,
+ )
+
+ teacherRelayNavGraph(
+ padding = innerPadding,
+ navigateRelayHistory = navigator::navigateTeacherRelayHistory,
+ navigateRelayDetail = navigator::navigateTeacherRelayDetail,
+ handleException = viewModel::handleException
+ )
+
}
with(uiState) {
UlbanSnackbar(
@@ -149,6 +244,7 @@ fun BottomNav(
modifier: Modifier,
itemClick: (MainNavigationTab) -> Unit = {},
selectedTab: MainNavigationTab,
+ bottomTavItems: List? = null
) {
val selectedItem = rememberUpdatedState(newValue = selectedTab)
@@ -166,7 +262,7 @@ fun BottomNav(
modifier = modifier,
containerColor = Cream,
) {
- MainNavigationTab.entries.forEach { item ->
+ bottomTavItems?.forEach { item ->
NavigationBarItem(
icon = {
Icon(
@@ -202,7 +298,7 @@ fun RequestNotificationPermission() {
)
LaunchedEffect(Unit) {
- if(notificationPermissionState.status.isGranted.not()) {
+ if (notificationPermissionState.status.isGranted.not()) {
notificationPermissionState.launchPermissionRequest()
}
}
diff --git a/android/feature/navigator/src/main/java/com/sixkids/feature/navigator/MainViewModel.kt b/android/feature/navigator/src/main/java/com/sixkids/feature/navigator/MainViewModel.kt
index d8ac2047..745bf33a 100644
--- a/android/feature/navigator/src/main/java/com/sixkids/feature/navigator/MainViewModel.kt
+++ b/android/feature/navigator/src/main/java/com/sixkids/feature/navigator/MainViewModel.kt
@@ -1,7 +1,6 @@
package com.sixkids.feature.navigator
import androidx.lifecycle.viewModelScope
-import com.sixkids.domain.usecase.user.UpdateFCMTokenUseCase
import com.sixkids.ui.SnackbarToken
import com.sixkids.ui.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
diff --git a/android/feature/navigator/src/main/res/drawable/folded_hands.xml b/android/feature/navigator/src/main/res/drawable/folded_hands.xml
new file mode 100644
index 00000000..6f9f566e
--- /dev/null
+++ b/android/feature/navigator/src/main/res/drawable/folded_hands.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
diff --git a/android/feature/navigator/src/main/res/drawable/home.xml b/android/feature/navigator/src/main/res/drawable/home.xml
new file mode 100644
index 00000000..7480e7b0
--- /dev/null
+++ b/android/feature/navigator/src/main/res/drawable/home.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
diff --git a/android/feature/navigator/src/main/res/drawable/board.xml b/android/feature/navigator/src/main/res/drawable/ic_board.xml
similarity index 100%
rename from android/feature/navigator/src/main/res/drawable/board.xml
rename to android/feature/navigator/src/main/res/drawable/ic_board.xml
diff --git a/android/feature/navigator/src/main/res/values/strings.xml b/android/feature/navigator/src/main/res/values/strings.xml
index 50fc3daa..ffa0a109 100644
--- a/android/feature/navigator/src/main/res/values/strings.xml
+++ b/android/feature/navigator/src/main/res/values/strings.xml
@@ -4,4 +4,6 @@
게시판
학생관리
학급관리
+ 함께달리기
+ 이어달리기
diff --git a/android/feature/signin/src/main/java/com/sixkids/feature/signin/login/LoginContract.kt b/android/feature/signin/src/main/java/com/sixkids/feature/signin/login/LoginContract.kt
index d4eaa1f6..82cac746 100644
--- a/android/feature/signin/src/main/java/com/sixkids/feature/signin/login/LoginContract.kt
+++ b/android/feature/signin/src/main/java/com/sixkids/feature/signin/login/LoginContract.kt
@@ -8,9 +8,10 @@ sealed interface LoginEffect : SideEffect{
data object NavigateToHome : LoginEffect
data object NavigateToSignUp : LoginEffect
data object NavigateToTeacherOrganizationList : LoginEffect
+ data object NavigateToStudentOrganizationList : LoginEffect
data class OnShowSnackBar(val tkn : SnackbarToken) : LoginEffect
}
data class LoginState(
val isLoading : Boolean = false,
-) : UiState
\ No newline at end of file
+) : UiState
diff --git a/android/feature/signin/src/main/java/com/sixkids/feature/signin/login/LoginScreen.kt b/android/feature/signin/src/main/java/com/sixkids/feature/signin/login/LoginScreen.kt
index a5424491..dfbe44ce 100644
--- a/android/feature/signin/src/main/java/com/sixkids/feature/signin/login/LoginScreen.kt
+++ b/android/feature/signin/src/main/java/com/sixkids/feature/signin/login/LoginScreen.kt
@@ -2,7 +2,6 @@ package com.sixkids.feature.signin.login
import android.util.Log
import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInHorizontally
import androidx.compose.foundation.Image
@@ -58,16 +57,12 @@ fun LoginRoute(
navigateToHome: () -> Unit,
navigateToSignUp: () -> Unit,
navigateToTeacherOrganizationList: () -> Unit,
+ navigateToStudentOrganizationList: () -> Unit,
onShowSnackBar: (SnackbarToken) -> Unit
) {
val context = LocalContext.current
val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
- val transitionState = remember {
- MutableTransitionState(false).apply {
- targetState = true
- }
- }
LaunchedEffect(key1 = Unit) {
viewModel.autoSignIn()
@@ -79,6 +74,7 @@ fun LoginRoute(
LoginEffect.NavigateToHome -> navigateToHome()
is LoginEffect.OnShowSnackBar -> onShowSnackBar(sideEffect.tkn)
LoginEffect.NavigateToTeacherOrganizationList -> navigateToTeacherOrganizationList()
+ LoginEffect.NavigateToStudentOrganizationList -> navigateToStudentOrganizationList()
}
}
diff --git a/android/feature/signin/src/main/java/com/sixkids/feature/signin/login/LoginViewModel.kt b/android/feature/signin/src/main/java/com/sixkids/feature/signin/login/LoginViewModel.kt
index 1f15dc33..47a450ec 100644
--- a/android/feature/signin/src/main/java/com/sixkids/feature/signin/login/LoginViewModel.kt
+++ b/android/feature/signin/src/main/java/com/sixkids/feature/signin/login/LoginViewModel.kt
@@ -28,7 +28,9 @@ class LoginViewModel @Inject constructor(
.onSuccess {
when(it){
"TEACHER" -> postSideEffect(LoginEffect.NavigateToTeacherOrganizationList)
- "STUDENT" -> {}
+ "STUDENT" -> {
+ Log.d(TAG, "autoSignIn: STUDENT")
+ postSideEffect(LoginEffect.NavigateToStudentOrganizationList)}
}
}.onFailure {
Log.d(TAG, "autoSignIn: ${it.message}")
@@ -48,7 +50,7 @@ class LoginViewModel @Inject constructor(
.onSuccess {
when(it){
"TEACHER" -> postSideEffect(LoginEffect.NavigateToTeacherOrganizationList)
- "STUDENT" -> {}
+ "STUDENT" -> {postSideEffect(LoginEffect.NavigateToStudentOrganizationList)}
}
}.onFailure {
postSideEffect(LoginEffect.OnShowSnackBar(SnackbarToken("로그인에 실패했습니다")))
@@ -56,7 +58,7 @@ class LoginViewModel @Inject constructor(
}.onFailure {
when(it){
is NotFoundException -> {
- postSideEffect(LoginEffect.OnShowSnackBar(SnackbarToken(it.message)))
+ postSideEffect(LoginEffect.OnShowSnackBar(SnackbarToken("회원가입을 진행 해주세요")))
postSideEffect(LoginEffect.NavigateToSignUp)
}
else -> {
@@ -67,4 +69,4 @@ class LoginViewModel @Inject constructor(
intent { copy(isLoading = false)}
}
}
-}
\ No newline at end of file
+}
diff --git a/android/feature/signin/src/main/java/com/sixkids/feature/signin/navigation/SignInNavigation.kt b/android/feature/signin/src/main/java/com/sixkids/feature/signin/navigation/SignInNavigation.kt
index b8a629e3..fa543d6b 100644
--- a/android/feature/signin/src/main/java/com/sixkids/feature/signin/navigation/SignInNavigation.kt
+++ b/android/feature/signin/src/main/java/com/sixkids/feature/signin/navigation/SignInNavigation.kt
@@ -29,6 +29,7 @@ fun NavGraphBuilder.signInNavGraph(
navigateSignUpPhoto: (Boolean) -> Unit,
navigateToHome: () -> Unit,
navigateToTeacherOrganizationList: () -> Unit,
+ navigateToStudentOrganizationList: () -> Unit,
onShowSnackBar: (SnackbarToken) -> Unit,
onBackClick : () -> Unit
) {
@@ -37,7 +38,8 @@ fun NavGraphBuilder.signInNavGraph(
navigateToSignUp = navigateToSignUp,
navigateToHome = navigateToHome,
onShowSnackBar = onShowSnackBar,
- navigateToTeacherOrganizationList = navigateToTeacherOrganizationList
+ navigateToTeacherOrganizationList = navigateToTeacherOrganizationList,
+ navigateToStudentOrganizationList = navigateToStudentOrganizationList
)
}
@@ -57,6 +59,7 @@ fun NavGraphBuilder.signInNavGraph(
SignUpPhotoRoute(
onShowSnackBar = onShowSnackBar,
navigateToTeacherOrganizationList = navigateToTeacherOrganizationList,
+ navigateToStudentOrganizationList = navigateToStudentOrganizationList,
onBackClick = onBackClick
)
}
@@ -71,4 +74,4 @@ object SignInRoute{
fun signUpPhotoRoute(isTeacher: Boolean) = "sign-up-photo/$isTeacher"
-}
\ No newline at end of file
+}
diff --git a/android/feature/signin/src/main/java/com/sixkids/feature/signin/signup/SignUpContract.kt b/android/feature/signin/src/main/java/com/sixkids/feature/signin/signup/SignUpContract.kt
index 71c7da2e..7b0722e2 100644
--- a/android/feature/signin/src/main/java/com/sixkids/feature/signin/signup/SignUpContract.kt
+++ b/android/feature/signin/src/main/java/com/sixkids/feature/signin/signup/SignUpContract.kt
@@ -21,7 +21,8 @@ enum class Role{
sealed interface SignUpPhotoEffect : SideEffect {
data object NavigateToTeacherOrganizationList : SignUpPhotoEffect
- data class onShowSnackBar(val tkn : SnackbarToken) : SignUpPhotoEffect
+ data object NavigateToStudentOrganizationList : SignUpPhotoEffect
+ data class OnShowSnackBar(val tkn : SnackbarToken) : SignUpPhotoEffect
}
data class SignUpPhotoState(
diff --git a/android/feature/signin/src/main/java/com/sixkids/feature/signin/signup/SignUpPhotoScreen.kt b/android/feature/signin/src/main/java/com/sixkids/feature/signin/signup/SignUpPhotoScreen.kt
index 25755a85..2cc2f451 100644
--- a/android/feature/signin/src/main/java/com/sixkids/feature/signin/signup/SignUpPhotoScreen.kt
+++ b/android/feature/signin/src/main/java/com/sixkids/feature/signin/signup/SignUpPhotoScreen.kt
@@ -60,6 +60,7 @@ private const val TAG = "D107"
fun SignUpPhotoRoute(
viewModel: SignUpPhotoViewModel = hiltViewModel(),
navigateToTeacherOrganizationList: () -> Unit,
+ navigateToStudentOrganizationList: () -> Unit,
onShowSnackBar: (SnackbarToken) -> Unit,
onBackClick: () -> Unit
) {
@@ -85,8 +86,9 @@ fun SignUpPhotoRoute(
viewModel.sideEffect.collectWithLifecycle {
when (it) {
- is SignUpPhotoEffect.onShowSnackBar -> onShowSnackBar(it.tkn)
+ is SignUpPhotoEffect.OnShowSnackBar -> onShowSnackBar(it.tkn)
SignUpPhotoEffect.NavigateToTeacherOrganizationList -> navigateToTeacherOrganizationList()
+ SignUpPhotoEffect.NavigateToStudentOrganizationList -> navigateToStudentOrganizationList()
}
}
diff --git a/android/feature/signin/src/main/java/com/sixkids/feature/signin/signup/SignUpPhotoViewModel.kt b/android/feature/signin/src/main/java/com/sixkids/feature/signin/signup/SignUpPhotoViewModel.kt
index d7637e15..1e538d86 100644
--- a/android/feature/signin/src/main/java/com/sixkids/feature/signin/signup/SignUpPhotoViewModel.kt
+++ b/android/feature/signin/src/main/java/com/sixkids/feature/signin/signup/SignUpPhotoViewModel.kt
@@ -63,20 +63,20 @@ class SignUpPhotoViewModel @Inject constructor(
defaultImage = defaultImage,
role = if (isTeacher) "TEACHER" else "STUDENT"
).onSuccess {
- postSideEffect(SignUpPhotoEffect.onShowSnackBar(SnackbarToken(
+ postSideEffect(SignUpPhotoEffect.OnShowSnackBar(SnackbarToken(
message = "환영합니다."
)))
getRoleUseCase()
.onSuccess {
when(it){
"TEACHER" -> postSideEffect(SignUpPhotoEffect.NavigateToTeacherOrganizationList)
-// "STUDENT" -> postSideEffect(SignUpPhotoEffect.NavigateToStudentOrganizationList)
+ "STUDENT" -> postSideEffect(SignUpPhotoEffect.NavigateToStudentOrganizationList)
}
}.onFailure {
}
}.onFailure {
- postSideEffect(SignUpPhotoEffect.onShowSnackBar(SnackbarToken(
+ postSideEffect(SignUpPhotoEffect.OnShowSnackBar(SnackbarToken(
message = it.message ?: "알 수 없는 에러 입니다."
)))
}
diff --git a/android/feature/student/board/build.gradle.kts b/android/feature/student/board/build.gradle.kts
index 72643535..c1033ab3 100644
--- a/android/feature/student/board/build.gradle.kts
+++ b/android/feature/student/board/build.gradle.kts
@@ -7,4 +7,5 @@ android {
}
dependencies {
+ implementation(libs.bundles.paging)
}
diff --git a/android/feature/student/board/src/main/java/com/sixkids/student/board/component/CommentCount.kt b/android/feature/student/board/src/main/java/com/sixkids/student/board/component/CommentCount.kt
new file mode 100644
index 00000000..46de2b50
--- /dev/null
+++ b/android/feature/student/board/src/main/java/com/sixkids/student/board/component/CommentCount.kt
@@ -0,0 +1,37 @@
+package com.sixkids.student.board.component
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.tooling.preview.Preview
+import com.sixkids.designsystem.theme.BlueDark
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.designsystem.R as UlbanRes
+
+@Composable
+fun CommentCount(
+ count: Int
+) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(
+ imageVector = ImageVector.vectorResource(id = UlbanRes.drawable.ic_chat_bubble),
+ contentDescription = null,
+ tint = BlueDark
+ )
+ Text(
+ text = count.toString(),
+ style = UlbanTypography.bodyMedium
+ )
+ }
+}
+
+
+@Preview(showBackground = true)
+@Composable
+fun CommentCountPreview() {
+ CommentCount(10)
+}
\ No newline at end of file
diff --git a/android/feature/student/board/src/main/java/com/sixkids/student/board/component/CommentItem.kt b/android/feature/student/board/src/main/java/com/sixkids/student/board/component/CommentItem.kt
new file mode 100644
index 00000000..f3795fbc
--- /dev/null
+++ b/android/feature/student/board/src/main/java/com/sixkids/student/board/component/CommentItem.kt
@@ -0,0 +1,133 @@
+package com.sixkids.student.board.component
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardColors
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+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.graphics.vector.ImageVector
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import com.sixkids.designsystem.theme.Blue
+import com.sixkids.designsystem.theme.Gray
+import com.sixkids.designsystem.theme.GrayLight
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.designsystem.R as UlbanRes
+
+@Composable
+fun CommentItem(
+ modifier: Modifier = Modifier,
+ selected: Boolean = false,
+ writer: String = "",
+ dateString: String = "00/00 00:00",
+ writerImageUrl: String = "",
+ commentString: String = "",
+ isRecomment: Boolean = false,
+ recommentOnclick: () -> Unit = {},
+ deleteOnclick: (() -> Unit)? = null
+){
+ Card(
+ colors = CardDefaults.cardColors(
+ containerColor =
+ if (selected) { Blue}
+ else if (isRecomment) {GrayLight}
+ else {Color.Transparent}
+ ),
+ ) {
+ Column(
+ modifier = modifier
+ .padding(start = 10.dp)
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ AsyncImage(
+ modifier = Modifier
+ .height(36.dp)
+ .aspectRatio(1f),
+ model = writerImageUrl,
+ contentScale = ContentScale.Crop,
+ contentDescription = "작성자 프로필 사진"
+ )
+ Spacer(modifier = Modifier.width(4.dp))
+ Text(
+ text = writer,
+ style = UlbanTypography.bodyMedium
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ if (!isRecomment) {
+ Icon(
+ modifier = Modifier.clickable{recommentOnclick()},
+ imageVector = ImageVector.vectorResource(id = UlbanRes.drawable.ic_chat_bubble_outline),
+ contentDescription = null
+ )
+ }
+ if (deleteOnclick != null) {
+ Icon(
+ modifier = Modifier.clickable{deleteOnclick()},
+ imageVector = ImageVector.vectorResource(id = UlbanRes.drawable.ic_delete),
+ contentDescription = null
+ )
+ }
+ }
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ text = commentString,
+ style = UlbanTypography.bodyMedium
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ text = dateString,
+ style = UlbanTypography.bodySmall.copy(
+ color = Gray
+ )
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun CommentItemPreview() {
+ Column {
+ CommentItem(
+ writer = "오하빈",
+ dateString = "09/01 12:00",
+ writerImageUrl = "https://www.google.com/images/branding/googlelogo/2x/googlelogo_light_color_272x92dp.png",
+ commentString = "댓글 내용",
+ deleteOnclick = {},
+ selected = true
+ )
+ CommentItem(
+ writer = "오하빈",
+ dateString = "09/01 12:00",
+ commentString = "댓글 내용",
+ writerImageUrl = "https://www.google.com/images/branding/googlelogo/2x/googlelogo_light_color_272x92dp.png",
+ )
+ CommentItem(
+ writer = "오하빈",
+ dateString = "09/01 12:00",
+ commentString = "댓글 내용",
+ writerImageUrl = "https://www.google.com/images/branding/googlelogo/2x/googlelogo_light_color_272x92dp.png",
+ isRecomment = true,
+ deleteOnclick = {}
+ )
+ }
+
+}
\ No newline at end of file
diff --git a/android/feature/student/board/src/main/java/com/sixkids/student/board/component/CommentTextField.kt b/android/feature/student/board/src/main/java/com/sixkids/student/board/component/CommentTextField.kt
new file mode 100644
index 00000000..a318fd14
--- /dev/null
+++ b/android/feature/student/board/src/main/java/com/sixkids/student/board/component/CommentTextField.kt
@@ -0,0 +1,77 @@
+package com.sixkids.student.board.component
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.outlined.Send
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+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.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.sixkids.designsystem.component.textfield.UlbanBasicTextField
+import com.sixkids.designsystem.theme.GrayLight
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.student.board.R
+
+
+@Composable
+fun CommentTextField(
+ msg: String = "",
+ onTextIuputChange: (String) -> Unit = {},
+ onSendClick: (String) -> Unit = {},
+) {
+ Card(
+ modifier = Modifier
+ .padding(6.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = GrayLight
+ )
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ UlbanBasicTextField(
+ text = msg,
+ onTextChange = onTextIuputChange,
+ modifier = Modifier
+ .padding(10.dp, 0.dp)
+ .weight(1f)
+ .wrapContentHeight(),
+ maxLines = 3,
+ textStyle = UlbanTypography.bodyMedium.copy(fontWeight = FontWeight.Normal),
+ hint = stringResource(id = R.string.student_board_detail_comment_hint)
+ )
+
+ Icon(
+ Icons.AutoMirrored.Outlined.Send,
+ contentDescription = "",
+ modifier = Modifier
+ .size(30.dp)
+ .padding(end = 4.dp)
+ .clickable {
+ onSendClick(msg)
+ }
+ )
+
+ }
+ }
+
+}
+
+@Preview(showBackground = true)
+@Composable
+fun CommentTextFieldPreview() {
+ CommentTextField()
+}
\ No newline at end of file
diff --git a/android/feature/student/board/src/main/java/com/sixkids/student/board/component/PageTitle.kt b/android/feature/student/board/src/main/java/com/sixkids/student/board/component/PageTitle.kt
new file mode 100644
index 00000000..28da552b
--- /dev/null
+++ b/android/feature/student/board/src/main/java/com/sixkids/student/board/component/PageTitle.kt
@@ -0,0 +1,55 @@
+package com.sixkids.student.board.component
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Icon
+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.vector.ImageVector
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.designsystem.R as UlbanRes
+
+@Composable
+fun PageTitle(
+ modifier: Modifier = Modifier,
+ title: String,
+ cancelOnclick: () -> Unit = {},
+) {
+ Row(
+ modifier = modifier,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ modifier = Modifier
+ .clickable { cancelOnclick() },
+ imageVector = ImageVector.vectorResource(id = UlbanRes.drawable.ic_cancel_post),
+ contentDescription = null
+ )
+ Spacer(modifier = Modifier.width(14.dp))
+ Text(
+ text = title,
+ style = UlbanTypography.titleLarge.copy(
+ fontWeight = FontWeight.Medium,
+ ),
+ )
+ }
+}
+
+
+@Preview(showBackground = true)
+@Composable
+fun PageTitlePreview() {
+ PageTitle(
+ title = "글 쓰기",
+ cancelOnclick = {}
+ )
+}
\ No newline at end of file
diff --git a/android/feature/student/board/src/main/java/com/sixkids/student/board/component/PostItem.kt b/android/feature/student/board/src/main/java/com/sixkids/student/board/component/PostItem.kt
new file mode 100644
index 00000000..cc833ca9
--- /dev/null
+++ b/android/feature/student/board/src/main/java/com/sixkids/student/board/component/PostItem.kt
@@ -0,0 +1,80 @@
+package com.sixkids.student.board.component
+
+import androidx.compose.foundation.clickable
+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.width
+import androidx.compose.material3.Divider
+import androidx.compose.material3.HorizontalDivider
+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.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.sixkids.designsystem.theme.UlbanTypography
+
+@Composable
+fun PostItem(
+ modifier: Modifier = Modifier ,
+ title: String,
+ writer: String,
+ commentCount: Int,
+ dateString: String,
+ dividerColor: Color = Color.Black,
+ onClick: () -> Unit = {}
+) {
+ Column(
+ modifier = modifier.clickable { onClick() }
+ ) {
+ Text(
+ text = title,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = UlbanTypography.titleMedium
+ )
+ Spacer(modifier = Modifier.height(10.dp))
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ if (commentCount > 0){
+ CommentCount(count = commentCount)
+ }
+ Spacer(modifier = Modifier.width(6.dp))
+ Text(
+ text = writer,
+ style = UlbanTypography.bodyMedium
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ Text(
+ text = dateString,
+ style = UlbanTypography.bodyMedium
+ )
+ }
+ HorizontalDivider(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 4.dp),
+ thickness = 2.dp,
+ color = dividerColor
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun PostItemPreview() {
+ PostItem(
+ title = "이따 마크 할 사람~~!",
+ writer = "오하빈",
+ commentCount = 3,
+ dateString = "2024.04.16 14:30"
+ )
+}
\ No newline at end of file
diff --git a/android/feature/student/board/src/main/java/com/sixkids/student/board/component/PostWriterInfo.kt b/android/feature/student/board/src/main/java/com/sixkids/student/board/component/PostWriterInfo.kt
new file mode 100644
index 00000000..d5218bd8
--- /dev/null
+++ b/android/feature/student/board/src/main/java/com/sixkids/student/board/component/PostWriterInfo.kt
@@ -0,0 +1,64 @@
+package com.sixkids.student.board.component
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxHeight
+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.layout.ContentScale
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import com.sixkids.designsystem.theme.UlbanTypography
+
+@Composable
+fun PostWriterInfo(
+ height: Dp = 60.dp,
+ writer: String = "",
+ dateString: String = "00/00 00:00",
+ writerImageUrl: String = ""
+) {
+ Row(
+ modifier = Modifier.height(height),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ AsyncImage(
+ modifier = Modifier
+ .fillMaxHeight()
+ .aspectRatio(1f),
+ model = writerImageUrl,
+ contentScale = ContentScale.Crop,
+ contentDescription = "프로필 사진"
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Column {
+ Text(
+ text = writer,
+ style = UlbanTypography.bodyLarge
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ text = dateString,
+ style = UlbanTypography.bodyMedium
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun PostWriterInfoPreview() {
+ PostWriterInfo(
+ height = 60.dp,
+ writer = "홍유준 선생님",
+ dateString = "10/10 10:10",
+ writerImageUrl = ""
+ )
+}
\ No newline at end of file
diff --git a/android/feature/student/board/src/main/java/com/sixkids/student/board/detail/StudentBoardDetailContract.kt b/android/feature/student/board/src/main/java/com/sixkids/student/board/detail/StudentBoardDetailContract.kt
new file mode 100644
index 00000000..80722b92
--- /dev/null
+++ b/android/feature/student/board/src/main/java/com/sixkids/student/board/detail/StudentBoardDetailContract.kt
@@ -0,0 +1,17 @@
+package com.sixkids.student.board.detail
+
+import com.sixkids.model.PostDetail
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
+sealed interface StudentBoardDetailEffect: SideEffect {
+ data object RefreshPostDetail: StudentBoardDetailEffect
+ data class OnShowSnackbar(val message: String) : StudentBoardDetailEffect
+}
+
+data class StudentBoardDetailState(
+ val isLoading: Boolean = false,
+ val postDetail: PostDetail = PostDetail(),
+ val commentText: String = "",
+ val selectedCommentId: Long? = null,
+) : UiState
\ No newline at end of file
diff --git a/android/feature/student/board/src/main/java/com/sixkids/student/board/detail/StudentBoardDetailScreen.kt b/android/feature/student/board/src/main/java/com/sixkids/student/board/detail/StudentBoardDetailScreen.kt
new file mode 100644
index 00000000..f21668ed
--- /dev/null
+++ b/android/feature/student/board/src/main/java/com/sixkids/student/board/detail/StudentBoardDetailScreen.kt
@@ -0,0 +1,241 @@
+package com.sixkids.student.board.detail
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.clickable
+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.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import coil.compose.AsyncImage
+import com.sixkids.designsystem.R
+import com.sixkids.designsystem.component.screen.LoadingScreen
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.model.Comment
+import com.sixkids.model.MemberSimple
+import com.sixkids.model.PostDetail
+import com.sixkids.model.Recomment
+import com.sixkids.student.board.component.CommentItem
+import com.sixkids.student.board.component.CommentTextField
+import com.sixkids.student.board.component.PostWriterInfo
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.util.formatToMonthDayTime
+import java.time.LocalDateTime
+
+@Composable
+fun StudentBoardDetailRoute(
+ viewModel: StudentBoardDetailViewModel = hiltViewModel(),
+ padding: PaddingValues,
+ onShowSnackBar: (SnackbarToken) -> Unit
+) {
+ val uiState by viewModel.uiState.collectAsState()
+
+ LaunchedEffect(Unit) {
+ viewModel.getPostDetail()
+ }
+
+ LaunchedEffect(viewModel.sideEffect) {
+ viewModel.sideEffect.collect { sideEffect ->
+ when (sideEffect) {
+ StudentBoardDetailEffect.RefreshPostDetail -> viewModel.getPostDetail()
+ is StudentBoardDetailEffect.OnShowSnackbar -> {
+ onShowSnackBar(SnackbarToken(message = sideEffect.message))
+ }
+ }
+ }
+ }
+
+ Box(modifier = Modifier.padding(padding)) {
+ StudentBoardDetailScreen(
+ studentBoardDetailState = uiState,
+ onCommentTextChanged = viewModel::onCommentTextChanged,
+ onClickComment = viewModel::onSelectedCommentId,
+ onClickSubmitComment = viewModel::onNewComment,
+ )
+ }
+}
+
+@Composable
+fun StudentBoardDetailScreen(
+ modifier: Modifier = Modifier,
+ studentBoardDetailState: StudentBoardDetailState = StudentBoardDetailState(),
+ onCommentTextChanged: (String) -> Unit = {},
+ onClickComment: (Long) -> Unit = {},
+ onClickSubmitComment: () -> Unit = {},
+ postDeleteOnclick: () -> Unit = {}
+) {
+
+ val scrollState = rememberScrollState()
+
+ BackHandler(
+ enabled = studentBoardDetailState.selectedCommentId != null,
+ onBack = {onClickComment(studentBoardDetailState.selectedCommentId?: 0)}
+ )
+
+ Box{
+ Column {
+ Column(
+ modifier = modifier
+ .weight(1f)
+ .padding(20.dp)
+ .verticalScroll(scrollState),
+ ) {
+ // 작성자 정보
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ PostWriterInfo(
+ height = 60.dp,
+ writer = studentBoardDetailState.postDetail.writeMember.name,
+ dateString = studentBoardDetailState.postDetail.createTime.formatToMonthDayTime(),
+ writerImageUrl = studentBoardDetailState.postDetail.writeMember.photo
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ Icon(
+ modifier = Modifier
+ .size(30.dp)
+ .clickable { postDeleteOnclick() },
+ imageVector = ImageVector.vectorResource(id = R.drawable.ic_delete),
+ contentDescription = "더보기"
+ )
+ }
+
+ Spacer(modifier = Modifier.height(10.dp))
+ Text(
+ text = studentBoardDetailState.postDetail.title,
+ style = UlbanTypography.titleLarge
+ )
+ Spacer(modifier = Modifier.height(10.dp))
+ // 이미지
+ if (studentBoardDetailState.postDetail.imageUri.isNotEmpty()) {
+ AsyncImage(
+ model = studentBoardDetailState.postDetail.imageUri,
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ )
+ }
+ Spacer(modifier = Modifier.height(10.dp))
+ Text(
+ text = studentBoardDetailState.postDetail.content,
+ style = UlbanTypography.bodyLarge
+ )
+ Spacer(modifier = Modifier.height(10.dp))
+ HorizontalDivider(
+ thickness = 2.dp,
+ color = Color.Black
+ )
+ // 댓글 목록
+ for (comment in studentBoardDetailState.postDetail.comments) {
+ CommentItem(
+ selected = studentBoardDetailState.selectedCommentId == comment.id,
+ writer = comment.member.name,
+ dateString = comment.createTime.formatToMonthDayTime(),
+ writerImageUrl = comment.member.photo,
+ commentString = comment.content,
+ recommentOnclick = {
+ onClickComment(comment.id)
+ }
+ )
+ // 대댓글 목록
+ for (recomment in comment.recomments) {
+ Row {
+ Icon(
+ modifier = Modifier.padding(4.dp),
+ imageVector = ImageVector.vectorResource(id = R.drawable.ic_recomment),
+ contentDescription = null
+ )
+ CommentItem(
+ writer = recomment.member.name,
+ dateString = recomment.createTime.formatToMonthDayTime(),
+ writerImageUrl = recomment.member.photo,
+ commentString = recomment.content,
+ isRecomment = true
+ )
+ }
+
+ }
+ }
+ }
+ CommentTextField(
+ msg = studentBoardDetailState.commentText,
+ onTextIuputChange = onCommentTextChanged,
+ onSendClick = { onClickSubmitComment() }
+ )
+ }
+
+ if (studentBoardDetailState.isLoading) {
+ LoadingScreen()
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun StudentBoardDetailScreenPreview() {
+ StudentBoardDetailScreen(
+ studentBoardDetailState = postDetailStateDummy
+ )
+}
+
+val recommentDummy = Recomment(
+ 1,
+ member = MemberSimple(
+ id = 1,
+ name = "작성자",
+ photo = "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTKe3bkhl96AgtmHyTiKW-KXRst2-5MoY6xB9-mZP74BQ&s"
+ ),
+ content = "내용내용내용내용내용내용내용내용내용내용내용내용내용내용내용",
+ createTime = LocalDateTime.now(),
+ updateTime = LocalDateTime.now(),
+ 1,
+)
+
+val commentDummy = Comment(
+ 1,
+ member = MemberSimple(
+ id = 1,
+ name = "작성자",
+ photo = "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTKe3bkhl96AgtmHyTiKW-KXRst2-5MoY6xB9-mZP74BQ&s"
+ ),
+ "내용내용내용 내용내용내용내용내용내용내용내용내용 내용내용내용내용내용내용내용내용내용",
+ LocalDateTime.now(),
+ LocalDateTime.now(),
+ listOf(recommentDummy, recommentDummy)
+)
+
+val postDetailStateDummy = StudentBoardDetailState(
+ isLoading = false,
+ postDetail = PostDetail(
+ title = "제목",
+ content = "내용내용내용내용내용내용내용내용내용내용내용내용내용",
+ writeMember = MemberSimple(
+ id = 1,
+ name = "작성자",
+ photo = "https://picsum.photos/200/300"
+ ),
+ createTime = LocalDateTime.now(),
+ imageUri = "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTKe3bkhl96AgtmHyTiKW-KXRst2-5MoY6xB9-mZP74BQ&s",
+ comments = listOf(commentDummy, commentDummy)
+ )
+)
\ No newline at end of file
diff --git a/android/feature/student/board/src/main/java/com/sixkids/student/board/detail/StudentBoardDetailViewModel.kt b/android/feature/student/board/src/main/java/com/sixkids/student/board/detail/StudentBoardDetailViewModel.kt
new file mode 100644
index 00000000..2e654aaa
--- /dev/null
+++ b/android/feature/student/board/src/main/java/com/sixkids/student/board/detail/StudentBoardDetailViewModel.kt
@@ -0,0 +1,102 @@
+package com.sixkids.student.board.detail
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.viewModelScope
+import com.sixkids.domain.usecase.comment.DeleteCommentUseCase
+import com.sixkids.domain.usecase.comment.NewCommentUseCase
+import com.sixkids.domain.usecase.comment.NewRecommentUseCase
+import com.sixkids.domain.usecase.comment.ReportCommentUseCase
+import com.sixkids.domain.usecase.comment.UpdateCommentUsecase
+import com.sixkids.domain.usecase.post.DeletePostUseCase
+import com.sixkids.domain.usecase.post.GetPostDetailUseCase
+import com.sixkids.domain.usecase.post.UpdatePostUseCase
+import com.sixkids.student.board.navigation.StudentBoardRoute
+import com.sixkids.ui.base.BaseViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class StudentBoardDetailViewModel @Inject constructor(
+ savedStateHandle: SavedStateHandle,
+ private val getPostDetailUseCase: GetPostDetailUseCase,
+ private val updatePostUseCase: UpdatePostUseCase,
+ private val deletePostUsecase: DeletePostUseCase,
+ private val deleteCommentUseCase: DeleteCommentUseCase,
+ private val updateCommentUsecase: UpdateCommentUsecase,
+ private val newCommentUseCase: NewCommentUseCase,
+ private val newRecommentUseCase: NewRecommentUseCase,
+ private val reportCommentUseCase: ReportCommentUseCase
+): BaseViewModel(StudentBoardDetailState()){
+
+ private val postId: Long = savedStateHandle.get(StudentBoardRoute.postDetailARG)!!
+
+ fun onCommentTextChanged(commentText: String) = intent { copy(commentText = commentText) }
+ fun onSelectedCommentId(commentId: Long?) = intent {
+ if (currentState.selectedCommentId == commentId) {
+ copy(selectedCommentId = null)
+ } else {
+ copy(selectedCommentId = commentId)
+ }
+ }
+
+ fun getPostDetail() {
+ viewModelScope.launch {
+ intent { copy(isLoading = true) }
+ getPostDetailUseCase(postId).onSuccess {
+ intent { copy(postDetail = it) }
+ }.onFailure {
+ postSideEffect(StudentBoardDetailEffect.OnShowSnackbar(it.message ?: "게시글을 불러오지 못했어요"))
+ }
+ intent { copy(isLoading = false) }
+ }
+ }
+
+ fun onNewComment() {
+ if (currentState.commentText.isBlank()) {
+ postSideEffect(StudentBoardDetailEffect.OnShowSnackbar("댓글을 입력해주세요"))
+ } else {
+ if (currentState.selectedCommentId == null) {
+ viewModelScope.launch {
+ intent { copy(isLoading = true) }
+ newCommentUseCase(
+ postId = postId,
+ content = currentState.commentText,
+ ).onSuccess {
+ postSideEffect(StudentBoardDetailEffect.OnShowSnackbar("댓글이 작성되었습니다"))
+ intent { copy(commentText = "", selectedCommentId = null) }
+ getPostDetail()
+ }.onFailure {
+ postSideEffect(
+ StudentBoardDetailEffect.OnShowSnackbar(
+ it.message ?: "댓글 작성에 실패했어요"
+ )
+ )
+ }
+ intent { copy(isLoading = false) }
+ }
+ } else {
+ viewModelScope.launch {
+ intent { copy(isLoading = true) }
+ newRecommentUseCase(
+ postId = postId,
+ content = currentState.commentText,
+ currentState.selectedCommentId!!
+ ).onSuccess {
+ postSideEffect(StudentBoardDetailEffect.OnShowSnackbar("댓글이 작성되었습니다"))
+ intent { copy(commentText = "", selectedCommentId = null)}
+ getPostDetail()
+ }.onFailure {
+ postSideEffect(
+ StudentBoardDetailEffect.OnShowSnackbar(
+ it.message ?: "댓글 작성에 실패했어요"
+ )
+ )
+ }
+ intent { copy(isLoading = false) }
+ }
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/android/feature/student/board/src/main/java/com/sixkids/student/board/main/StudentBoardMainContract.kt b/android/feature/student/board/src/main/java/com/sixkids/student/board/main/StudentBoardMainContract.kt
new file mode 100644
index 00000000..4c6b1fb5
--- /dev/null
+++ b/android/feature/student/board/src/main/java/com/sixkids/student/board/main/StudentBoardMainContract.kt
@@ -0,0 +1,15 @@
+package com.sixkids.student.board.main
+
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
+sealed interface StudentBoardMainEffect : SideEffect {
+ data object NavigateToPostDetail: StudentBoardMainEffect
+ data object NavigateToWritePost: StudentBoardMainEffect
+ data class OnShowSnackBar(val message : String) : StudentBoardMainEffect
+}
+
+data class StudentBoardMainState(
+ val isLoding: Boolean = false,
+ val classString: String = "",
+): UiState
\ No newline at end of file
diff --git a/android/feature/student/board/src/main/java/com/sixkids/student/board/main/StudentBoardMainScreen.kt b/android/feature/student/board/src/main/java/com/sixkids/student/board/main/StudentBoardMainScreen.kt
new file mode 100644
index 00000000..be7e9d24
--- /dev/null
+++ b/android/feature/student/board/src/main/java/com/sixkids/student/board/main/StudentBoardMainScreen.kt
@@ -0,0 +1,159 @@
+package com.sixkids.student.board.main
+
+import android.util.Log
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.paging.compose.LazyPagingItems
+import androidx.paging.compose.collectAsLazyPagingItems
+import com.sixkids.designsystem.component.appbar.UlbanDefaultAppBar
+import com.sixkids.designsystem.R as UlbanRes
+import com.sixkids.designsystem.component.appbar.UlbanDetailAppBar
+import com.sixkids.designsystem.component.button.EditFAB
+import com.sixkids.designsystem.component.screen.LoadingScreen
+import com.sixkids.designsystem.theme.Blue
+import com.sixkids.designsystem.theme.BlueDark
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.model.Post
+import com.sixkids.student.board.R
+import com.sixkids.student.board.component.PostItem
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.util.formatToMonthDayTimeKorean
+
+private const val TAG = "D107"
+
+@Composable
+fun StudentBoardMainRoute(
+ viewModel: StudentBoardMainViewModel = hiltViewModel(),
+ navigateToStudentBoardDetail: (postId:Long) -> Unit,
+ navigateToStudentBoardWrite: () -> Unit,
+ onShowSnackBar: (SnackbarToken) -> Unit,
+ padding: PaddingValues
+) {
+
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ LaunchedEffect(Unit) {
+ Log.d(TAG, "StudentBoardMainRoute: ")
+ viewModel.getPostList()
+ }
+
+ LaunchedEffect(key1 = viewModel.sideEffect) {
+ viewModel.sideEffect.collect { sideEffect ->
+ when (sideEffect) {
+ StudentBoardMainEffect.NavigateToPostDetail -> {}
+ StudentBoardMainEffect.NavigateToWritePost -> {}
+ is StudentBoardMainEffect.OnShowSnackBar -> {
+ onShowSnackBar(SnackbarToken(message = sideEffect.message))
+ }
+ }
+ }
+ }
+
+ Box(
+ modifier = Modifier
+ .padding(padding)
+ .fillMaxSize()
+ ) {
+ StudentBoardMainScreen(
+ postState = uiState,
+ postItems = viewModel.postList?.collectAsLazyPagingItems(),
+ postItemOnclick = navigateToStudentBoardDetail,
+ fabClick = navigateToStudentBoardWrite
+ )
+ }
+}
+
+@Composable
+fun StudentBoardMainScreen(
+ modifier: Modifier = Modifier,
+ postState: StudentBoardMainState = StudentBoardMainState(),
+ postItems: LazyPagingItems? = null,
+ postItemOnclick: (postId: Long) -> Unit = {},
+ fabClick: () -> Unit = {}
+) {
+ val listState = rememberLazyListState()
+
+ Box(
+ modifier = modifier
+ .fillMaxSize()
+ ) {
+ Column(
+
+ ) {
+ UlbanDefaultAppBar(
+ leftIcon = UlbanRes.drawable.board,
+ title = stringResource(id = R.string.student_board_main_post),
+ content = stringResource(id = R.string.student_board_main_post),
+ body = postState.classString.replace("\n", " "),
+ color = Blue
+ )
+
+ if (postItems == null){
+ Spacer(modifier = Modifier.weight(1f))
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ text = stringResource(id = R.string.student_board_post_no_items),
+ textAlign = TextAlign.Center,
+ style = UlbanTypography.bodyLarge,
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ } else {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ state = listState,
+ ) {
+ items(postItems.itemCount) { index ->
+ postItems[index]?.let { post ->
+ PostItem(
+ title = post.title,
+ writer = post.writer,
+ dateString = post.time.formatToMonthDayTimeKorean(),
+ commentCount = post.commentCount,
+ onClick = { postItemOnclick(post.id) }
+ )
+ }
+ }
+ }
+ }
+ }
+ //FAB
+ EditFAB(
+ modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .padding(16.dp),
+ buttonColor = Blue,
+ iconColor = BlueDark,
+ onClick = fabClick
+ )
+ if (postState.isLoding){
+ LoadingScreen()
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun StudentBoardMainScreenPreview() {
+ StudentBoardMainScreen()
+}
\ No newline at end of file
diff --git a/android/feature/student/board/src/main/java/com/sixkids/student/board/main/StudentBoardMainViewModel.kt b/android/feature/student/board/src/main/java/com/sixkids/student/board/main/StudentBoardMainViewModel.kt
new file mode 100644
index 00000000..1ddf48f6
--- /dev/null
+++ b/android/feature/student/board/src/main/java/com/sixkids/student/board/main/StudentBoardMainViewModel.kt
@@ -0,0 +1,54 @@
+package com.sixkids.student.board.main
+
+import android.util.Log
+import androidx.lifecycle.viewModelScope
+import androidx.paging.PagingData
+import androidx.paging.cachedIn
+import com.sixkids.domain.usecase.organization.GetSelectedOrganizationIdUseCase
+import com.sixkids.domain.usecase.post.GetPostListUseCase
+import com.sixkids.model.Post
+import com.sixkids.model.PostCategory
+import com.sixkids.ui.base.BaseViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+private const val TAG = "D107"
+@HiltViewModel
+class StudentBoardMainViewModel @Inject constructor(
+ private val getPostListUseCase: GetPostListUseCase,
+ private val getSelectedOrganizationIdUseCase: GetSelectedOrganizationIdUseCase
+): BaseViewModel(StudentBoardMainState()) {
+ private var organizationId: Int? = null
+
+ var postList: Flow>? = null
+
+ fun getPostList() {
+ viewModelScope.launch {
+ intent { copy(isLoding = true) }
+
+ if (organizationId == null){
+ organizationId = getSelectedOrganizationIdUseCase().getOrNull()
+ Log.d(TAG, "getPostList1: $organizationId")
+ }
+
+ Log.d(TAG, "getPostList: 2")
+
+ if (organizationId != null){
+ Log.d(TAG, "getPostList:3 ")
+ postList = getPostListUseCase(
+ organizationId = organizationId!!,
+ postCategory = PostCategory.FREE
+ ).cachedIn(viewModelScope).catch {
+ Log.d(TAG, "getPostList4: ${it.message}")
+ }
+ } else {
+ postSideEffect(StudentBoardMainEffect.OnShowSnackBar("학급 정보를 불러오지 못했어요 ;("))
+ }
+
+ intent { copy(isLoding = false) }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/feature/student/board/src/main/java/com/sixkids/student/board/navigation/StudentBoardNavigation.kt b/android/feature/student/board/src/main/java/com/sixkids/student/board/navigation/StudentBoardNavigation.kt
new file mode 100644
index 00000000..d5cea20b
--- /dev/null
+++ b/android/feature/student/board/src/main/java/com/sixkids/student/board/navigation/StudentBoardNavigation.kt
@@ -0,0 +1,70 @@
+package com.sixkids.student.board.navigation
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavOptions
+import androidx.navigation.NavType
+import androidx.navigation.compose.composable
+import androidx.navigation.navArgument
+import com.sixkids.student.board.detail.StudentBoardDetailRoute
+import com.sixkids.student.board.main.StudentBoardMainRoute
+import com.sixkids.student.board.write.StudentBoardWriteRoute
+import com.sixkids.ui.SnackbarToken
+
+fun NavController.navigateStudentBoard(navOptions: NavOptions) {
+ navigate(StudentBoardRoute.defaultRoute,navOptions)
+}
+
+fun NavController.navigateStudentBoardWrite() {
+ navigate(StudentBoardRoute.writeRoute)
+}
+
+fun NavController.navigateStudentBoardDetail(postId: Long) {
+ navigate(StudentBoardRoute.detailRoute(postId))
+}
+
+fun NavGraphBuilder.studentBoardNavGraph(
+ padding: PaddingValues,
+ onShowSnackBar: (SnackbarToken) -> Unit,
+ navigateToStudentBoardDetail: (postId:Long) -> Unit,
+ navigateToStudentBoardWrite: () -> Unit,
+ navigateBack: () -> Unit
+) {
+ composable(route = StudentBoardRoute.defaultRoute) {
+ StudentBoardMainRoute(
+ padding = padding,
+ navigateToStudentBoardDetail = navigateToStudentBoardDetail,
+ navigateToStudentBoardWrite = navigateToStudentBoardWrite,
+ onShowSnackBar = onShowSnackBar
+ )
+ }
+
+ composable(route = StudentBoardRoute.writeRoute) {
+ StudentBoardWriteRoute(
+ padding = padding,
+ navigateBack = navigateBack,
+ onShowSnackBar = onShowSnackBar
+ )
+ }
+
+ composable(
+ route = StudentBoardRoute.detailRoute,
+ arguments = listOf(navArgument(StudentBoardRoute.postDetailARG) { type = NavType.LongType })
+ ) {
+ StudentBoardDetailRoute(
+ padding = padding,
+ onShowSnackBar = onShowSnackBar
+ )
+ }
+}
+
+object StudentBoardRoute {
+ const val postDetailARG = "postId"
+
+ const val defaultRoute = "student_board"
+ const val detailRoute = "student_board_detail/{$postDetailARG}"
+ const val writeRoute = "student_board_write"
+
+ fun detailRoute(postId: Long) = "student_board_detail/$postId"
+}
\ No newline at end of file
diff --git a/android/feature/student/board/src/main/java/com/sixkids/student/board/write/StudentBoardWriteContract.kt b/android/feature/student/board/src/main/java/com/sixkids/student/board/write/StudentBoardWriteContract.kt
new file mode 100644
index 00000000..972f921e
--- /dev/null
+++ b/android/feature/student/board/src/main/java/com/sixkids/student/board/write/StudentBoardWriteContract.kt
@@ -0,0 +1,18 @@
+package com.sixkids.student.board.write
+
+import android.graphics.Bitmap
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
+sealed interface StudentBoardWriteEffect: SideEffect {
+ data object NavigateBack : StudentBoardWriteEffect
+ data class OnShowSnackbar(val message: String) : StudentBoardWriteEffect
+}
+
+data class StudentBoardWriteState(
+ val isLoading: Boolean = false,
+ val title: String = "",
+ val content: String = "",
+ val anonymousChecked: Boolean = false,
+ val photo: Bitmap? = null
+): UiState
\ No newline at end of file
diff --git a/android/feature/student/board/src/main/java/com/sixkids/student/board/write/StudentBoardWriteScreen.kt b/android/feature/student/board/src/main/java/com/sixkids/student/board/write/StudentBoardWriteScreen.kt
new file mode 100644
index 00000000..34d2fd57
--- /dev/null
+++ b/android/feature/student/board/src/main/java/com/sixkids/student/board/write/StudentBoardWriteScreen.kt
@@ -0,0 +1,284 @@
+package com.sixkids.student.board.write
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.ImageDecoder
+import android.os.Build
+import android.provider.MediaStore
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.PickVisualMediaRequest
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+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.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+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.layout.wrapContentHeight
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.CheckboxDefaults
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.scale
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.sixkids.designsystem.R as UlbanRes
+import com.sixkids.designsystem.component.button.UlbanFilledButton
+import com.sixkids.designsystem.component.screen.LoadingScreen
+import com.sixkids.designsystem.theme.Blue
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.student.board.R
+import com.sixkids.student.board.component.PageTitle
+import com.sixkids.ui.SnackbarToken
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+
+@Composable
+fun StudentBoardWriteRoute(
+ viewModel: StudentBoardWriteViewModel = hiltViewModel(),
+ padding: PaddingValues,
+ navigateBack: () -> Unit,
+ onShowSnackBar: (SnackbarToken) -> Unit
+) {
+
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ val context = LocalContext.current
+ val photoLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.PickVisualMedia()
+ ) { uri ->
+ uri?.let {
+ try {
+ val bitmap = if (Build.VERSION.SDK_INT < 28) {
+ MediaStore.Images.Media.getBitmap(context.contentResolver, it)
+ } else {
+ ImageDecoder.decodeBitmap(
+ ImageDecoder.createSource(
+ context.contentResolver,
+ it
+ )
+ )
+ }
+ viewModel.onAddPhoto(bitmap)
+ } catch (e: IOException) {
+ viewModel.showToast("사진 호출에 실패했습니다.")
+ }
+ }
+ }
+
+ LaunchedEffect(key1 = viewModel.sideEffect) {
+ viewModel.sideEffect.collect { sideEffect ->
+ when (sideEffect) {
+ StudentBoardWriteEffect.NavigateBack -> navigateBack()
+ is StudentBoardWriteEffect.OnShowSnackbar -> {
+ onShowSnackBar(SnackbarToken(message = sideEffect.message))
+ }
+ }
+ }
+ }
+
+
+
+ StudentBoardWriteScreen(
+ postWriteState = uiState,
+ cancelOnClick = { viewModel.onBack() },
+ submitOnClick = {
+ viewModel.onPost(
+ uiState.photo?.let { saveBitmapToFile(context, it, "post_photo.jpg") }
+ )
+ },
+ titleValueChange = { viewModel.onTitleChanged(it) },
+ contentValueChange = { viewModel.onContentChanged(it) },
+ anonymousCheckedChange = { viewModel.onAnonymousChecked(it) },
+ addPhotoOnClick = { photoLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) }
+ )
+}
+
+@Composable
+fun StudentBoardWriteScreen(
+ modifier: Modifier = Modifier,
+ postWriteState: StudentBoardWriteState = StudentBoardWriteState(),
+ cancelOnClick: () -> Unit = {},
+ submitOnClick: () -> Unit = {},
+ titleValueChange: (String) -> Unit = {},
+ contentValueChange: (String) -> Unit = {},
+ anonymousCheckedChange: (Boolean) -> Unit = {},
+ addPhotoOnClick: () -> Unit = {}
+) {
+
+ val scrollState = rememberScrollState()
+
+ LaunchedEffect(postWriteState.content) {
+ scrollState.scrollTo(scrollState.maxValue)
+ }
+
+ Box {
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .padding(20.dp)
+ ) {
+ Spacer(modifier = Modifier.height(20.dp))
+ PageTitle(
+ title = stringResource(id = R.string.student_board_write_title),
+ cancelOnclick = cancelOnClick
+ )
+ //title
+ OutlinedTextField(
+ value = postWriteState.title,
+ onValueChange = titleValueChange,
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedBorderColor = Color.Transparent,
+ unfocusedBorderColor = Color.Transparent
+ ),
+ placeholder = {
+ Text(
+ text = stringResource(id = R.string.student_board_write_content_title),
+ style = UlbanTypography.bodyLarge
+ )
+ },
+ textStyle = UlbanTypography.bodyLarge
+ )
+ HorizontalDivider(
+ thickness = 2.dp,
+ color = Color.Black
+ )
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f)
+ .verticalScroll(scrollState)
+ ) {
+ //photo
+ if (postWriteState.photo != null) {
+ Spacer(modifier = Modifier.height(10.dp))
+ Image(
+ modifier = Modifier
+ .fillMaxWidth()
+ .aspectRatio(1f),
+ bitmap = postWriteState.photo.asImageBitmap(),
+ contentDescription = null,
+ contentScale = ContentScale.Crop
+ )
+ }
+ //content
+ OutlinedTextField(
+ value = postWriteState.content,
+ onValueChange = { string ->
+ contentValueChange(string)
+ },
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedBorderColor = Color.Transparent,
+ unfocusedBorderColor = Color.Transparent
+ ),
+ placeholder = {
+ Text(
+ text = stringResource(id = R.string.student_board_write_content_content),
+ style = UlbanTypography.bodyLarge
+ )
+ },
+ textStyle = UlbanTypography.bodyLarge
+ )
+ }
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ // 이미지 추가 아이콘
+ Icon(
+ modifier = Modifier
+ .size(40.dp)
+ .clickable(onClick = addPhotoOnClick),
+ imageVector = ImageVector.vectorResource(id = UlbanRes.drawable.ic_photo_camera),
+ contentDescription = null
+ )
+ Spacer(modifier = Modifier.width(10.dp))
+ // 익명 체크박스
+ Checkbox(
+ modifier = Modifier.scale(1.2f),
+ checked = postWriteState.anonymousChecked,
+ onCheckedChange = anonymousCheckedChange,
+ colors = CheckboxDefaults.colors(
+ checkedColor = Blue,
+ uncheckedColor = Color.Black
+ )
+ )
+ Text(
+ text = stringResource(id = R.string.student_board_write_anonymous),
+ style = UlbanTypography.bodyLarge
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ // 등록 버튼
+ UlbanFilledButton(
+ text = stringResource(id = R.string.student_board_write_submit),
+ onClick = submitOnClick
+ )
+ }
+ }
+
+ if (postWriteState.isLoading) {
+ LoadingScreen()
+ }
+ }
+}
+
+
+fun saveBitmapToFile(context: Context, bitmap: Bitmap?, fileName: String): File? {
+ val directory = context.getExternalFilesDir(null) ?: return null
+
+ val file = File(directory, fileName)
+ var fileOutputStream: FileOutputStream? = null
+
+ try {
+ fileOutputStream = FileOutputStream(file)
+ bitmap?.compress(Bitmap.CompressFormat.JPEG, 100, fileOutputStream)
+ fileOutputStream.flush()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return null
+ } finally {
+ try {
+ fileOutputStream?.close()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ return file
+}
+
+@Preview(showBackground = true)
+@Composable
+fun StudentBoardWriteScreenPreview() {
+ StudentBoardWriteScreen()
+}
\ No newline at end of file
diff --git a/android/feature/student/board/src/main/java/com/sixkids/student/board/write/StudentBoardWriteViewModel.kt b/android/feature/student/board/src/main/java/com/sixkids/student/board/write/StudentBoardWriteViewModel.kt
new file mode 100644
index 00000000..7688b459
--- /dev/null
+++ b/android/feature/student/board/src/main/java/com/sixkids/student/board/write/StudentBoardWriteViewModel.kt
@@ -0,0 +1,57 @@
+package com.sixkids.student.board.write
+
+import android.graphics.Bitmap
+import androidx.lifecycle.viewModelScope
+import com.sixkids.domain.usecase.organization.GetSelectedOrganizationIdUseCase
+import com.sixkids.domain.usecase.post.NewPostUseCase
+import com.sixkids.ui.base.BaseViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import java.io.File
+import javax.inject.Inject
+
+@HiltViewModel
+class StudentBoardWriteViewModel @Inject constructor(
+ private val newPostUseCase: NewPostUseCase,
+ private val getSelectedOrganizationIdUseCase: GetSelectedOrganizationIdUseCase
+): BaseViewModel(StudentBoardWriteState()){
+
+ private var organizationId: Int? = null
+
+ fun onBack() = postSideEffect(StudentBoardWriteEffect.NavigateBack)
+ fun onTitleChanged(title: String) = intent { copy(title = title) }
+ fun onContentChanged(content: String) = intent { copy(content = content) }
+ fun onAnonymousChecked(checked: Boolean) = intent { copy(anonymousChecked = checked) }
+ fun onAddPhoto(bitmap: Bitmap) = intent { copy(photo = bitmap) }
+ fun showToast(message: String) = postSideEffect(StudentBoardWriteEffect.OnShowSnackbar(message))
+
+ fun onPost(photo: File?) {
+ viewModelScope.launch {
+ intent { copy(isLoading = true) }
+
+ if (organizationId == null) {
+ organizationId = getSelectedOrganizationIdUseCase().getOrNull()
+ }
+
+ if (organizationId != null) {
+ newPostUseCase(
+ organizationId = organizationId!!.toLong(),
+ title = currentState.title,
+ content = currentState.content,
+ secretStatus = currentState.anonymousChecked,
+ postCategory = "FREE",
+ file = photo
+ ).onSuccess {
+ postSideEffect(StudentBoardWriteEffect.OnShowSnackbar("게시글 작성에 성공했어요 :)"))
+ postSideEffect(StudentBoardWriteEffect.NavigateBack)
+ }.onFailure {
+ postSideEffect(StudentBoardWriteEffect.OnShowSnackbar(it.message ?: "게시글 작성에 실패했어요 ;("))
+ }
+ } else {
+ postSideEffect(StudentBoardWriteEffect.OnShowSnackbar("학급 정보를 불러오지 못했어요 ;("))
+ }
+
+ intent { copy(isLoading = false) }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/feature/student/board/src/main/res/values/strings.xml b/android/feature/student/board/src/main/res/values/strings.xml
new file mode 100644
index 00000000..676dd2a7
--- /dev/null
+++ b/android/feature/student/board/src/main/res/values/strings.xml
@@ -0,0 +1,14 @@
+
+
+ 자유게시판
+
+ 글쓰기
+ 제목
+ 내용을 입력하세요
+ 익명
+ 게시
+
+ 게시글이 없어용!
+
+ 댓글을 입력하세요
+
\ No newline at end of file
diff --git a/android/feature/student/challenge/build.gradle.kts b/android/feature/student/challenge/build.gradle.kts
index 14fc159d..c28438d0 100644
--- a/android/feature/student/challenge/build.gradle.kts
+++ b/android/feature/student/challenge/build.gradle.kts
@@ -1,10 +1,37 @@
+import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
+import java.util.Properties
+
plugins {
alias(libs.plugins.sixkids.android.feature.compose)
+ alias(libs.plugins.kotlin.serialization)
}
+fun getProperty(propertyKey: String): String =
+ gradleLocalProperties(rootDir, providers).getProperty(propertyKey)
+
android {
namespace = "com.sixkids.student.challenge"
+ defaultConfig {
+ val localProperties = Properties()
+ val localPropertiesFile = rootProject.file("local.properties")
+ if (localPropertiesFile.exists()) {
+ localProperties.load(localPropertiesFile.inputStream())
+ }
+
+ val sseUrl = localProperties.getProperty("SSE_ENDPOINT") ?: ""
+
+ buildConfigField("String", "SSE_ENDPOINT", "\"${sseUrl}\"")
+ }
+
+ buildFeatures {
+ buildConfig = true
+ }
}
dependencies {
+ implementation(libs.bundles.paging)
+ implementation(projects.core.bluetooth)
+ implementation(libs.okhttp.sse)
+ implementation(libs.okhttp.logginginterceptor)
+ implementation(libs.kotlinx.serialization.json)
}
diff --git a/android/feature/student/challenge/src/main/AndroidManifest.xml b/android/feature/student/challenge/src/main/AndroidManifest.xml
index a5918e68..87167fef 100644
--- a/android/feature/student/challenge/src/main/AndroidManifest.xml
+++ b/android/feature/student/challenge/src/main/AndroidManifest.xml
@@ -1,4 +1,7 @@
-
\ No newline at end of file
+
+
+
+
diff --git a/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/challeng/history/ChallengeHistoryContract.kt b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/challeng/history/ChallengeHistoryContract.kt
new file mode 100644
index 00000000..85571e4e
--- /dev/null
+++ b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/challeng/history/ChallengeHistoryContract.kt
@@ -0,0 +1,34 @@
+package com.sixkids.student.challeng.history
+
+import androidx.paging.PagingData
+import com.sixkids.model.Challenge
+import com.sixkids.model.GroupType
+import com.sixkids.model.MemberSimple
+import com.sixkids.model.RunningChallengeByStudent
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+import kotlinx.coroutines.flow.Flow
+
+data class ChallengeHistoryState(
+ val isLoading: Boolean = false,
+ val runningChallenge: RunningChallengeByStudent? = null,
+ val totalChallengeCount: Int = 0,
+ val organizationId: Int = 0,
+ val challengeHistory: Flow>? = null
+) : UiState
+
+sealed interface ChallengeHistoryEffect : SideEffect {
+ data class NavigateToChallengeDetail(val challengeId: Long, val groupId: Long? = null) :
+ ChallengeHistoryEffect
+
+ data class NavigateToCreateGroup(val challengeId: Long, val groupType: GroupType) :
+ ChallengeHistoryEffect
+
+ data class NavigateToMathedGroupCreate(val challengeId: Long, val members: List) :
+ ChallengeHistoryEffect
+
+ data class NavigateToJoinGroup(val challengeId: Long) : ChallengeHistoryEffect
+ data object ShowGroupDialog : ChallengeHistoryEffect
+ data class HandleException(val throwable: Throwable, val retry: () -> Unit) :
+ ChallengeHistoryEffect
+}
diff --git a/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/challeng/history/ChallengeHistoryScreen.kt b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/challeng/history/ChallengeHistoryScreen.kt
new file mode 100644
index 00000000..294cacea
--- /dev/null
+++ b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/challeng/history/ChallengeHistoryScreen.kt
@@ -0,0 +1,242 @@
+package com.sixkids.student.challeng.history
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.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.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.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.paging.compose.collectAsLazyPagingItems
+import com.sixkids.designsystem.component.appbar.UlbanDefaultAppBar
+import com.sixkids.designsystem.component.item.UlbanChallengeItem
+import com.sixkids.designsystem.theme.Red
+import com.sixkids.designsystem.theme.UlbanTheme
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.model.GroupType
+import com.sixkids.model.MemberSimple
+import com.sixkids.student.challeng.history.component.GroupParticipationDialog
+import com.sixkids.student.challeng.history.component.UlbanStudentRunningChallengeAppBar
+import com.sixkids.student.challenge.R
+import com.sixkids.ui.util.formatToMonthDayTime
+import com.sixkids.designsystem.R as DesignSystemR
+
+@Composable
+fun ChallengeRoute(
+ viewModel: ChallengeHistoryViewModel = hiltViewModel(),
+ navigateToDetail: (Long, Long?) -> Unit,
+ navigateToCreateGroup: (Long, GroupType) -> Unit,
+ navigateToMatchedGroupCreate: (Long, List) -> Unit,
+ navigateToJoinGroup: (Long) -> Unit,
+ handleException: (Throwable, () -> Unit) -> Unit
+) {
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ LaunchedEffect(key1 = Unit) {
+ viewModel.initData()
+ }
+
+ var showDialog by remember {
+ mutableStateOf(false)
+ }
+
+ LaunchedEffect(key1 = viewModel.sideEffect) {
+ viewModel.sideEffect.collect { sideEffect ->
+ when (sideEffect) {
+ is ChallengeHistoryEffect.NavigateToChallengeDetail -> navigateToDetail(
+ sideEffect.challengeId,
+ sideEffect.groupId
+ )
+
+ is ChallengeHistoryEffect.HandleException -> handleException(
+ sideEffect.throwable,
+ sideEffect.retry
+ )
+
+ ChallengeHistoryEffect.ShowGroupDialog -> {
+ showDialog = true
+ }
+
+ is ChallengeHistoryEffect.NavigateToCreateGroup -> navigateToCreateGroup(
+ sideEffect.challengeId,
+ sideEffect.groupType
+ )
+
+ is ChallengeHistoryEffect.NavigateToJoinGroup -> navigateToJoinGroup(sideEffect.challengeId)
+ is ChallengeHistoryEffect.NavigateToMathedGroupCreate -> navigateToMatchedGroupCreate(
+ sideEffect.challengeId,
+ sideEffect.members
+ )
+ }
+ }
+ }
+
+ ChallengeHistoryScreen(
+ uiState = uiState,
+ updateTotalCount = viewModel::updateTotalCount,
+ navigateToDetail = viewModel::navigateChallengeDetail,
+ navigateToMatchedGroupCrateOrJoin = viewModel::navigateToCrateOrJoinGroup,
+ showDialog = viewModel::showGroupDialog,
+ )
+
+ if (showDialog) {
+ GroupParticipationDialog(
+ onCreateGroupClick = viewModel::navigateToCreateGroup,
+ onJoinGroupClick = viewModel::navigateToJoinGroup,
+ onDismiss = { showDialog = false }
+ )
+ }
+}
+
+@Composable
+fun ChallengeHistoryScreen(
+ uiState: ChallengeHistoryState = ChallengeHistoryState(),
+ updateTotalCount: (Int) -> Unit = {},
+ navigateToDetail: (Long) -> Unit = {},
+ navigateToMatchedGroupCrateOrJoin: (Boolean) -> Unit = {},
+ showDialog: () -> Unit = {},
+) {
+ val listState = rememberLazyListState()
+ val isScrolled by remember {
+ derivedStateOf {
+ listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 100
+ }
+ }
+
+ val challengeItems = uiState.challengeHistory?.collectAsLazyPagingItems()
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ ) {
+ val runningChallenge = uiState.runningChallenge?.challenge
+ if (runningChallenge == null) {
+ UlbanDefaultAppBar(
+ leftIcon = com.sixkids.designsystem.R.drawable.hifive,
+ title = stringResource(id = R.string.hifive_challenge),
+ content = stringResource(R.string.no_running_challenge),
+ color = Red,
+ expanded = !isScrolled
+ )
+ } else {
+ UlbanStudentRunningChallengeAppBar(
+ leftIcon = DesignSystemR.drawable.hifive,
+ title = stringResource(id = R.string.hifive_challenge),
+ content = runningChallenge.title,
+ topDescription = "${runningChallenge.startTime.formatToMonthDayTime()} ~ ${runningChallenge.endTime.formatToMonthDayTime()}",
+ bottomDescription = runningChallenge.content,
+ color = Red,
+ onClick = if (uiState.runningChallenge.createTime == null) {
+ if (uiState.runningChallenge.type == GroupType.FREE) {
+ showDialog
+ } else {
+ {
+ navigateToMatchedGroupCrateOrJoin(
+ uiState.runningChallenge.leaderStatus ?: false
+ )
+ }
+ }
+ } else {
+ {}
+ },
+ onReportEnable = (uiState.runningChallenge.leaderStatus == true && uiState.runningChallenge.endStatus?.not() ?: false),
+ onReportClick = {
+ //TODO 과제 제출로 이동
+ },
+ teamDescription = if (uiState.runningChallenge.type == GroupType.DESIGN) {
+ uiState.runningChallenge.memberNames.joinToString(", ") { it.name }
+ } else {
+ if (uiState.runningChallenge.memberNames.isNotEmpty()) {
+ uiState.runningChallenge.memberNames.joinToString(", ") { it.name }
+ } else {
+ stringResource(R.string.free_group_matching)
+ }
+ },
+ runningTimeDescription = "과제 참여 후 진행시간 표시하기",
+ expanded = !isScrolled
+ )
+ }
+ Spacer(modifier = Modifier.padding(12.dp))
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 16.dp)
+ ) {
+ Text(
+ modifier = Modifier.padding(horizontal = 4.dp),
+ text = stringResource(
+ id = R.string.total_challenge_count,
+ uiState.totalChallengeCount
+ ),
+ style = UlbanTypography.bodyMedium.copy(fontWeight = FontWeight.Bold)
+ )
+ HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
+
+
+ if (challengeItems == null || challengeItems.itemCount == 0) {
+ Spacer(modifier = Modifier.weight(1f))
+ Text(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ text = stringResource(id = R.string.no_challenge_history),
+ style = UlbanTypography.titleLarge,
+ textAlign = TextAlign.Center
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ } else {
+ LazyColumn(
+ modifier = Modifier.weight(1f),
+ state = listState,
+ contentPadding = PaddingValues(vertical = 4.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ items(challengeItems.itemCount) { index ->
+ challengeItems[index]?.let { challenge ->
+ if (index == 0) {
+ updateTotalCount(challenge.totalCount)
+ }
+ UlbanChallengeItem(
+ title = challenge.title,
+ description = challenge.content,
+ startDate = challenge.startTime,
+ endDate = challenge.endTime,
+ userCount = challenge.headCount,
+ onClick = { navigateToDetail(challenge.id) }
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun MyPageDefaultScreenPreview() {
+ UlbanTheme {
+ ChallengeHistoryScreen()
+ }
+}
diff --git a/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/challeng/history/ChallengeHistoryViewModel.kt b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/challeng/history/ChallengeHistoryViewModel.kt
new file mode 100644
index 00000000..f37019ac
--- /dev/null
+++ b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/challeng/history/ChallengeHistoryViewModel.kt
@@ -0,0 +1,92 @@
+package com.sixkids.student.challeng.history
+
+import androidx.lifecycle.viewModelScope
+import com.sixkids.domain.usecase.challenge.GetChallengeHistoryUseCase
+import com.sixkids.domain.usecase.challenge.GetRunningChallengeByStudentUseCase
+import com.sixkids.domain.usecase.organization.GetSelectedOrganizationIdUseCase
+import com.sixkids.domain.usecase.user.GetUserInfoUseCase
+import com.sixkids.model.UserInfo
+import com.sixkids.ui.base.BaseViewModel
+import com.sixkids.ui.extension.flatMap
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class ChallengeHistoryViewModel @Inject constructor(
+ private val getSelectedOrganizationIdUseCase: GetSelectedOrganizationIdUseCase,
+ private val getUserInfoUseCase: GetUserInfoUseCase,
+ private val getChallengeHistoryUseCase: GetChallengeHistoryUseCase,
+ private val getRunningChallengeByStudentUseCase: GetRunningChallengeByStudentUseCase
+) : BaseViewModel(
+ ChallengeHistoryState()
+) {
+ private var isFirstVisited: Boolean = true
+ private var organizationId: Long = 0
+ private lateinit var userInfo: UserInfo
+ fun initData() = viewModelScope.launch {
+ if (isFirstVisited.not()) return@launch
+ isFirstVisited = false
+
+ intent { copy(isLoading = true) }
+
+ getUserInfoUseCase().flatMap { userInfo ->
+ this@ChallengeHistoryViewModel.userInfo = userInfo
+ getSelectedOrganizationIdUseCase().flatMap { organizationId ->
+ this@ChallengeHistoryViewModel.organizationId = organizationId.toLong()
+ val challengeHistory = getChallengeHistoryUseCase(organizationId, userInfo.id)
+ intent { copy(challengeHistory = challengeHistory) }
+ getRunningChallengeByStudentUseCase(organizationId)
+ }.onSuccess {
+ intent { copy(isLoading = false, runningChallenge = it) }
+ }.onFailure {
+ }
+ }
+
+ intent { copy(isLoading = false) }
+ }
+
+ fun navigateChallengeDetail(challengeId: Long) = postSideEffect(
+ ChallengeHistoryEffect.NavigateToChallengeDetail(challengeId)
+ )
+
+ fun showGroupDialog() = postSideEffect(
+ ChallengeHistoryEffect.ShowGroupDialog
+ )
+
+ fun navigateToCreateGroup() {
+ val runningChallenge = uiState.value.runningChallenge ?: return
+ postSideEffect(
+ ChallengeHistoryEffect.NavigateToCreateGroup(
+ runningChallenge.challenge.id,
+ runningChallenge.type
+ )
+ )
+ }
+
+ private fun navigateToMatchedGroupCreate(){
+ val runningChallenge = uiState.value.runningChallenge ?: return
+ postSideEffect(
+ ChallengeHistoryEffect.NavigateToMathedGroupCreate(
+ runningChallenge.challenge.id,
+ runningChallenge.memberNames
+ )
+ )
+ }
+
+ fun navigateToJoinGroup() = postSideEffect(
+ ChallengeHistoryEffect.NavigateToJoinGroup(organizationId)
+ )
+
+ fun updateTotalCount(totalCount: Int) {
+ intent { copy(totalChallengeCount = totalCount) }
+ }
+
+ fun navigateToCrateOrJoinGroup(leaderStatus: Boolean) {
+ if (leaderStatus) {
+ navigateToMatchedGroupCreate()
+ } else {
+ navigateToJoinGroup()
+ }
+ }
+}
diff --git a/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/challeng/history/component/GroupParticipationDialog.kt b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/challeng/history/component/GroupParticipationDialog.kt
new file mode 100644
index 00000000..7772c19d
--- /dev/null
+++ b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/challeng/history/component/GroupParticipationDialog.kt
@@ -0,0 +1,103 @@
+package com.sixkids.student.challeng.history.component
+
+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.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+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.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.sixkids.designsystem.component.button.UlbanFilledButton
+import com.sixkids.designsystem.component.dialog.UlbanBasicDialog
+import com.sixkids.designsystem.theme.Green
+import com.sixkids.designsystem.theme.Orange
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.student.challenge.R
+
+@Composable
+fun GroupParticipationDialog(
+ modifier: Modifier = Modifier,
+ onCreateGroupClick: () -> Unit,
+ onJoinGroupClick: () -> Unit,
+ onDismiss: () -> Unit
+) {
+ UlbanBasicDialog(
+ onDismiss = onDismiss,
+ ) {
+ Column(
+ modifier = modifier,
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.End
+ ) {
+ Row(
+ modifier = Modifier.width(240.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceEvenly,
+ ) {
+ TextButton(
+ modifier = Modifier
+ .width(100.dp)
+ .height(140.dp),
+ onClick = onCreateGroupClick,
+ colors = ButtonDefaults.textButtonColors(
+ containerColor = Orange
+ ),
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.create_group),
+ textAlign = TextAlign.Center,
+ color = Color.Black,
+ style = UlbanTypography.titleSmall
+ )
+ }
+ TextButton(
+ modifier = Modifier
+ .width(100.dp)
+ .height(140.dp),
+ onClick = onJoinGroupClick,
+ colors = ButtonDefaults.textButtonColors(
+ containerColor = Green
+ ),
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.join_group),
+ textAlign = TextAlign.Center,
+ color = Color.Black,
+ style = UlbanTypography.titleSmall
+ )
+ }
+ }
+ Spacer(modifier = Modifier.height(16.dp))
+ UlbanFilledButton(
+ text = stringResource(R.string.cancel),
+ onClick = onDismiss
+ )
+ }
+
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun GroupParticipationDialogPreview() {
+ GroupParticipationDialog(
+ onCreateGroupClick = {},
+ onJoinGroupClick = {},
+ onDismiss = {}
+ )
+}
diff --git a/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/challeng/history/component/UlbanStudentRunningChallengeAppBar.kt b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/challeng/history/component/UlbanStudentRunningChallengeAppBar.kt
new file mode 100644
index 00000000..a8216d00
--- /dev/null
+++ b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/challeng/history/component/UlbanStudentRunningChallengeAppBar.kt
@@ -0,0 +1,109 @@
+package com.sixkids.student.challeng.history.component
+
+import androidx.annotation.DrawableRes
+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.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.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.sixkids.designsystem.component.appbar.AppBarDetailInfo
+import com.sixkids.designsystem.component.appbar.BasicAppBar
+import com.sixkids.designsystem.component.button.UlbanFilledButton
+import com.sixkids.designsystem.theme.Cream
+import com.sixkids.designsystem.theme.Red
+import com.sixkids.designsystem.theme.RedDark
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.student.challenge.R
+
+@Composable
+fun UlbanStudentRunningChallengeAppBar(
+ modifier: Modifier = Modifier,
+ @DrawableRes leftIcon: Int,
+ title: String,
+ content: String,
+ topDescription: String,
+ bottomDescription: String,
+ teamDescription: String,
+ runningTimeDescription: String,
+ color: Color,
+ expanded: Boolean = true,
+ onReportEnable: Boolean = true,
+ onReportClick: () -> Unit,
+ onClick: () -> Unit = {}
+
+) {
+ BasicAppBar(
+ modifier = modifier,
+ leftIcon = leftIcon,
+ title = title,
+ content = {
+ Column(
+ modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.Start
+ ) {
+ if(onReportEnable){
+ Row {
+ Spacer(modifier = Modifier.weight(1f))
+ UlbanFilledButton(
+ text = stringResource(R.string.end),
+ modifier = Modifier.padding(end = 16.dp),
+ color = RedDark,
+ textColor = Cream,
+ textStyle = UlbanTypography.titleSmall.copy(fontSize = 12.sp),
+ onClick = onReportClick
+ )
+ }
+ }
+
+ AppBarDetailInfo(
+ title = content,
+ topDescription = topDescription,
+ bottomDescription = bottomDescription,
+ )
+ Text(
+ modifier = Modifier.padding(top = 8.dp),
+ text = stringResource(R.string.team_member),
+ style = UlbanTypography.titleSmall.copy(fontSize = 12.sp)
+ )
+ Text(
+ text = teamDescription,
+ style = UlbanTypography.bodySmall
+ )
+// Text(
+// modifier = Modifier.padding(top = 12.dp),
+// text = runningTimeDescription,
+// style = UlbanTypography.titleSmall
+// )
+ }
+ },
+ color = color,
+ expanded = expanded,
+ onclick = onClick
+ )
+}
+
+
+@Preview(showBackground = true)
+@Composable
+private fun UlbanStudentRunningChallengeAppBarPreview() {
+ UlbanStudentRunningChallengeAppBar(
+ leftIcon = com.sixkids.designsystem.R.drawable.hifive,
+ title = "title",
+ content = "4월 22일 함께 달리기",
+ topDescription = "04.17 15:00 ~ 04.19 20:00",
+ bottomDescription = "문화의 날을 맞아 우리반 친구들 4명 이상 만나서 영화를 보자",
+ color = Red,
+ teamDescription = "4명 이상 자율적으로 구성해 봐요",
+ runningTimeDescription = "1시간 20분째 진행 중입니다",
+ onReportClick = {}
+ )
+}
diff --git a/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/component/GroupWaiting.kt b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/component/GroupWaiting.kt
new file mode 100644
index 00000000..d5116bb0
--- /dev/null
+++ b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/component/GroupWaiting.kt
@@ -0,0 +1,147 @@
+package com.sixkids.student.group.component
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+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 com.sixkids.designsystem.component.button.UlbanFilledButton
+import com.sixkids.designsystem.theme.Cream
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.model.GroupType
+import com.sixkids.model.MemberSimple
+import com.sixkids.student.challenge.R
+
+@Composable
+fun GroupWaiting(
+ leader: MemberSimple = MemberSimple(),
+ memberList: List = emptyList(),
+ waitingMemberList: List = emptyList(),
+ onDoneClick: () -> Unit = {},
+ onRemoveClick: (Long) -> Unit = {},
+ groupSize: Int = 0,
+ groupType: GroupType = GroupType.FREE
+) {
+ Card(
+ modifier = Modifier
+ .padding(top = 16.dp)
+ .fillMaxWidth(),
+ shape = RoundedCornerShape(
+ topStart = 12.dp,
+ topEnd = 12.dp,
+ bottomStart = 0.dp,
+ bottomEnd = 0.dp
+ ),
+ elevation = CardDefaults.cardElevation(16.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = Cream
+ )
+ ) {
+ val remainingMember = groupSize - memberList.size - 1
+ Column(
+ modifier = Modifier.padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ if (groupType == GroupType.FREE) {
+ Text(
+ modifier = Modifier.padding(top = 16.dp),
+ text = if (remainingMember > 0) {
+ stringResource(R.string.friend_waiting_message, remainingMember)
+ } else {
+ stringResource(R.string.can_create_group)
+ },
+ style = UlbanTypography.bodyMedium
+ )
+ } else {
+ Text(
+ modifier = Modifier.padding(top = 16.dp),
+ text = if (remainingMember != 0) {
+ stringResource(R.string.need_to_waiting_friend_message, remainingMember)
+ } else {
+ stringResource(R.string.can_create_group)
+ },
+ style = UlbanTypography.bodyMedium
+ )
+ }
+
+ LazyRow(
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ if (leader.id != 0L) {
+ item {
+ MemberIcon(
+ memberIconItem = MemberIconItem(
+ member = leader,
+ showX = false,
+ isActive = true
+ ),
+ onRemoveClick = {}
+ )
+ }
+ }
+ items(memberList) { item ->
+ MemberIcon(
+ memberIconItem = MemberIconItem(
+ member = item,
+ showX = true,
+ isActive = true,
+ ),
+ onRemoveClick = onRemoveClick
+ )
+ }
+ items(waitingMemberList){
+ MemberIcon(
+ memberIconItem = MemberIconItem(
+ member = it,
+ showX = false,
+ isActive = false
+ )
+ )
+ }
+ }
+ UlbanFilledButton(
+ modifier = Modifier.fillMaxWidth(),
+ text = stringResource(R.string.done),
+ onClick = onDoneClick,
+ enabled = remainingMember <= 0
+ )
+ }
+ }
+}
+
+data class MemberIconItem(
+ val member: MemberSimple = MemberSimple(),
+ val showX: Boolean = false,
+ val isActive: Boolean = false
+)
+
+
+@Preview(showBackground = true)
+@Composable
+fun GroupWaitingPreview() {
+ GroupWaiting(
+ leader = MemberSimple(
+ id = 1,
+ name = "leader",
+ photo = ""
+ ),
+ memberList = List(4) {
+ MemberSimple(
+ id = it.toLong(),
+ name = "member $it",
+ photo = "",
+ )
+ },
+ groupSize = 5
+ )
+}
diff --git a/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/component/InviteDialog.kt b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/component/InviteDialog.kt
new file mode 100644
index 00000000..237a6ec2
--- /dev/null
+++ b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/component/InviteDialog.kt
@@ -0,0 +1,79 @@
+package com.sixkids.student.group.component
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.sixkids.designsystem.component.button.UlbanFilledButton
+import com.sixkids.designsystem.component.dialog.UlbanBasicDialog
+import com.sixkids.designsystem.theme.Red
+import com.sixkids.model.MemberSimple
+import com.sixkids.student.challenge.R
+import com.sixkids.designsystem.R as DesignSystemR
+
+@Composable
+fun InviteDialog(
+ leader: MemberIconItem,
+ modifier: Modifier = Modifier,
+ onConfirmClick: () -> Unit = {},
+ onCancelClick: () -> Unit = {}
+) {
+
+ UlbanBasicDialog(
+ modifier = modifier,
+ ) {
+ Column(
+ modifier = modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ MemberIcon(modifier = Modifier.padding(top = 4.dp), memberIconItem = leader)
+ Text(
+ modifier = Modifier.padding(vertical = 16.dp),
+ text = stringResource(R.string.invited_group)
+ )
+
+ Row(
+ modifier = modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+
+ UlbanFilledButton(
+ modifier = Modifier.weight(1f),
+ text = stringResource(id = DesignSystemR.string.cancel),
+ onClick = onCancelClick,
+ color = Red
+ )
+ UlbanFilledButton(
+ modifier = Modifier.weight(1f),
+ text = stringResource(id = DesignSystemR.string.confirm),
+ onClick = onConfirmClick
+ )
+ }
+ }
+ }
+
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun InviteDialogPreview() {
+ InviteDialog(
+ leader = MemberIconItem(
+ member = MemberSimple(
+ id = 1,
+ name = "Leader",
+ photo = "https://www.example.com/image.jpg"
+ ),
+ isActive = true
+ )
+ )
+}
diff --git a/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/component/MatchedGroupWaiting.kt b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/component/MatchedGroupWaiting.kt
new file mode 100644
index 00000000..58f24d9c
--- /dev/null
+++ b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/component/MatchedGroupWaiting.kt
@@ -0,0 +1,127 @@
+package com.sixkids.student.group.component
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+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 com.sixkids.designsystem.component.button.UlbanFilledButton
+import com.sixkids.designsystem.theme.Cream
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.model.MemberSimple
+import com.sixkids.student.challenge.R
+
+@Composable
+fun MatchedGroupWaiting(
+ leader: MemberSimple = MemberSimple(),
+ memberList: List = emptyList(),
+ waitingMemberList: List = emptyList(),
+ onDoneClick: () -> Unit = {},
+ onRemoveClick: (Long) -> Unit = {},
+ groupSize: Int = 0,
+) {
+ Card(
+ modifier = Modifier
+ .padding(top = 16.dp)
+ .fillMaxWidth(),
+ shape = RoundedCornerShape(
+ topStart = 12.dp,
+ topEnd = 12.dp,
+ bottomStart = 0.dp,
+ bottomEnd = 0.dp
+ ),
+ elevation = CardDefaults.cardElevation(16.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = Cream
+ )
+ ) {
+ val remainingMember = groupSize - memberList.size - 1
+ Column(
+ modifier = Modifier.padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Text(
+ modifier = Modifier.padding(top = 16.dp),
+ text = if (remainingMember != 0) {
+ stringResource(R.string.need_to_waiting_friend_message, remainingMember)
+ } else {
+ stringResource(R.string.can_create_group)
+ },
+ style = UlbanTypography.bodyMedium
+ )
+
+ LazyRow(
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ if (leader.id != 0L) {
+ item {
+ MemberIcon(
+ memberIconItem = MemberIconItem(
+ member = leader,
+ showX = false,
+ isActive = true
+ ),
+ onRemoveClick = {}
+ )
+ }
+ }
+ items(memberList){
+ MemberIcon(
+ memberIconItem = MemberIconItem(
+ member = it,
+ showX = false,
+ isActive = true
+ ),
+ onRemoveClick = onRemoveClick
+ )
+ }
+ items(waitingMemberList){
+ MemberIcon(
+ memberIconItem = MemberIconItem(
+ member = it,
+ showX = false,
+ isActive = false
+ )
+ )
+ }
+ }
+ UlbanFilledButton(
+ modifier = Modifier.fillMaxWidth(),
+ text = stringResource(R.string.done),
+ onClick = onDoneClick,
+ enabled = remainingMember == 0
+ )
+ }
+ }
+}
+
+
+@Preview(showBackground = true)
+@Composable
+fun MatchedGroupWaitingPreview() {
+ MatchedGroupWaiting(
+ leader = MemberSimple(
+ id = 1,
+ name = "leader",
+ photo = ""
+ ),
+ waitingMemberList = List(4) {
+ MemberSimple(
+ id = it.toLong(),
+ name = "member $it",
+ photo = "",
+ )
+ },
+ groupSize = 5
+ )
+}
diff --git a/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/component/MemberIcon.kt b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/component/MemberIcon.kt
new file mode 100644
index 00000000..fed8e88b
--- /dev/null
+++ b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/component/MemberIcon.kt
@@ -0,0 +1,116 @@
+package com.sixkids.student.group.component
+
+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.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+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.graphics.graphicsLayer
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.model.MemberSimple
+import com.sixkids.designsystem.R as DesignSystemR
+
+@Composable
+fun MemberIcon(
+ modifier: Modifier = Modifier,
+ memberIconItem: MemberIconItem,
+ onIconClick: (MemberIconItem) -> Unit = {},
+ onRemoveClick: (Long) -> Unit = {},
+) {
+ Card(
+ modifier = modifier
+ .wrapContentSize()
+ .background(
+ if (memberIconItem.isActive) Color.Transparent else Color.Gray,
+ shape = RoundedCornerShape(8.dp),
+ )
+ .graphicsLayer {
+ alpha = if (memberIconItem.isActive) 1f else 0.5f
+ },
+ colors = CardDefaults.cardColors(containerColor = Color.Transparent),
+
+ onClick = { onIconClick(memberIconItem) }
+ ) {
+ Box(modifier.wrapContentSize()) {
+
+ Column(
+ modifier = modifier.wrapContentSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Box(
+ modifier = modifier.wrapContentSize(),
+ ) {
+ Card {
+ AsyncImage(
+ model = memberIconItem.member.photo,
+ contentDescription = null,
+ modifier = modifier.size(48.dp),
+ contentScale = ContentScale.Crop
+ )
+ }
+ if (memberIconItem.showX) {
+ Icon(
+ imageVector = ImageVector.vectorResource(DesignSystemR.drawable.ic_close_filled),
+ contentDescription = "Close icon",
+ tint = Color.Red,
+ modifier = Modifier
+ .align(Alignment.TopEnd)
+ .size(24.dp)
+ .clickable {
+ onRemoveClick(memberIconItem.member.id)
+ }
+ )
+ }
+ }
+ Text(
+ text = memberIconItem.member.name,
+ style = UlbanTypography.bodyMedium,
+ modifier = Modifier.padding(4.dp),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ }
+ }
+
+
+}
+
+@Preview(showBackground = true)
+@Composable
+fun MemberIconPreview() {
+ MemberIcon(
+ memberIconItem = MemberIconItem(
+ member = MemberSimple(
+ id = 1,
+ name = "Leader",
+ photo = "https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png"
+
+ ),
+ showX = true,
+ isActive = true
+ ),
+ onIconClick = {},
+ onRemoveClick = {}
+ )
+}
diff --git a/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/component/MultiLayeredCircles.kt b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/component/MultiLayeredCircles.kt
new file mode 100644
index 00000000..4f46a8f2
--- /dev/null
+++ b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/component/MultiLayeredCircles.kt
@@ -0,0 +1,40 @@
+package com.sixkids.student.group.component
+
+import androidx.compose.foundation.Canvas
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.sixkids.designsystem.theme.Blue
+import com.sixkids.designsystem.theme.Green
+import com.sixkids.designsystem.theme.Orange
+import com.sixkids.designsystem.theme.Red
+import com.sixkids.designsystem.theme.UlbanTheme
+
+@Composable
+fun MultiLayeredCircles(modifier: Modifier = Modifier) {
+ Canvas(modifier = modifier ){
+ val strokeWidth = 2.dp.toPx()
+ val radiusIncrement = 46.dp.toPx() // 각 원의 반지름 증가량
+
+ val colors = listOf(Red, Orange, Green, Blue) // 원의 색상 리스트
+
+ for (i in colors.indices) {
+ drawCircle(
+ color = colors[i],
+ radius = radiusIncrement * (i + 1),
+ style = Stroke(width = strokeWidth)
+ )
+ }
+ }
+}
+
+
+@Preview(showBackground = true)
+@Composable
+fun MultiLayeredCirclesPreview() {
+ UlbanTheme {
+ MultiLayeredCircles()
+ }
+}
diff --git a/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/create/free/CreateGroupContract.kt b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/create/free/CreateGroupContract.kt
new file mode 100644
index 00000000..1b9bc0f4
--- /dev/null
+++ b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/create/free/CreateGroupContract.kt
@@ -0,0 +1,27 @@
+package com.sixkids.student.group.create.free
+
+import com.sixkids.model.MemberSimple
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
+data class CreateGroupState(
+ val isLoading: Boolean = false,
+ val isScanning: Boolean = false,
+ val foundMembers: List = emptyList(),
+ val showMembers: Array = Array(5) { null },
+ val selectedMembers: List = emptyList(),
+ val waitingMembers: List = emptyList(),
+ val groupSize: Int = 0,
+ val leader: MemberSimple = MemberSimple(),
+ val roomKey: String = "",
+) : UiState
+
+
+sealed interface CreateGroupEffect : SideEffect {
+ data object NavigateToChallengeHistory : CreateGroupEffect
+ data class HandleException(
+ val throwable: Throwable,
+ val retryAction: () -> Unit
+ ) : CreateGroupEffect
+
+}
diff --git a/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/create/free/CreateGroupRoute.kt b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/create/free/CreateGroupRoute.kt
new file mode 100644
index 00000000..8652d878
--- /dev/null
+++ b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/create/free/CreateGroupRoute.kt
@@ -0,0 +1,213 @@
+package com.sixkids.student.group.create.free
+
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+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 androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.sixkids.designsystem.theme.UlbanTheme
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.model.MemberSimple
+import com.sixkids.student.challenge.R
+import com.sixkids.student.group.component.GroupWaiting
+import com.sixkids.student.group.component.MemberIcon
+import com.sixkids.student.group.component.MemberIconItem
+import com.sixkids.student.group.component.MultiLayeredCircles
+import com.sixkids.ui.extension.collectWithLifecycle
+
+@Composable
+fun CreateGroupRoute(
+ viewModel: CreateGroupViewModel = hiltViewModel(),
+ navigateToChallengeHistory: () -> Unit,
+ handleException: (Throwable, () -> Unit) -> Unit,
+) {
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ LaunchedEffect(Unit) {
+ viewModel.connectSse()
+ viewModel.createGroupMatchingRoom()
+ viewModel.startScan()
+ }
+
+ DisposableEffect(Unit) {
+ onDispose {
+ viewModel.disconnectSse()
+ viewModel.stopScan()
+ }
+ }
+
+
+ viewModel.sideEffect.collectWithLifecycle {
+ when (it) {
+ is CreateGroupEffect.NavigateToChallengeHistory -> navigateToChallengeHistory()
+ is CreateGroupEffect.HandleException -> handleException(it.throwable, it.retryAction)
+ }
+ }
+ CreateGroupScreen(
+ uiState = uiState,
+ onMemberSelect = viewModel::selectMember,
+ onMemberRemove = viewModel::removeMember,
+ onGroupCreate = viewModel::createGroup
+ )
+}
+
+@Composable
+fun CreateGroupScreen(
+ uiState: CreateGroupState = CreateGroupState(),
+ onMemberSelect: (MemberSimple) -> Unit = { },
+ onMemberRemove: (Long) -> Unit = { },
+ onGroupCreate: () -> Unit = { }
+) {
+ Box {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(
+ text = stringResource(R.string.invite_friend),
+ style = UlbanTypography.titleSmall,
+ modifier = Modifier.padding(top = 32.dp)
+ )
+ Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.Center) {
+ MultiLayeredCircles(modifier = Modifier.align(Alignment.Center))
+ MemberIcon(
+ memberIconItem = MemberIconItem(
+ member = uiState.leader,
+ isActive = true
+ ), modifier = Modifier.align(Alignment.Center)
+ )
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center,
+ ) {
+ val member0 = uiState.showMembers[0]
+ val member1 = uiState.showMembers[1]
+ val member2 = uiState.showMembers[2]
+ val member3 = uiState.showMembers[3]
+ val member4 = uiState.showMembers[4]
+ if (member0 != null) {
+ androidx.compose.animation.AnimatedVisibility(
+ visible = true,
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
+ Box(modifier = Modifier.offset((-100).dp, (-100).dp)) {
+ MemberIcon(
+ memberIconItem = MemberIconItem(
+ member = member0,
+ isActive = true
+ ), modifier = Modifier.align(Alignment.Center),
+ onIconClick = { onMemberSelect(member0) }
+ )
+ }
+ }
+ }
+ if (member1 != null) {
+ androidx.compose.animation.AnimatedVisibility(
+ visible = true,
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
+ Box(modifier = Modifier.offset((-135).dp, (70).dp)) {
+ MemberIcon(
+ memberIconItem = MemberIconItem(
+ member = member1,
+ isActive = true
+ ), modifier = Modifier.align(Alignment.Center),
+ onIconClick = { onMemberSelect(member1) }
+ )
+ }
+ }
+ }
+
+ if (member2 != null) {
+ androidx.compose.animation.AnimatedVisibility(
+ visible = true,
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
+ Box(modifier = Modifier.offset((120).dp, (60).dp)) {
+ MemberIcon(
+ memberIconItem = MemberIconItem(
+ member = member2,
+ isActive = true
+ ), modifier = Modifier.align(Alignment.Center),
+ onIconClick = { onMemberSelect(member2) }
+ )
+ }
+ }
+ }
+
+ if (member3 != null) {
+ androidx.compose.animation.AnimatedVisibility(
+ visible = true,
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
+ Box(modifier = Modifier.offset((80).dp, (-120).dp)) {
+ MemberIcon(
+ memberIconItem = MemberIconItem(
+ member = member3,
+ isActive = true
+ ), modifier = Modifier.align(Alignment.Center),
+ onIconClick = { onMemberSelect(member3) }
+ )
+ }
+ }
+ }
+
+ if (member4 != null) {
+ androidx.compose.animation.AnimatedVisibility(
+ visible = true,
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
+ Box(modifier = Modifier.offset((80).dp, (-120).dp)) {
+ MemberIcon(
+ memberIconItem = MemberIconItem(
+ member = member4,
+ isActive = true
+ ), modifier = Modifier.align(Alignment.Center),
+ onIconClick = { onMemberSelect(member4) }
+ )
+ }
+ }
+ }
+ }
+ }
+
+ GroupWaiting(
+ groupSize = uiState.groupSize,
+ leader = uiState.leader,
+ memberList = uiState.selectedMembers,
+ waitingMemberList = uiState.waitingMembers,
+ onRemoveClick = {
+ onMemberRemove(it)
+ },
+ onDoneClick = onGroupCreate
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun CreateGroupScreenPreview() {
+ UlbanTheme {
+ CreateGroupScreen()
+ }
+}
diff --git a/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/create/free/CreateGroupViewModel.kt b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/create/free/CreateGroupViewModel.kt
new file mode 100644
index 00000000..f6b36c58
--- /dev/null
+++ b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/create/free/CreateGroupViewModel.kt
@@ -0,0 +1,292 @@
+package com.sixkids.student.group.create.free
+
+import android.Manifest
+import android.util.Log
+import androidx.annotation.RequiresPermission
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.viewModelScope
+import com.sixkids.core.bluetooth.BluetoothScanner
+import com.sixkids.domain.usecase.group.CreateGroupMatchingRoomUseCase
+import com.sixkids.domain.usecase.group.CreateGroupUseCase
+import com.sixkids.domain.usecase.group.DeportFriendUseCase
+import com.sixkids.domain.usecase.group.InviteFriendUseCase
+import com.sixkids.domain.usecase.user.GetATKUseCase
+import com.sixkids.domain.usecase.user.GetMemberSimpleUseCase
+import com.sixkids.domain.usecase.user.LoadUserInfoUseCase
+import com.sixkids.model.MemberSimple
+import com.sixkids.model.SseData
+import com.sixkids.model.SseEventType
+import com.sixkids.student.challenge.BuildConfig
+import com.sixkids.ui.base.BaseViewModel
+import com.sixkids.ui.extension.flatMap
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.serialization.json.Json
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import okhttp3.logging.HttpLoggingInterceptor
+import okhttp3.sse.EventSource
+import okhttp3.sse.EventSourceListener
+import okhttp3.sse.EventSources
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+
+private const val TAG = "D107"
+
+@HiltViewModel
+class CreateGroupViewModel @Inject constructor(
+ private val bluetoothScanner: BluetoothScanner,
+ private val getMemberSimpleUseCase: GetMemberSimpleUseCase,
+ private val loadUserInfoUseCase: LoadUserInfoUseCase,
+ private val getATKUseCase: GetATKUseCase,
+ private val createGroupMatchingRoomUseCase: CreateGroupMatchingRoomUseCase,
+ private val inviteFriendUseCase: InviteFriendUseCase,
+ private val deportFriendUseCase: DeportFriendUseCase,
+ private val createGroupUseCase: CreateGroupUseCase,
+ savedStateHandle: SavedStateHandle
+) : BaseViewModel(CreateGroupState()) {
+
+ private var showUserJob: Job? = null
+
+ private val challengeId: Long = savedStateHandle.get("challengeId") ?: 0L
+
+ private var eventSource: EventSource? = null
+
+ private val client = OkHttpClient.Builder()
+ .addInterceptor {
+ HttpLoggingInterceptor().apply {
+ level = HttpLoggingInterceptor.Level.HEADERS
+ }.intercept(it)
+ }
+ .connectTimeout(10, TimeUnit.MINUTES)
+ .readTimeout(10, TimeUnit.MINUTES)
+ .writeTimeout(10, TimeUnit.MINUTES)
+ .build()
+
+ private val request = Request.Builder()
+ .url(BuildConfig.SSE_ENDPOINT)
+ .header("Authorization", "Bearer ${runBlocking { getATKUseCase().getOrNull() }}")
+ .build()
+
+ private val eventSourceListener = object : EventSourceListener() {
+ override fun onOpen(eventSource: EventSource, response: Response) {
+ super.onOpen(eventSource, response)
+ Log.d(TAG, "Connection Opened")
+ }
+
+ override fun onClosed(eventSource: EventSource) {
+ super.onClosed(eventSource)
+ Log.d(TAG, "Connection Closed")
+ }
+
+ override fun onEvent(
+ eventSource: EventSource,
+ id: String?,
+ type: String?,
+ data: String
+ ) {
+ super.onEvent(eventSource, id, type, data)
+ val sseEventType: SseEventType = SseEventType.valueOf(type ?: "")
+ val sseData = Json.decodeFromString(data)
+ val url = sseData.url
+ val realData = sseData.data
+ when (sseEventType) {
+ SseEventType.SSE_CONNECT -> {}
+ SseEventType.INVITE_REQUEST -> {}
+ SseEventType.INVITE_RESPONSE -> {
+ if (url == null) return
+ if (realData == null) return
+ Log.d(TAG, "onEvent: $realData")
+ handelInviteResult(realData.toBoolean(), url)
+ }
+
+ SseEventType.CREATE_GROUP -> Log.d(TAG, "onEvent: 그룹 생성")
+ SseEventType.KICK_MEMBER -> Log.d(TAG, "onEvent: 추방")
+ }
+ }
+
+ override fun onFailure(eventSource: EventSource, t: Throwable?, response: Response?) {
+ super.onFailure(eventSource, t, response)
+ Log.d(TAG, "On Failure -: ${response?.body}")
+ }
+ }
+
+ fun createGroupMatchingRoom() {
+ viewModelScope.launch {
+ createGroupMatchingRoomUseCase(challengeId).flatMap { matchingRoom ->
+ intent {
+ copy(
+ roomKey = matchingRoom.dataKey,
+ groupSize = matchingRoom.minCount,
+ )
+ }
+ loadUserInfoUseCase()
+ }.onSuccess { member ->
+ intent {
+ copy(
+ leader = MemberSimple(
+ id = member.id.toLong(),
+ name = member.name,
+ photo = member.photo
+ )
+ )
+ }
+ }.onFailure {
+ postSideEffect(CreateGroupEffect.HandleException(it) {
+ createGroupMatchingRoom()
+ })
+ }
+ }
+ }
+
+ @RequiresPermission(Manifest.permission.BLUETOOTH_SCAN)
+ fun startScan() {
+ bluetoothScanner.startScanning()
+ viewModelScope.launch {
+ bluetoothScanner.foundDevices.collect { memberIds ->
+ if (memberIds.isEmpty()) return@collect
+ val newMembers = mutableListOf()
+ Log.d(TAG, "startScan: $memberIds")
+ for (memberId in memberIds) {
+ getMemberSimpleUseCase(memberId).onSuccess { member ->
+ newMembers.add(member)
+ }.onFailure {
+ stopScan()
+ postSideEffect(CreateGroupEffect.HandleException(it) {
+ startScan()
+ })
+ }
+ }
+ updateMembers(newMembers)
+ }
+ }
+ }
+
+ @RequiresPermission(Manifest.permission.BLUETOOTH_SCAN)
+ fun stopScan() {
+ bluetoothScanner.stopScanning()
+ }
+
+ private fun updateMembers(newMembers: List) {
+ showUserJob?.cancel()
+ showUserJob = viewModelScope.launch {
+ val showingMembers = uiState.value.showMembers.map { it?.copy() }.toTypedArray()
+ newMembers.forEach { newMember ->
+ if(showingMembers.any { it?.id == newMember.id }) return@forEach
+ if(uiState.value.selectedMembers.any { it.id == newMember.id }) return@forEach
+ if(uiState.value.waitingMembers.any { it.id == newMember.id }) return@forEach
+ var added = false
+ while (!added) {
+ showingMembers.indexOfFirst { it == null }.let { index ->
+ if (index != -1) {
+ showingMembers[index] = newMember
+ added = true
+ } else {
+ delay(1000L) // 1초 후 다시 시도
+ }
+ }
+ }
+ intent {
+ copy(
+ foundMembers = uiState.value.foundMembers + newMember,
+ showMembers = showingMembers
+ )
+ }
+ }
+ }
+ }
+
+ fun connectSse() = viewModelScope.launch {
+ eventSource = EventSources.createFactory(client)
+ .newEventSource(request, eventSourceListener)
+ }
+
+ fun disconnectSse() {
+ eventSource?.cancel()
+ eventSource = null
+ }
+
+ private fun handelInviteResult(isAccepted: Boolean, memberId: Long) {
+ if (isAccepted) {
+ intent {
+ val member = waitingMembers.first { it.id == memberId }
+ copy(
+ selectedMembers = selectedMembers.toMutableList().apply {
+ add(member)
+ }
+ )
+ }
+ } else {
+ bluetoothScanner.removeDevice(memberId)
+ }
+ intent {
+ copy(
+ waitingMembers = waitingMembers.toMutableList().apply {
+ removeIf { it.id == memberId }
+ },
+ )
+ }
+ }
+
+ fun selectMember(member: MemberSimple) {
+ viewModelScope.launch {
+ inviteFriendUseCase(uiState.value.roomKey, member.id).onSuccess {
+ val showingIdx = uiState.value.showMembers.indexOfFirst { member.id == it?.id }
+ uiState.value.showMembers[showingIdx] = null
+ intent {
+ copy(
+ foundMembers = foundMembers.toMutableList().apply {
+ remove(member)
+ },
+ waitingMembers = waitingMembers.toMutableList().apply {
+ add(member)
+ }
+ )
+ }
+ }.onFailure {
+ postSideEffect(CreateGroupEffect.HandleException(it) {
+ selectMember(member)
+ })
+ }
+ }
+ }
+
+ fun removeMember(memberId: Long) {
+ viewModelScope.launch {
+ deportFriendUseCase(uiState.value.roomKey, memberId).onSuccess {
+ bluetoothScanner.removeDevice(memberId)
+ intent {
+ copy(
+ selectedMembers = selectedMembers.toMutableList().apply {
+ removeIf { it.id == memberId }
+ },
+ foundMembers = foundMembers.toMutableList().apply {
+ removeIf { it.id == memberId }
+ }
+ )
+ }
+ }.onFailure {
+ postSideEffect(CreateGroupEffect.HandleException(it) {
+ removeMember(memberId)
+ })
+ }
+
+ }
+ }
+
+ fun createGroup() {
+ viewModelScope.launch {
+ createGroupUseCase(uiState.value.roomKey).onSuccess {
+ postSideEffect(CreateGroupEffect.NavigateToChallengeHistory)
+ }.onFailure {
+ postSideEffect(CreateGroupEffect.HandleException(it) {
+ createGroup()
+ })
+ }
+ }
+ }
+}
diff --git a/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/create/matched/MatchedCreateGroupContract.kt b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/create/matched/MatchedCreateGroupContract.kt
new file mode 100644
index 00000000..e23d02ce
--- /dev/null
+++ b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/create/matched/MatchedCreateGroupContract.kt
@@ -0,0 +1,28 @@
+package com.sixkids.student.group.create.matched
+
+import com.sixkids.model.MemberSimple
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
+data class MatchedCreateGroupState(
+ val isLoading: Boolean = false,
+ val isScanning: Boolean = false,
+ val foundMembers: List = emptyList(),
+ val showMembers: Array = Array(5) { null },
+ val selectedMembers: List = emptyList(),
+ val waitingMembers: List = emptyList(),
+ val groupSize: Int = 0,
+ val leader: MemberSimple = MemberSimple(),
+ val roomKey: String = "",
+) : UiState
+
+
+sealed interface MatchedCreateGroupEffect : SideEffect {
+ data object NavigateToChallengeHistory :
+ MatchedCreateGroupEffect
+ data class HandleException(
+ val throwable: Throwable,
+ val retryAction: () -> Unit
+ ) : MatchedCreateGroupEffect
+
+}
diff --git a/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/create/matched/MatchedCreateGroupRoute.kt b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/create/matched/MatchedCreateGroupRoute.kt
new file mode 100644
index 00000000..ccf7226d
--- /dev/null
+++ b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/create/matched/MatchedCreateGroupRoute.kt
@@ -0,0 +1,213 @@
+package com.sixkids.student.group.create.matched
+
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+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 androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.sixkids.designsystem.theme.UlbanTheme
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.model.MemberSimple
+import com.sixkids.student.challenge.R
+import com.sixkids.student.group.component.MatchedGroupWaiting
+import com.sixkids.student.group.component.MemberIcon
+import com.sixkids.student.group.component.MemberIconItem
+import com.sixkids.student.group.component.MultiLayeredCircles
+import com.sixkids.ui.extension.collectWithLifecycle
+
+@Composable
+fun MatchedCreateGroupRoute(
+ viewModel: MatchedCreateGroupViewModel = hiltViewModel(),
+ navigateToChallengeHistory: () -> Unit,
+ handleException: (Throwable, () -> Unit) -> Unit,
+) {
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ LaunchedEffect(Unit) {
+ viewModel.connectSse()
+ viewModel.createGroupMatchingRoom()
+ viewModel.startScan()
+ }
+
+ DisposableEffect(Unit) {
+ onDispose {
+ viewModel.disconnectSse()
+ viewModel.stopScan()
+ }
+ }
+
+
+ viewModel.sideEffect.collectWithLifecycle {
+ when (it) {
+ is MatchedCreateGroupEffect.NavigateToChallengeHistory -> navigateToChallengeHistory()
+ is MatchedCreateGroupEffect.HandleException -> handleException(
+ it.throwable,
+ it.retryAction
+ )
+ }
+ }
+ MatchedCreateGroupScreen(
+ uiState = uiState,
+ onMemberSelect = viewModel::selectMember,
+ onMemberRemove = viewModel::removeMember,
+ onGroupCreate = viewModel::createGroup
+ )
+}
+
+@Composable
+fun MatchedCreateGroupScreen(
+ uiState: MatchedCreateGroupState = MatchedCreateGroupState(),
+ onMemberSelect: (MemberSimple) -> Unit = { },
+ onMemberRemove: (Long) -> Unit = { },
+ onGroupCreate: () -> Unit = { }
+) {
+ Box {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(
+ text = stringResource(R.string.invite_friend),
+ style = UlbanTypography.titleSmall,
+ modifier = Modifier.padding(top = 32.dp)
+ )
+ Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.Center) {
+ MultiLayeredCircles(modifier = Modifier.align(Alignment.Center))
+ MemberIcon(
+ memberIconItem = MemberIconItem(
+ member = uiState.leader,
+ isActive = true
+ ), modifier = Modifier.align(Alignment.Center)
+ )
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center,
+ ) {
+ val member0 = uiState.showMembers[0]
+ val member1 = uiState.showMembers[1]
+ val member2 = uiState.showMembers[2]
+ val member3 = uiState.showMembers[3]
+ val member4 = uiState.showMembers[4]
+ if (member0 != null) {
+ androidx.compose.animation.AnimatedVisibility(
+ visible = true,
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
+ Box(modifier = Modifier.offset((-100).dp, (-100).dp)) {
+ MemberIcon(
+ memberIconItem = MemberIconItem(
+ member = member0,
+ isActive = true
+ ), modifier = Modifier.align(Alignment.Center),
+ onIconClick = { onMemberSelect(member0) }
+ )
+ }
+ }
+ }
+ if (member1 != null) {
+ androidx.compose.animation.AnimatedVisibility(
+ visible = true,
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
+ Box(modifier = Modifier.offset((-135).dp, (70).dp)) {
+ MemberIcon(
+ memberIconItem = MemberIconItem(
+ member = member1,
+ isActive = true
+ ), modifier = Modifier.align(Alignment.Center),
+ onIconClick = { onMemberSelect(member1) }
+ )
+ }
+ }
+ }
+
+ if (member2 != null) {
+ androidx.compose.animation.AnimatedVisibility(
+ visible = true,
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
+ Box(modifier = Modifier.offset((120).dp, (60).dp)) {
+ MemberIcon(
+ memberIconItem = MemberIconItem(
+ member = member2,
+ isActive = true
+ ), modifier = Modifier.align(Alignment.Center),
+ onIconClick = { onMemberSelect(member2) }
+ )
+ }
+ }
+ }
+
+ if (member3 != null) {
+ androidx.compose.animation.AnimatedVisibility(
+ visible = true,
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
+ Box(modifier = Modifier.offset((80).dp, (-120).dp)) {
+ MemberIcon(
+ memberIconItem = MemberIconItem(
+ member = member3,
+ isActive = true
+ ), modifier = Modifier.align(Alignment.Center),
+ onIconClick = { onMemberSelect(member3) }
+ )
+ }
+ }
+ }
+
+ if (member4 != null) {
+ androidx.compose.animation.AnimatedVisibility(
+ visible = true,
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
+ Box(modifier = Modifier.offset((80).dp, (-120).dp)) {
+ MemberIcon(
+ memberIconItem = MemberIconItem(
+ member = member4,
+ isActive = true
+ ), modifier = Modifier.align(Alignment.Center),
+ onIconClick = { onMemberSelect(member4) }
+ )
+ }
+ }
+ }
+ }
+ }
+
+ MatchedGroupWaiting(
+ groupSize = uiState.groupSize,
+ leader = uiState.leader,
+ memberList = uiState.selectedMembers,
+ waitingMemberList = uiState.waitingMembers,
+ onDoneClick = onGroupCreate
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun CreateGroupScreenPreview() {
+ UlbanTheme {
+ MatchedCreateGroupScreen()
+ }
+}
diff --git a/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/create/matched/MatchedCreateGroupViewModel.kt b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/create/matched/MatchedCreateGroupViewModel.kt
new file mode 100644
index 00000000..1a02c81c
--- /dev/null
+++ b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/create/matched/MatchedCreateGroupViewModel.kt
@@ -0,0 +1,291 @@
+package com.sixkids.student.group.create.matched
+
+import android.Manifest
+import android.util.Log
+import androidx.annotation.RequiresPermission
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.viewModelScope
+import com.sixkids.core.bluetooth.BluetoothScanner
+import com.sixkids.domain.usecase.group.CreateGroupMatchingRoomUseCase
+import com.sixkids.domain.usecase.group.CreateGroupUseCase
+import com.sixkids.domain.usecase.group.DeportFriendUseCase
+import com.sixkids.domain.usecase.group.InviteFriendUseCase
+import com.sixkids.domain.usecase.user.GetATKUseCase
+import com.sixkids.domain.usecase.user.GetMemberSimpleUseCase
+import com.sixkids.domain.usecase.user.LoadUserInfoUseCase
+import com.sixkids.model.MemberSimple
+import com.sixkids.model.SseData
+import com.sixkids.model.SseEventType
+import com.sixkids.student.challenge.BuildConfig
+import com.sixkids.student.navigation.GroupRoute
+import com.sixkids.ui.base.BaseViewModel
+import com.sixkids.ui.extension.flatMap
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.serialization.json.Json
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import okhttp3.logging.HttpLoggingInterceptor
+import okhttp3.sse.EventSource
+import okhttp3.sse.EventSourceListener
+import okhttp3.sse.EventSources
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+
+private const val TAG = "D107"
+
+@HiltViewModel
+class MatchedCreateGroupViewModel @Inject constructor(
+ private val bluetoothScanner: BluetoothScanner,
+ private val getMemberSimpleUseCase: GetMemberSimpleUseCase,
+ private val loadUserInfoUseCase: LoadUserInfoUseCase,
+ private val getATKUseCase: GetATKUseCase,
+ private val createGroupMatchingRoomUseCase: CreateGroupMatchingRoomUseCase,
+ private val inviteFriendUseCase: InviteFriendUseCase,
+ private val deportFriendUseCase: DeportFriendUseCase,
+ private val createGroupUseCase: CreateGroupUseCase,
+ savedStateHandle: SavedStateHandle
+) : BaseViewModel(MatchedCreateGroupState()) {
+
+ private var showUserJob: Job? = null
+
+ private val challengeId: Long = savedStateHandle.get("challengeId") ?: 0L
+
+ private val members: List =
+ Json.decodeFromString((savedStateHandle.get(GroupRoute.MEMBERS_NAME) ?: ""))
+
+ private var eventSource: EventSource? = null
+
+ private val client = OkHttpClient.Builder()
+ .addInterceptor {
+ HttpLoggingInterceptor().apply {
+ level = HttpLoggingInterceptor.Level.HEADERS
+ }.intercept(it)
+ }
+ .connectTimeout(10, TimeUnit.MINUTES)
+ .readTimeout(10, TimeUnit.MINUTES)
+ .writeTimeout(10, TimeUnit.MINUTES)
+ .build()
+
+ private val request = Request.Builder()
+ .url(BuildConfig.SSE_ENDPOINT)
+ .header("Authorization", "Bearer ${runBlocking { getATKUseCase().getOrNull() }}")
+ .build()
+
+ private val eventSourceListener = object : EventSourceListener() {
+ override fun onOpen(eventSource: EventSource, response: Response) {
+ super.onOpen(eventSource, response)
+ Log.d(TAG, "Connection Opened")
+ }
+
+ override fun onClosed(eventSource: EventSource) {
+ super.onClosed(eventSource)
+ Log.d(TAG, "Connection Closed")
+ }
+
+ override fun onEvent(
+ eventSource: EventSource,
+ id: String?,
+ type: String?,
+ data: String
+ ) {
+ super.onEvent(eventSource, id, type, data)
+ val sseEventType: SseEventType = SseEventType.valueOf(type ?: "")
+ val sseData = Json.decodeFromString(data)
+ val url = sseData.url
+ val realData = sseData.data
+ when (sseEventType) {
+ SseEventType.SSE_CONNECT -> {}
+ SseEventType.INVITE_REQUEST -> {}
+ SseEventType.INVITE_RESPONSE -> {
+ if (url == null) return
+ if (realData == null) return
+ Log.d(TAG, "onEvent: $realData")
+ handelInviteResult(realData.toBoolean(), url)
+ }
+
+ SseEventType.CREATE_GROUP -> Log.d(TAG, "onEvent: 그룹 생성")
+ SseEventType.KICK_MEMBER -> Log.d(TAG, "onEvent: 추방")
+ }
+ }
+
+ override fun onFailure(eventSource: EventSource, t: Throwable?, response: Response?) {
+ super.onFailure(eventSource, t, response)
+ Log.d(TAG, "On Failure -: ${response?.body}")
+ }
+ }
+
+ fun createGroupMatchingRoom() {
+ viewModelScope.launch {
+ createGroupMatchingRoomUseCase(challengeId).flatMap { matchingRoom ->
+ intent {
+ copy(
+ roomKey = matchingRoom.dataKey,
+ groupSize = members.size,
+ )
+ }
+ loadUserInfoUseCase()
+ }.onSuccess { member ->
+ intent {
+ copy(
+ leader = MemberSimple(
+ id = member.id.toLong(),
+ name = member.name,
+ photo = member.photo
+ ),
+ waitingMembers = members.filter { it.id != member.id.toLong()}
+ )
+ }
+ }.onFailure {
+ postSideEffect(MatchedCreateGroupEffect.HandleException(it) {
+ createGroupMatchingRoom()
+ })
+ }
+ }
+ }
+
+ @RequiresPermission(Manifest.permission.BLUETOOTH_SCAN)
+ fun startScan() {
+ bluetoothScanner.startScanning()
+ viewModelScope.launch {
+ bluetoothScanner.foundDevices.collect { memberIds ->
+ if (memberIds.isEmpty()) return@collect
+ val newMembers = mutableListOf()
+ Log.d(TAG, "startScan: $memberIds")
+ for (memberId in memberIds) {
+ if(uiState.value.waitingMembers.all { it.id != memberId }) continue
+ getMemberSimpleUseCase(memberId).onSuccess { member ->
+ newMembers.add(member)
+ }.onFailure {
+ stopScan()
+ postSideEffect(MatchedCreateGroupEffect.HandleException(it) {
+ startScan()
+ })
+ }
+ }
+ updateMembers(newMembers)
+ }
+ }
+ }
+
+ @RequiresPermission(Manifest.permission.BLUETOOTH_SCAN)
+ fun stopScan() {
+ bluetoothScanner.stopScanning()
+ }
+
+ private fun updateMembers(newMembers: List) {
+ showUserJob?.cancel()
+ showUserJob = viewModelScope.launch {
+ val showingMembers = uiState.value.showMembers.map { it?.copy() }.toTypedArray()
+ newMembers.forEach { newMember ->
+ if(showingMembers.any { it?.id == newMember.id }) return@forEach
+ if(uiState.value.selectedMembers.any { it.id == newMember.id }) return@forEach
+ var added = false
+ while (!added) {
+ showingMembers.indexOfFirst { it == null }.let { index ->
+ if (index != -1) {
+ showingMembers[index] = newMember
+ added = true
+ } else {
+ delay(1000L) // 1초 후 다시 시도
+ }
+ }
+ }
+ intent {
+ copy(
+ foundMembers = uiState.value.foundMembers + newMember,
+ showMembers = showingMembers
+ )
+ }
+ }
+ }
+ }
+
+ fun connectSse() = viewModelScope.launch {
+ eventSource = EventSources.createFactory(client)
+ .newEventSource(request, eventSourceListener)
+ }
+
+ fun disconnectSse() {
+ eventSource?.cancel()
+ eventSource = null
+ }
+
+ private fun handelInviteResult(isAccepted: Boolean, memberId: Long) {
+ if (isAccepted) {
+ intent {
+ val member = waitingMembers.first { it.id == memberId }
+ copy(
+ selectedMembers = selectedMembers.toMutableList().apply {
+ add(member)
+ },
+ waitingMembers = waitingMembers.toMutableList().apply {
+ remove(member)
+ }
+ )
+ }
+ } else {
+ bluetoothScanner.removeDevice(memberId)
+ }
+
+ }
+
+ fun selectMember(member: MemberSimple) {
+ viewModelScope.launch {
+ inviteFriendUseCase(uiState.value.roomKey, member.id).onSuccess {
+ val showingIdx = uiState.value.showMembers.indexOfFirst { member.id == it?.id }
+ uiState.value.showMembers[showingIdx] = null
+ intent {
+ copy(
+ foundMembers = foundMembers.toMutableList().apply {
+ remove(member)
+ },
+ )
+ }
+ }.onFailure {
+ postSideEffect(MatchedCreateGroupEffect.HandleException(it) {
+ selectMember(member)
+ })
+ }
+ }
+ }
+
+ fun removeMember(memberId: Long) {
+ viewModelScope.launch {
+ deportFriendUseCase(uiState.value.roomKey, memberId).onSuccess {
+ bluetoothScanner.removeDevice(memberId)
+ intent {
+ copy(
+ selectedMembers = selectedMembers.toMutableList().apply {
+ removeIf { it.id == memberId }
+ },
+ foundMembers = foundMembers.toMutableList().apply {
+ removeIf { it.id == memberId }
+ }
+ )
+ }
+ }.onFailure {
+ postSideEffect(MatchedCreateGroupEffect.HandleException(it) {
+ removeMember(memberId)
+ })
+ }
+
+ }
+ }
+
+ fun createGroup() {
+ viewModelScope.launch {
+ createGroupUseCase(uiState.value.roomKey).onSuccess {
+ postSideEffect(MatchedCreateGroupEffect.NavigateToChallengeHistory)
+ }.onFailure {
+ postSideEffect(MatchedCreateGroupEffect.HandleException(it) {
+ createGroup()
+ })
+ }
+ }
+ }
+}
diff --git a/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/join/JoinGroupContract.kt b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/join/JoinGroupContract.kt
new file mode 100644
index 00000000..62de75b0
--- /dev/null
+++ b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/join/JoinGroupContract.kt
@@ -0,0 +1,23 @@
+package com.sixkids.student.group.join
+
+import com.sixkids.model.MemberSimple
+import com.sixkids.student.group.component.MemberIconItem
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
+
+data class JoinGroupState(
+ val isLoading: Boolean = false,
+ val leader: MemberIconItem = MemberIconItem(),
+ val member: MemberSimple = MemberSimple(),
+ val roomKey: String = "",
+ val isJoinedGroup: Boolean = false,
+) : UiState
+
+sealed interface JoinGroupEffect : SideEffect {
+ data class HandleException(val it: Throwable, val retryAction: () -> Unit) : JoinGroupEffect
+ data object ReceiveInviteRequest : JoinGroupEffect
+ data object CloseInviteDialog : JoinGroupEffect
+ data object NavigateToChallengeHistory : JoinGroupEffect
+
+}
diff --git a/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/join/JoinGroupScreen.kt b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/join/JoinGroupScreen.kt
new file mode 100644
index 00000000..b4f9d27c
--- /dev/null
+++ b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/join/JoinGroupScreen.kt
@@ -0,0 +1,133 @@
+package com.sixkids.student.group.join
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+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.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.model.MemberSimple
+import com.sixkids.student.challenge.R
+import com.sixkids.student.group.component.InviteDialog
+import com.sixkids.student.group.component.MemberIcon
+import com.sixkids.student.group.component.MemberIconItem
+import com.sixkids.student.group.component.MultiLayeredCircles
+import com.sixkids.ui.extension.collectWithLifecycle
+
+
+@Composable
+fun JoinGroupRoute(
+ viewModel: JoinGroupViewModel = hiltViewModel(),
+ handleException: (Throwable, () -> Unit) -> Unit = { _, _ -> },
+ navigateToChallengeHistory: () -> Unit
+) {
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ var isShowInviteDialog by remember { mutableStateOf(false) }
+
+
+ LaunchedEffect(Unit) {
+ viewModel.connectSse()
+ viewModel.loadUserInfo()
+ viewModel.startAdvertise()
+ }
+
+ DisposableEffect(Unit) {
+ onDispose {
+ viewModel.disconnectSse()
+ viewModel.stopAdvertise()
+ }
+ }
+
+ viewModel.sideEffect.collectWithLifecycle { sideEffect ->
+ when (sideEffect) {
+ is JoinGroupEffect.HandleException -> handleException(
+ sideEffect.it,
+ sideEffect.retryAction
+ )
+
+ is JoinGroupEffect.ReceiveInviteRequest -> {
+ isShowInviteDialog = true
+ }
+
+ JoinGroupEffect.CloseInviteDialog -> {
+ isShowInviteDialog = false
+ }
+
+ JoinGroupEffect.NavigateToChallengeHistory -> navigateToChallengeHistory()
+
+ }
+
+ }
+ JoinGroupScreen(uiState)
+
+ if (isShowInviteDialog) {
+ InviteDialog(
+ leader = uiState.leader,
+ onConfirmClick = {
+ viewModel.answerInvite(true)
+ },
+ onCancelClick = {
+ viewModel.answerInvite(false)
+ }
+ )
+ }
+}
+
+@Composable
+fun JoinGroupScreen(
+ uiState: JoinGroupState = JoinGroupState()
+) {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Text(
+ text = if(uiState.isJoinedGroup) stringResource(R.string.waiting_group_start) else stringResource(R.string.wait_group_invite),
+ style = UlbanTypography.titleSmall,
+ modifier = Modifier.padding(top = 32.dp)
+ )
+ Box(modifier = Modifier.weight(1f)) {
+ MultiLayeredCircles(modifier = Modifier.align(Alignment.Center))
+ MemberIcon(
+ memberIconItem = MemberIconItem(
+ member = uiState.member,
+ isActive = true
+ ), modifier = Modifier.align(Alignment.Center)
+ )
+ }
+ }
+
+
+}
+
+@Preview(showBackground = true)
+@Composable
+fun JoinGroupScreenPreview() {
+ JoinGroupScreen(
+ uiState = JoinGroupState(
+ isLoading = false,
+ member = MemberSimple(
+ id = 1,
+ name = "김철수",
+ photo = "https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png"
+ )
+ )
+ )
+}
diff --git a/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/join/JoinGroupViewModel.kt b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/join/JoinGroupViewModel.kt
new file mode 100644
index 00000000..2bd2832d
--- /dev/null
+++ b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/group/join/JoinGroupViewModel.kt
@@ -0,0 +1,185 @@
+package com.sixkids.student.group.join
+
+import android.util.Log
+import androidx.lifecycle.viewModelScope
+import com.sixkids.core.bluetooth.BluetoothServer
+import com.sixkids.domain.usecase.group.JoinGroupUseCase
+import com.sixkids.domain.usecase.user.GetATKUseCase
+import com.sixkids.domain.usecase.user.GetMemberSimpleUseCase
+import com.sixkids.domain.usecase.user.LoadUserInfoUseCase
+import com.sixkids.model.MemberSimple
+import com.sixkids.model.SseData
+import com.sixkids.model.SseEventType
+import com.sixkids.student.challenge.BuildConfig
+import com.sixkids.student.group.component.MemberIconItem
+import com.sixkids.ui.base.BaseViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.serialization.json.Json
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import okhttp3.logging.HttpLoggingInterceptor
+import okhttp3.sse.EventSource
+import okhttp3.sse.EventSourceListener
+import okhttp3.sse.EventSources
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+
+private const val TAG = "D107"
+
+@HiltViewModel
+class JoinGroupViewModel @Inject constructor(
+ private val bluetoothServer: BluetoothServer,
+ private val loadUserInfoUseCase: LoadUserInfoUseCase,
+ private val getATKUseCase: GetATKUseCase,
+ private val getMemberSimpleUseCase: GetMemberSimpleUseCase,
+ private val joinGroupUseCase: JoinGroupUseCase
+) : BaseViewModel(JoinGroupState()) {
+
+ private var eventSource: EventSource? = null
+
+ private val client = OkHttpClient.Builder()
+ .addInterceptor {
+ HttpLoggingInterceptor().apply {
+ level = HttpLoggingInterceptor.Level.HEADERS
+ }.intercept(it)
+ }
+ .connectTimeout(10, TimeUnit.MINUTES)
+ .readTimeout(10, TimeUnit.MINUTES)
+ .writeTimeout(10, TimeUnit.MINUTES)
+ .build()
+
+ private val request = Request.Builder()
+ .url(BuildConfig.SSE_ENDPOINT)
+ .header("Authorization", "Bearer ${runBlocking { getATKUseCase().getOrNull() }}")
+ .build()
+
+ private val eventSourceListener = object : EventSourceListener() {
+ override fun onOpen(eventSource: EventSource, response: Response) {
+ super.onOpen(eventSource, response)
+ Log.d(TAG, "Connection Opened")
+ }
+
+ override fun onClosed(eventSource: EventSource) {
+ super.onClosed(eventSource)
+ Log.d(TAG, "Connection Closed")
+ }
+
+ override fun onEvent(
+ eventSource: EventSource,
+ id: String?,
+ type: String?,
+ data: String
+ ) {
+ super.onEvent(eventSource, id, type, data)
+ val sseEventType: SseEventType = SseEventType.valueOf(type ?: "")
+ val sseData = Json.decodeFromString(data)
+ val url = sseData.url
+ val roomKey = sseData.data
+ when (sseEventType) {
+ SseEventType.SSE_CONNECT -> {}
+ SseEventType.INVITE_REQUEST -> {
+ if (url == null) return
+ if (roomKey == null) return
+ viewModelScope.launch {
+ getMemberSimpleUseCase(url).onSuccess {
+ intent {
+ copy(
+ leader = MemberIconItem(
+ member = it,
+ showX = false,
+ isActive = true
+ ),
+ roomKey = roomKey,
+ isJoinedGroup = true
+ )
+ }
+ postSideEffect(JoinGroupEffect.ReceiveInviteRequest)
+ }.onFailure {
+ postSideEffect(JoinGroupEffect.HandleException(it, ::loadUserInfo))
+ }
+ }
+ }
+
+ SseEventType.INVITE_RESPONSE -> Log.d(TAG, "onEvent: 초대 응답")
+ SseEventType.KICK_MEMBER -> {
+ intent {
+ copy(
+ isJoinedGroup = false
+ )
+ }
+ startAdvertise()
+ }
+ SseEventType.CREATE_GROUP -> {
+ postSideEffect(JoinGroupEffect.NavigateToChallengeHistory)
+ }
+ }
+ }
+
+ override fun onFailure(eventSource: EventSource, t: Throwable?, response: Response?) {
+ super.onFailure(eventSource, t, response)
+ Log.d(TAG, "On Failure -: ${response?.body} ${t?.message}")
+ }
+ }
+
+ fun connectSse() = viewModelScope.launch {
+ eventSource = EventSources.createFactory(client)
+ .newEventSource(request, eventSourceListener)
+ }
+
+ fun disconnectSse() {
+ eventSource?.cancel()
+ eventSource = null
+ }
+
+ fun loadUserInfo() {
+ viewModelScope.launch {
+ loadUserInfoUseCase().onSuccess {
+ intent {
+ copy(
+ member = MemberSimple(
+ id = it.id.toLong(),
+ name = it.name,
+ photo = it.photo
+ )
+ )
+ }
+ }.onFailure {
+ postSideEffect(JoinGroupEffect.HandleException(it, ::loadUserInfo))
+ }
+ }
+ }
+
+ fun startAdvertise() {
+ viewModelScope.launch {
+ bluetoothServer.startAdvertising(uiState.value.member.id)
+ }
+ }
+
+ fun stopAdvertise() {
+ bluetoothServer.stopAdvertising()
+ }
+
+ fun answerInvite(joinStatus: Boolean) {
+ viewModelScope.launch {
+ joinGroupUseCase(uiState.value.roomKey, joinStatus).onSuccess {
+ intent {
+ copy(isJoinedGroup = joinStatus)
+ }
+ if(joinStatus){
+ stopAdvertise()
+ }
+ closeDialog()
+ }.onFailure {
+ postSideEffect(JoinGroupEffect.HandleException(it){
+ answerInvite(joinStatus)
+ })
+ }
+ }
+ }
+
+ private fun closeDialog() = postSideEffect(JoinGroupEffect.CloseInviteDialog)
+
+}
diff --git a/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/navigation/ChallengeNavigation.kt b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/navigation/ChallengeNavigation.kt
new file mode 100644
index 00000000..c4b5299c
--- /dev/null
+++ b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/navigation/ChallengeNavigation.kt
@@ -0,0 +1,47 @@
+package com.sixkids.student.navigation
+
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavOptions
+import androidx.navigation.compose.composable
+import com.sixkids.model.GroupType
+import com.sixkids.model.MemberSimple
+import com.sixkids.student.challeng.history.ChallengeRoute
+
+fun NavController.navigateStudentChallengeHistory(navOptions: NavOptions) {
+ navigate(ChallengeRoute.defaultRoute, navOptions)
+}
+
+fun NavController.navigatePopupToStudentChallengeHistory() {
+ navigate(ChallengeRoute.defaultRoute) {
+ popUpTo(ChallengeRoute.defaultRoute) {
+ inclusive = true
+ }
+ }
+}
+
+
+fun NavGraphBuilder.studentChallengeNavGraph(
+ navigateChallengeDetail: (Long, Long?) -> Unit,
+ navigateToCreateGroup: (Long, GroupType) -> Unit,
+ navigateToMatchedGroupCreate: (Long, List) -> Unit,
+ navigateToJoinGroup: (Long) -> Unit,
+ handleException: (Throwable, () -> Unit) -> Unit
+) {
+ composable(route = ChallengeRoute.defaultRoute) {
+ ChallengeRoute(
+ navigateToDetail = { challengeId, groupId ->
+ navigateChallengeDetail(challengeId, groupId)
+ },
+ navigateToCreateGroup = navigateToCreateGroup,
+ navigateToMatchedGroupCreate = navigateToMatchedGroupCreate,
+ navigateToJoinGroup = navigateToJoinGroup,
+ handleException = handleException
+ )
+ }
+
+}
+
+object ChallengeRoute {
+ const val defaultRoute = "student/challenge-history"
+}
diff --git a/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/navigation/GroupNavigation.kt b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/navigation/GroupNavigation.kt
new file mode 100644
index 00000000..d0d850ce
--- /dev/null
+++ b/android/feature/student/challenge/src/main/kotlin/com/sixkids/student/navigation/GroupNavigation.kt
@@ -0,0 +1,89 @@
+package com.sixkids.student.navigation
+
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavType
+import androidx.navigation.compose.composable
+import androidx.navigation.navArgument
+import com.sixkids.model.GroupType
+import com.sixkids.model.MemberSimple
+import com.sixkids.student.group.create.free.CreateGroupRoute
+import com.sixkids.student.group.create.matched.MatchedCreateGroupRoute
+import com.sixkids.student.group.join.JoinGroupRoute
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.encodeToJsonElement
+
+fun NavController.navigateStudentGroupCreate(
+ challengeId: Long,
+ groupType: GroupType
+) {
+ navigate(GroupRoute.createGroupRoute(challengeId, groupType))
+}
+
+fun NavController.navigateStudentMatchedGroupCreate(
+ challengeId: Long,
+ members: List
+) {
+ navigate(GroupRoute.matchedGroupCreateRoute(challengeId, members))
+}
+
+
+fun NavController.navigateStudentGroupJoin() {
+ navigate(GroupRoute.joinGroupRoute)
+}
+
+fun NavGraphBuilder.studentGroupNavGraph(
+ navigateToChallengeHistory: () -> Unit,
+ handleException: (Throwable, () -> Unit) -> Unit
+) {
+ composable(
+ route = GroupRoute.creatGroupRoute,
+ arguments = listOf(navArgument(GroupRoute.CHALLENGE_ID_NAME) { type = NavType.LongType })
+ ) {
+ CreateGroupRoute(
+ navigateToChallengeHistory = navigateToChallengeHistory,
+ handleException = handleException
+ )
+ }
+ composable(route = GroupRoute.joinGroupRoute) {
+ JoinGroupRoute(
+ navigateToChallengeHistory = navigateToChallengeHistory,
+ handleException = handleException
+ )
+ }
+ composable(
+ route = GroupRoute.matchedGroupCreateRoute,
+ arguments = listOf(
+ navArgument(GroupRoute.CHALLENGE_ID_NAME) { type = NavType.LongType },
+ navArgument(GroupRoute.MEMBERS_NAME) { type = NavType.StringType }
+ )
+ ) {
+ MatchedCreateGroupRoute(
+ navigateToChallengeHistory = navigateToChallengeHistory,
+ handleException = handleException
+ )
+
+ }
+}
+
+object GroupRoute {
+
+ const val CHALLENGE_ID_NAME = "challengeId"
+ const val GROUP_TYPE_NAME = "groupType"
+ const val MEMBERS_NAME = "members"
+
+ const val creatGroupRoute =
+ "student/group/create?challengeId={$CHALLENGE_ID_NAME}?groupType={$GROUP_TYPE_NAME}"
+ const val matchedGroupCreateRoute =
+ "student/group/matched-create?challengeId={$CHALLENGE_ID_NAME}?members={$MEMBERS_NAME}"
+ const val joinGroupRoute = "student/group/join"
+
+ fun createGroupRoute(challengeId: Long, groupType: GroupType): String {
+ return "student/group/create?challengeId=$challengeId?groupType=$groupType"
+ }
+
+ fun matchedGroupCreateRoute(challengeId: Long, members: List): String {
+ val jsonMembers = Json.encodeToJsonElement(members)
+ return "student/group/matched-create?challengeId=$challengeId?members=$jsonMembers"
+ }
+}
diff --git a/android/feature/student/challenge/src/main/res/values/strings.xml b/android/feature/student/challenge/src/main/res/values/strings.xml
new file mode 100644
index 00000000..168ed8c2
--- /dev/null
+++ b/android/feature/student/challenge/src/main/res/values/strings.xml
@@ -0,0 +1,22 @@
+
+
+ 함께달리기
+ 진행 중인\n함께 달리기가\n없습니다.
+ 지금까지 %d번 함께 달리기를 진행했어요
+ 기록이 없어요
+ 취소
+ 그룹\n만들기
+ 그룹\n참여하기
+ 완료
+ 근처에 있는 친구를 초대해 보세요
+ %d명의 친구들을 더 모아보세요
+ 아직 %d명의 친구가 모이지 않았어요
+
+ 그룹을 만들 수 있어요
+ 친구의 초대를 기다리고 있어요
+ 팀원
+ 종료
+ 자유롭게 팀을 구성해봐요
+ 친구가 나를 초대했어요!
+ 그룹이 시작되기를 기다리고 있어요.
+
diff --git a/android/feature/student/home/build.gradle.kts b/android/feature/student/home/build.gradle.kts
index 3fe4cf9f..38dc2203 100644
--- a/android/feature/student/home/build.gradle.kts
+++ b/android/feature/student/home/build.gradle.kts
@@ -1,10 +1,43 @@
+import java.util.Properties
+
plugins {
alias(libs.plugins.sixkids.android.feature.compose)
+ alias(libs.plugins.kotlin.serialization)
}
android {
namespace = "com.sixkids.student.home"
+
+ defaultConfig {
+ val localProperties = Properties()
+ val localPropertiesFile = rootProject.file("local.properties")
+ if (localPropertiesFile.exists()) {
+ localProperties.load(localPropertiesFile.inputStream())
+ }
+
+ val stompUrl = localProperties.getProperty("STOMP_ENDPOINT") ?: ""
+
+ buildConfigField("String", "STOMP_ENDPOINT", "\"${stompUrl}\"")
+ }
+
+ buildFeatures {
+ buildConfig = true
+ }
}
dependencies {
+ implementation(libs.okhttp)
+ implementation(libs.okhttp.logginginterceptor)
+
+ implementation(libs.moshi.kotlin)
+ implementation(libs.moshi.converter)
+ implementation(libs.krossbow.stomp. core)
+ implementation(libs.krossbow.websocket.okhttp)
+ implementation(libs.krossbow.stomp.moshi)
+
+ implementation(libs.bundles.paging)
+
+ implementation(libs.kotlinx.serialization.json)
+ implementation(projects.core.nfc)
+
}
diff --git a/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/announcedetail/StudentAnnounceDetailContract.kt b/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/announcedetail/StudentAnnounceDetailContract.kt
new file mode 100644
index 00000000..78dd38a6
--- /dev/null
+++ b/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/announcedetail/StudentAnnounceDetailContract.kt
@@ -0,0 +1,17 @@
+package com.sixkids.student.home.announce.announcedetail
+
+import com.sixkids.model.PostDetail
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
+sealed interface StudentAnnounceDetailEffect : SideEffect {
+ data object RefreshAnnounceDetail : StudentAnnounceDetailEffect
+ data class OnShowSnackbar(val message: String) : StudentAnnounceDetailEffect
+}
+
+data class StudentAnnounceDetailState(
+ val isLoading: Boolean = false,
+ val postDetail: PostDetail = PostDetail(),
+ val commentText: String = "",
+ val selectedCommentId: Long? = null,
+) : UiState
\ No newline at end of file
diff --git a/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/announcedetail/StudentAnnounceDetailScreen.kt b/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/announcedetail/StudentAnnounceDetailScreen.kt
new file mode 100644
index 00000000..fdcdf7a1
--- /dev/null
+++ b/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/announcedetail/StudentAnnounceDetailScreen.kt
@@ -0,0 +1,199 @@
+package com.sixkids.student.home.announce.announcedetail
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.clickable
+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.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+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.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import coil.compose.AsyncImage
+import com.sixkids.designsystem.R
+import com.sixkids.designsystem.component.screen.LoadingScreen
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.student.home.announce.component.CommentItem
+import com.sixkids.student.home.announce.component.CommentTextField
+import com.sixkids.student.home.announce.component.PostWriterInfo
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.util.formatToMonthDayTime
+
+@Composable
+fun StudentAnnounceDetailRoute(
+ viewModel: StudentAnnounceDetailViewModel = hiltViewModel(),
+ padding: PaddingValues,
+ onShowSnackBar: (SnackbarToken) -> Unit
+) {
+ val uiState by viewModel.uiState.collectAsState()
+ // 키보드 숨기기
+ var keyboardHideState by remember { mutableStateOf(false) }
+ if (keyboardHideState) {
+ LocalSoftwareKeyboardController.current?.hide()
+ keyboardHideState = false
+ }
+
+ LaunchedEffect(Unit) {
+ viewModel.getAnnounceDetail()
+ }
+
+ LaunchedEffect(viewModel.sideEffect) {
+ viewModel.sideEffect.collect { sideEffect ->
+ when (sideEffect) {
+ StudentAnnounceDetailEffect.RefreshAnnounceDetail -> {
+ keyboardHideState = true
+ viewModel.getAnnounceDetail()
+ }
+
+ is StudentAnnounceDetailEffect.OnShowSnackbar -> {
+ onShowSnackBar(SnackbarToken(message = sideEffect.message))
+ }
+ }
+ }
+ }
+
+ Box(modifier = Modifier.padding(padding)) {
+ StudentAnnounceDetailScreen(
+ studentAnnounceDetailState = uiState,
+ onCommentTextChanged = viewModel::onCommentTextChanged,
+ onClickComment = viewModel::onSelectedCommentId,
+ onClickSubmitComment = viewModel::onNewComment,
+ )
+ }
+}
+
+@Composable
+fun StudentAnnounceDetailScreen(
+ modifier: Modifier = Modifier,
+ studentAnnounceDetailState: StudentAnnounceDetailState = StudentAnnounceDetailState(),
+ onCommentTextChanged: (String) -> Unit = {},
+ onClickComment: (Long) -> Unit = {},
+ onClickSubmitComment: () -> Unit = {},
+) {
+
+ val scrollState = rememberScrollState()
+
+ BackHandler(
+ enabled = studentAnnounceDetailState.selectedCommentId != null,
+ onBack = { onClickComment(studentAnnounceDetailState.selectedCommentId ?: 0) }
+ )
+
+ Box {
+ Column {
+ Column(
+ modifier = modifier
+ .weight(1f)
+ .padding(20.dp)
+ .verticalScroll(scrollState),
+ ) {
+ // 작성자 정보
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ PostWriterInfo(
+ height = 60.dp,
+ writer = studentAnnounceDetailState.postDetail.writeMember.name,
+ dateString = studentAnnounceDetailState.postDetail.createTime.formatToMonthDayTime(),
+ writerImageUrl = studentAnnounceDetailState.postDetail.writeMember.photo
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ }
+
+ Spacer(modifier = Modifier.height(10.dp))
+ Text(
+ text = studentAnnounceDetailState.postDetail.title,
+ style = UlbanTypography.titleLarge
+ )
+ Spacer(modifier = Modifier.height(10.dp))
+ // 이미지
+ if (studentAnnounceDetailState.postDetail.imageUri.isNotEmpty()) {
+ AsyncImage(
+ model = studentAnnounceDetailState.postDetail.imageUri,
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ )
+ }
+ Spacer(modifier = Modifier.height(10.dp))
+ Text(
+ text = studentAnnounceDetailState.postDetail.content,
+ style = UlbanTypography.bodyLarge
+ )
+ Spacer(modifier = Modifier.height(10.dp))
+ HorizontalDivider(
+ thickness = 2.dp,
+ color = Color.Black
+ )
+ // 댓글 목록
+ for (comment in studentAnnounceDetailState.postDetail.comments) {
+ CommentItem(
+ selected = studentAnnounceDetailState.selectedCommentId == comment.id,
+ writer = comment.member.name,
+ dateString = comment.createTime.formatToMonthDayTime(),
+ writerImageUrl = comment.member.photo,
+ commentString = comment.content,
+ recommentOnclick = {
+ onClickComment(comment.id)
+ }
+ )
+ // 대댓글 목록
+ for (recomment in comment.recomments) {
+ Row {
+ Icon(
+ modifier = Modifier.padding(4.dp),
+ imageVector = ImageVector.vectorResource(id = R.drawable.ic_recomment),
+ contentDescription = null
+ )
+ CommentItem(
+ writer = recomment.member.name,
+ dateString = recomment.createTime.formatToMonthDayTime(),
+ writerImageUrl = recomment.member.photo,
+ commentString = recomment.content,
+ isRecomment = true
+ )
+ }
+
+ }
+ }
+ }
+ CommentTextField(
+ msg = studentAnnounceDetailState.commentText,
+ onTextIuputChange = onCommentTextChanged,
+ onSendClick = { onClickSubmitComment() }
+ )
+ }
+
+
+ if (studentAnnounceDetailState.isLoading) {
+ LoadingScreen()
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun StudentAnnounceDetailScreenPreview() {
+ StudentAnnounceDetailScreen()
+}
\ No newline at end of file
diff --git a/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/announcedetail/StudentAnnounceDetailViewModel.kt b/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/announcedetail/StudentAnnounceDetailViewModel.kt
new file mode 100644
index 00000000..8985498c
--- /dev/null
+++ b/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/announcedetail/StudentAnnounceDetailViewModel.kt
@@ -0,0 +1,99 @@
+package com.sixkids.student.home.announce.announcedetail
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.viewModelScope
+import com.sixkids.domain.usecase.comment.DeleteCommentUseCase
+import com.sixkids.domain.usecase.comment.NewCommentUseCase
+import com.sixkids.domain.usecase.comment.NewRecommentUseCase
+import com.sixkids.domain.usecase.comment.ReportCommentUseCase
+import com.sixkids.domain.usecase.comment.UpdateCommentUsecase
+import com.sixkids.domain.usecase.post.DeletePostUseCase
+import com.sixkids.domain.usecase.post.GetPostDetailUseCase
+import com.sixkids.domain.usecase.post.UpdatePostUseCase
+import com.sixkids.student.home.navigation.StudentHomeRoute
+import com.sixkids.ui.base.BaseViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class StudentAnnounceDetailViewModel @Inject constructor(
+ savedStateHandle: SavedStateHandle,
+ private val getPostDetailUseCase: GetPostDetailUseCase,
+ private val deleteCommentUseCase: DeleteCommentUseCase,
+ private val updateCommentUsecase: UpdateCommentUsecase,
+ private val newCommentUseCase: NewCommentUseCase,
+ private val newRecommentUseCase: NewRecommentUseCase,
+ private val reportCommentUseCase: ReportCommentUseCase
+): BaseViewModel(StudentAnnounceDetailState()){
+
+ private val postId: Long = savedStateHandle.get(StudentHomeRoute.announceDetailARG)!!
+
+ fun onCommentTextChanged(commentText: String) = intent { copy(commentText = commentText) }
+ fun onSelectedCommentId(commentId: Long?) = intent {
+ if (currentState.selectedCommentId == commentId) {
+ copy(selectedCommentId = null)
+ } else {
+ copy(selectedCommentId = commentId)
+ }
+ }
+
+ fun getAnnounceDetail() {
+ viewModelScope.launch {
+ intent { copy(isLoading = true) }
+ getPostDetailUseCase(postId).onSuccess {
+ intent { copy(postDetail = it) }
+ }.onFailure {
+ postSideEffect(StudentAnnounceDetailEffect.OnShowSnackbar(it.message ?: "게시글을 불러오지 못했어요"))
+ }
+ intent { copy(isLoading = false) }
+ }
+ }
+
+ fun onNewComment() {
+ if (currentState.commentText.isBlank()) {
+ postSideEffect(StudentAnnounceDetailEffect.OnShowSnackbar("댓글을 입력해주세요"))
+ } else {
+ if (currentState.selectedCommentId == null) {
+ viewModelScope.launch {
+ intent { copy(isLoading = true) }
+ newCommentUseCase(
+ postId = postId,
+ content = currentState.commentText,
+ ).onSuccess {
+ postSideEffect(StudentAnnounceDetailEffect.OnShowSnackbar("댓글이 작성되었습니다"))
+ intent { copy(commentText = "", selectedCommentId = null) }
+ getAnnounceDetail()
+ }.onFailure {
+ postSideEffect(
+ StudentAnnounceDetailEffect.OnShowSnackbar(
+ it.message ?: "댓글 작성에 실패했어요"
+ )
+ )
+ }
+ intent { copy(isLoading = false) }
+ }
+ } else {
+ viewModelScope.launch {
+ intent { copy(isLoading = true) }
+ newRecommentUseCase(
+ postId = postId,
+ content = currentState.commentText,
+ currentState.selectedCommentId!!
+ ).onSuccess {
+ postSideEffect(StudentAnnounceDetailEffect.OnShowSnackbar("댓글이 작성되었습니다"))
+ intent { copy(commentText = "", selectedCommentId = null)}
+ getAnnounceDetail()
+ }.onFailure {
+ postSideEffect(
+ StudentAnnounceDetailEffect.OnShowSnackbar(
+ it.message ?: "댓글 작성에 실패했어요"
+ )
+ )
+ }
+ intent { copy(isLoading = false) }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/announcelist/StudentAnnounceListContract.kt b/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/announcelist/StudentAnnounceListContract.kt
new file mode 100644
index 00000000..0dd87818
--- /dev/null
+++ b/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/announcelist/StudentAnnounceListContract.kt
@@ -0,0 +1,16 @@
+package com.sixkids.student.home.announce.announcelist
+
+import com.sixkids.model.Post
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
+sealed interface StudentAnnounceListEffect : SideEffect {
+ data object NavigateToAnnounceDetail: StudentAnnounceListEffect
+ data class OnShowSnackBar(val message : String) : StudentAnnounceListEffect
+}
+
+data class StudentAnnounceListState(
+ val isLoding: Boolean = false,
+ val classString: String = "",
+ val postList: List = emptyList(),
+): UiState
\ No newline at end of file
diff --git a/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/announcelist/StudentAnnounceListScreen.kt b/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/announcelist/StudentAnnounceListScreen.kt
new file mode 100644
index 00000000..ac8dc2a5
--- /dev/null
+++ b/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/announcelist/StudentAnnounceListScreen.kt
@@ -0,0 +1,140 @@
+package com.sixkids.student.home.announce.announcelist
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.paging.compose.LazyPagingItems
+import androidx.paging.compose.collectAsLazyPagingItems
+import com.sixkids.designsystem.component.appbar.UlbanDefaultAppBar
+import com.sixkids.designsystem.component.appbar.UlbanDetailAppBar
+import com.sixkids.designsystem.component.screen.LoadingScreen
+import com.sixkids.designsystem.theme.Orange
+import com.sixkids.designsystem.theme.OrangeDark
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.model.Post
+import com.sixkids.student.home.R
+import com.sixkids.student.home.announce.component.PostItem
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.util.formatToMonthDayTimeKorean
+import com.sixkids.designsystem.R as UlbanRes
+
+@Composable
+fun StudentAnnounceListRoute(
+ viewModel: StudentAnnounceListViewModel = hiltViewModel(),
+ navigateToStudentAnnounceDetail: (postId:Long) -> Unit,
+ onShowSnackBar: (SnackbarToken) -> Unit,
+ padding: PaddingValues
+) {
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ LaunchedEffect(Unit) {
+ viewModel.getAnnounceList()
+ }
+
+ LaunchedEffect(key1 = viewModel.sideEffect) {
+ viewModel.sideEffect.collect { sideEffect ->
+ when (sideEffect) {
+ StudentAnnounceListEffect.NavigateToAnnounceDetail -> {}
+ is StudentAnnounceListEffect.OnShowSnackBar -> {
+ onShowSnackBar(SnackbarToken(message = sideEffect.message))
+ }
+ }
+ }
+ }
+
+ Box(
+ modifier = Modifier
+ .padding(padding)
+ .fillMaxSize()
+ ) {
+ StudentAnnounceListScreen(
+ studentAnnounceListState = uiState,
+ postItems = viewModel.postList?.collectAsLazyPagingItems(),
+ postItemOnclick = navigateToStudentAnnounceDetail,
+ )
+ }
+}
+
+@Composable
+fun StudentAnnounceListScreen(
+ modifier: Modifier = Modifier,
+ studentAnnounceListState: StudentAnnounceListState = StudentAnnounceListState(),
+ postItems: LazyPagingItems? = null,
+ postItemOnclick: (postId: Long) -> Unit = {},
+) {
+ val listState = rememberLazyListState()
+
+ Box(
+ modifier = modifier
+ .fillMaxSize()
+ ) {
+ Column {
+ UlbanDefaultAppBar(
+ leftIcon = UlbanRes.drawable.announce,
+ title = stringResource(id = R.string.student_home_main_announce),
+ content = stringResource(id = R.string.student_home_main_announce),
+ body = studentAnnounceListState.classString.replace("\n", " "),
+ color = Orange
+ )
+
+ if (postItems != null){
+ if (postItems.itemCount == 0){
+ Spacer(modifier = Modifier.weight(1f))
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ text = stringResource(id = R.string.student_announce_no_item),
+ textAlign = TextAlign.Center,
+ style = UlbanTypography.bodyLarge,
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ } else {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ state = listState,
+ ) {
+ items(postItems.itemCount) { index ->
+ postItems[index]?.let { post ->
+ PostItem(
+ title = post.title,
+ writer = post.writer,
+ dateString = post.time.formatToMonthDayTimeKorean(),
+ commentCount = post.commentCount,
+ dividerColor = OrangeDark,
+ onClick = { postItemOnclick(post.id) }
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ if (studentAnnounceListState.isLoding){
+ LoadingScreen()
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun StudentAnnounceListScreenPreview() {
+ StudentAnnounceListScreen()
+}
\ No newline at end of file
diff --git a/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/announcelist/StudentAnnounceListViewModel.kt b/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/announcelist/StudentAnnounceListViewModel.kt
new file mode 100644
index 00000000..d1cfe46a
--- /dev/null
+++ b/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/announcelist/StudentAnnounceListViewModel.kt
@@ -0,0 +1,53 @@
+package com.sixkids.student.home.announce.announcelist
+
+import androidx.lifecycle.viewModelScope
+import androidx.paging.PagingData
+import androidx.paging.cachedIn
+import com.sixkids.domain.usecase.organization.GetSelectedOrganizationIdUseCase
+import com.sixkids.domain.usecase.organization.LoadSelectedOrganizationNameUseCase
+import com.sixkids.domain.usecase.post.GetPostListUseCase
+import com.sixkids.model.Post
+import com.sixkids.model.PostCategory
+import com.sixkids.ui.base.BaseViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class StudentAnnounceListViewModel @Inject constructor(
+ private val getPostListUseCase: GetPostListUseCase,
+ private val getSelectedOrganizationIdUseCase: GetSelectedOrganizationIdUseCase,
+ private val loadSelectedOrganizationNameUseCase: LoadSelectedOrganizationNameUseCase
+): BaseViewModel(StudentAnnounceListState()){
+ private var organizationId: Int? = null
+
+ var postList: Flow>? = null
+
+ fun getAnnounceList() {
+ viewModelScope.launch {
+ intent { copy(isLoding = true) }
+
+ loadSelectedOrganizationNameUseCase().onSuccess {
+ intent { copy(classString = it) }
+ }.onFailure {
+ intent { copy(classString = "") }
+ }
+
+ if (organizationId == null){
+ organizationId = getSelectedOrganizationIdUseCase().getOrNull()
+ }
+
+ if (organizationId != null){
+ postList = getPostListUseCase(
+ organizationId = organizationId!!,
+ postCategory = PostCategory.NOTICE
+ ).cachedIn(viewModelScope)
+ } else {
+ postSideEffect(StudentAnnounceListEffect.OnShowSnackBar("학급 정보를 불러오지 못했어요 ;("))
+ }
+
+ intent { copy(isLoding = false) }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/component/CommentCount.kt b/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/component/CommentCount.kt
new file mode 100644
index 00000000..b9d2f0e8
--- /dev/null
+++ b/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/component/CommentCount.kt
@@ -0,0 +1,37 @@
+package com.sixkids.student.home.announce.component
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.tooling.preview.Preview
+import com.sixkids.designsystem.theme.OrangeText
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.designsystem.R as UlbanRes
+
+@Composable
+fun CommentCount(
+ count: Int
+) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(
+ imageVector = ImageVector.vectorResource(id = UlbanRes.drawable.ic_chat_bubble),
+ contentDescription = null,
+ tint = OrangeText
+ )
+ Text(
+ text = count.toString(),
+ style = UlbanTypography.bodyMedium
+ )
+ }
+}
+
+
+@Preview(showBackground = true)
+@Composable
+fun CommentCountPreview() {
+ CommentCount(10)
+}
\ No newline at end of file
diff --git a/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/component/CommentItem.kt b/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/component/CommentItem.kt
new file mode 100644
index 00000000..72fd12dc
--- /dev/null
+++ b/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/component/CommentItem.kt
@@ -0,0 +1,133 @@
+package com.sixkids.student.home.announce.component
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardColors
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+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.graphics.vector.ImageVector
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import com.sixkids.designsystem.theme.Blue
+import com.sixkids.designsystem.theme.Gray
+import com.sixkids.designsystem.theme.GrayLight
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.designsystem.R as UlbanRes
+
+@Composable
+fun CommentItem(
+ modifier: Modifier = Modifier,
+ selected: Boolean = false,
+ writer: String = "",
+ dateString: String = "00/00 00:00",
+ writerImageUrl: String = "",
+ commentString: String = "",
+ isRecomment: Boolean = false,
+ recommentOnclick: () -> Unit = {},
+ deleteOnclick: (() -> Unit)? = null
+){
+ Card(
+ colors = CardDefaults.cardColors(
+ containerColor =
+ if (selected) { Blue}
+ else if (isRecomment) {GrayLight}
+ else {Color.Transparent}
+ ),
+ ) {
+ Column(
+ modifier = modifier
+ .padding(start = 10.dp)
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ AsyncImage(
+ modifier = Modifier
+ .height(36.dp)
+ .aspectRatio(1f),
+ model = writerImageUrl,
+ contentScale = ContentScale.Crop,
+ contentDescription = "작성자 프로필 사진"
+ )
+ Spacer(modifier = Modifier.width(4.dp))
+ Text(
+ text = writer,
+ style = UlbanTypography.bodyMedium
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ if (!isRecomment) {
+ Icon(
+ modifier = Modifier.clickable{recommentOnclick()},
+ imageVector = ImageVector.vectorResource(id = UlbanRes.drawable.ic_chat_bubble_outline),
+ contentDescription = null
+ )
+ }
+ if (deleteOnclick != null) {
+ Icon(
+ modifier = Modifier.clickable{deleteOnclick()},
+ imageVector = ImageVector.vectorResource(id = UlbanRes.drawable.ic_delete),
+ contentDescription = null
+ )
+ }
+ }
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ text = commentString,
+ style = UlbanTypography.bodyMedium
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ text = dateString,
+ style = UlbanTypography.bodySmall.copy(
+ color = Gray
+ )
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun CommentItemPreview() {
+ Column {
+ CommentItem(
+ writer = "오하빈",
+ dateString = "09/01 12:00",
+ writerImageUrl = "https://www.google.com/images/branding/googlelogo/2x/googlelogo_light_color_272x92dp.png",
+ commentString = "댓글 내용",
+ deleteOnclick = {},
+ selected = true
+ )
+ CommentItem(
+ writer = "오하빈",
+ dateString = "09/01 12:00",
+ commentString = "댓글 내용",
+ writerImageUrl = "https://www.google.com/images/branding/googlelogo/2x/googlelogo_light_color_272x92dp.png",
+ )
+ CommentItem(
+ writer = "오하빈",
+ dateString = "09/01 12:00",
+ commentString = "댓글 내용",
+ writerImageUrl = "https://www.google.com/images/branding/googlelogo/2x/googlelogo_light_color_272x92dp.png",
+ isRecomment = true,
+ deleteOnclick = {}
+ )
+ }
+
+}
\ No newline at end of file
diff --git a/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/component/CommentTextField.kt b/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/component/CommentTextField.kt
new file mode 100644
index 00000000..dbdb94b8
--- /dev/null
+++ b/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/component/CommentTextField.kt
@@ -0,0 +1,77 @@
+package com.sixkids.student.home.announce.component
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.outlined.Send
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+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.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.sixkids.designsystem.component.textfield.UlbanBasicTextField
+import com.sixkids.designsystem.theme.GrayLight
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.student.home.R
+
+
+@Composable
+fun CommentTextField(
+ msg: String = "",
+ onTextIuputChange: (String) -> Unit = {},
+ onSendClick: (String) -> Unit = {},
+) {
+ Card(
+ modifier = Modifier
+ .padding(6.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = GrayLight
+ )
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ UlbanBasicTextField(
+ text = msg,
+ onTextChange = onTextIuputChange,
+ modifier = Modifier
+ .padding(10.dp, 0.dp)
+ .weight(1f)
+ .wrapContentHeight(),
+ maxLines = 3,
+ textStyle = UlbanTypography.bodyMedium.copy(fontWeight = FontWeight.Normal),
+ hint = stringResource(id = R.string.student_announce_detail_comment_hint)
+ )
+
+ Icon(
+ Icons.AutoMirrored.Outlined.Send,
+ contentDescription = "",
+ modifier = Modifier
+ .size(30.dp)
+ .padding(end = 4.dp)
+ .clickable {
+ onSendClick(msg)
+ }
+ )
+
+ }
+ }
+
+}
+
+@Preview(showBackground = true)
+@Composable
+fun CommentTextFieldPreview() {
+ CommentTextField()
+}
\ No newline at end of file
diff --git a/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/component/PageTitle.kt b/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/component/PageTitle.kt
new file mode 100644
index 00000000..7e8ca2fb
--- /dev/null
+++ b/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/component/PageTitle.kt
@@ -0,0 +1,55 @@
+package com.sixkids.student.home.announce.component
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Icon
+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.vector.ImageVector
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.designsystem.R as UlbanRes
+
+@Composable
+fun PageTitle(
+ modifier: Modifier = Modifier,
+ title: String,
+ cancelOnclick: () -> Unit = {},
+) {
+ Row(
+ modifier = modifier,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ modifier = Modifier
+ .clickable { cancelOnclick() },
+ imageVector = ImageVector.vectorResource(id = UlbanRes.drawable.ic_cancel_post),
+ contentDescription = null
+ )
+ Spacer(modifier = Modifier.width(14.dp))
+ Text(
+ text = title,
+ style = UlbanTypography.titleLarge.copy(
+ fontWeight = FontWeight.Medium,
+ ),
+ )
+ }
+}
+
+
+@Preview(showBackground = true)
+@Composable
+fun PageTitlePreview() {
+ PageTitle(
+ title = "글 쓰기",
+ cancelOnclick = {}
+ )
+}
\ No newline at end of file
diff --git a/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/component/PostItem.kt b/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/component/PostItem.kt
new file mode 100644
index 00000000..8639fde6
--- /dev/null
+++ b/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/component/PostItem.kt
@@ -0,0 +1,80 @@
+package com.sixkids.student.home.announce.component
+
+import androidx.compose.foundation.clickable
+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.width
+import androidx.compose.material3.Divider
+import androidx.compose.material3.HorizontalDivider
+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.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.sixkids.designsystem.theme.UlbanTypography
+
+@Composable
+fun PostItem(
+ modifier: Modifier = Modifier ,
+ title: String,
+ writer: String,
+ commentCount: Int,
+ dateString: String,
+ dividerColor: Color = Color.Black,
+ onClick: () -> Unit = {}
+) {
+ Column(
+ modifier = modifier.padding(bottom = 8.dp).clickable { onClick() }
+ ) {
+ Text(
+ text = title,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = UlbanTypography.titleMedium
+ )
+ Spacer(modifier = Modifier.height(10.dp))
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ if (commentCount > 0){
+ CommentCount(count = commentCount)
+ }
+ Spacer(modifier = Modifier.width(6.dp))
+ Text(
+ text = writer,
+ style = UlbanTypography.bodyMedium
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ Text(
+ text = dateString,
+ style = UlbanTypography.bodyMedium
+ )
+ }
+ HorizontalDivider(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 4.dp),
+ thickness = 2.dp,
+ color = dividerColor
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun PostItemPreview() {
+ PostItem(
+ title = "이따 마크 할 사람~~!",
+ writer = "오하빈",
+ commentCount = 3,
+ dateString = "2024.04.16 14:30"
+ )
+}
\ No newline at end of file
diff --git a/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/component/PostWriterInfo.kt b/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/component/PostWriterInfo.kt
new file mode 100644
index 00000000..29ac07db
--- /dev/null
+++ b/android/feature/student/home/src/main/java/com/sixkids/student/home/announce/component/PostWriterInfo.kt
@@ -0,0 +1,64 @@
+package com.sixkids.student.home.announce.component
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxHeight
+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.layout.ContentScale
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import com.sixkids.designsystem.theme.UlbanTypography
+
+@Composable
+fun PostWriterInfo(
+ height: Dp = 60.dp,
+ writer: String = "",
+ dateString: String = "00/00 00:00",
+ writerImageUrl: String = ""
+) {
+ Row(
+ modifier = Modifier.height(height),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ AsyncImage(
+ modifier = Modifier
+ .fillMaxHeight()
+ .aspectRatio(1f),
+ model = writerImageUrl,
+ contentScale = ContentScale.Crop,
+ contentDescription = "프로필 사진"
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Column {
+ Text(
+ text = writer,
+ style = UlbanTypography.bodyLarge
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ text = dateString,
+ style = UlbanTypography.bodyMedium
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun PostWriterInfoPreview() {
+ PostWriterInfo(
+ height = 60.dp,
+ writer = "홍유준 선생님",
+ dateString = "10/10 10:10",
+ writerImageUrl = ""
+ )
+}
\ No newline at end of file
diff --git a/android/feature/student/home/src/main/java/com/sixkids/student/home/chatting/StudentChattingContract.kt b/android/feature/student/home/src/main/java/com/sixkids/student/home/chatting/StudentChattingContract.kt
new file mode 100644
index 00000000..a25a2b24
--- /dev/null
+++ b/android/feature/student/home/src/main/java/com/sixkids/student/home/chatting/StudentChattingContract.kt
@@ -0,0 +1,18 @@
+package com.sixkids.student.home.chatting
+
+import com.sixkids.model.Chat
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
+sealed interface StudentChattingSideEffect : SideEffect{
+
+}
+
+data class StudentChattingState(
+ val isLoading : Boolean = false,
+ val organizationName: String = "",
+ val memberCount : Int = 0,
+ val memberId: Int = 0,
+ val message: String = "",
+ val chatList: List = emptyList()
+) : UiState
\ No newline at end of file
diff --git a/android/feature/student/home/src/main/java/com/sixkids/student/home/chatting/StudentChattingScreen.kt b/android/feature/student/home/src/main/java/com/sixkids/student/home/chatting/StudentChattingScreen.kt
new file mode 100644
index 00000000..e5fab5ef
--- /dev/null
+++ b/android/feature/student/home/src/main/java/com/sixkids/student/home/chatting/StudentChattingScreen.kt
@@ -0,0 +1,354 @@
+package com.sixkids.student.home.chatting
+
+import android.annotation.SuppressLint
+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.Box
+import androidx.compose.foundation.layout.Column
+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.layout.size
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.outlined.Send
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
+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 androidx.compose.ui.unit.sp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.paging.compose.LazyPagingItems
+import androidx.paging.compose.collectAsLazyPagingItems
+import coil.compose.AsyncImage
+import com.sixkids.designsystem.component.textfield.UlbanBasicTextField
+import com.sixkids.designsystem.theme.Cream
+import com.sixkids.designsystem.theme.Gray
+import com.sixkids.designsystem.theme.GrayLight
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.designsystem.theme.Yellow
+import com.sixkids.model.Chat
+import com.sixkids.student.home.R
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.extension.collectWithLifecycle
+import java.text.SimpleDateFormat
+import java.util.TimeZone
+import com.sixkids.designsystem.R as UlbanRes
+
+@Composable
+fun StudentChattingRoute(
+ viewModel: StudentChattingViewModel = hiltViewModel(),
+ onBackClick: () -> Unit,
+ onShowSnackBar: (SnackbarToken) -> Unit
+) {
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ viewModel.sideEffect.collectWithLifecycle { sideEffect ->
+ when (sideEffect) {
+ else -> {}
+ }
+ }
+
+ DisposableEffect(key1 = Unit) {
+ viewModel.initStomp()
+
+ onDispose {
+ viewModel.cancelStomp()
+ }
+ }
+
+ StudentChattingScreen(
+ uiState = uiState,
+ onUpdateMessage = viewModel::updateMessage,
+ onBackClick = onBackClick,
+ onSendClick = viewModel::sendMessage,
+ onPhotoClick = {
+ //사진
+ },
+ chatItems = viewModel.originalChatList?.collectAsLazyPagingItems()
+ )
+}
+
+@Composable
+fun StudentChattingScreen(
+ uiState: StudentChattingState = StudentChattingState(),
+ onUpdateMessage: (String) -> Unit = {},
+ onBackClick: () -> Unit = {},
+ onSendClick: (String) -> Unit = {},
+ onPhotoClick: () -> Unit = {},
+ chatItems: LazyPagingItems? = null
+) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ ) {
+ Column {
+ TopSection(
+ uiState.organizationName,
+ onBackClick
+ )
+
+ ChatSection(
+ uiState.memberId,
+ chatItems = chatItems,
+ uiState.chatList,
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxSize()
+ )
+
+ InputSection(
+ msg = uiState.message,
+ onUpdateMessage = onUpdateMessage,
+ onSendClick = onSendClick,
+ onPhotoClick = onPhotoClick
+ )
+ }
+ }
+}
+
+
+@Composable
+fun TopSection(
+ organizationName: String = "",
+ onBackClick: () -> Unit = {}
+) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(20.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Image(
+ painter = painterResource(id = UlbanRes.drawable.ic_arrow_back),
+ contentDescription = "back button",
+ modifier = Modifier.clickable { onBackClick() }
+ )
+
+ Text(
+ text = organizationName.replace("\n", " "),
+ style = UlbanTypography.titleSmall,
+ modifier = Modifier.padding(10.dp, 0.dp)
+ )
+
+ }
+}
+
+@Composable
+fun ChatSection(
+ memberId: Int,
+ chatItems: LazyPagingItems? = null,
+ chatList: List,
+ modifier: Modifier = Modifier
+) {
+// Log.d(TAG, "ChatSection: ")
+ val scrollState = rememberLazyListState()
+
+ if (chatItems == null) {
+ Text(text = "데이터 없음")
+ } else {
+ Column(modifier = modifier) {
+ LazyColumn(
+ state = scrollState, modifier = Modifier.weight(1f),
+
+ ) {
+ items(chatItems.itemCount) { idx ->
+ if (chatItems[idx]?.memberId == memberId.toLong()) {
+ MyChat(chatItems[idx]!!)
+ } else {
+ OtherChat(chatItems[idx]!!)
+ }
+ }
+
+ items(chatList) { chat ->
+ if (chat.memberId == memberId.toLong()) {
+ MyChat(chat)
+ } else {
+ OtherChat(chat)
+ }
+ }
+
+
+ }
+ }
+ }
+ val serverChatSize = chatItems?.itemCount ?: 0
+ val socketChatSize = chatList.size
+ val totalChatSize = serverChatSize + socketChatSize
+
+ if (totalChatSize > 0) {
+ LaunchedEffect(totalChatSize) {
+ scrollState.scrollToItem(totalChatSize - 1)
+ }
+ }
+}
+
+@Composable
+fun OtherChat(chat: Chat) {
+ val screenWidthDp = with(LocalDensity.current) {
+ LocalContext.current.resources.displayMetrics.widthPixels.toDp()
+ }
+ val maxWidthDp = screenWidthDp * 0.6f
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(6.dp)
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ AsyncImage(
+ model = chat.memberImageUrl,
+ contentDescription = "profile photo",
+ modifier = Modifier
+ .size(32.dp)
+ .clip(RoundedCornerShape(4.dp)),
+ contentScale = ContentScale.Crop
+ )
+ Text(
+ text = chat.memberName,
+ style = UlbanTypography.bodySmall,
+ modifier = Modifier.padding(10.dp)
+ )
+ }
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(32.dp, 0.dp, 0.dp, 10.dp),
+ horizontalArrangement = Arrangement.Start,
+ verticalAlignment = Alignment.Bottom
+ ) {
+
+ Text(
+ text = chat.content,
+ style = UlbanTypography.bodyMedium.copy(fontWeight = FontWeight.Normal),
+ modifier = Modifier
+ .background(GrayLight, shape = RoundedCornerShape(10.dp))
+ .padding(12.dp)
+ .widthIn(max = maxWidthDp)
+ .wrapContentWidth()
+ )
+ Text(
+ text = chatTimeFormat(chat.sendDateTime),
+ style = UlbanTypography.bodySmall.copy(
+ fontSize = 8.sp,
+ lineHeight = 10.sp,
+ fontWeight = FontWeight.Normal
+ ),
+ modifier = Modifier.padding(4.dp, 0.dp, 0.dp, 6.dp)
+ )
+ }
+ }
+}
+
+@Composable
+fun MyChat(chat: Chat) {
+ val screenWidthDp = with(LocalDensity.current) {
+ LocalContext.current.resources.displayMetrics.widthPixels.toDp()
+ }
+ val maxWidthDp = screenWidthDp * 0.6f
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(6.dp),
+ horizontalArrangement = Arrangement.End,
+ verticalAlignment = Alignment.Bottom
+ ) {
+ Text(
+ text = chatTimeFormat(chat.sendDateTime),
+ style = UlbanTypography.bodySmall.copy(
+ fontSize = 8.sp,
+ lineHeight = 10.sp,
+ fontWeight = FontWeight.Normal
+ ),
+ modifier = Modifier.padding(0.dp, 0.dp, 4.dp, 6.dp)
+ )
+ Text(
+ text = chat.content,
+ style = UlbanTypography.bodyMedium.copy(fontWeight = FontWeight.Normal),
+ modifier = Modifier
+ .background(Yellow, shape = RoundedCornerShape(10.dp))
+ .padding(12.dp)
+ .widthIn(max = maxWidthDp)
+ )
+ }
+}
+
+@SuppressLint("SimpleDateFormat")
+fun chatTimeFormat(time: Long): String {
+ val formatter = SimpleDateFormat("MM/dd\na h:mm").apply {
+ timeZone = TimeZone.getTimeZone("Asia/Seoul")
+ }
+
+ return formatter.format(time)
+}
+
+@Composable
+fun InputSection(
+ msg: String = "",
+ onUpdateMessage: (String) -> Unit = {},
+ onSendClick: (String) -> Unit = {},
+ onPhotoClick: () -> Unit = {}
+) {
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(Cream)
+ .padding(6.dp),
+ verticalAlignment = Alignment.Bottom
+ ) {
+
+ UlbanBasicTextField(
+ text = msg,
+ onTextChange = onUpdateMessage,
+ modifier = Modifier
+ .padding(10.dp, 0.dp)
+ .weight(1f)
+ .wrapContentHeight(),
+ maxLines = 3,
+ textStyle = UlbanTypography.bodyMedium.copy(fontWeight = FontWeight.Normal)
+
+ )
+
+ Icon(
+ Icons.AutoMirrored.Outlined.Send,
+ contentDescription = "",
+ modifier = Modifier
+ .size(30.dp)
+ .clickable {
+ onSendClick(msg)
+ }
+ )
+
+ }
+
+
+}
+
+@Preview(showBackground = true)
+@Composable
+fun StudentChattingScreenPreview() {
+ StudentChattingScreen()
+}
\ No newline at end of file
diff --git a/android/feature/student/home/src/main/java/com/sixkids/student/home/chatting/StudentChattingViewModel.kt b/android/feature/student/home/src/main/java/com/sixkids/student/home/chatting/StudentChattingViewModel.kt
new file mode 100644
index 00000000..9af10086
--- /dev/null
+++ b/android/feature/student/home/src/main/java/com/sixkids/student/home/chatting/StudentChattingViewModel.kt
@@ -0,0 +1,183 @@
+package com.sixkids.student.home.chatting
+
+import android.annotation.SuppressLint
+import android.util.Log
+import androidx.lifecycle.viewModelScope
+import androidx.paging.PagingData
+import androidx.paging.cachedIn
+import com.sixkids.domain.usecase.chatting.GetChattingHistoryUseCase
+import com.sixkids.domain.usecase.organization.GetSelectedOrganizationIdUseCase
+import com.sixkids.domain.usecase.organization.LoadSelectedOrganizationNameUseCase
+import com.sixkids.domain.usecase.user.GetATKUseCase
+import com.sixkids.domain.usecase.user.LoadUserInfoUseCase
+import com.sixkids.model.Chat
+import com.sixkids.model.ChatMessage
+import com.sixkids.model.UserInfo
+import com.sixkids.student.home.BuildConfig
+import com.sixkids.ui.base.BaseViewModel
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.async
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.launch
+import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
+import org.hildan.krossbow.stomp.StompClient
+import org.hildan.krossbow.stomp.StompSession
+import org.hildan.krossbow.stomp.conversions.convertAndSend
+import org.hildan.krossbow.stomp.conversions.moshi.withMoshi
+import org.hildan.krossbow.stomp.frame.StompFrame
+import org.hildan.krossbow.stomp.headers.StompSendHeaders
+import org.hildan.krossbow.stomp.headers.StompSubscribeHeaders
+import org.hildan.krossbow.websocket.okhttp.OkHttpWebSocketClient
+import javax.inject.Inject
+
+private const val TAG = "D107"
+
+@HiltViewModel
+class StudentChattingViewModel @Inject constructor(
+ private val getATKUseCase: GetATKUseCase,
+ private val getSelectedOrganizationIdUseCase: GetSelectedOrganizationIdUseCase,
+ private val loadSelectedOrganizationNameUseCase: LoadSelectedOrganizationNameUseCase,
+ private val loadUserInfoUseCase: LoadUserInfoUseCase,
+ private val getChattingHistoryUseCase: GetChattingHistoryUseCase
+) : BaseViewModel(
+ StudentChattingState()
+) {
+
+ private var roomId = 1L
+ private lateinit var tkn: String
+ private lateinit var userInfo: UserInfo
+
+ private lateinit var stompSession: StompSession
+ private val moshi: Moshi = Moshi.Builder()
+ .addLast(KotlinJsonAdapterFactory())
+ .build()
+ private lateinit var newChatMessage: Flow
+
+ private lateinit var chattingList: List
+
+ var originalChatList: Flow>? = null
+
+ init {
+ viewModelScope.launch {
+ loadSelectedOrganizationNameUseCase().onSuccess {
+ Log.d(TAG, "initStomp1: $it")
+ intent { copy(organizationName = it) }
+ }.onFailure {
+ Log.d(TAG, "initStomp1: $it")
+ }
+ }
+ }
+ @SuppressLint("CheckResult")
+ fun initStomp() {
+ viewModelScope.launch {
+ try {
+ loadLocalData()
+
+ originalChatList =
+ getChattingHistoryUseCase(roomId).cachedIn(viewModelScope)
+
+ } catch (e: Exception) {
+ Log.d(TAG, "initStomp: ${e.message}")
+ }
+ }
+ }
+
+ private fun loadLocalData() {
+ viewModelScope.launch {
+ val tknJob = async { getATKUseCase().getOrThrow() }
+ val roomIdJob = async { getSelectedOrganizationIdUseCase().getOrThrow() }
+ val userInfoJob = async { loadUserInfoUseCase().getOrThrow() }
+
+ tkn = tknJob.await()
+ roomId = roomIdJob.await().toLong()
+ userInfo = userInfoJob.await()
+
+ connectStomp()
+
+ intent { copy(memberId = userInfo.id) }
+ }
+ }
+
+ suspend fun connectStomp(){
+ viewModelScope.launch {
+ val okHttpClient = OkHttpClient.Builder()
+ .addInterceptor(
+ HttpLoggingInterceptor().apply {
+ level = HttpLoggingInterceptor.Level.BODY
+ }
+ )
+ .build()
+
+ val client = StompClient(
+ OkHttpWebSocketClient(okHttpClient)
+ )
+
+ stompSession = client.connect(
+ BuildConfig.STOMP_ENDPOINT,
+ customStompConnectHeaders = mapOf(
+ HEADER_AUTHORIZATION to tkn,
+ HEADER_ROOM_ID to roomId.toString()
+ )
+ ).withMoshi(moshi)
+
+ newChatMessage = stompSession.subscribe(
+ StompSubscribeHeaders(
+ destination = "$SUBSCRIBE_URL$roomId",
+ customHeaders = mapOf(
+ HEADER_AUTHORIZATION to tkn
+ )
+ )
+ )
+
+
+ newChatMessage.collect {
+ val chatMessage = moshi.adapter(Chat::class.java).fromJson(it.bodyAsText)
+ intent { copy(chatList = chatList + chatMessage!!) }
+ }
+
+
+ }
+ }
+
+ fun updateMessage(message: String) {
+ intent { copy(message = message) }
+ }
+
+ fun sendMessage(message: String) {
+ viewModelScope.launch {
+ Log.d(TAG, "sendMessage: ${userInfo.photo}")
+ stompSession.withMoshi(moshi).convertAndSend(
+ StompSendHeaders(
+ destination = SEND_URL,
+ customHeaders = mapOf(
+ HEADER_AUTHORIZATION to tkn
+ )
+ ),
+ ChatMessage(roomId, userInfo.photo, message)
+ )
+ }
+ intent { copy(message = "") }
+ }
+
+ fun cancelStomp() {
+ try {
+ viewModelScope.launch {
+ stompSession.disconnect()
+ }
+ } catch (e: Exception) {
+ Log.d(TAG, "cancelStomp: ${e.message}")
+ }
+ }
+
+ companion object {
+ const val HEADER_AUTHORIZATION = "Authorization"
+ const val HEADER_ROOM_ID = "roomId"
+
+ const val SEND_URL = "/publish/chat/message"
+ const val SUBSCRIBE_URL = "/subscribe/public/"
+ }
+
+}
\ No newline at end of file
diff --git a/android/feature/student/home/src/main/java/com/sixkids/student/home/greeting/receiver/GreetingReceiverContract.kt b/android/feature/student/home/src/main/java/com/sixkids/student/home/greeting/receiver/GreetingReceiverContract.kt
new file mode 100644
index 00000000..ab8f68d8
--- /dev/null
+++ b/android/feature/student/home/src/main/java/com/sixkids/student/home/greeting/receiver/GreetingReceiverContract.kt
@@ -0,0 +1,17 @@
+package com.sixkids.student.home.greeting.receiver
+
+import com.sixkids.model.GreetingNFC
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
+data class GreetingReceiverState(
+ val organizationId: Int = -1,
+ val greetingNfc: GreetingNFC = GreetingNFC()
+): UiState
+
+sealed interface GreetingReceiverEffect: SideEffect{
+ data class OnShowSnackBar(val tkn : SnackbarToken) : GreetingReceiverEffect
+
+ data object NavigateToHome : GreetingReceiverEffect
+}
\ No newline at end of file
diff --git a/android/feature/student/home/src/main/java/com/sixkids/student/home/greeting/receiver/GreetingReceiverScreen.kt b/android/feature/student/home/src/main/java/com/sixkids/student/home/greeting/receiver/GreetingReceiverScreen.kt
new file mode 100644
index 00000000..3fb69e19
--- /dev/null
+++ b/android/feature/student/home/src/main/java/com/sixkids/student/home/greeting/receiver/GreetingReceiverScreen.kt
@@ -0,0 +1,86 @@
+package com.sixkids.student.home.greeting.receiver
+
+import android.app.Activity
+import android.nfc.NfcAdapter
+import android.nfc.Tag
+import android.nfc.tech.IsoDep
+import android.util.Log
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.sixkids.core.nfc.HCEService
+import com.sixkids.designsystem.component.screen.GreetingScreen
+import com.sixkids.model.GreetingNFC
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.extension.collectWithLifecycle
+import kotlinx.serialization.json.Json
+
+private const val TAG = "D107"
+@Composable
+fun GreetingReceiverRoute(
+ viewModel: GreetingReceiverViewModel = hiltViewModel(),
+ onBackClick : () -> Unit = {},
+ onShowSnackBar : (SnackbarToken) -> Unit = {}
+){
+ val context = LocalContext.current
+ val activity = context as Activity
+
+ val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
+ val nfcAdapter: NfcAdapter = NfcAdapter.getDefaultAdapter(context)
+
+ DisposableEffect(key1 = Unit) {
+ viewModel.initData()
+
+ onDispose {
+ nfcAdapter.disableReaderMode(activity)
+ }
+ }
+
+ viewModel.sideEffect.collectWithLifecycle {
+ when(it){
+ is GreetingReceiverEffect.OnShowSnackBar -> onShowSnackBar(it.tkn)
+ is GreetingReceiverEffect.NavigateToHome -> onBackClick()
+ }
+ }
+
+ if (uiState.organizationId != -1 && nfcAdapter.isEnabled) {
+ Log.d(TAG, "RelayTaggingReceiverRoute: Ready!")
+ nfcAdapter.enableReaderMode(activity, { tag : Tag? ->
+ tag?.let {
+ val isoDep = IsoDep.get(it)
+ isoDep.use { iso ->
+ iso.connect()
+ val response = isoDep.transceive(HCEService.SELECT_APDU)
+ val message = String(response.copyOfRange(0, response.size - 2))
+ Log.d(TAG, "GreetingReceiverRoute: $message")
+ val greetingNfc = Json.decodeFromString(message)
+ viewModel.onNfcReceived(greetingNfc)
+ }
+ }
+ }, NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK, null)
+
+ }
+
+ GreetingReceiverScreen()
+}
+
+@Composable
+fun GreetingReceiverScreen(){
+ Box(modifier = Modifier.fillMaxSize()){
+ GreetingScreen(
+ isSender = false
+ )
+ }
+}
+
+@Composable
+@Preview(showBackground = true)
+fun GreetingReceiverScreenPreview(){
+ GreetingReceiverScreen()
+}
\ No newline at end of file
diff --git a/android/feature/student/home/src/main/java/com/sixkids/student/home/greeting/receiver/GreetingReceiverViewModel.kt b/android/feature/student/home/src/main/java/com/sixkids/student/home/greeting/receiver/GreetingReceiverViewModel.kt
new file mode 100644
index 00000000..46123b84
--- /dev/null
+++ b/android/feature/student/home/src/main/java/com/sixkids/student/home/greeting/receiver/GreetingReceiverViewModel.kt
@@ -0,0 +1,55 @@
+package com.sixkids.student.home.greeting.receiver
+
+import androidx.lifecycle.viewModelScope
+import com.sixkids.domain.usecase.organization.GetSelectedOrganizationIdUseCase
+import com.sixkids.domain.usecase.organization.GreetingUseCase
+import com.sixkids.model.GreetingNFC
+import com.sixkids.model.NotFoundException
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.base.BaseViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class GreetingReceiverViewModel @Inject constructor(
+ private val getSelectedOrganizationIdUseCase: GetSelectedOrganizationIdUseCase,
+ private val greetingUseCase: GreetingUseCase
+): BaseViewModel(GreetingReceiverState()){
+
+ fun initData(){
+ viewModelScope.launch {
+ getSelectedOrganizationIdUseCase()
+ .onSuccess {
+ intent { copy(organizationId = it) }
+ }
+ .onFailure {
+ intent { copy(organizationId = -1) }
+ }
+ }
+ }
+
+ fun onNfcReceived(greetingNfc: GreetingNFC){
+ if (greetingNfc.organizationId == uiState.value.organizationId){
+ viewModelScope.launch {
+ greetingUseCase(uiState.value.organizationId.toLong(), greetingNfc.senderId.toLong())
+ .onSuccess {
+ if (it > 0){
+ postSideEffect(GreetingReceiverEffect.OnShowSnackBar(SnackbarToken("친구와 인사를 주고 받았어요!")))
+ postSideEffect(GreetingReceiverEffect.NavigateToHome)
+ }else{
+ postSideEffect(GreetingReceiverEffect.OnShowSnackBar(SnackbarToken("인사에 실패했어요")))
+ }
+ }
+ .onFailure {
+ when(it){
+ is NotFoundException -> postSideEffect(GreetingReceiverEffect.OnShowSnackBar(SnackbarToken("같은 반 친구가 아닙니다!")))
+ else -> postSideEffect(GreetingReceiverEffect.OnShowSnackBar(SnackbarToken("인사에 실패했어요")))
+ }
+ }
+ }
+ }else{
+ postSideEffect(GreetingReceiverEffect.OnShowSnackBar(SnackbarToken("같은 반 친구가 아닙니다!")))
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/feature/student/home/src/main/java/com/sixkids/student/home/greeting/sender/GreetingSenderContract.kt b/android/feature/student/home/src/main/java/com/sixkids/student/home/greeting/sender/GreetingSenderContract.kt
new file mode 100644
index 00000000..3514fa8e
--- /dev/null
+++ b/android/feature/student/home/src/main/java/com/sixkids/student/home/greeting/sender/GreetingSenderContract.kt
@@ -0,0 +1,13 @@
+package com.sixkids.student.home.greeting.sender
+
+import com.sixkids.model.GreetingNFC
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
+data class GreetingSenderState(
+ val greetingNfc: GreetingNFC = GreetingNFC()
+): UiState
+
+sealed interface GreetingSenderEffect: SideEffect {
+
+}
\ No newline at end of file
diff --git a/android/feature/student/home/src/main/java/com/sixkids/student/home/greeting/sender/GreetingSenderScreen.kt b/android/feature/student/home/src/main/java/com/sixkids/student/home/greeting/sender/GreetingSenderScreen.kt
new file mode 100644
index 00000000..4886d63c
--- /dev/null
+++ b/android/feature/student/home/src/main/java/com/sixkids/student/home/greeting/sender/GreetingSenderScreen.kt
@@ -0,0 +1,61 @@
+package com.sixkids.student.home.greeting.sender
+
+import android.util.Log
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.sixkids.core.nfc.HCEService
+import com.sixkids.designsystem.component.screen.GreetingScreen
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+
+private const val TAG = "D107"
+@Composable
+fun GreetingSenderRoute(
+ viewModel: GreetingSenderViewModel = hiltViewModel(),
+ onBackClick: () -> Unit,
+){
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ LaunchedEffect(key1 = Unit) {
+ viewModel.initData()
+ }
+
+ GreetingSenderScreen(
+ uiState = uiState,
+ onBackClick = onBackClick
+ )
+
+}
+
+@Composable
+fun GreetingSenderScreen(
+ uiState: GreetingSenderState = GreetingSenderState(),
+ onBackClick: () -> Unit = {},
+){
+ if (uiState.greetingNfc.organizationId != -1) {
+ val serializedGreetingNfc = Json.encodeToString(uiState.greetingNfc)
+ Log.d(TAG, "RelayTaggingSenderScreen: $serializedGreetingNfc")
+ HCEService.setData(serializedGreetingNfc)
+ }
+ Box(modifier = Modifier.fillMaxSize()) {
+ GreetingScreen(
+ isSender = true,
+ onClick = onBackClick
+ )
+ }
+
+
+}
+
+@Composable
+@Preview(showBackground = true)
+fun GreetingSenderScreenPreview(){
+ GreetingSenderScreen()
+}
\ No newline at end of file
diff --git a/android/feature/student/home/src/main/java/com/sixkids/student/home/greeting/sender/GreetingSenderViewModel.kt b/android/feature/student/home/src/main/java/com/sixkids/student/home/greeting/sender/GreetingSenderViewModel.kt
new file mode 100644
index 00000000..15f595de
--- /dev/null
+++ b/android/feature/student/home/src/main/java/com/sixkids/student/home/greeting/sender/GreetingSenderViewModel.kt
@@ -0,0 +1,39 @@
+package com.sixkids.student.home.greeting.sender
+
+import android.util.Log
+import androidx.lifecycle.viewModelScope
+import com.sixkids.domain.usecase.organization.GetSelectedOrganizationIdUseCase
+import com.sixkids.domain.usecase.user.LoadUserInfoUseCase
+import com.sixkids.model.GreetingNFC
+import com.sixkids.model.UserInfo
+import com.sixkids.ui.base.BaseViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+private const val TAG = "D107"
+@HiltViewModel
+class GreetingSenderViewModel @Inject constructor(
+ private val loadUserInfoUseCase: LoadUserInfoUseCase,
+ private val getSelectedOrganizationIdUseCase: GetSelectedOrganizationIdUseCase
+): BaseViewModel(GreetingSenderState()){
+ private var organizationId: Int = -1
+ private lateinit var userInfo: UserInfo
+
+ fun initData(){
+ viewModelScope.launch {
+ loadUserInfoUseCase().onSuccess {user ->
+ userInfo = user
+
+ getSelectedOrganizationIdUseCase()
+ .onSuccess {
+ organizationId = it
+ intent { copy(greetingNfc = GreetingNFC(user.id, it)) }
+ }
+ }.onFailure {
+ Log.d(TAG, "initData: $it")
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/android/feature/student/home/src/main/java/com/sixkids/student/home/main/StudentHomeMainContract.kt b/android/feature/student/home/src/main/java/com/sixkids/student/home/main/StudentHomeMainContract.kt
new file mode 100644
index 00000000..cb2e1380
--- /dev/null
+++ b/android/feature/student/home/src/main/java/com/sixkids/student/home/main/StudentHomeMainContract.kt
@@ -0,0 +1,23 @@
+package com.sixkids.student.home.main
+
+import com.sixkids.model.MemberSimpleWithScore
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
+sealed interface StudentHomeMainEffect: SideEffect {
+ data object navigateToAnnounce: StudentHomeMainEffect
+ data object navigateToTagHello : StudentHomeMainEffect
+ data object navigateToRank : StudentHomeMainEffect
+ data object navigateToChatting : StudentHomeMainEffect
+ data class onShowSnackBar(val message: String) : StudentHomeMainEffect
+}
+
+data class StudentHomeMainState(
+ val isLoading: Boolean = false,
+ val studentName: String = "",
+ val studentImageUrl: String = "",
+ val studentClass: String = "",
+ val studentExp: Int = 0,
+ val bestFriendList: List = emptyList(),
+ val isShowGreetingDialog: Boolean = false
+): UiState
\ No newline at end of file
diff --git a/android/feature/student/home/src/main/java/com/sixkids/student/home/main/StudentHomeMainScreen.kt b/android/feature/student/home/src/main/java/com/sixkids/student/home/main/StudentHomeMainScreen.kt
new file mode 100644
index 00000000..96258628
--- /dev/null
+++ b/android/feature/student/home/src/main/java/com/sixkids/student/home/main/StudentHomeMainScreen.kt
@@ -0,0 +1,241 @@
+package com.sixkids.student.home.main
+
+import android.util.Log
+import androidx.activity.compose.BackHandler
+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.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+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.width
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+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 androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.sixkids.designsystem.component.item.StudentSimpleCardItem
+import com.sixkids.designsystem.theme.Blue
+import com.sixkids.designsystem.theme.BlueText
+import com.sixkids.designsystem.theme.Orange
+import com.sixkids.designsystem.theme.OrangeText
+import com.sixkids.designsystem.theme.Purple
+import com.sixkids.designsystem.theme.PurpleText
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.designsystem.theme.Yellow
+import com.sixkids.designsystem.theme.YellowText
+import com.sixkids.designsystem.theme.component.card.ContentVerticalCard
+import com.sixkids.model.MemberSimple
+import com.sixkids.model.MemberSimpleWithScore
+import com.sixkids.student.home.R
+import com.sixkids.student.home.main.component.GreetingDialog
+import com.sixkids.student.home.main.component.StudentMainInfo
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.designsystem.R as UlbanRes
+
+private const val TAG = "D107"
+@Composable
+fun StudentHomeMainRoute(
+ viewModel: StudentHomeMainViewModel = hiltViewModel(),
+ padding: PaddingValues,
+ navigateToAnnounce: () -> Unit,
+ navigateToTagHello: () -> Unit,
+ navigateToRank: () -> Unit,
+ navigateToChatting: () -> Unit,
+ navigateToGreetingSender: () -> Unit,
+ navigateToGreetingReceiver: () -> Unit,
+ onShowSnackBar: (SnackbarToken) -> Unit
+) {
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ LaunchedEffect(Unit) {
+ viewModel.getStudentHomeInfo()
+ }
+
+ LaunchedEffect(viewModel.sideEffect) {
+ viewModel.sideEffect.collect { sideEffect ->
+ when (sideEffect) {
+ StudentHomeMainEffect.navigateToAnnounce -> navigateToAnnounce()
+ StudentHomeMainEffect.navigateToChatting -> navigateToChatting()
+ StudentHomeMainEffect.navigateToRank -> navigateToRank()
+ StudentHomeMainEffect.navigateToTagHello -> navigateToTagHello()
+ is StudentHomeMainEffect.onShowSnackBar -> onShowSnackBar(SnackbarToken(message = sideEffect.message))
+ }
+ }
+ }
+
+ Box(
+ modifier = Modifier
+ .padding(padding)
+ .fillMaxSize()
+ ) {
+ StudentHomeMainScreen(
+ studentHomeMainState = uiState,
+ announceCardOnClick = navigateToAnnounce,
+ chattingCardOnClick = navigateToChatting,
+ rankCardOnClick = navigateToRank,
+ showGreetingDialog = viewModel::showGreetingDialog,
+ )
+ if (uiState.isShowGreetingDialog) {
+ BackHandler(enabled = true) {
+ Log.d(TAG, "BackHandler triggered")
+ viewModel.offDialog()
+ }
+
+ GreetingDialog(
+ senderClick = navigateToGreetingSender,
+ receiverClick = navigateToGreetingReceiver,
+ cancelClick = viewModel::offDialog
+ )
+ }
+ }
+}
+
+@Composable
+fun StudentHomeMainScreen(
+ modifier: Modifier = Modifier,
+ studentHomeMainState: StudentHomeMainState = StudentHomeMainState(),
+ announceCardOnClick: () -> Unit = {},
+ chattingCardOnClick: () -> Unit = {},
+ rankCardOnClick: () -> Unit = {},
+ showGreetingDialog: () -> Unit = {},
+) {
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .padding(20.dp)
+ ) {
+ Spacer(modifier = Modifier.height(10.dp))
+
+ StudentMainInfo(
+ name = studentHomeMainState.studentName,
+ imageUrlString = studentHomeMainState.studentImageUrl,
+ classString = studentHomeMainState.studentClass,
+ exp = studentHomeMainState.studentExp
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ Row {
+ ContentVerticalCard(
+ cardModifier = Modifier
+ .weight(1f)
+ .aspectRatio(1f)
+ .padding(end = 10.dp),
+ cardColor = Orange,
+ textColor = OrangeText,
+ imageDrawable = UlbanRes.drawable.announce,
+ text = stringResource(id = R.string.student_home_main_announce),
+ onClick = announceCardOnClick
+ )
+ ContentVerticalCard(
+ cardModifier = Modifier
+ .weight(1f)
+ .aspectRatio(1f)
+ .padding(start = 10.dp),
+ cardColor = Blue,
+ textColor = BlueText,
+ imageDrawable = UlbanRes.drawable.tag_hello,
+ text = stringResource(id = R.string.student_home_main_hi),
+ onClick = showGreetingDialog
+ )
+ }
+ Spacer(modifier = Modifier.height(20.dp))
+ Text(
+ text = stringResource(id = R.string.student_home_main_best_friends),
+ style = UlbanTypography.titleSmall,
+ modifier = Modifier.padding(start = 15.dp)
+ )
+ Spacer(modifier = Modifier.height(10.dp))
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight(),
+ horizontalArrangement = Arrangement.Center
+ ) {
+ studentHomeMainState.bestFriendList.take(3).forEach {
+ Spacer(modifier = Modifier.width(10.dp))
+ StudentSimpleCardItem(
+ modifier = modifier
+ .height(130.dp)
+ .width(100.dp),
+ name = it.memberSimple.name,
+ photo = it.memberSimple.photo,
+ score = it.relationPoint
+ )
+ Spacer(modifier = Modifier.width(10.dp))
+ }
+ }
+ Spacer(modifier = Modifier.height(20.dp))
+ Row {
+ ContentVerticalCard(
+ cardModifier = Modifier
+ .weight(1f)
+ .aspectRatio(1f)
+ .padding(end = 10.dp),
+ cardColor = Purple,
+ textColor = PurpleText,
+ imageDrawable = UlbanRes.drawable.chat,
+ text = stringResource(id = R.string.student_home_main_chatting),
+ onClick = chattingCardOnClick
+ )
+ ContentVerticalCard(
+ cardModifier = Modifier
+ .weight(1f)
+ .aspectRatio(1f)
+ .padding(start = 10.dp),
+ cardColor = Yellow,
+ textColor = YellowText,
+ imageDrawable = UlbanRes.drawable.rank,
+ text = stringResource(id = R.string.student_home_main_rank),
+ onClick = rankCardOnClick
+ )
+ }
+ Spacer(modifier = Modifier.weight(1f))
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun StudentHomeMainScreenPreview() {
+ StudentHomeMainScreen(
+ studentHomeMainState = StudentHomeMainState(
+ studentName = "홍유준",
+ studentImageUrl = "https://www.google.com",
+ studentClass = "구미초등학교 1학년 1반",
+ studentExp = 2340,
+ bestFriendList = listOf(
+ MemberSimpleWithScore(
+ memberSimple = MemberSimple(
+ name = "김철수",
+ photo = "https://www.google.com"
+ ),
+ relationPoint = 234
+ ),
+ MemberSimpleWithScore(
+ memberSimple = MemberSimple(
+ name = "김영희",
+ photo = "https://www.google.com"
+ ),
+ relationPoint = 234
+ ),
+ MemberSimpleWithScore(
+ memberSimple = MemberSimple(
+ name = "박영수",
+ photo = "https://www.google.com"
+ ),
+ relationPoint = 234
+ )
+ )
+ )
+ )
+}
\ No newline at end of file
diff --git a/android/feature/student/home/src/main/java/com/sixkids/student/home/main/StudentHomeMainViewModel.kt b/android/feature/student/home/src/main/java/com/sixkids/student/home/main/StudentHomeMainViewModel.kt
new file mode 100644
index 00000000..4706638c
--- /dev/null
+++ b/android/feature/student/home/src/main/java/com/sixkids/student/home/main/StudentHomeMainViewModel.kt
@@ -0,0 +1,65 @@
+package com.sixkids.student.home.main
+
+import android.util.Log
+import androidx.lifecycle.viewModelScope
+import com.sixkids.domain.usecase.organization.GetSelectedOrganizationIdUseCase
+import com.sixkids.domain.usecase.user.GetStudentHomeInfoUseCase
+import com.sixkids.ui.base.BaseViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+private const val TAG = "D107"
+@HiltViewModel
+class StudentHomeMainViewModel @Inject constructor(
+ private val getSelectedOrganizationIdUseCase: GetSelectedOrganizationIdUseCase,
+ private val getStudentHomeInfoUseCase: GetStudentHomeInfoUseCase
+) : BaseViewModel(StudentHomeMainState()) {
+
+ private var organizationId: Long? = null
+
+ fun getStudentHomeInfo() {
+ viewModelScope.launch {
+ intent { copy(isLoading = true) }
+
+ if (organizationId == null) {
+ organizationId = getSelectedOrganizationIdUseCase().getOrNull()?.toLong()
+ }
+
+ if (organizationId != null) {
+ getStudentHomeInfoUseCase(organizationId!!)
+ .onSuccess { info ->
+ intent {
+ copy(
+ studentName = info.name,
+ studentImageUrl = info.photo,
+ studentClass = info.className,
+ studentExp = info.exp,
+ bestFriendList = info.relations
+ )
+ }
+ }
+ .onFailure {
+ postSideEffect(
+ StudentHomeMainEffect.onShowSnackBar(
+ it.message ?: "알 수 없는 오류가 발생했습니다."
+ )
+ )
+ }
+ } else {
+ postSideEffect(StudentHomeMainEffect.onShowSnackBar("학급 정보를 불러오지 못했어요 ;("))
+ }
+
+
+ }
+ }
+
+ fun showGreetingDialog(){
+ intent { copy(isShowGreetingDialog = true) }
+ }
+
+ fun offDialog(){
+ Log.d(TAG, "offDialog: ")
+ intent { copy(isShowGreetingDialog = false) }
+ }
+}
\ No newline at end of file
diff --git a/android/feature/student/home/src/main/java/com/sixkids/student/home/main/component/GreetingDialog.kt b/android/feature/student/home/src/main/java/com/sixkids/student/home/main/component/GreetingDialog.kt
new file mode 100644
index 00000000..95f0a40b
--- /dev/null
+++ b/android/feature/student/home/src/main/java/com/sixkids/student/home/main/component/GreetingDialog.kt
@@ -0,0 +1,121 @@
+package com.sixkids.student.home.main.component
+
+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.Row
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxWidth
+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.Button
+import androidx.compose.material3.ButtonDefaults
+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.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import coil.compose.AsyncImage
+import com.sixkids.designsystem.component.dialog.UlbanBasicDialog
+import com.sixkids.designsystem.theme.Blue
+import com.sixkids.designsystem.theme.BlueDark
+import com.sixkids.designsystem.theme.Red
+import com.sixkids.designsystem.theme.RedDark
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.designsystem.R as DesignR
+
+@Composable
+fun GreetingDialog(
+ senderClick: () -> Unit = {},
+ receiverClick: () -> Unit = {},
+ cancelClick: () -> Unit = {}
+) {
+ UlbanBasicDialog {
+
+ Column(
+ modifier = Modifier
+ .width(280.dp)
+ .padding(vertical = 16.dp, horizontal = 10.dp),
+ verticalArrangement = Arrangement.spacedBy(15.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Image(
+ painter = painterResource(id = DesignR.drawable.ic_cancel),
+ contentDescription = "cancel",
+ modifier = Modifier.align(Alignment.End).size(32.dp).clickable {
+ cancelClick()
+ }
+ )
+
+ AsyncImage(
+ model = DesignR.drawable.tag_hello,
+ modifier = Modifier.fillMaxWidth(),
+ placeholder = painterResource(id = DesignR.drawable.relay_tag),
+ contentDescription = "tag"
+ )
+
+ Text(
+ text = "친구와 인사를 하고\n점수를 올려봐요",
+ style = UlbanTypography.titleSmall,
+ textAlign = TextAlign.Center
+ )
+
+ Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
+ Button(
+ modifier = Modifier
+ .weight(1f)
+ .aspectRatio(1f)
+ .clip(RoundedCornerShape(16.dp))
+ .background(Red),
+ colors = ButtonDefaults.buttonColors(containerColor = Red),
+ onClick = { senderClick() }) {
+ Text(
+ text = "친구에게\n인사 건네기",
+ style = UlbanTypography.titleSmall.copy(
+ fontSize = 14.sp,
+ lineHeight = 24.sp,
+ color = RedDark
+ ),
+ textAlign = TextAlign.Center
+ )
+ }
+
+ Button(
+ modifier = Modifier
+ .weight(1f)
+ .aspectRatio(1f)
+ .clip(RoundedCornerShape(16.dp))
+ .background(Blue),
+ colors = ButtonDefaults.buttonColors(containerColor = Blue),
+ onClick = { receiverClick() }) {
+ Text(
+ text = "친구의\n인사 받기",
+ style = UlbanTypography.titleSmall.copy(
+ fontSize = 14.sp,
+ lineHeight = 24.sp,
+ color = BlueDark
+ ),
+ textAlign = TextAlign.Center
+ )
+ }
+ }
+ }
+ }
+
+
+}
+
+@Composable
+@Preview(showBackground = true)
+fun GreetingDialogPreview() {
+ GreetingDialog()
+}
\ No newline at end of file
diff --git a/android/feature/student/home/src/main/java/com/sixkids/student/home/main/component/StudentInfo.kt b/android/feature/student/home/src/main/java/com/sixkids/student/home/main/component/StudentInfo.kt
new file mode 100644
index 00000000..dcf2365d
--- /dev/null
+++ b/android/feature/student/home/src/main/java/com/sixkids/student/home/main/component/StudentInfo.kt
@@ -0,0 +1,90 @@
+package com.sixkids.student.home.main.component
+
+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.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import coil.compose.AsyncImage
+import com.sixkids.designsystem.theme.UlbanTypography
+
+@Preview(showBackground = true)
+@Composable
+fun StudentMainInfoCardPreview() {
+ StudentMainInfo(
+ name = "홍유준",
+ classString = "구미초등학교 1학년 1반",
+ exp = 2340,
+ )
+}
+
+@Composable
+fun StudentMainInfo(
+ modifier: Modifier = Modifier,
+ name: String = "",
+ imageUrlString: String = "",
+ classString: String = "",
+ exp: Int = 0
+) {
+ val height = 80.dp
+
+ Card(
+ modifier = modifier
+ .fillMaxWidth()
+ .wrapContentHeight(),
+ colors = CardDefaults.cardColors(
+ containerColor = Color.Transparent
+ )
+ ) {
+ Row {
+ Card {
+ AsyncImage(
+ model = imageUrlString,
+ contentDescription = null,
+ modifier = Modifier.size(height),
+ contentScale = ContentScale.Crop
+ )
+ }
+ Spacer(modifier = Modifier.width(16.dp))
+ Column(
+ modifier = Modifier.height(height),
+ verticalArrangement = Arrangement.Center
+ ) {
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ textAlign = TextAlign.Start,
+ text = "$name 학생",
+ style = UlbanTypography.titleMedium
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ textAlign = TextAlign.Start,
+ text = classString.replace("\n"," "),
+ style = UlbanTypography.titleSmall
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ textAlign = TextAlign.Start,
+ text = "${exp}점",
+ style = UlbanTypography.bodyMedium
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/feature/student/home/src/main/java/com/sixkids/student/home/navigation/StudentHomeNavigation.kt b/android/feature/student/home/src/main/java/com/sixkids/student/home/navigation/StudentHomeNavigation.kt
new file mode 100644
index 00000000..7ea79327
--- /dev/null
+++ b/android/feature/student/home/src/main/java/com/sixkids/student/home/navigation/StudentHomeNavigation.kt
@@ -0,0 +1,134 @@
+package com.sixkids.student.home.navigation
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavOptions
+import androidx.navigation.NavType
+import androidx.navigation.compose.composable
+import androidx.navigation.navArgument
+import com.sixkids.student.home.announce.announcedetail.StudentAnnounceDetailRoute
+import com.sixkids.student.home.announce.announcelist.StudentAnnounceListRoute
+import com.sixkids.student.home.chatting.StudentChattingRoute
+import com.sixkids.student.home.greeting.receiver.GreetingReceiverRoute
+import com.sixkids.student.home.greeting.sender.GreetingSenderRoute
+import com.sixkids.student.home.main.StudentHomeMainRoute
+import com.sixkids.student.home.rank.StudentRankRoute
+import com.sixkids.ui.SnackbarToken
+
+fun NavController.navigateStudentHome(navOptions: NavOptions) {
+ navigate(StudentHomeRoute.defaultRoute,navOptions)
+}
+
+fun NavController.navigateStudentAnnounceList() {
+ navigate(StudentHomeRoute.announceListRoute)
+}
+
+fun NavController.navigateStudentAnnounceDetail(announceDetailId: Long) {
+ navigate(StudentHomeRoute.announceDetailRoute(announceDetailId))
+}
+
+fun NavController.navigateStudentChatting() {
+ navigate(StudentHomeRoute.chattingRoute)
+}
+
+fun NavController.navigateStudentRank() {
+ navigate(StudentHomeRoute.rankRoute)
+}
+
+fun NavController.navigateStudentGreetingSender() {
+ navigate(StudentHomeRoute.greetingSenderRoute)
+}
+
+fun NavController.navigateStudentGreetingReceiver() {
+ navigate(StudentHomeRoute.greetingReceiverRoute)
+}
+
+fun NavGraphBuilder.studentHomeNavGraph(
+ padding: PaddingValues,
+ onShowSnackbar: (SnackbarToken) -> Unit,
+ navigateBack: () -> Unit,
+ navigateToStudentAnnounceList: () -> Unit,
+ navigateToStudentAnnounceDetail: (Long) -> Unit,
+ navigateToTagHello: () -> Unit,
+ navigateToRank: () -> Unit,
+ navigateToChatting: () -> Unit,
+ navigateToGreetingSender: () -> Unit,
+ navigateToGreetingReceiver: () -> Unit,
+ onBackClick: () -> Unit
+) {
+ composable(route = StudentHomeRoute.defaultRoute) {
+ StudentHomeMainRoute(
+ padding = padding,
+ navigateToAnnounce = navigateToStudentAnnounceList,
+ navigateToTagHello = navigateToTagHello,
+ navigateToRank = navigateToRank,
+ navigateToChatting = navigateToChatting,
+ navigateToGreetingSender = navigateToGreetingSender,
+ navigateToGreetingReceiver = navigateToGreetingReceiver,
+ onShowSnackBar = onShowSnackbar
+ )
+ }
+
+ composable(route = StudentHomeRoute.announceListRoute) {
+ StudentAnnounceListRoute(
+ padding = padding,
+ navigateToStudentAnnounceDetail = navigateToStudentAnnounceDetail,
+ onShowSnackBar = onShowSnackbar
+ )
+ }
+
+ composable(
+ route = StudentHomeRoute.announceDetailRoute,
+ arguments = listOf(navArgument(StudentHomeRoute.announceDetailARG) { type = NavType.LongType })
+ ) {
+ StudentAnnounceDetailRoute(
+ padding = padding,
+ onShowSnackBar = onShowSnackbar
+ )
+ }
+
+ composable(route = StudentHomeRoute.chattingRoute) {
+ StudentChattingRoute(
+ onBackClick = navigateBack,
+ onShowSnackBar = onShowSnackbar
+ )
+ }
+
+ composable(route = StudentHomeRoute.rankRoute) {
+ StudentRankRoute(
+ padding = padding,
+ onShowSnackBar = onShowSnackbar
+ )
+ }
+
+ composable(route = StudentHomeRoute.greetingSenderRoute) {
+ GreetingSenderRoute(
+ onBackClick = navigateBack,
+ )
+ }
+
+ composable(route = StudentHomeRoute.greetingReceiverRoute) {
+ GreetingReceiverRoute(
+ onBackClick = navigateBack,
+ )
+ }
+}
+
+object StudentHomeRoute {
+ const val announceDetailARG = "announceDetailId"
+
+ const val defaultRoute = "student_home"
+
+ const val announceListRoute = "student_announce_list"
+ const val announceDetailRoute = "student_announce_detail/{$announceDetailARG}"
+
+ const val chattingRoute = "student_chatting"
+
+ const val rankRoute = "student_rank"
+
+ const val greetingSenderRoute = "student_greeting_sender"
+ const val greetingReceiverRoute = "student_greeting_receiver"
+
+ fun announceDetailRoute(announceDetailId: Long) = "student_announce_detail/$announceDetailId"
+}
\ No newline at end of file
diff --git a/android/feature/student/home/src/main/java/com/sixkids/student/home/rank/StudentRankContract.kt b/android/feature/student/home/src/main/java/com/sixkids/student/home/rank/StudentRankContract.kt
new file mode 100644
index 00000000..e129605c
--- /dev/null
+++ b/android/feature/student/home/src/main/java/com/sixkids/student/home/rank/StudentRankContract.kt
@@ -0,0 +1,15 @@
+package com.sixkids.student.home.rank
+
+import com.sixkids.model.MemberRankItem
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
+sealed interface StudentRankEffect : SideEffect {
+ data class onShowSnackBar(val message: String): StudentRankEffect
+}
+
+data class StudentRankState(
+ val isLoading: Boolean = false,
+ val classString: String = "",
+ val rankList: List = emptyList(),
+): UiState
diff --git a/android/feature/student/home/src/main/java/com/sixkids/student/home/rank/StudentRankScreen.kt b/android/feature/student/home/src/main/java/com/sixkids/student/home/rank/StudentRankScreen.kt
new file mode 100644
index 00000000..619edfef
--- /dev/null
+++ b/android/feature/student/home/src/main/java/com/sixkids/student/home/rank/StudentRankScreen.kt
@@ -0,0 +1,166 @@
+package com.sixkids.student.home.rank
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+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 androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.sixkids.designsystem.component.appbar.UlbanDetailAppBar
+import com.sixkids.designsystem.component.screen.LoadingScreen
+import com.sixkids.designsystem.theme.Yellow
+import com.sixkids.model.MemberRankItem
+import com.sixkids.student.home.R
+import com.sixkids.student.home.rank.component.RankItem
+import com.sixkids.student.home.rank.component.StudentRankViewModel
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.extension.collectWithLifecycle
+import com.sixkids.designsystem.R as UlbanRes
+
+@Composable
+fun StudentRankRoute(
+ viewModel: StudentRankViewModel = hiltViewModel(),
+ padding: PaddingValues,
+ onShowSnackBar: (SnackbarToken) -> Unit
+) {
+
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ viewModel.sideEffect.collectWithLifecycle {
+ when (it) {
+ is StudentRankEffect.onShowSnackBar -> onShowSnackBar(SnackbarToken(it.message))
+ }
+ }
+
+ LaunchedEffect(Unit) {
+ viewModel.getOrganizationName()
+ viewModel.getClassRank()
+ }
+
+ Box(
+ modifier = Modifier
+ .padding(padding)
+ .fillMaxSize()
+ ) {
+ StudentRankScreen(
+ studentRankState = uiState
+ )
+ if (uiState.isLoading) {
+ LoadingScreen()
+ }
+ }
+
+}
+
+@Composable
+fun StudentRankScreen(
+ modifier: Modifier = Modifier,
+ studentRankState: StudentRankState = StudentRankState()
+) {
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ ) {
+ UlbanDetailAppBar(
+ leftIcon = UlbanRes.drawable.rank,
+ title = stringResource(id = R.string.student_home_main_rank),
+ content = stringResource(id = R.string.student_home_main_rank),
+ topDescription = "",
+ bottomDescription = studentRankState.classString,
+ color = Yellow
+ )
+ LazyColumn(
+ modifier = Modifier
+ .padding(16.dp)
+ ) {
+ items(studentRankState.rankList.size) { index ->
+ RankItem(
+ rank = studentRankState.rankList[index].rank,
+ name = studentRankState.rankList[index].name,
+ exp = studentRankState.rankList[index].exp
+ )
+ if (index != studentRankState.rankList.size - 1) {
+ HorizontalDivider(
+ modifier = Modifier.padding(vertical = 4.dp),
+ color = Color.Black,
+ thickness = 2.dp
+ )
+ }
+ }
+ }
+ }
+}
+
+
+@Preview(showBackground = true)
+@Composable
+fun RankScreenPreview() {
+ StudentRankScreen(
+ studentRankState = StudentRankState(
+ classString = "구미 초등학교 1학년 1반",
+ rankList = listOf(
+ MemberRankItem(
+ rank = 1,
+ name = "김철수",
+ exp = 100
+ ),
+ MemberRankItem(
+ rank = 2,
+ name = "박영희",
+ exp = 90
+ ),
+ MemberRankItem(
+ rank = 3,
+ name = "이영수",
+ exp = 80
+ ),
+ MemberRankItem(
+ rank = 4,
+ name = "최영희",
+ exp = 70
+ ),
+ MemberRankItem(
+ rank = 5,
+ name = "홍길동",
+ exp = 60
+ ),
+ MemberRankItem(
+ rank = 6,
+ name = "김철수",
+ exp = 50
+ ),
+ MemberRankItem(
+ rank = 7,
+ name = "박영희",
+ exp = 40
+ ),
+ MemberRankItem(
+ rank = 8,
+ name = "이영수",
+ exp = 30
+ ),
+ MemberRankItem(
+ rank = 9,
+ name = "최영희",
+ exp = 20
+ ),
+ MemberRankItem(
+ rank = 10,
+ name = "홍길동",
+ exp = 10
+ )
+ )
+ )
+ )
+}
\ No newline at end of file
diff --git a/android/feature/student/home/src/main/java/com/sixkids/student/home/rank/component/RankItem.kt b/android/feature/student/home/src/main/java/com/sixkids/student/home/rank/component/RankItem.kt
new file mode 100644
index 00000000..ac799c8d
--- /dev/null
+++ b/android/feature/student/home/src/main/java/com/sixkids/student/home/rank/component/RankItem.kt
@@ -0,0 +1,90 @@
+package com.sixkids.student.home.rank.component
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.AbsoluteAlignment
+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.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.designsystem.R as UlbanRes
+
+@Composable
+fun RankItem(
+ modifier: Modifier = Modifier,
+ rank: Int = 0,
+ name: String = "이름",
+ exp: Int = 0,
+) {
+ val rankHeight = 40
+ Box(
+ modifier = modifier.fillMaxWidth(),
+ ) {
+ // Rank
+ when (rank) {
+ 1, 2, 3 -> {
+ Image(
+ modifier = Modifier
+ .height(rankHeight.dp)
+ .aspectRatio(1f)
+ .align(AbsoluteAlignment.CenterLeft),
+ painter = painterResource(
+ id = when (rank) {
+ 1 -> UlbanRes.drawable.rank_first
+ 2 -> UlbanRes.drawable.rank_second
+ 3 -> UlbanRes.drawable.rank_third
+ else -> UlbanRes.drawable.rank_third
+ },
+ ),
+ contentDescription = null
+ )
+ }
+
+ else -> {
+ Box(
+ modifier = Modifier
+ .height(rankHeight.dp)
+ .align(Alignment.CenterStart),
+ ){
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ text = "${rank}등",
+ style = UlbanTypography.titleMedium,
+ textAlign = TextAlign.Center,
+ )
+ }
+ }
+ }
+ // Name
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ text = name,
+ style = UlbanTypography.titleMedium,
+ maxLines = 1
+ )
+ // Exp
+ Text(
+ modifier = Modifier.align(Alignment.CenterEnd),
+ text = "${exp}exp",
+ style = UlbanTypography.titleMedium,
+ maxLines = 1,
+
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun RankItemPreview() {
+ RankItem(
+ rank = 4
+ )
+}
\ No newline at end of file
diff --git a/android/feature/student/home/src/main/java/com/sixkids/student/home/rank/component/StudentRankViewModel.kt b/android/feature/student/home/src/main/java/com/sixkids/student/home/rank/component/StudentRankViewModel.kt
new file mode 100644
index 00000000..bdf95f1e
--- /dev/null
+++ b/android/feature/student/home/src/main/java/com/sixkids/student/home/rank/component/StudentRankViewModel.kt
@@ -0,0 +1,53 @@
+package com.sixkids.student.home.rank.component
+
+import androidx.lifecycle.viewModelScope
+import com.sixkids.domain.usecase.organization.GetClassRankUseCase
+import com.sixkids.domain.usecase.organization.GetSelectedOrganizationIdUseCase
+import com.sixkids.domain.usecase.organization.LoadSelectedOrganizationNameUseCase
+import com.sixkids.student.home.rank.StudentRankEffect
+import com.sixkids.student.home.rank.StudentRankState
+import com.sixkids.ui.base.BaseViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class StudentRankViewModel @Inject constructor(
+ private val getSelectedOrganizationIdUseCase: GetSelectedOrganizationIdUseCase,
+ private val loadSelectedOrganizationNameUseCase: LoadSelectedOrganizationNameUseCase,
+ private val getClassRankUseCase: GetClassRankUseCase
+): BaseViewModel(StudentRankState()){
+ private var organizationId: Int? = null
+
+ private suspend fun getOrganizationId() {
+ organizationId = getSelectedOrganizationIdUseCase().getOrNull()
+ }
+
+ fun getOrganizationName() {
+ viewModelScope.launch {
+ loadSelectedOrganizationNameUseCase().onSuccess {
+ intent { copy(classString = it) }
+ }
+ }
+ }
+
+ fun getClassRank() {
+ viewModelScope.launch {
+ if (organizationId == null) {
+ getOrganizationId()
+ }
+
+ if (organizationId != null) {
+ val result = getClassRankUseCase(organizationId!!)
+ result.onSuccess {
+ intent { copy(rankList = it) }
+ }
+ result.onFailure {
+ postSideEffect(StudentRankEffect.onShowSnackBar(it.message ?: "학급 랭킹 불러오기에 실패했습니다 ;("))
+ }
+ } else {
+ postSideEffect(StudentRankEffect.onShowSnackBar("학급 정보를 불러오는데 실패했습니다 ;("))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/feature/student/home/src/main/res/drawable/ic_camera.xml b/android/feature/student/home/src/main/res/drawable/ic_camera.xml
new file mode 100644
index 00000000..7283b1a9
--- /dev/null
+++ b/android/feature/student/home/src/main/res/drawable/ic_camera.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/android/feature/student/home/src/main/res/values/strings.xml b/android/feature/student/home/src/main/res/values/strings.xml
new file mode 100644
index 00000000..414f7f59
--- /dev/null
+++ b/android/feature/student/home/src/main/res/values/strings.xml
@@ -0,0 +1,12 @@
+
+
+ 인사
+ 알림장
+ 채팅
+ 랭킹
+ 친한 친구들
+
+ 알림장이 없어용!
+
+ 댓글을 입력하세요
+
\ No newline at end of file
diff --git a/android/feature/student/main/.gitignore b/android/feature/student/main/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/android/feature/student/main/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/android/feature/student/main/build.gradle.kts b/android/feature/student/main/build.gradle.kts
new file mode 100644
index 00000000..1402795a
--- /dev/null
+++ b/android/feature/student/main/build.gradle.kts
@@ -0,0 +1,13 @@
+plugins {
+ alias(libs.plugins.sixkids.android.feature.compose)
+}
+
+android {
+ namespace = "com.sixkids.student.main"
+}
+
+dependencies {
+ implementation(libs.accompanist.pager)
+ implementation(platform(libs.firebase.bom))
+ implementation(libs.bundles.firebase)
+}
diff --git a/android/feature/student/main/consumer-rules.pro b/android/feature/student/main/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/android/feature/student/main/proguard-rules.pro b/android/feature/student/main/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/android/feature/student/main/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/android/feature/student/main/src/androidTest/java/com/sixkids/student/main/ExampleInstrumentedTest.kt b/android/feature/student/main/src/androidTest/java/com/sixkids/student/main/ExampleInstrumentedTest.kt
new file mode 100644
index 00000000..3f1c064f
--- /dev/null
+++ b/android/feature/student/main/src/androidTest/java/com/sixkids/student/main/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.sixkids.student.main
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * 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.sixkids.student.main.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/android/feature/student/main/src/main/AndroidManifest.xml b/android/feature/student/main/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..a5918e68
--- /dev/null
+++ b/android/feature/student/main/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/android/feature/student/main/src/main/java/com/sixkids/student/main/joinorganization/JoinOrganizationContract.kt b/android/feature/student/main/src/main/java/com/sixkids/student/main/joinorganization/JoinOrganizationContract.kt
new file mode 100644
index 00000000..447ef8c5
--- /dev/null
+++ b/android/feature/student/main/src/main/java/com/sixkids/student/main/joinorganization/JoinOrganizationContract.kt
@@ -0,0 +1,16 @@
+package com.sixkids.student.main.joinorganization
+
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
+sealed interface JoinOrganizationEffect : SideEffect {
+ data object NavigateToOrganizationList : JoinOrganizationEffect
+ data class OnShowSnackBar(val tkn: SnackbarToken) : JoinOrganizationEffect
+}
+
+data class JoinOrganizationState(
+ val isLoading: Boolean = false,
+ val code: String = "",
+ val id: String = "",
+) : UiState
\ No newline at end of file
diff --git a/android/feature/student/main/src/main/java/com/sixkids/student/main/joinorganization/JoinOrganizationScreen.kt b/android/feature/student/main/src/main/java/com/sixkids/student/main/joinorganization/JoinOrganizationScreen.kt
new file mode 100644
index 00000000..753075a5
--- /dev/null
+++ b/android/feature/student/main/src/main/java/com/sixkids/student/main/joinorganization/JoinOrganizationScreen.kt
@@ -0,0 +1,125 @@
+package com.sixkids.student.main.joinorganization
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+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.material3.Text
+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 androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.sixkids.designsystem.component.button.UlbanFilledButton
+import com.sixkids.designsystem.component.screen.UlbanTopSection
+import com.sixkids.designsystem.component.textfield.UlbanUnderLineTextField
+import com.sixkids.designsystem.theme.UlbanTheme
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.student.main.R
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.extension.collectWithLifecycle
+
+@Composable
+fun JoinOrganizationRoute(
+ viewModel: JoinOrganizationViewModel = hiltViewModel(),
+ navigateToStudentOrganizationList: () -> Unit,
+ onBackClick: () -> Unit,
+ onShowSnackBar: (SnackbarToken) -> Unit,
+) {
+ val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
+
+ viewModel.sideEffect.collectWithLifecycle { sideEffect ->
+ when (sideEffect) {
+ JoinOrganizationEffect.NavigateToOrganizationList -> navigateToStudentOrganizationList()
+ is JoinOrganizationEffect.OnShowSnackBar -> onShowSnackBar(sideEffect.tkn)
+ }
+ }
+
+ JoinOrganizationScreen(
+ uiState = uiState,
+ onJoinOrganizationClick = viewModel::joinOrganizationClick,
+ onBackClick = onBackClick,
+ onUpdateCode = viewModel::updateCode,
+ onUpdateId = viewModel::updateId
+ )
+}
+
+@Composable
+fun JoinOrganizationScreen(
+ paddingValues: PaddingValues = PaddingValues(20.dp),
+ uiState: JoinOrganizationState = JoinOrganizationState(),
+ onJoinOrganizationClick: () -> Unit = {},
+ onBackClick: () -> Unit = {},
+ onUpdateCode: (String) -> Unit = {},
+ onUpdateId: (String) -> Unit = {},
+){
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize(),
+ horizontalAlignment = Alignment.Start,
+ ) {
+ UlbanTopSection(stringResource(R.string.join_organization_top), onBackClick)
+
+ Spacer(modifier = Modifier.height(36.dp))
+
+ Text(
+ text = stringResource(id = R.string.join_organization_title),
+ style = UlbanTypography.bodyLarge,
+ modifier = Modifier.padding(top = 10.dp, bottom = 10.dp)
+ )
+
+ UlbanUnderLineTextField(
+ text = uiState.code,
+ hint = stringResource(R.string.join_organization_code_hint),
+ onTextChange = onUpdateCode,
+ onIconClick = {
+ onUpdateCode("")
+ }
+ )
+
+ Spacer(modifier = Modifier.height(10.dp))
+
+ Text(
+ text = stringResource(id = R.string.join_organization_id),
+ style = UlbanTypography.bodyLarge,
+ modifier = Modifier.padding(top = 10.dp, bottom = 10.dp)
+ )
+
+ UlbanUnderLineTextField(
+ text = uiState.id,
+ hint = stringResource(R.string.join_organization_id_hint),
+ onTextChange = onUpdateId,
+ onIconClick = {
+ onUpdateId("")
+ }
+ )
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ UlbanFilledButton(
+ text = stringResource(R.string.profile_done),
+ onClick = { onJoinOrganizationClick() },
+ modifier = Modifier.fillMaxWidth())
+ }
+ }
+}
+
+@Composable
+@Preview(showBackground = true)
+fun JoinOrganizationScreenPreview() {
+ UlbanTheme {
+ JoinOrganizationScreen()
+ }
+}
\ No newline at end of file
diff --git a/android/feature/student/main/src/main/java/com/sixkids/student/main/joinorganization/JoinOrganizationViewModel.kt b/android/feature/student/main/src/main/java/com/sixkids/student/main/joinorganization/JoinOrganizationViewModel.kt
new file mode 100644
index 00000000..83a7b687
--- /dev/null
+++ b/android/feature/student/main/src/main/java/com/sixkids/student/main/joinorganization/JoinOrganizationViewModel.kt
@@ -0,0 +1,36 @@
+package com.sixkids.student.main.joinorganization
+
+import androidx.lifecycle.viewModelScope
+import com.sixkids.domain.usecase.organization.JoinOrganizationUseCase
+import com.sixkids.ui.base.BaseViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class JoinOrganizationViewModel @Inject constructor(
+ private val joinOrganizationUseCase: JoinOrganizationUseCase
+): BaseViewModel(JoinOrganizationState()){
+
+ fun updateCode(code: String){
+ intent { copy(code = code) }
+ }
+
+ fun updateId(id: String){
+ intent { copy(id = id) }
+ }
+
+ fun joinOrganizationClick(){
+ viewModelScope.launch {
+ intent { copy(isLoading = true) }
+ joinOrganizationUseCase(uiState.value.id.toInt(), uiState.value.code)
+ .onSuccess {
+ if (it>0){
+ postSideEffect(JoinOrganizationEffect.NavigateToOrganizationList)
+ }
+ }.onFailure {
+
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/feature/student/main/src/main/java/com/sixkids/student/main/navigation/StudentMainNavigation.kt b/android/feature/student/main/src/main/java/com/sixkids/student/main/navigation/StudentMainNavigation.kt
new file mode 100644
index 00000000..250a0a4c
--- /dev/null
+++ b/android/feature/student/main/src/main/java/com/sixkids/student/main/navigation/StudentMainNavigation.kt
@@ -0,0 +1,62 @@
+package com.sixkids.student.main.navigation
+
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.compose.composable
+import com.sixkids.student.main.joinorganization.JoinOrganizationRoute
+import com.sixkids.student.main.organization.StudentOrganizationListRoute
+import com.sixkids.student.main.profile.StudentProfileRoute
+import com.sixkids.ui.SnackbarToken
+
+fun NavController.navigateStudentOrganizationList() {
+ navigate(StudentMainRoute.defaultRoute)
+}
+
+fun NavController.navigateStudentProfile(){
+ navigate(StudentMainRoute.profileRoute)
+}
+
+fun NavController.navigateJoinOrganization(){
+ navigate(StudentMainRoute.joinOrganizationRoute)
+}
+
+fun NavGraphBuilder.studentOrganizationListNavGraph(
+ navigateToJoinOrganization: () -> Unit,
+ navigateToProfile: () -> Unit,
+ navigateToHome: () -> Unit,
+ navigateToSignIn: () -> Unit,
+ onShowSnackBar: (SnackbarToken) -> Unit,
+ onBackClick: () -> Unit
+) {
+ composable(route = StudentMainRoute.defaultRoute) {
+ StudentOrganizationListRoute(
+ navigateToJoinOrganization = navigateToJoinOrganization,
+ navigateToProfile = navigateToProfile,
+ navigateToHome = navigateToHome,
+ onShowSnackBar = onShowSnackBar
+ )
+ }
+
+ composable(route = StudentMainRoute.joinOrganizationRoute) {
+ JoinOrganizationRoute(
+ navigateToStudentOrganizationList = onBackClick,
+ onBackClick = onBackClick,
+ onShowSnackBar = onShowSnackBar
+ )
+ }
+
+ composable(route = StudentMainRoute.profileRoute) {
+ StudentProfileRoute(
+ navigateToSignIn = navigateToSignIn,
+ navigateToStudentOrganizationList = onBackClick,
+ onBackClick = onBackClick,
+ onShowSnackBar = onShowSnackBar
+ )
+ }
+}
+
+object StudentMainRoute {
+ const val defaultRoute = "student-organization-list"
+ const val joinOrganizationRoute = "join-organization"
+ const val profileRoute = "student-profile"
+}
\ No newline at end of file
diff --git a/android/feature/student/main/src/main/java/com/sixkids/student/main/organization/OrganizationContract.kt b/android/feature/student/main/src/main/java/com/sixkids/student/main/organization/OrganizationContract.kt
new file mode 100644
index 00000000..ffa85a2d
--- /dev/null
+++ b/android/feature/student/main/src/main/java/com/sixkids/student/main/organization/OrganizationContract.kt
@@ -0,0 +1,21 @@
+package com.sixkids.student.main.organization
+
+import com.sixkids.model.Organization
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
+
+sealed interface OrganizationListEffect : SideEffect {
+ data object NavigateToJoinClass : OrganizationListEffect
+ data object NavigateToProfile : OrganizationListEffect
+ data object NavigateToHome : OrganizationListEffect
+ data class OnShowSnackBar(val tkn: SnackbarToken) : OrganizationListEffect
+}
+
+data class OrganizationListState(
+ val isLoading: Boolean = false,
+ val name: String = "",
+ val profilePhoto: String = "",
+ val organizationList: List = emptyList(),
+) : UiState
\ No newline at end of file
diff --git a/android/feature/student/main/src/main/java/com/sixkids/student/main/organization/OrganizationScreen.kt b/android/feature/student/main/src/main/java/com/sixkids/student/main/organization/OrganizationScreen.kt
new file mode 100644
index 00000000..ee864a5c
--- /dev/null
+++ b/android/feature/student/main/src/main/java/com/sixkids/student/main/organization/OrganizationScreen.kt
@@ -0,0 +1,299 @@
+package com.sixkids.student.main.organization
+
+
+import android.util.Log
+import androidx.compose.foundation.ExperimentalFoundationApi
+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.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.PagerState
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.AccountCircle
+import androidx.compose.material.icons.outlined.Add
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.lerp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import coil.compose.AsyncImage
+import com.google.android.gms.tasks.OnCompleteListener
+import com.google.firebase.messaging.FirebaseMessaging
+import com.sixkids.designsystem.component.screen.LoadingScreen
+import com.sixkids.designsystem.theme.Blue
+import com.sixkids.designsystem.theme.BlueDark
+import com.sixkids.designsystem.theme.Green
+import com.sixkids.designsystem.theme.Orange
+import com.sixkids.designsystem.theme.Purple
+import com.sixkids.designsystem.theme.Red
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.designsystem.theme.Yellow
+import com.sixkids.model.Organization
+import com.sixkids.student.main.R
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.extension.collectWithLifecycle
+import kotlin.math.absoluteValue
+
+private const val TAG = "D107"
+@Composable
+fun StudentOrganizationListRoute(
+ viewModel: OrganizationViewModel = hiltViewModel(),
+ navigateToJoinOrganization: () -> Unit,
+ navigateToProfile: () -> Unit,
+ navigateToHome: () -> Unit,
+ onShowSnackBar: (SnackbarToken) -> Unit
+) {
+ val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
+
+ viewModel.sideEffect.collectWithLifecycle { sideEffect ->
+ when (sideEffect) {
+ OrganizationListEffect.NavigateToJoinClass -> navigateToJoinOrganization()
+ OrganizationListEffect.NavigateToProfile -> navigateToProfile()
+ OrganizationListEffect.NavigateToHome -> navigateToHome()
+ is OrganizationListEffect.OnShowSnackBar -> onShowSnackBar(sideEffect.tkn)
+ }
+ }
+
+ LaunchedEffect(key1 = Unit) {
+ viewModel.initData()
+ }
+
+ OrganizationListScreen(
+ uiState = uiState,
+ onJoinClassClick = viewModel::joinOrganizationClick,
+ onProfileClick = viewModel::profileClick,
+ onClassClick = { classId ->
+ viewModel.organizationClick(classId)
+ }
+ )
+
+
+
+ FirebaseMessaging.getInstance().token.addOnCompleteListener(
+ OnCompleteListener { task ->
+ if (!task.isSuccessful) {
+ return@OnCompleteListener
+ }
+ if (task.result != null) {
+ viewModel.onTokenRefresh(task.result)
+ }
+ },
+ )
+
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun OrganizationListScreen(
+ uiState: OrganizationListState = OrganizationListState(),
+ onJoinClassClick: () -> Unit = {},
+ onProfileClick: () -> Unit = {},
+ onClassClick: (Int) -> Unit = {}
+) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ val pagerState = rememberPagerState(pageCount = { uiState.organizationList.size })
+ Icon(
+ imageVector = Icons.Outlined.AccountCircle,
+ contentDescription = "profile",
+ modifier = Modifier
+ .padding(24.dp)
+ .size(40.dp)
+ .align(Alignment.End)
+ .clickable { onProfileClick() },
+ )
+
+ StudentUserInfoSection(name = uiState.name, photo = uiState.profilePhoto)
+
+ OrganizationListSection(
+ pagerState = pagerState,
+ organizationList = uiState.organizationList,
+ onClassClick = onClassClick
+ )
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ NewClassButton(
+ Modifier
+ .padding(18.dp, 28.dp)
+ .align(Alignment.End),
+ onNewClassClick = onJoinClassClick
+ )
+
+ }
+ if (uiState.isLoading) {
+ LoadingScreen()
+ }
+ }
+
+}
+
+@Composable
+fun StudentUserInfoSection(name: String, photo: String) {
+ Log.d(TAG, "UserInfoSection: ")
+ Column {
+ AsyncImage(
+ model = photo,
+ contentDescription = "profile image",
+ modifier = Modifier
+ .padding(20.dp)
+ .size(200.dp)
+ .clip(RoundedCornerShape(16.dp))
+ .align(Alignment.CenterHorizontally),
+ contentScale = ContentScale.Crop
+ )
+
+ Text(
+ text = String.format(stringResource(id = R.string.student_organization_welcome), name).also {
+ Log.d(TAG, "UserInfoSection: $it")
+ },
+ style = UlbanTypography.titleMedium,
+ modifier = Modifier
+ .padding(0.dp, 0.dp, 0.dp, 60.dp)
+ .align(Alignment.CenterHorizontally)
+ )
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun OrganizationListSection(
+ pagerState: PagerState,
+ organizationList: List,
+ onClassClick: (Int) -> Unit
+) {
+ val backgroundColorList = listOf(Red, Blue, Orange, Yellow, Green, Purple)
+
+ val screenWidthDp = with(LocalDensity.current) {
+ LocalContext.current.resources.displayMetrics.widthPixels.toDp()
+ }
+ val cardWidth = 220.dp
+ val horizontalPadding = (screenWidthDp - cardWidth) / 2
+
+ if (organizationList.isEmpty()) {
+ Text(
+ text = stringResource(id = R.string.student_organization_no_organization),
+ style = UlbanTypography.titleMedium,
+ )
+ } else {
+
+ HorizontalPager(
+ pageSpacing = 10.dp,
+ state = pagerState,
+ contentPadding = PaddingValues(horizontal = horizontalPadding),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ val item = organizationList[it]
+ val name = item.name.split("\n")
+ Card(
+ modifier = Modifier
+ .padding(10.dp)
+ .size(cardWidth)
+ .clickable { onClassClick(item.id) }
+ .graphicsLayer {
+ val pageOffset = (
+ (pagerState.currentPage - it) + pagerState
+ .currentPageOffsetFraction
+ ).absoluteValue
+
+ alpha = lerp(
+ start = 0.5f,
+ stop = 1f,
+ fraction = 1f - pageOffset.coerceIn(0f, 1f)
+ )
+ },
+ shape = RoundedCornerShape(20.dp),
+ colors = CardDefaults.cardColors(containerColor = backgroundColorList[it % backgroundColorList.size]),
+ elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
+ ) {
+ Column(
+ modifier = Modifier
+ .padding(20.dp, 40.dp)
+ .fillMaxSize(),
+ verticalArrangement = Arrangement.SpaceBetween,
+ horizontalAlignment = Alignment.Start
+ ) {
+ Text(text = name[0], style = UlbanTypography.titleMedium)
+ Text(
+ text = name[1],
+ style = UlbanTypography.titleMedium.copy(fontWeight = FontWeight.SemiBold)
+ )
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Image(
+ painter = painterResource(id = com.sixkids.designsystem.R.drawable.member),
+ contentDescription = "member count"
+ )
+
+ Text(
+ text = "${item.memberCount}명",
+ style = UlbanTypography.bodyLarge.copy(fontWeight = FontWeight.SemiBold)
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun NewClassButton(
+ modifier: Modifier = Modifier,
+ onNewClassClick: () -> Unit
+) {
+ Button(
+ onClick = { onNewClassClick() },
+ modifier = modifier,
+ shape = RoundedCornerShape(16.dp),
+ colors = ButtonDefaults.buttonColors(Blue, contentColor = BlueDark),
+ elevation = ButtonDefaults.buttonElevation(defaultElevation = 4.dp, pressedElevation = 8.dp)
+ ) {
+ Row(modifier = Modifier.padding(10.dp), verticalAlignment = Alignment.CenterVertically) {
+ Icon(imageVector = Icons.Outlined.Add, contentDescription = "new class")
+ Spacer(modifier = Modifier.width(2.dp))
+ Text(
+ text = stringResource(id = R.string.student_organization_join_class),
+ style = UlbanTypography.bodyMedium.copy(fontWeight = FontWeight.SemiBold)
+ )
+ }
+ }
+}
+
+@Composable
+@Preview(showBackground = true)
+fun OrganizationListScreenPreview() {
+ OrganizationListScreen()
+}
diff --git a/android/feature/student/main/src/main/java/com/sixkids/student/main/organization/OrganizationViewModel.kt b/android/feature/student/main/src/main/java/com/sixkids/student/main/organization/OrganizationViewModel.kt
new file mode 100644
index 00000000..2838d1f4
--- /dev/null
+++ b/android/feature/student/main/src/main/java/com/sixkids/student/main/organization/OrganizationViewModel.kt
@@ -0,0 +1,76 @@
+package com.sixkids.student.main.organization
+
+import android.util.Log
+import androidx.lifecycle.viewModelScope
+import com.sixkids.domain.usecase.organization.GetOrganizationListUseCase
+import com.sixkids.domain.usecase.organization.SaveSelectedOrganizationIdUseCase
+import com.sixkids.domain.usecase.organization.SaveSelectedOrganizationNameUseCase
+import com.sixkids.domain.usecase.user.GetUserInfoUseCase
+import com.sixkids.domain.usecase.user.UpdateFCMTokenUseCase
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.base.BaseViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+private const val TAG = "D107"
+@HiltViewModel
+class OrganizationViewModel @Inject constructor(
+ private val getUserInfoUseCase: GetUserInfoUseCase,
+ private val getOrganizationListUseCase: GetOrganizationListUseCase,
+ private val saveSelectedOrganizationIdUseCase: SaveSelectedOrganizationIdUseCase,
+ private val saveSelectedOrganizationNameUseCase: SaveSelectedOrganizationNameUseCase,
+ private val updateFCMTokenUseCase: UpdateFCMTokenUseCase
+) : BaseViewModel(OrganizationListState()){
+
+ fun initData() {
+ viewModelScope.launch {
+ intent { copy(isLoading = true) }
+
+ val userInfoJob = async { getUserInfoUseCase() }
+ val organizationListJob = async { getOrganizationListUseCase() }
+
+ val userInfoResult = userInfoJob.await()
+ .onSuccess {
+ intent { copy(name = it.name, profilePhoto = it.photo) }
+ }.onFailure {
+ postSideEffect(OrganizationListEffect.OnShowSnackBar(SnackbarToken(message = it.message ?: "알 수 없는 오류가 발생했습니다.")))
+ }
+ val organizationListResult = organizationListJob.await()
+ .onSuccess {
+ intent { copy(organizationList = it) }
+ }.onFailure {
+ postSideEffect(OrganizationListEffect.OnShowSnackBar(SnackbarToken(message = it.message ?: "알 수 없는 오류가 발생했습니다.")))
+ }
+ intent { copy(isLoading = false) }
+ }
+ }
+
+ fun joinOrganizationClick(){
+ postSideEffect(OrganizationListEffect.NavigateToJoinClass)
+ }
+
+ fun profileClick(){
+ postSideEffect(OrganizationListEffect.NavigateToProfile)
+ }
+
+ fun organizationClick(id: Int){
+ viewModelScope.launch {
+ saveSelectedOrganizationIdUseCase(id)
+ currentState.organizationList.find { it.id == id }?.let {
+ saveSelectedOrganizationNameUseCase(it.name)
+ }
+ postSideEffect(OrganizationListEffect.NavigateToHome)
+ }
+
+ }
+
+ fun onTokenRefresh(fcmToken: String) {
+ viewModelScope.launch {
+ updateFCMTokenUseCase(fcmToken).onFailure {
+ Log.d(TAG, "onTokenRefresh: 토큰 갱신 실패 ${it.message}")
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/feature/student/main/src/main/java/com/sixkids/student/main/profile/ProfileContract.kt b/android/feature/student/main/src/main/java/com/sixkids/student/main/profile/ProfileContract.kt
new file mode 100644
index 00000000..eb8cb1f5
--- /dev/null
+++ b/android/feature/student/main/src/main/java/com/sixkids/student/main/profile/ProfileContract.kt
@@ -0,0 +1,26 @@
+package com.sixkids.student.main.profile
+
+import android.graphics.Bitmap
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
+sealed interface ProfileEffect : SideEffect {
+ data object NavigateToSignIn : ProfileEffect
+ data object NavigateToOrganizationList : ProfileEffect
+ data class OnShowSnackBar(val tkn: SnackbarToken) : ProfileEffect
+}
+
+data class ProfileState(
+ val isLoading: Boolean = false,
+ val name: String = "",
+ val gender: Gender? = null,
+ val originalProfilePhoto: String? = null,
+ val changedProfileDefaultPhoto: Int? = null,
+ val changedProfileUserPhoto: Bitmap? = null
+) : UiState
+
+enum class Gender{
+ MAN,
+ WOMAN
+}
\ No newline at end of file
diff --git a/android/feature/student/main/src/main/java/com/sixkids/student/main/profile/ProfileScreen.kt b/android/feature/student/main/src/main/java/com/sixkids/student/main/profile/ProfileScreen.kt
new file mode 100644
index 00000000..0d64e1ff
--- /dev/null
+++ b/android/feature/student/main/src/main/java/com/sixkids/student/main/profile/ProfileScreen.kt
@@ -0,0 +1,356 @@
+package com.sixkids.student.main.profile
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.ImageDecoder
+import android.os.Build
+import android.provider.MediaStore
+import android.util.Log
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.PickVisualMediaRequest
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+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.aspectRatio
+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.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import coil.compose.AsyncImage
+import com.sixkids.designsystem.R as DesignSystemR
+import com.sixkids.designsystem.component.button.UlbanFilledButton
+import com.sixkids.designsystem.component.screen.LoadingScreen
+import com.sixkids.designsystem.component.screen.UlbanTopSection
+import com.sixkids.designsystem.theme.Cream
+import com.sixkids.designsystem.theme.Red
+import com.sixkids.designsystem.theme.RedDark
+import com.sixkids.designsystem.theme.UlbanTheme
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.student.main.R
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.extension.collectWithLifecycle
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+
+private const val TAG = "D107"
+
+@Composable
+fun StudentProfileRoute(
+ viewModel: ProfileViewModel = hiltViewModel(),
+ navigateToStudentOrganizationList: () -> Unit,
+ navigateToSignIn: () -> Unit,
+ onShowSnackBar: (SnackbarToken) -> Unit,
+ onBackClick: () -> Unit
+) {
+ val context = LocalContext.current
+ val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
+
+ LaunchedEffect(key1 = Unit) {
+ viewModel.initData()
+ }
+
+ val launcher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.PickVisualMedia()
+ ) { uri ->
+ uri?.let {
+ try {
+ val bitmap = if (Build.VERSION.SDK_INT < 28) {
+ MediaStore.Images.Media.getBitmap(context.contentResolver, it)
+ } else {
+ ImageDecoder.decodeBitmap(
+ ImageDecoder.createSource(
+ context.contentResolver,
+ it
+ )
+ )
+ }
+ viewModel.onProfilePhotoSelected(bitmap)
+ } catch (e: IOException) {
+ Log.e(TAG, "Error decoding bitmap", e)
+ }
+ }
+ }
+
+ viewModel.sideEffect.collectWithLifecycle {
+ when (it) {
+ ProfileEffect.NavigateToOrganizationList -> navigateToStudentOrganizationList()
+ is ProfileEffect.OnShowSnackBar -> onShowSnackBar(it.tkn)
+ ProfileEffect.NavigateToSignIn -> navigateToSignIn()
+ }
+ }
+
+ StudentProfileScreen(
+ uiState = uiState,
+ onClickPhoto = { resId ->
+ when (resId) {
+ DesignSystemR.drawable.camera ->
+ launcher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
+
+ DesignSystemR.drawable.teacher_man ->
+ viewModel.onProfileDefaultPhotoSelected(resId, Gender.MAN)
+
+ DesignSystemR.drawable.teacher_woman ->
+ viewModel.onProfileDefaultPhotoSelected(resId, Gender.WOMAN)
+ }
+ },
+ onDoneClick = {
+ viewModel.onChangeDoneClick(
+ saveBitmapToFile(context, uiState.changedProfileUserPhoto, "profile.jpg")
+ )
+ },
+ onSignOutClick = viewModel::onSignOutClick,
+ onBackClick = onBackClick
+ )
+
+}
+
+@Composable
+fun StudentProfileScreen(
+ uiState: ProfileState = ProfileState(),
+ onClickPhoto: (Int) -> Unit = {},
+ onDoneClick: () -> Unit = {},
+ onSignOutClick: () -> Unit = {},
+ onBackClick: () -> Unit = {}
+) {
+ val imageMan = DesignSystemR.drawable.student_boy
+ val imageWoman = DesignSystemR.drawable.student_girl
+
+ Box(modifier = Modifier.fillMaxSize()) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(21.dp)
+ ) {
+ UlbanTopSection(
+ stringResource(id = R.string.student_profile_welcome, uiState.name),
+ onBackClick
+ )
+
+ Spacer(modifier = Modifier.height(60.dp))
+
+ SelectedPhotoCard(
+ uiState.changedProfileDefaultPhoto,
+ uiState.originalProfilePhoto,
+ uiState.changedProfileUserPhoto,
+ modifier = Modifier
+ .padding(10.dp)
+ .size(180.dp)
+ .align(Alignment.CenterHorizontally),
+ )
+
+ Spacer(modifier = Modifier.height(60.dp))
+
+ Row {
+ PhotoCard(
+ modifier = Modifier
+ .padding(10.dp)
+ .weight(1f)
+ .aspectRatio(1f),
+ img = imageMan,
+ onClickPhoto = onClickPhoto
+ )
+
+ PhotoCard(
+ modifier = Modifier
+ .padding(10.dp)
+ .weight(1f)
+ .aspectRatio(1f),
+ img = imageWoman,
+ onClickPhoto = onClickPhoto
+ )
+
+ PhotoCard(
+ modifier = Modifier
+ .padding(10.dp)
+ .weight(1f)
+ .aspectRatio(1f),
+ img = DesignSystemR.drawable.camera,
+ onClickPhoto = onClickPhoto
+ )
+ }
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ BottomSection(onDoneClick = onDoneClick, onSignOutClick = onSignOutClick)
+
+ }
+ if (uiState.isLoading) {
+ LoadingScreen()
+ }
+ }
+}
+
+@Composable
+fun BottomSection(
+ onDoneClick: () -> Unit,
+ onSignOutClick: () -> Unit,
+ onExitClick: () -> Unit = { }
+) {
+ Column {
+ UlbanFilledButton(
+ text = stringResource(id = R.string.profile_done),
+ onClick = onDoneClick,
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ Spacer(modifier = Modifier.height(4.dp))
+
+ UlbanFilledButton(
+ text = stringResource(id = R.string.profile_sign_out),
+ onClick = onSignOutClick,
+ modifier = Modifier.fillMaxWidth(),
+ color = Red,
+ textColor = RedDark
+ )
+
+ Text(
+ text = stringResource(id = R.string.profile_exit),
+ style = UlbanTypography.titleSmall.copy(textDecoration = TextDecoration.Underline),
+ modifier = Modifier
+ .padding(10.dp)
+ .align(Alignment.CenterHorizontally)
+ .clickable { onExitClick() }
+ )
+ }
+}
+
+@Composable
+fun SelectedPhotoCard(
+ defaultImage: Int?,
+ originalImage: String?,
+ bitmap: Bitmap?,
+ modifier: Modifier = Modifier
+) {
+ Card(
+ elevation = CardDefaults.cardElevation(
+ defaultElevation = 4.dp,
+ pressedElevation = 8.dp
+ ),
+ colors = CardDefaults.cardColors(
+ containerColor = Cream
+ ),
+ modifier = modifier,
+ ) {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ ) {
+ if (originalImage == null && bitmap == null && defaultImage == null) {
+ Image(
+ painter = painterResource(
+ id = DesignSystemR.drawable.teacher_man
+ ),
+ contentDescription = "selected photo",
+ modifier = Modifier.fillMaxSize(),
+ )
+
+ } else if (originalImage != null) {
+ if (bitmap == null && defaultImage == null) {
+ AsyncImage(
+ model = originalImage,
+ contentDescription = "original image",
+ modifier = Modifier.fillMaxWidth(),
+ contentScale = ContentScale.Crop
+ )
+ } else {
+ if (bitmap != null) {
+ Image(
+ bitmap = bitmap.asImageBitmap(),
+ contentDescription = "selected photo",
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.Crop
+ )
+ } else {
+ Image(
+ painter = painterResource(
+ id = defaultImage!!
+ ),
+ contentDescription = "selected photo",
+ modifier = Modifier.fillMaxSize(),
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun PhotoCard(modifier: Modifier = Modifier, img: Int, onClickPhoto: (Int) -> Unit) {
+ Card(
+ elevation = CardDefaults.cardElevation(
+ defaultElevation = 4.dp,
+ pressedElevation = 8.dp
+ ),
+ colors = CardDefaults.cardColors(
+ containerColor = Cream
+ ),
+ modifier = modifier.clickable {
+ onClickPhoto(img)
+ },
+ ) {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ ) {
+ Image(
+ painter = painterResource(id = img),
+ contentDescription = "profile",
+ modifier = Modifier,
+ )
+ }
+ }
+}
+
+fun saveBitmapToFile(context: Context, bitmap: Bitmap?, fileName: String): File? {
+ val directory = context.getExternalFilesDir(null) ?: return null
+ if (bitmap == null) return null
+ val file = File(directory, fileName)
+ var fileOutputStream: FileOutputStream? = null
+
+ try {
+ fileOutputStream = FileOutputStream(file)
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fileOutputStream)
+ fileOutputStream.flush()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return null
+ } finally {
+ try {
+ fileOutputStream?.close()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ return file
+}
+
+@Composable
+@Preview(showBackground = true)
+fun TeacherProfileScreenPreview() {
+ UlbanTheme {
+ StudentProfileScreen()
+ }
+}
\ No newline at end of file
diff --git a/android/feature/student/main/src/main/java/com/sixkids/student/main/profile/ProfileViewModel.kt b/android/feature/student/main/src/main/java/com/sixkids/student/main/profile/ProfileViewModel.kt
new file mode 100644
index 00000000..c808379f
--- /dev/null
+++ b/android/feature/student/main/src/main/java/com/sixkids/student/main/profile/ProfileViewModel.kt
@@ -0,0 +1,116 @@
+package com.sixkids.student.main.profile
+
+import android.graphics.Bitmap
+import android.util.Log
+import androidx.annotation.DrawableRes
+import androidx.lifecycle.viewModelScope
+import com.sixkids.domain.usecase.user.GetUserInfoUseCase
+import com.sixkids.domain.usecase.user.SignOutUseCase
+import com.sixkids.domain.usecase.user.UpdateUserProfilePhotoUseCase
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.base.BaseViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import java.io.File
+import javax.inject.Inject
+
+private const val TAG = "D107"
+
+@HiltViewModel
+class ProfileViewModel @Inject constructor(
+ private val getUserInfoUseCase: GetUserInfoUseCase,
+ private val updateUserProfilePhotoUseCase: UpdateUserProfilePhotoUseCase,
+ private val signOutUseCase: SignOutUseCase
+): BaseViewModel(ProfileState()){
+ fun initData() {
+ viewModelScope.launch {
+ getUserInfoUseCase()
+ .onSuccess {
+ intent {
+ copy(
+ name = it.name,
+ originalProfilePhoto = it.photo
+ )
+ }
+ }.onFailure {
+ postSideEffect(
+ ProfileEffect.OnShowSnackBar(
+ SnackbarToken(
+ it.message ?: "알 수 없는 오류가 발생했습니다."
+ )
+ )
+ )
+ }
+
+ }
+ }
+
+ fun onProfilePhotoSelected(bitmap: Bitmap) {
+ intent {
+ copy(
+ changedProfileUserPhoto = bitmap,
+ changedProfileDefaultPhoto = null,
+ gender = null
+ )
+ }
+ }
+
+ fun onProfileDefaultPhotoSelected(@DrawableRes photo: Int, gender: Gender) {
+ intent {
+ copy(
+ changedProfileDefaultPhoto = photo,
+ changedProfileUserPhoto = null,
+ gender = gender
+ )
+ }
+ }
+
+ fun onChangeDoneClick(newProfilePhoto: File?) {
+ viewModelScope.launch {
+ intent { copy(isLoading = true) }
+ var defaultImage = 0
+ if (newProfilePhoto == null && uiState.value.changedProfileDefaultPhoto == null) {
+ // 변경사항 없음 뒤로가기
+ postSideEffect(ProfileEffect.NavigateToOrganizationList)
+ } else {
+ defaultImage = when (newProfilePhoto) {
+ null -> {
+ when (uiState.value.gender) {
+ null -> 0
+ Gender.MAN -> 3
+ Gender.WOMAN -> 4
+ }
+ }
+ else -> 0
+ }
+ }
+
+ updateUserProfilePhotoUseCase(newProfilePhoto, defaultImage)
+ .onSuccess {
+ postSideEffect(ProfileEffect.NavigateToOrganizationList)
+ }.onFailure {
+ Log.d(TAG, "onChangeDoneClick: ${it.message}")
+ postSideEffect(
+ ProfileEffect.OnShowSnackBar(
+ SnackbarToken(
+ it.message ?: "알 수 없는 오류가 발생했습니다."
+ )
+ )
+ )
+ }
+ intent { copy(isLoading = false) }
+ }
+ }
+
+ fun onSignOutClick() {
+ viewModelScope.launch {
+ if(signOutUseCase()){
+ postSideEffect(ProfileEffect.NavigateToSignIn)
+ }else{
+ ProfileEffect.OnShowSnackBar(
+ SnackbarToken("로그아웃에 실패했습니다. 다시 시도해주세요.")
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/feature/student/main/src/main/res/values/strings.xml b/android/feature/student/main/src/main/res/values/strings.xml
new file mode 100644
index 00000000..d16af1f5
--- /dev/null
+++ b/android/feature/student/main/src/main/res/values/strings.xml
@@ -0,0 +1,20 @@
+
+
+
+ %s 학생 환영합니다
+ 학급이 없습니다
+ 입장
+
+
+ 안녕하세요 %s 학생!
+ 완료
+ 로그아웃
+ 회원 탈퇴
+
+
+ 선생님께 받은 초대코드를 입력하세요
+ 선생님께 받은 학급 ID를 입력하세요
+ 학급 가입
+ 초대 코드
+ 초대 코드
+
\ No newline at end of file
diff --git a/android/feature/student/main/src/test/java/com/sixkids/student/main/ExampleUnitTest.kt b/android/feature/student/main/src/test/java/com/sixkids/student/main/ExampleUnitTest.kt
new file mode 100644
index 00000000..cc209d45
--- /dev/null
+++ b/android/feature/student/main/src/test/java/com/sixkids/student/main/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.sixkids.student.main
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/android/feature/student/relay/build.gradle.kts b/android/feature/student/relay/build.gradle.kts
index 02cba040..99f78b85 100644
--- a/android/feature/student/relay/build.gradle.kts
+++ b/android/feature/student/relay/build.gradle.kts
@@ -1,5 +1,6 @@
plugins {
alias(libs.plugins.sixkids.android.feature.compose)
+ kotlin("plugin.serialization") version "1.9.24"
}
android {
@@ -7,4 +8,9 @@ android {
}
dependencies {
+ implementation(libs.bundles.paging)
+
+ implementation(libs.kotlinx.serialization.json)
+
+ implementation(projects.core.nfc)
}
diff --git a/android/feature/student/relay/src/main/java/com/sixkids/student/relay/create/RelayCreateContract.kt b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/create/RelayCreateContract.kt
new file mode 100644
index 00000000..2359b180
--- /dev/null
+++ b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/create/RelayCreateContract.kt
@@ -0,0 +1,16 @@
+package com.sixkids.student.relay.create
+
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
+data class RelayCreateState(
+ val isLoading: Boolean = false,
+ val question: String = "",
+ val orgId: Int = -1
+) : UiState
+
+sealed interface RelayCreateEffect: SideEffect {
+ data object NavigateToRelayResult : RelayCreateEffect
+ data class OnShowSnackBar(val tkn : SnackbarToken) : RelayCreateEffect
+}
\ No newline at end of file
diff --git a/android/feature/student/relay/src/main/java/com/sixkids/student/relay/create/RelayCreateScreen.kt b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/create/RelayCreateScreen.kt
new file mode 100644
index 00000000..c23846f9
--- /dev/null
+++ b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/create/RelayCreateScreen.kt
@@ -0,0 +1,101 @@
+package com.sixkids.student.relay.create
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+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.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 androidx.hilt.navigation.compose.hiltViewModel
+import com.sixkids.designsystem.component.button.UlbanFilledButton
+import com.sixkids.designsystem.component.screen.UlbanTopSection
+import com.sixkids.designsystem.theme.UlbanTheme
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.student.relay.R
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.extension.collectWithLifecycle
+import com.sixkids.designsystem.R as DesignSystemR
+
+@Composable
+fun RelayCreateRoute(
+ viewModel: RelayCreateViewModel = hiltViewModel(),
+ navigateToRelayResult: () -> Unit,
+ onBackClick: () -> Unit,
+ onShowSnackBar: (SnackbarToken) -> Unit
+) {
+ viewModel.sideEffect.collectWithLifecycle { sideEffect ->
+ when (sideEffect) {
+ is RelayCreateEffect.NavigateToRelayResult -> navigateToRelayResult()
+ is RelayCreateEffect.OnShowSnackBar -> onShowSnackBar(sideEffect.tkn)
+ }
+ }
+
+ LaunchedEffect(key1 = Unit) {
+ viewModel.init()
+ }
+
+ RelayCreateScreen(
+ onNewRelayClick = viewModel::newRelayClick,
+ onBackClick = onBackClick,
+ )
+
+}
+
+@Composable
+fun RelayCreateScreen(
+ paddingValues: PaddingValues = PaddingValues(20.dp),
+ onNewRelayClick: () -> Unit = {},
+ onBackClick: () -> Unit = {}
+) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ UlbanTopSection(stringResource(id = R.string.relay_create_topsection), onBackClick)
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ Image(
+ painter = painterResource(id = DesignSystemR.drawable.relay),
+ contentDescription = "relay",
+ modifier = Modifier.padding(bottom = 20.dp).size(250.dp)
+ )
+
+ Text(text = "새로운 이어 달리기를 만듭니다!", style = UlbanTypography.titleMedium)
+
+ Spacer(modifier = Modifier.weight(2f))
+
+ UlbanFilledButton(
+ text = stringResource(R.string.relay_create_create),
+ onClick = { onNewRelayClick() },
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ }
+}
+
+@Composable
+@Preview(showBackground = true)
+fun RelayCreateScreenPreview() {
+ UlbanTheme {
+ RelayCreateScreen()
+ }
+}
\ No newline at end of file
diff --git a/android/feature/student/relay/src/main/java/com/sixkids/student/relay/create/RelayCreateViewModel.kt b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/create/RelayCreateViewModel.kt
new file mode 100644
index 00000000..4c0de27e
--- /dev/null
+++ b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/create/RelayCreateViewModel.kt
@@ -0,0 +1,50 @@
+package com.sixkids.student.relay.create
+
+import android.util.Log
+import androidx.lifecycle.viewModelScope
+import com.sixkids.domain.usecase.organization.GetSelectedOrganizationIdUseCase
+import com.sixkids.domain.usecase.relay.CreateRelayUseCase
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.base.BaseViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+private const val TAG = "D107"
+@HiltViewModel
+class RelayCreateViewModel @Inject constructor(
+ private val getSelectedOrganizationIdUseCase: GetSelectedOrganizationIdUseCase,
+ private val createRelayUseCase: CreateRelayUseCase
+) : BaseViewModel(RelayCreateState()) {
+
+ fun init() {
+ viewModelScope.launch {
+ getSelectedOrganizationIdUseCase()
+ .onSuccess {
+ intent { copy(orgId = it) }
+ }.onFailure {
+ postSideEffect(RelayCreateEffect.OnShowSnackBar(SnackbarToken("학급 정보를 불러오는데 실패했습니다")))
+ }
+ }
+ }
+
+ fun newRelayClick() {
+ viewModelScope.launch {
+ intent { copy(isLoading = true) }
+ if (uiState.value.orgId == -1){
+ init()
+ }else{
+ Log.d(TAG, "newRelayClick: ${uiState.value.question} ${uiState.value.orgId}")
+ createRelayUseCase(uiState.value.orgId, "첫번째 주자입니다!")
+ .onSuccess {
+ if (it>0){
+ postSideEffect(RelayCreateEffect.NavigateToRelayResult)
+ }
+
+ }.onFailure {
+ postSideEffect(RelayCreateEffect.OnShowSnackBar(SnackbarToken("이어 달리기 생성에 실패했습니다")))
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/feature/student/relay/src/main/java/com/sixkids/student/relay/detail/RelayDetailContract.kt b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/detail/RelayDetailContract.kt
new file mode 100644
index 00000000..e0871b22
--- /dev/null
+++ b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/detail/RelayDetailContract.kt
@@ -0,0 +1,15 @@
+package com.sixkids.student.relay.detail
+
+import com.sixkids.model.RelayDetail
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
+data class RelayDetailState(
+ val isLoading: Boolean = false,
+ val relayDetail: RelayDetail = RelayDetail(),
+) : UiState
+
+sealed interface RelayDetailSideEffect : SideEffect{
+ data class HandleException(val throwable: Throwable, val retry: () -> Unit) :
+ RelayDetailSideEffect
+}
\ No newline at end of file
diff --git a/android/feature/student/relay/src/main/java/com/sixkids/student/relay/detail/RelayDetailScreen.kt b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/detail/RelayDetailScreen.kt
new file mode 100644
index 00000000..5f61df2a
--- /dev/null
+++ b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/detail/RelayDetailScreen.kt
@@ -0,0 +1,149 @@
+package com.sixkids.student.relay.detail
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.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.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.sixkids.designsystem.component.appbar.UlbanDetailAppBar
+import com.sixkids.designsystem.component.item.UlbanRunnerItem
+import com.sixkids.designsystem.theme.Orange
+import com.sixkids.designsystem.theme.UlbanTheme
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.student.relay.R
+import com.sixkids.ui.extension.collectWithLifecycle
+import com.sixkids.ui.util.formatToMonthDayTime
+import com.sixkids.designsystem.R as DesignSystemR
+
+
+@Composable
+fun RelayDetailRoute(
+ viewModel: RelayDetailViewModel = hiltViewModel(),
+ handleException: (Throwable, () -> Unit) -> Unit
+){
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ viewModel.sideEffect.collectWithLifecycle {
+ when (it) {
+ is RelayDetailSideEffect.HandleException -> handleException(it.throwable, it.retry)
+ }
+ }
+ LaunchedEffect(key1 = Unit) {
+ viewModel.getRelayDetail()
+ }
+
+ RelayDetailScreen(
+ uiState = uiState
+ )
+
+}
+
+@Composable
+fun RelayDetailScreen(
+ uiState: RelayDetailState = RelayDetailState()
+) {
+ val listState = rememberLazyListState()
+ val isScrolled by remember {
+ derivedStateOf {
+ listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 100
+ }
+ }
+
+ Box(modifier = Modifier.fillMaxSize())
+ {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ ) {
+ UlbanDetailAppBar(
+ leftIcon = DesignSystemR.drawable.relay,
+ title = stringResource(id = R.string.relay_challenge),
+ content = stringResource(id = R.string.relay_challenge),
+ topDescription = "${uiState.relayDetail.startTime.formatToMonthDayTime()} ~ ${uiState.relayDetail.endTime.formatToMonthDayTime()}",
+ bottomDescription = stringResource(
+ id = R.string.relay_detail_last_member,
+ uiState.relayDetail.lastMemberName
+ ),
+ color = Orange,
+ expanded = !isScrolled,
+ )
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(top = 8.dp, start = 16.dp, end = 16.dp, bottom = 16.dp)
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ modifier = Modifier
+ .padding(start = 4.dp)
+ .size(32.dp),
+ painter = painterResource(id = DesignSystemR.drawable.member),
+ tint = Color.Unspecified,
+ contentDescription = null
+ )
+ Text(
+ text = stringResource(
+ id = R.string.relay_detail_total_count,
+ uiState.relayDetail.lastTurn
+ ),
+ style = UlbanTypography.bodyMedium.copy(fontWeight = FontWeight.Bold)
+ )
+ }
+ HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
+
+ if (uiState.relayDetail.runnerList.isEmpty()) {
+ Text(
+ text = stringResource(id = R.string.relay_no_history),
+ style = UlbanTypography.bodyMedium.copy(fontWeight = FontWeight.Bold)
+ )
+ } else {
+ LazyColumn(
+ state = listState,
+ ) {
+ items(uiState.relayDetail.runnerList) { runner ->
+ UlbanRunnerItem(
+ memberPhoto = runner.memberPhoto,
+ memberName = runner.memberName,
+ time = runner.time,
+ question = runner.question,
+ isLastTurn = runner.endStatus
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+@Preview(showBackground = true)
+fun RelayDetailScreenPreview() {
+ UlbanTheme {
+ RelayDetailScreen()
+ }
+}
diff --git a/android/feature/student/relay/src/main/java/com/sixkids/student/relay/detail/RelayDetailViewModel.kt b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/detail/RelayDetailViewModel.kt
new file mode 100644
index 00000000..8c9b78d5
--- /dev/null
+++ b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/detail/RelayDetailViewModel.kt
@@ -0,0 +1,34 @@
+package com.sixkids.student.relay.detail
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.viewModelScope
+import com.sixkids.domain.usecase.relay.GetRelayDetailUseCase
+import com.sixkids.student.relay.navigation.RelayRoute.RELAY_ID_NAME
+import com.sixkids.ui.base.BaseViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+
+@HiltViewModel
+class RelayDetailViewModel @Inject constructor(
+ private val getRelayDetailUseCase: GetRelayDetailUseCase,
+ savedStateHandle: SavedStateHandle
+) : BaseViewModel(RelayDetailState())
+{
+ private val relayId = savedStateHandle.get(RELAY_ID_NAME)
+
+ fun getRelayDetail() {
+ viewModelScope.launch {
+ intent { copy(isLoading = true) }
+ getRelayDetailUseCase(relayId!!)
+ .onSuccess { relayDetail ->
+ intent { copy(relayDetail = relayDetail) }
+ }
+ .onFailure { exception ->
+ postSideEffect(RelayDetailSideEffect.HandleException(exception, ::getRelayDetail))
+ }
+ intent { copy(isLoading = false) }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/feature/student/relay/src/main/java/com/sixkids/student/relay/history/RelayHistoryContract.kt b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/history/RelayHistoryContract.kt
new file mode 100644
index 00000000..09a5997d
--- /dev/null
+++ b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/history/RelayHistoryContract.kt
@@ -0,0 +1,20 @@
+package com.sixkids.student.relay.history
+
+import com.sixkids.model.RunningRelay
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
+data class RelayHistoryState(
+ val isLoading: Boolean = false,
+ val runningRelay: RunningRelay? = null,
+ val totalRelayCount: Int = 0,
+) : UiState
+
+sealed interface RelayHistoryEffect : SideEffect {
+ data class NavigateToRelayDetail(val relayId: Long) : RelayHistoryEffect
+ data object NavigateToCreateRelay : RelayHistoryEffect
+ data class NavigateToAnswerRelay(val relayId: Long) : RelayHistoryEffect
+ data class NavigateToTaggingReceiverRelay(val relayId: Long) : RelayHistoryEffect
+ data object NavigateToJoinRelay : RelayHistoryEffect
+ data class HandleException(val throwable: Throwable, val retry: () -> Unit) : RelayHistoryEffect
+}
\ No newline at end of file
diff --git a/android/feature/student/relay/src/main/java/com/sixkids/student/relay/history/RelayHistoryScreen.kt b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/history/RelayHistoryScreen.kt
new file mode 100644
index 00000000..7e956012
--- /dev/null
+++ b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/history/RelayHistoryScreen.kt
@@ -0,0 +1,213 @@
+package com.sixkids.student.relay.history
+
+import android.util.Log
+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.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.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.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.paging.compose.LazyPagingItems
+import androidx.paging.compose.collectAsLazyPagingItems
+import com.sixkids.designsystem.component.appbar.UlbanDefaultAppBar
+import com.sixkids.designsystem.component.appbar.UlbanDetailAppBar
+import com.sixkids.designsystem.component.item.UlbanRelayItem
+import com.sixkids.designsystem.component.screen.LoadingScreen
+import com.sixkids.designsystem.theme.Orange
+import com.sixkids.designsystem.theme.UlbanTheme
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.model.Relay
+import com.sixkids.student.relay.R
+import com.sixkids.ui.util.formatToMonthDayTime
+import com.sixkids.designsystem.R as DesignSystemR
+
+private const val TAG = "D107"
+@Composable
+fun RelayRoute(
+ viewModel: RelayHistoryViewModel = hiltViewModel(),
+ padding: PaddingValues,
+ navigateToDetail: (Long) -> Unit,
+ navigateToCreate: () -> Unit,
+ navigateToAnswer: (Long) -> Unit,
+ navigateToTaggingReceiver: (Long) -> Unit,
+ navigateToJoin: () -> Unit,
+ handleException: (Throwable, () -> Unit) -> Unit
+) {
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ LaunchedEffect(key1 = Unit) {
+ Log.d(TAG, "RelayRoute: ")
+ viewModel.initData()
+ }
+
+ LaunchedEffect(key1 = viewModel.sideEffect) {
+ viewModel.sideEffect.collect { sideEffect ->
+ when (sideEffect) {
+ is RelayHistoryEffect.NavigateToRelayDetail -> navigateToDetail(sideEffect.relayId)
+ is RelayHistoryEffect.NavigateToCreateRelay -> navigateToCreate()
+ is RelayHistoryEffect.NavigateToJoinRelay -> navigateToJoin()
+ is RelayHistoryEffect.NavigateToAnswerRelay -> navigateToAnswer(sideEffect.relayId)
+ is RelayHistoryEffect.NavigateToTaggingReceiverRelay -> navigateToTaggingReceiver(sideEffect.relayId)
+ is RelayHistoryEffect.HandleException -> handleException(
+ sideEffect.throwable,
+ sideEffect.retry
+ )
+ }
+ }
+ }
+
+ RelayHistoryScreen(
+ uiState = uiState,
+ padding = padding,
+ relayItems = viewModel.relayHistory?.collectAsLazyPagingItems(),
+ navigateToDetail = { relayId ->
+ viewModel.navigateToRelayDetail(relayId)
+ },
+ navigateToCreate = navigateToCreate,
+ navigateToAnswer = { relayId ->
+ viewModel.navigateToAnswerRelay(relayId)
+ },
+ navigateToTaggingReceiver = { relayId ->
+ viewModel.navigateToTaggingReceiverRelay(relayId)
+ },
+ updateTotalCount = {
+ viewModel.updateTotalCount(it)
+ }
+ )
+}
+
+@Composable
+fun RelayHistoryScreen(
+ uiState: RelayHistoryState = RelayHistoryState(),
+ padding: PaddingValues = PaddingValues(0.dp),
+ relayItems: LazyPagingItems? = null,
+ navigateToDetail: (Long) -> Unit = {},
+ navigateToCreate: () -> Unit = {},
+ navigateToAnswer: (Long) -> Unit = {},
+ navigateToTaggingReceiver: (Long) -> Unit = {},
+ updateTotalCount: (Int) -> Unit = {}
+) {
+ val listState = rememberLazyListState()
+ val isScrolled by remember {
+ derivedStateOf {
+ listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 100
+ }
+ }
+
+ Box(modifier = Modifier
+ .fillMaxSize()
+ .padding(padding)) {
+ Column(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ val currentRelay = uiState.runningRelay
+ if (currentRelay == null) {
+ UlbanDefaultAppBar(
+ leftIcon = DesignSystemR.drawable.relay,
+ title = stringResource(R.string.relay_challenge),
+ content = stringResource(R.string.relay_create),
+ color = Orange,
+ onclick = navigateToCreate,
+ expanded = !isScrolled
+ )
+ } else {
+ UlbanDetailAppBar(
+ leftIcon = DesignSystemR.drawable.relay,
+ title = stringResource(R.string.relay_challenge),
+ content = if (currentRelay.myTurnStatus) stringResource(R.string.relay_running_myturn) else stringResource(
+ R.string.relay_running_not_myturn
+ ),
+ topDescription = "${currentRelay.startTime.formatToMonthDayTime()} ~",
+ bottomDescription = if (currentRelay.myTurnStatus) stringResource(R.string.relay_answer_myturn) else stringResource(
+ R.string.relay_answer_not_myturn
+ ),
+ color = Orange,
+ onclick = { if (currentRelay.myTurnStatus) navigateToAnswer(currentRelay.id) else navigateToTaggingReceiver(currentRelay.id) },
+ expanded = !isScrolled,
+ )
+ }
+
+ Spacer(modifier = Modifier.padding(12.dp))
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 16.dp)
+ ) {
+ Text(
+ modifier = Modifier.padding(horizontal = 4.dp),
+ text = stringResource(
+ id = R.string.relay_relay_count,
+ uiState.totalRelayCount
+ ),
+ style = UlbanTypography.bodyMedium.copy(fontWeight = FontWeight.Bold)
+ )
+ HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
+
+ if (relayItems == null || relayItems.itemCount == 0) {
+ Spacer(modifier = Modifier.weight(1f))
+ Text(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ text = stringResource(R.string.relay_no_history),
+ style = UlbanTypography.titleLarge,
+ textAlign = TextAlign.Center
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ } else {
+ LazyColumn(
+ state = listState,
+ contentPadding = PaddingValues(vertical = 4.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ items(relayItems.itemCount) { index ->
+ relayItems[index]?.let { relay ->
+ if (index == 0)
+ updateTotalCount(relay.totalCount)
+ UlbanRelayItem(
+ startDate = relay.startTime,
+ endDate = relay.endTime,
+ userCount = relay.lastTurn,
+ lastMemberName = relay.lastMemberName,
+ onClick = {navigateToDetail(relay.id)}
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ if (uiState.isLoading) {
+ LoadingScreen()
+ }
+ }
+}
+
+@Composable
+@Preview(showBackground = true)
+fun RelayHistoryScreenPreview() {
+ UlbanTheme {
+ RelayHistoryScreen()
+ }
+}
\ No newline at end of file
diff --git a/android/feature/student/relay/src/main/java/com/sixkids/student/relay/history/RelayHistoryViewModel.kt b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/history/RelayHistoryViewModel.kt
new file mode 100644
index 00000000..35ec70e2
--- /dev/null
+++ b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/history/RelayHistoryViewModel.kt
@@ -0,0 +1,103 @@
+package com.sixkids.student.relay.history
+
+import android.util.Log
+import androidx.lifecycle.viewModelScope
+import androidx.paging.PagingData
+import androidx.paging.cachedIn
+import androidx.paging.map
+import com.sixkids.domain.usecase.organization.GetSelectedOrganizationIdUseCase
+import com.sixkids.domain.usecase.relay.GetRelayHistoryUseCase
+import com.sixkids.domain.usecase.relay.GetRunningRelayUseCase
+import com.sixkids.domain.usecase.user.LoadUserInfoUseCase
+import com.sixkids.model.NotFoundException
+import com.sixkids.model.Relay
+import com.sixkids.model.RunningRelay
+import com.sixkids.model.UserInfo
+import com.sixkids.ui.base.BaseViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import java.time.LocalDateTime
+import javax.inject.Inject
+
+private const val TAG = "D107"
+@HiltViewModel
+class RelayHistoryViewModel @Inject constructor(
+ private val getSelectedOrganizationIdUseCase: GetSelectedOrganizationIdUseCase,
+ private val loadUserInfoUseCase: LoadUserInfoUseCase,
+ private val getRunningRelayUseCase: GetRunningRelayUseCase,
+ private val getRelayHistoryUseCase: GetRelayHistoryUseCase
+) : BaseViewModel(RelayHistoryState())
+{
+ private var orgId = 0L
+ private lateinit var userInfo: UserInfo
+ var relayHistory: Flow>? = null
+ private var isFirstVisited: Boolean = true
+
+ fun initData() = viewModelScope.launch {
+ if (isFirstVisited.not()) return@launch
+ isFirstVisited = false
+
+ intent { copy(isLoading = true) }
+
+ getSelectedOrganizationIdUseCase().onSuccess {
+ orgId = it.toLong()
+ }.onFailure {
+ postSideEffect(RelayHistoryEffect.HandleException(it, ::initData))
+ }
+
+ loadUserInfoUseCase().onSuccess {
+ userInfo = it
+ }.onFailure {
+ postSideEffect(RelayHistoryEffect.HandleException(it, ::initData))
+ }
+
+ getRunningRelay()
+ getRelayHistory()
+
+ intent { copy(isLoading = false) }
+ }
+
+ private fun getRunningRelay() {
+ viewModelScope.launch {
+ getRunningRelayUseCase(organizationId = orgId)
+ .onSuccess {
+ intent { copy(runningRelay = it) }
+ }.onFailure {
+ if (it is NotFoundException){
+ intent { copy(runningRelay = null) }
+ }else{
+ postSideEffect(
+ RelayHistoryEffect.HandleException(
+ it,
+ ::getRunningRelay
+ )
+ )
+ }
+ }
+ }
+ }
+
+ private fun getRelayHistory() {
+ viewModelScope.launch {
+ relayHistory = getRelayHistoryUseCase(organizationId = orgId.toInt(), memberId = userInfo.id)
+ .cachedIn(viewModelScope)
+ }
+ }
+
+ fun updateTotalCount(totalCount: Int) = intent { copy(totalRelayCount = totalCount) }
+
+ fun navigateToRelayDetail(relayId: Long) = postSideEffect(
+ RelayHistoryEffect.NavigateToRelayDetail(relayId)
+ )
+
+ fun navigateToAnswerRelay(relayId: Long) = postSideEffect(
+ RelayHistoryEffect.NavigateToAnswerRelay(relayId)
+ )
+
+ fun navigateToTaggingReceiverRelay(relayId: Long) = postSideEffect(
+ RelayHistoryEffect.NavigateToTaggingReceiverRelay(relayId)
+ )
+
+}
\ No newline at end of file
diff --git a/android/feature/student/relay/src/main/java/com/sixkids/student/relay/navigation/RelayNavigation.kt b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/navigation/RelayNavigation.kt
new file mode 100644
index 00000000..0da7b65f
--- /dev/null
+++ b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/navigation/RelayNavigation.kt
@@ -0,0 +1,163 @@
+package com.sixkids.student.relay.navigation
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavOptions
+import androidx.navigation.NavType
+import androidx.navigation.compose.composable
+import androidx.navigation.navArgument
+import com.sixkids.student.relay.create.RelayCreateRoute
+import com.sixkids.student.relay.detail.RelayDetailRoute
+import com.sixkids.student.relay.history.RelayRoute
+import com.sixkids.student.relay.navigation.RelayRoute.RELAY_ID_NAME
+import com.sixkids.student.relay.pass.answer.RelayAnswerRoute
+import com.sixkids.student.relay.pass.tagging.receiver.RelayTaggingReceiverRoute
+import com.sixkids.student.relay.pass.tagging.sender.RelayTaggingSenderRoute
+import com.sixkids.student.relay.result.RelayCreateResultRoute
+import com.sixkids.ui.SnackbarToken
+
+fun NavController.navigateStudentRelayHistory(navOptions: NavOptions) {
+ navigate(RelayRoute.defaultRoute, navOptions)
+}
+
+fun NavController.navigateStudentRelayDetail(relayId: Long) {
+ navigate(RelayRoute.detailRoute(relayId))
+}
+
+fun NavController.navigateStudentRelayCreate() {
+ navigate(RelayRoute.createRoute)
+}
+
+fun NavController.navigateStudentRelayCreateResult() {
+ navigate(RelayRoute.createResultRoute)
+}
+
+fun NavController.navigateStudentRelayJoin() {
+ navigate(RelayRoute.joinRoute)
+}
+
+fun NavController.navigateStudentRelayAnswer(relayId: Long) {
+ navigate(RelayRoute.answerRoute(relayId))
+}
+
+fun NavController.navigateStudentRelayTaggingSender(relayId: Long, question: String) {
+ navigate(RelayRoute.taggingSenderRoute(relayId, question))
+}
+
+fun NavController.navigateStudentRelayTaggingReceiver(relayId: Long) {
+ navigate(RelayRoute.taggingReceiverRoute(relayId))
+}
+
+fun NavGraphBuilder.studentRelayNavGraph(
+ padding: PaddingValues,
+ navigateRelayHistory: () -> Unit,
+ navigateRelayDetail: (Long) -> Unit,
+ navigateCreateRelay: () -> Unit,
+ navigateJoinRelay: () -> Unit,
+ navigateCreateRelayResult: () -> Unit,
+ navigateAnswerRelay: (Long) -> Unit,
+ navigateTaggingSender: (Long, String) -> Unit,
+ navigateTaggingReceiver: (Long) -> Unit,
+ onBackClick: () -> Unit,
+ onShowSnackBar: (SnackbarToken) -> Unit,
+ handleException: (Throwable, () -> Unit) -> Unit
+) {
+ composable(route = RelayRoute.defaultRoute)
+ {
+ RelayRoute(
+ padding = padding,
+ navigateToDetail = { relayId ->
+ navigateRelayDetail(relayId)
+ },
+ navigateToCreate = navigateCreateRelay,
+ navigateToAnswer = navigateAnswerRelay,
+ navigateToTaggingReceiver = navigateTaggingReceiver,
+ navigateToJoin = navigateJoinRelay,
+ handleException = handleException
+ )
+ }
+
+ composable(route = RelayRoute.detailRoute,
+ arguments = listOf(
+ navArgument(RELAY_ID_NAME) { type = NavType.LongType },
+
+ ))
+ {
+
+ RelayDetailRoute(
+ handleException = handleException
+ )
+ }
+
+ composable(route = RelayRoute.createRoute)
+ {
+ RelayCreateRoute(
+ navigateToRelayResult = navigateCreateRelayResult,
+ onBackClick = onBackClick,
+ onShowSnackBar = onShowSnackBar
+ )
+ }
+
+ composable(route = RelayRoute.createResultRoute)
+ {
+ RelayCreateResultRoute(
+ navigateToRelayHistory = navigateRelayHistory,
+ handleException = handleException
+ )
+ }
+
+ composable(route = RelayRoute.answerRoute,
+ arguments = listOf(
+ navArgument(RELAY_ID_NAME) { type = NavType.LongType },
+
+ ))
+ {
+ RelayAnswerRoute(
+ navigateToTaggingSenderRelay = navigateTaggingSender,
+ onBackClick = onBackClick,
+ onShowSnackBar = onShowSnackBar
+ )
+ }
+
+ composable(route = RelayRoute.taggingSenderRoute,
+ arguments = listOf(
+ navArgument(RELAY_ID_NAME) { type = NavType.LongType },
+ navArgument(RelayRoute.RELAY_QUESTION_NAME) { type = NavType.StringType }
+ ))
+ {
+ RelayTaggingSenderRoute(
+ navigateToRelayHistory = navigateRelayHistory,
+ onShowSnackBar = onShowSnackBar
+ )
+ }
+
+ composable(route = RelayRoute.taggingReceiverRoute,
+ arguments = listOf(
+ navArgument(RELAY_ID_NAME) { type = NavType.LongType }
+ ))
+ {
+ RelayTaggingReceiverRoute(
+ navigateToRelayHistory = navigateRelayHistory,
+ )
+ }
+}
+
+object RelayRoute {
+ const val RELAY_ID_NAME = "relayId"
+ const val RELAY_QUESTION_NAME = "question"
+
+ const val defaultRoute = "student/relay-history"
+ const val createRoute = "relay-create"
+ const val detailRoute = "relay-detail?relayId={$RELAY_ID_NAME}"
+ const val answerRoute = "relay-answer?relayId={$RELAY_ID_NAME}"
+ const val taggingSenderRoute = "relay-tagging-sender?relayId={$RELAY_ID_NAME}&question={$RELAY_QUESTION_NAME}"
+ const val taggingReceiverRoute = "relay-tagging-receiver?relayId={$RELAY_ID_NAME}"
+ const val createResultRoute = "relay-create-result"
+ const val joinRoute = "relay-join"
+
+ fun detailRoute(relayId: Long) = "relay-detail?relayId=$relayId"
+ fun answerRoute(relayId: Long) = "relay-answer?relayId=$relayId"
+ fun taggingSenderRoute(relayId: Long, question: String) = "relay-tagging-sender?relayId=$relayId&question=$question"
+ fun taggingReceiverRoute(relayId: Long) = "relay-tagging-receiver?relayId=$relayId"
+}
\ No newline at end of file
diff --git a/android/feature/student/relay/src/main/java/com/sixkids/student/relay/pass/answer/RelayAnswerContract.kt b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/pass/answer/RelayAnswerContract.kt
new file mode 100644
index 00000000..32ea42b7
--- /dev/null
+++ b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/pass/answer/RelayAnswerContract.kt
@@ -0,0 +1,16 @@
+package com.sixkids.student.relay.pass.answer
+
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
+data class RelayAnswerState(
+ val isLoading: Boolean = false,
+ val preQuestion: String = "",
+ val nextQuestion: String = ""
+): UiState
+
+sealed interface RelayAnswerEffect: SideEffect{
+ data class NavigateToTaggingSenderRelay(val relayId: Long, val question: String): RelayAnswerEffect
+ data class OnShowSnackBar(val tkn: SnackbarToken): RelayAnswerEffect
+}
\ No newline at end of file
diff --git a/android/feature/student/relay/src/main/java/com/sixkids/student/relay/pass/answer/RelayAnswerScreen.kt b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/pass/answer/RelayAnswerScreen.kt
new file mode 100644
index 00000000..e71c5705
--- /dev/null
+++ b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/pass/answer/RelayAnswerScreen.kt
@@ -0,0 +1,128 @@
+package com.sixkids.student.relay.pass.answer
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+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.material3.Divider
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+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 androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.sixkids.designsystem.component.button.UlbanFilledButton
+import com.sixkids.designsystem.component.screen.UlbanTopSection
+import com.sixkids.designsystem.component.textfield.UlbanUnderLineTextField
+import com.sixkids.designsystem.theme.Blue
+import com.sixkids.designsystem.theme.UlbanTheme
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.student.relay.R
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.extension.collectWithLifecycle
+
+@Composable
+fun RelayAnswerRoute(
+ viewModel: RelayAnswerViewModel = hiltViewModel(),
+ navigateToTaggingSenderRelay: (Long, String) -> Unit,
+ onBackClick: () -> Unit,
+ onShowSnackBar: (SnackbarToken) -> Unit
+) {
+ val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
+
+ viewModel.sideEffect.collectWithLifecycle { sideEffect ->
+ when (sideEffect) {
+ is RelayAnswerEffect.NavigateToTaggingSenderRelay -> navigateToTaggingSenderRelay(sideEffect.relayId, sideEffect.question)
+ is RelayAnswerEffect.OnShowSnackBar -> onShowSnackBar(sideEffect.tkn)
+ }
+ }
+
+ LaunchedEffect(key1 = Unit) {
+ viewModel.init()
+ }
+
+ RelayAnswerScreen(
+ uiState = uiState,
+ onNextClick = viewModel::nextClick,
+ onBackClick = onBackClick,
+ onUpdateNextQuestion = viewModel::updateNextQuestion
+ )
+}
+
+@Composable
+fun RelayAnswerScreen(
+ paddingValues: PaddingValues = PaddingValues(20.dp),
+ uiState: RelayAnswerState = RelayAnswerState(),
+ onNextClick: () -> Unit = {},
+ onBackClick: () -> Unit = {},
+ onUpdateNextQuestion: (String) -> Unit = {}
+) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues),
+ horizontalAlignment = Alignment.Start,
+ ) {
+ UlbanTopSection(stringResource(R.string.relay_answer_title), onBackClick)
+
+ Spacer(modifier = Modifier.height(36.dp))
+
+ Text(
+ text = stringResource(R.string.relay_create_question),
+ style = UlbanTypography.bodyLarge,
+ modifier = Modifier.padding(top = 10.dp, bottom = 10.dp)
+ )
+ UlbanUnderLineTextField(
+ text = uiState.nextQuestion,
+ hint = stringResource(R.string.relay_create_question_hint),
+ onTextChange = onUpdateNextQuestion,
+ onIconClick = {
+ onUpdateNextQuestion("")
+ }
+ )
+
+ Spacer(modifier = Modifier.height(24.dp))
+
+ Text(
+ text = stringResource(R.string.relay_answer_pre_question),
+ style = UlbanTypography.bodyLarge,
+ modifier = Modifier.padding(top = 10.dp, bottom = 10.dp)
+ )
+
+ Column {
+ Text(text = uiState.preQuestion, style = UlbanTypography.bodyMedium)
+
+ Divider(
+ modifier = Modifier.fillMaxWidth().padding(top = 4.dp),
+ color = Blue,
+ thickness = 2.dp
+ )
+ }
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ UlbanFilledButton(
+ text = stringResource(R.string.button_next),
+ onClick = { onNextClick() },
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ }
+}
+
+@Composable
+@Preview(showBackground = true)
+fun RelayAnswerScreenPreview() {
+ UlbanTheme {
+ RelayAnswerScreen()
+ }
+}
\ No newline at end of file
diff --git a/android/feature/student/relay/src/main/java/com/sixkids/student/relay/pass/answer/RelayAnswerViewModel.kt b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/pass/answer/RelayAnswerViewModel.kt
new file mode 100644
index 00000000..95f89078
--- /dev/null
+++ b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/pass/answer/RelayAnswerViewModel.kt
@@ -0,0 +1,44 @@
+package com.sixkids.student.relay.pass.answer
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.viewModelScope
+import com.sixkids.domain.usecase.relay.GetRelayQuestionUseCase
+import com.sixkids.model.NotFoundException
+import com.sixkids.student.relay.navigation.RelayRoute
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.base.BaseViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class RelayAnswerViewModel @Inject constructor(
+ private val getRelayQuestionUseCase: GetRelayQuestionUseCase,
+ savedStateHandle: SavedStateHandle
+): BaseViewModel(RelayAnswerState()) {
+ private val relayId = savedStateHandle.get(RelayRoute.RELAY_ID_NAME)
+
+ fun init() {
+ viewModelScope.launch {
+ intent { copy(isLoading = true) }
+ getRelayQuestionUseCase(relayId!!)
+ .onSuccess { question ->
+ intent { copy(preQuestion = question) }
+ }
+ .onFailure { exception ->
+ when(exception){
+ is NotFoundException -> intent { copy(preQuestion = "첫번째 주자입니다!")}
+ else -> postSideEffect(RelayAnswerEffect.OnShowSnackBar(SnackbarToken(exception.message ?: "질문을 받아오는데 실패했습니다")))
+ }
+ }
+ }
+ }
+
+ fun updateNextQuestion(question: String) {
+ intent { copy(nextQuestion = question) }
+ }
+
+ fun nextClick() {
+ postSideEffect(RelayAnswerEffect.NavigateToTaggingSenderRelay(relayId!!, uiState.value.nextQuestion))
+ }
+}
\ No newline at end of file
diff --git a/android/feature/student/relay/src/main/java/com/sixkids/student/relay/pass/tagging/RelayNfc.kt b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/pass/tagging/RelayNfc.kt
new file mode 100644
index 00000000..10279711
--- /dev/null
+++ b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/pass/tagging/RelayNfc.kt
@@ -0,0 +1,10 @@
+package com.sixkids.student.relay.pass.tagging
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class RelayNfc(
+ val relayId: Long,
+ val senderId: Long,
+ val question: String
+)
diff --git a/android/feature/student/relay/src/main/java/com/sixkids/student/relay/pass/tagging/receiver/RelayTaggingReceiverContract.kt b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/pass/tagging/receiver/RelayTaggingReceiverContract.kt
new file mode 100644
index 00000000..629eb284
--- /dev/null
+++ b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/pass/tagging/receiver/RelayTaggingReceiverContract.kt
@@ -0,0 +1,18 @@
+package com.sixkids.student.relay.pass.tagging.receiver
+
+import com.sixkids.model.RelayReceive
+import com.sixkids.student.relay.pass.tagging.RelayNfc
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
+data class RelayTaggingReceiverState(
+ val isLoading: Boolean = false,
+ val relayId: Long = -1L,
+ val relayNfc: RelayNfc = RelayNfc(-1, -1, ""),
+ val relayReceive: RelayReceive = RelayReceive("", "", false, 0)
+): UiState
+
+sealed interface RelayTaggingReceiverEffect: SideEffect {
+ data class OnShowSnackBar(val tkn: SnackbarToken): RelayTaggingReceiverEffect
+}
\ No newline at end of file
diff --git a/android/feature/student/relay/src/main/java/com/sixkids/student/relay/pass/tagging/receiver/RelayTaggingReceiverScreen.kt b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/pass/tagging/receiver/RelayTaggingReceiverScreen.kt
new file mode 100644
index 00000000..dbb2ebea
--- /dev/null
+++ b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/pass/tagging/receiver/RelayTaggingReceiverScreen.kt
@@ -0,0 +1,126 @@
+package com.sixkids.student.relay.pass.tagging.receiver
+
+import android.app.Activity
+import android.nfc.NfcAdapter
+import android.nfc.Tag
+import android.nfc.tech.IsoDep
+import android.util.Log
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.sixkids.core.nfc.HCEService
+import com.sixkids.designsystem.component.screen.RelayPassResultScreen
+import com.sixkids.designsystem.component.screen.RelayTaggingScreen
+import com.sixkids.designsystem.theme.Purple
+import com.sixkids.model.RelayReceive
+import com.sixkids.student.relay.pass.tagging.RelayNfc
+import kotlinx.serialization.json.Json
+import com.sixkids.designsystem.R as DesignSystemR
+
+private const val TAG = "D107"
+
+@Composable
+fun RelayTaggingReceiverRoute(
+ viewModel: RelayTaggingReceiverViewModel = hiltViewModel(),
+ navigateToRelayHistory : () -> Unit
+){
+ val context = LocalContext.current
+ val activity = context as Activity
+
+ val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
+ val nfcAdapter: NfcAdapter = NfcAdapter.getDefaultAdapter(context)
+
+ DisposableEffect(key1 = Unit) {
+ viewModel.init()
+
+ onDispose {
+ nfcAdapter.disableReaderMode(activity)
+ }
+ }
+
+ if (uiState.relayId != -1L && nfcAdapter.isEnabled) {
+ Log.d(TAG, "RelayTaggingReceiverRoute: Ready!")
+ nfcAdapter.enableReaderMode(activity, { tag : Tag? ->
+ tag?.let {
+ val isoDep = IsoDep.get(it)
+ isoDep.use { iso ->
+ iso.connect()
+ val response = isoDep.transceive(HCEService.SELECT_APDU)
+ val message = String(response.copyOfRange(0, response.size - 2))
+ Log.d(TAG, "RelayTaggingReceiverRoute: $message")
+ val relayNfc = Json.decodeFromString(message)
+ viewModel.onNfcReceived(relayNfc)
+ }
+ }
+ }, NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK, null)
+
+ }
+
+ if (uiState.relayReceive.senderName != ""){
+ RelayTaggingResultScreen(uiState.relayReceive, navigateToRelayHistory)
+ }else{
+ RelayTaggingReceiverScreen()
+ }
+
+}
+
+@Composable
+fun RelayTaggingReceiverScreen(){
+ Box(modifier = Modifier.fillMaxSize()){
+ RelayTaggingScreen(
+ isSender = false
+ )
+ }
+}
+
+@Composable
+fun RelayTaggingResultScreen(
+ relayReceive: RelayReceive,
+ navigateToRelayHistory: () -> Unit = {}
+){
+ Box(modifier = Modifier.fillMaxSize()){
+ if (!relayReceive.lastStatus){
+ RelayPassResultScreen(
+ title = stringResource(id = DesignSystemR.string.relay_pass_result_title),
+ subTitle = stringResource(DesignSystemR.string.relay_pass_result_subtitle_receiver),
+ bodyTop = stringResource(id = DesignSystemR.string.relay_pass_result_body_top_receiver),
+ bodyMiddle = "${relayReceive.senderName} 학생이",
+ bodyBottom = stringResource(id = DesignSystemR.string.relay_pass_result_body_bottom_receiver),
+ onClick = {navigateToRelayHistory()}
+ )
+ }else{
+ RelayPassResultScreen(
+ title = stringResource(id = DesignSystemR.string.relay_pass_result_title_bomb),
+ subTitle = stringResource(DesignSystemR.string.relay_pass_result_subtitle_bomb, relayReceive.demerit),
+ bodyTop = "${relayReceive.senderName} 학생이",
+ bodyMiddle = "\'${relayReceive.question}\'",
+ bodyBottom = stringResource(id = DesignSystemR.string.relay_pass_result_body_bottom_sender),
+ imgRes = DesignSystemR.drawable.bomb,
+ backgroundColor = Purple,
+ onClick = {navigateToRelayHistory()}
+ )
+ }
+
+ }
+}
+
+@Composable
+@Preview(showBackground = true)
+fun RelayTaggingReceiverScreenPreview() {
+ RelayTaggingReceiverScreen()
+}
+
+@Composable
+@Preview(showBackground = true)
+fun RelayTaggingResultScreenPreview() {
+ RelayTaggingResultScreen(
+ RelayReceive("오하빈", "", true, 0)
+ )
+}
\ No newline at end of file
diff --git a/android/feature/student/relay/src/main/java/com/sixkids/student/relay/pass/tagging/receiver/RelayTaggingReceiverViewModel.kt b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/pass/tagging/receiver/RelayTaggingReceiverViewModel.kt
new file mode 100644
index 00000000..11eb31bc
--- /dev/null
+++ b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/pass/tagging/receiver/RelayTaggingReceiverViewModel.kt
@@ -0,0 +1,40 @@
+package com.sixkids.student.relay.pass.tagging.receiver
+
+import android.util.Log
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.viewModelScope
+import com.sixkids.domain.usecase.relay.ReceiveRelayUseCase
+import com.sixkids.student.relay.navigation.RelayRoute
+import com.sixkids.student.relay.pass.tagging.RelayNfc
+import com.sixkids.ui.base.BaseViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+private const val TAG = "D107"
+@HiltViewModel
+class RelayTaggingReceiverViewModel @Inject constructor(
+ private val receiveRelayUseCase: ReceiveRelayUseCase,
+ savedStateHandle: SavedStateHandle
+): BaseViewModel(RelayTaggingReceiverState()){
+ private val receivedRelayId = savedStateHandle.get(RelayRoute.RELAY_ID_NAME)
+
+ fun init() {
+ intent { copy(relayId = receivedRelayId?:-1L) }
+ }
+
+ fun onNfcReceived(relayNfc: RelayNfc) {
+ if (relayNfc.relayId == receivedRelayId){
+ viewModelScope.launch {
+ receiveRelayUseCase(relayNfc.relayId.toInt(), relayNfc.senderId, relayNfc.question)
+ .onSuccess {
+ intent { copy(relayReceive = it) }
+ }
+ .onFailure {
+ Log.e(TAG, "Failed to receive relay", it)
+ }
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/android/feature/student/relay/src/main/java/com/sixkids/student/relay/pass/tagging/sender/RelayTaggingSenderContract.kt b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/pass/tagging/sender/RelayTaggingSenderContract.kt
new file mode 100644
index 00000000..8317901b
--- /dev/null
+++ b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/pass/tagging/sender/RelayTaggingSenderContract.kt
@@ -0,0 +1,18 @@
+package com.sixkids.student.relay.pass.tagging.sender
+
+import com.sixkids.model.RelaySend
+import com.sixkids.student.relay.pass.tagging.RelayNfc
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
+data class RelayTaggingSenderState(
+ val isLoading: Boolean = false,
+ val relayNfc: RelayNfc = RelayNfc(-1, -1, ""),
+ val relaySend: RelaySend = RelaySend(),
+): UiState
+
+sealed interface RelayTaggingSenderEffect: SideEffect {
+ data class NavigateToTaggingResult(val prevMemberName: String, val prevQuestion: String): RelayTaggingSenderEffect
+ data class OnShowSnackBar(val tkn: SnackbarToken): RelayTaggingSenderEffect
+}
\ No newline at end of file
diff --git a/android/feature/student/relay/src/main/java/com/sixkids/student/relay/pass/tagging/sender/RelayTaggingSenderScreen.kt b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/pass/tagging/sender/RelayTaggingSenderScreen.kt
new file mode 100644
index 00000000..05914438
--- /dev/null
+++ b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/pass/tagging/sender/RelayTaggingSenderScreen.kt
@@ -0,0 +1,107 @@
+package com.sixkids.student.relay.pass.tagging.sender
+
+import android.util.Log
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.sixkids.core.nfc.HCEService
+import com.sixkids.designsystem.R
+import com.sixkids.designsystem.component.screen.RelayPassResultScreen
+import com.sixkids.designsystem.component.screen.RelayTaggingScreen
+import com.sixkids.model.RelaySend
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.extension.collectWithLifecycle
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+
+private const val TAG = "D107"
+
+@Composable
+fun RelayTaggingSenderRoute(
+ viewModel: RelayTaggingSenderViewModel = hiltViewModel(),
+ navigateToRelayHistory: () -> Unit,
+ onShowSnackBar: (SnackbarToken) -> Unit
+) {
+ val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
+
+ viewModel.sideEffect.collectWithLifecycle { sideEffect ->
+ when (sideEffect) {
+ is RelayTaggingSenderEffect.NavigateToTaggingResult -> navigateToRelayHistory()
+ is RelayTaggingSenderEffect.OnShowSnackBar -> onShowSnackBar(sideEffect.tkn)
+ }
+ }
+
+ LaunchedEffect(key1 = Unit) {
+ viewModel.init()
+ }
+
+ if (uiState.relaySend.prevMemberName == "" && uiState.relaySend.prevQuestion == "") {
+ RelayTaggingSenderScreen(
+ uiState,
+ checkRelaySent = viewModel::checkRelaySent
+ )
+ } else {
+ RelayTaggingResultScreen(
+ relaySend = uiState.relaySend,
+ navigateToRelayHistory = navigateToRelayHistory
+ )
+ }
+
+
+}
+
+@Composable
+fun RelayTaggingSenderScreen(
+ uiState: RelayTaggingSenderState = RelayTaggingSenderState(),
+ checkRelaySent: () -> Unit = {}
+) {
+ if (uiState.relayNfc.relayId != -1L) {
+ val serializedRelayNfc = Json.encodeToString(uiState.relayNfc)
+ Log.d(TAG, "RelayTaggingSenderScreen: $serializedRelayNfc")
+ HCEService.setData(serializedRelayNfc)
+ }
+
+
+ Box(modifier = Modifier.fillMaxSize()) {
+ RelayTaggingScreen(
+ isSender = true,
+ onClick = checkRelaySent
+ )
+ }
+}
+
+@Composable
+fun RelayTaggingResultScreen(
+ relaySend: RelaySend,
+ navigateToRelayHistory: () -> Unit = {}
+) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ RelayPassResultScreen(
+ title = stringResource(id = R.string.relay_pass_result_title),
+ subTitle = stringResource(R.string.relay_pass_result_subtitle_sender),
+ bodyTop = if (relaySend.prevMemberName != "") "${relaySend.prevMemberName} 학생이"
+ else relaySend.prevMemberName,
+ bodyMiddle = "\'${relaySend.prevQuestion}\'",
+ bodyBottom = if (relaySend.prevMemberName != "") stringResource(id = R.string.relay_pass_result_body_bottom_sender)
+ else "",
+ onClick = { navigateToRelayHistory() }
+ )
+ }
+}
+
+@Composable
+@Preview(showBackground = true)
+fun RelayTaggingSenderScreenPreview() {
+ RelayTaggingResultScreen(
+ relaySend = RelaySend(
+ prevMemberName = "김철수",
+ prevQuestion = "오늘 점심은 뭐먹지?"
+ )
+ )
+}
\ No newline at end of file
diff --git a/android/feature/student/relay/src/main/java/com/sixkids/student/relay/pass/tagging/sender/RelayTaggingSenderViewModel.kt b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/pass/tagging/sender/RelayTaggingSenderViewModel.kt
new file mode 100644
index 00000000..dc8cafe3
--- /dev/null
+++ b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/pass/tagging/sender/RelayTaggingSenderViewModel.kt
@@ -0,0 +1,68 @@
+package com.sixkids.student.relay.pass.tagging.sender
+
+import android.util.Log
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.viewModelScope
+import com.sixkids.domain.usecase.relay.SendRelayUseCase
+import com.sixkids.domain.usecase.user.LoadUserInfoUseCase
+import com.sixkids.model.BadRequestException
+import com.sixkids.model.NotFoundException
+import com.sixkids.model.RelaySend
+import com.sixkids.student.relay.navigation.RelayRoute
+import com.sixkids.student.relay.pass.tagging.RelayNfc
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.base.BaseViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+private const val TAG = "D107"
+
+@HiltViewModel
+class RelayTaggingSenderViewModel @Inject constructor(
+ private val loadUserInfoUseCase: LoadUserInfoUseCase,
+ private val sendRelayUseCase: SendRelayUseCase,
+ savedStateHandle: SavedStateHandle
+) : BaseViewModel(RelayTaggingSenderState()) {
+ private val relayId = savedStateHandle.get(RelayRoute.RELAY_ID_NAME)
+ private val question = savedStateHandle.get(RelayRoute.RELAY_QUESTION_NAME)
+
+ fun init() {
+ viewModelScope.launch {
+ loadUserInfoUseCase().onSuccess {
+ intent { copy(relayNfc = RelayNfc(relayId ?: -1, it.id.toLong(), question ?: "")) }
+ }.onFailure {
+ Log.d(TAG, "init: ${it.message}")
+ }
+ }
+ }
+
+ fun checkRelaySent() {
+ viewModelScope.launch {
+ sendRelayUseCase(relayId?.toInt() ?: -1)
+ .onSuccess {
+ intent { copy(relaySend = it) }
+ }.onFailure {
+ when(it){
+ is NotFoundException -> {
+ intent { copy(relaySend = RelaySend("", "첫번째 주자입니다!")) }
+ }
+ is BadRequestException -> {
+ postSideEffect(
+ RelayTaggingSenderEffect.OnShowSnackBar(
+ SnackbarToken("이어 달리기가 전달되지 않았어요. 다시 시도해 보세요")
+ )
+ )
+ }
+ else -> {
+ postSideEffect(
+ RelayTaggingSenderEffect.OnShowSnackBar(
+ SnackbarToken(it.message ?: "이어 달리기 전달에 실패했어요")
+ )
+ )
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/feature/student/relay/src/main/java/com/sixkids/student/relay/result/RelayCreateResultContract.kt b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/result/RelayCreateResultContract.kt
new file mode 100644
index 00000000..30140c0d
--- /dev/null
+++ b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/result/RelayCreateResultContract.kt
@@ -0,0 +1,18 @@
+package com.sixkids.student.relay.result
+
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
+data class RelayCreateResultState(
+ val showResultDialog: Boolean = false,
+) : UiState
+
+
+sealed interface RelayCreateResultEffect : SideEffect {
+ data object NavigateToRelayHistory : RelayCreateResultEffect
+
+ data class HandleException(
+ val throwable: Throwable, val retry: () -> Unit
+ ) : RelayCreateResultEffect
+
+}
diff --git a/android/feature/student/relay/src/main/java/com/sixkids/student/relay/result/RelayCreateResultScreen.kt b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/result/RelayCreateResultScreen.kt
new file mode 100644
index 00000000..297ed439
--- /dev/null
+++ b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/result/RelayCreateResultScreen.kt
@@ -0,0 +1,96 @@
+package com.sixkids.student.relay.result
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+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.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.sixkids.designsystem.component.card.UlbanMissionCard
+import com.sixkids.designsystem.theme.Orange
+import com.sixkids.designsystem.theme.UlbanTheme
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.student.relay.R
+import com.sixkids.ui.extension.collectWithLifecycle
+import com.sixkids.designsystem.R as DesignSystemR
+
+@Composable
+fun RelayCreateResultRoute(
+ viewModel: RelayCreateResultViewModel = hiltViewModel(),
+ navigateToRelayHistory: () -> Unit,
+ handleException: (Throwable, () -> Unit) -> Unit
+) {
+ viewModel.sideEffect.collectWithLifecycle {
+ when (it) {
+ is RelayCreateResultEffect.HandleException -> handleException(it.throwable, it.retry)
+ RelayCreateResultEffect.NavigateToRelayHistory -> navigateToRelayHistory()
+ }
+ }
+
+ RelayCreateResultScreen(
+ onClickConfirm = viewModel::navigateToChallengeHistory
+ )
+}
+
+
+@Composable
+fun RelayCreateResultScreen(
+ paddingValues: PaddingValues = PaddingValues(32.dp),
+ onClickConfirm: () -> Unit = {}
+) {
+ BackHandler {
+ onClickConfirm()
+ }
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(
+ text = stringResource(R.string.create_relay_success),
+ style = UlbanTypography.titleSmall,
+ textAlign = TextAlign.Center
+ )
+ Spacer(modifier = Modifier.size(16.dp))
+ UlbanMissionCard(
+ imgRes = DesignSystemR.drawable.relay,
+ title = "친구에게 전달해 봐요!",
+ backGroundColor = Orange
+ )
+ Image(
+ modifier = Modifier
+ .size(160.dp)
+ .clickable {
+ onClickConfirm()
+ },
+ painter = painterResource(id = R.drawable.relay_created_success),
+ contentDescription = "challenge success"
+ )
+ }
+}
+
+
+@Preview(showBackground = true)
+@Composable
+fun PreviewResultContent() {
+ UlbanTheme {
+ RelayCreateResultScreen()
+ }
+}
diff --git a/android/feature/student/relay/src/main/java/com/sixkids/student/relay/result/RelayCreateResultViewModel.kt b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/result/RelayCreateResultViewModel.kt
new file mode 100644
index 00000000..827e8506
--- /dev/null
+++ b/android/feature/student/relay/src/main/java/com/sixkids/student/relay/result/RelayCreateResultViewModel.kt
@@ -0,0 +1,17 @@
+package com.sixkids.student.relay.result
+
+import com.sixkids.ui.base.BaseViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+
+@HiltViewModel
+class RelayCreateResultViewModel @Inject constructor(
+
+): BaseViewModel(
+ RelayCreateResultState()
+){
+
+ fun navigateToChallengeHistory() {
+ postSideEffect(RelayCreateResultEffect.NavigateToRelayHistory)
+ }
+}
\ No newline at end of file
diff --git a/android/feature/student/relay/src/main/res/drawable/relay_created_success.png b/android/feature/student/relay/src/main/res/drawable/relay_created_success.png
new file mode 100644
index 00000000..52170aed
Binary files /dev/null and b/android/feature/student/relay/src/main/res/drawable/relay_created_success.png differ
diff --git a/android/feature/student/relay/src/main/res/values/strings.xml b/android/feature/student/relay/src/main/res/values/strings.xml
new file mode 100644
index 00000000..23821d0b
--- /dev/null
+++ b/android/feature/student/relay/src/main/res/values/strings.xml
@@ -0,0 +1,21 @@
+
+
+ 이어 달리기
+ 새로운\n이어 달리기\n만들기
+ 이어 달리기\n답변하기
+ 이어 달리기가\n진행 중입니다!
+ 질문에 답변하고\n친구에게 전달 해봐요!
+ 여기를 터치해서\n이어 달리기를 이어 받아 봐요!
+ 지금까지 %d번 이어 달리기를 진행했어요
+ 기록이 없어요
+ %s 학생이 폭탄을 터트렸어요
+ %d명 학생이 참여했어요
+ 이어 달리기 만들기
+ 다음 친구가 대답할 질문을 작성해 주세요
+ 질문을 작성해 주세요
+ 만들기
+ 새로운\n이어 달리기가 만들어졌습니다!
+ 이어 달리기 참여하기
+ 다음
+ 내가 받은 질문
+
\ No newline at end of file
diff --git a/android/feature/teacher/board/build.gradle.kts b/android/feature/teacher/board/build.gradle.kts
index dddf7a76..14ffda68 100644
--- a/android/feature/teacher/board/build.gradle.kts
+++ b/android/feature/teacher/board/build.gradle.kts
@@ -1,10 +1,42 @@
+import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
+import java.util.Properties
+
plugins {
alias(libs.plugins.sixkids.android.feature.compose)
}
+fun getProperty(propertyKey: String): String =
+ gradleLocalProperties(rootDir, providers).getProperty(propertyKey)
+
android {
namespace = "com.sixkids.teacher.board"
+
+ defaultConfig {
+ val localProperties = Properties()
+ val localPropertiesFile = rootProject.file("local.properties")
+ if (localPropertiesFile.exists()) {
+ localProperties.load(localPropertiesFile.inputStream())
+ }
+
+ val stompUrl = localProperties.getProperty("STOMP_ENDPOINT") ?: ""
+
+ buildConfigField("String", "STOMP_ENDPOINT", "\"${stompUrl}\"")
+ }
+
+ buildFeatures {
+ buildConfig = true
+ }
}
dependencies {
+ implementation(libs.okhttp)
+ implementation(libs.okhttp.logginginterceptor)
+
+ implementation(libs.moshi.kotlin)
+ implementation(libs.moshi.converter)
+ implementation(libs.krossbow.stomp. core)
+ implementation(libs.krossbow.websocket.okhttp)
+ implementation(libs.krossbow.stomp.moshi)
+
+ implementation(libs.bundles.paging)
}
diff --git a/android/feature/teacher/board/src/main/AndroidManifest.xml b/android/feature/teacher/board/src/main/AndroidManifest.xml
index a5918e68..bf8960fd 100644
--- a/android/feature/teacher/board/src/main/AndroidManifest.xml
+++ b/android/feature/teacher/board/src/main/AndroidManifest.xml
@@ -1,4 +1,8 @@
+
+
+
\ No newline at end of file
diff --git a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/announce/announcedetail/AnnounceDetailContract.kt b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/announce/announcedetail/AnnounceDetailContract.kt
new file mode 100644
index 00000000..76ef3a05
--- /dev/null
+++ b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/announce/announcedetail/AnnounceDetailContract.kt
@@ -0,0 +1,17 @@
+package com.sixkids.teacher.board.announce.announcedetail
+
+import com.sixkids.model.PostDetail
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
+sealed interface AnnounceDetailEffect : SideEffect {
+ data object RefreshAnnounceDetail : AnnounceDetailEffect
+ data class OnShowSnackbar(val message: String) : AnnounceDetailEffect
+}
+
+data class AnnounceDetailState(
+ val isLoading: Boolean = false,
+ val postDetail: PostDetail = PostDetail(),
+ val commentText: String = "",
+ val selectedCommentId: Long? = null,
+) : UiState
\ No newline at end of file
diff --git a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/announce/announcedetail/AnnounceDetailScreen.kt b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/announce/announcedetail/AnnounceDetailScreen.kt
new file mode 100644
index 00000000..6a9ee7d0
--- /dev/null
+++ b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/announce/announcedetail/AnnounceDetailScreen.kt
@@ -0,0 +1,227 @@
+package com.sixkids.teacher.board.announce.announcedetail
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.clickable
+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.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+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.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import coil.compose.AsyncImage
+import com.sixkids.designsystem.R
+import com.sixkids.designsystem.component.screen.LoadingScreen
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.model.MemberSimple
+import com.sixkids.model.PostDetail
+import com.sixkids.teacher.board.post.postdetail.commentDummy
+import com.sixkids.teacher.board.post.postdetail.component.CommentItem
+import com.sixkids.teacher.board.post.postdetail.component.CommentTextField
+import com.sixkids.teacher.board.post.postdetail.component.PostWriterInfo
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.util.formatToMonthDayTime
+import java.time.LocalDateTime
+
+@Composable
+fun AnnounceDetailRoute(
+ viewModel: AnnounceDetailViewModel = hiltViewModel(),
+ padding: PaddingValues,
+ onShowSnackBar: (SnackbarToken) -> Unit
+) {
+ val uiState by viewModel.uiState.collectAsState()
+ // 키보드 숨기기
+ var keyboardHideState by remember { mutableStateOf(false) }
+ if (keyboardHideState) {
+ LocalSoftwareKeyboardController.current?.hide()
+ keyboardHideState = false
+ }
+
+ LaunchedEffect(Unit) {
+ viewModel.getAnnounceDetail()
+ }
+
+ LaunchedEffect(viewModel.sideEffect) {
+ viewModel.sideEffect.collect { sideEffect ->
+ when (sideEffect) {
+ AnnounceDetailEffect.RefreshAnnounceDetail -> {
+ keyboardHideState = true
+ viewModel.getAnnounceDetail()
+ }
+
+ is AnnounceDetailEffect.OnShowSnackbar -> {
+ onShowSnackBar(SnackbarToken(message = sideEffect.message))
+ }
+ }
+ }
+ }
+
+ Box(modifier = Modifier.padding(padding)) {
+ AnnounceDetailScreen(
+ announceDetailState = uiState,
+ onCommentTextChanged = viewModel::onCommentTextChanged,
+ onClickComment = viewModel::onSelectedCommentId,
+ onClickSubmitComment = viewModel::onNewComment,
+ )
+ }
+}
+
+@Composable
+fun AnnounceDetailScreen(
+ modifier: Modifier = Modifier,
+ announceDetailState: AnnounceDetailState,
+ onCommentTextChanged: (String) -> Unit = {},
+ onClickComment: (Long) -> Unit = {},
+ onClickSubmitComment: () -> Unit = {},
+ postDeleteOnclick: () -> Unit = {}
+) {
+
+ val scrollState = rememberScrollState()
+
+ BackHandler(
+ enabled = announceDetailState.selectedCommentId != null,
+ onBack = { onClickComment(announceDetailState.selectedCommentId ?: 0) }
+ )
+
+ Box {
+ Column {
+ Column(
+ modifier = modifier
+ .weight(1f)
+ .padding(20.dp)
+ .verticalScroll(scrollState),
+ ) {
+ // 작성자 정보
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ PostWriterInfo(
+ height = 60.dp,
+ writer = announceDetailState.postDetail.writeMember.name,
+ dateString = announceDetailState.postDetail.createTime.formatToMonthDayTime(),
+ writerImageUrl = announceDetailState.postDetail.writeMember.photo
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ Icon(
+ modifier = Modifier
+ .size(30.dp)
+ .clickable { postDeleteOnclick() },
+ imageVector = ImageVector.vectorResource(id = R.drawable.ic_delete),
+ contentDescription = "더보기"
+ )
+ }
+
+ Spacer(modifier = Modifier.height(10.dp))
+ Text(
+ text = announceDetailState.postDetail.title,
+ style = UlbanTypography.titleLarge
+ )
+ Spacer(modifier = Modifier.height(10.dp))
+ // 이미지
+ if (announceDetailState.postDetail.imageUri.isNotEmpty()) {
+ AsyncImage(
+ model = announceDetailState.postDetail.imageUri,
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ )
+ }
+ Spacer(modifier = Modifier.height(10.dp))
+ Text(
+ text = announceDetailState.postDetail.content,
+ style = UlbanTypography.bodyLarge
+ )
+ Spacer(modifier = Modifier.height(10.dp))
+ HorizontalDivider(
+ thickness = 2.dp,
+ color = Color.Black
+ )
+ // 댓글 목록
+ for (comment in announceDetailState.postDetail.comments) {
+ CommentItem(
+ selected = announceDetailState.selectedCommentId == comment.id,
+ writer = comment.member.name,
+ dateString = comment.createTime.formatToMonthDayTime(),
+ writerImageUrl = comment.member.photo,
+ commentString = comment.content,
+ recommentOnclick = {
+ onClickComment(comment.id)
+ }
+ )
+ // 대댓글 목록
+ for (recomment in comment.recomments) {
+ Row {
+ Icon(
+ modifier = Modifier.padding(4.dp),
+ imageVector = ImageVector.vectorResource(id = R.drawable.ic_recomment),
+ contentDescription = null
+ )
+ CommentItem(
+ writer = recomment.member.name,
+ dateString = recomment.createTime.formatToMonthDayTime(),
+ writerImageUrl = recomment.member.photo,
+ commentString = recomment.content,
+ isRecomment = true
+ )
+ }
+
+ }
+ }
+ }
+ CommentTextField(
+ msg = announceDetailState.commentText,
+ onTextIuputChange = onCommentTextChanged,
+ onSendClick = { onClickSubmitComment() }
+ )
+ }
+
+
+ if (announceDetailState.isLoading) {
+ LoadingScreen()
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun AnnounceDetailScreenPreview() {
+ AnnounceDetailScreen(
+ announceDetailState = AnnounceDetailState(
+ postDetail = PostDetail(
+ title = "제목",
+ content = "내용내용내용내용내용내용내용내용내용내용내용내용내용",
+ writeMember = MemberSimple(
+ id = 1,
+ name = "작성자",
+ photo = "https://picsum.photos/200/300"
+ ),
+ createTime = LocalDateTime.now(),
+ imageUri = "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTKe3bkhl96AgtmHyTiKW-KXRst2-5MoY6xB9-mZP74BQ&s",
+ comments = listOf(commentDummy, commentDummy)
+ )
+ )
+ )
+}
+
diff --git a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/announce/announcedetail/AnnounceDetailViewModel.kt b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/announce/announcedetail/AnnounceDetailViewModel.kt
new file mode 100644
index 00000000..dcf83826
--- /dev/null
+++ b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/announce/announcedetail/AnnounceDetailViewModel.kt
@@ -0,0 +1,101 @@
+package com.sixkids.teacher.board.announce.announcedetail
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.viewModelScope
+import com.sixkids.domain.usecase.comment.DeleteCommentUseCase
+import com.sixkids.domain.usecase.comment.NewCommentUseCase
+import com.sixkids.domain.usecase.comment.NewRecommentUseCase
+import com.sixkids.domain.usecase.comment.ReportCommentUseCase
+import com.sixkids.domain.usecase.comment.UpdateCommentUsecase
+import com.sixkids.domain.usecase.post.DeletePostUseCase
+import com.sixkids.domain.usecase.post.GetPostDetailUseCase
+import com.sixkids.domain.usecase.post.UpdatePostUseCase
+import com.sixkids.teacher.board.navigation.BoardRoute
+import com.sixkids.ui.base.BaseViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class AnnounceDetailViewModel @Inject constructor(
+ savedStateHandle: SavedStateHandle,
+ private val getPostDetailUseCase: GetPostDetailUseCase,
+ private val updatePostUseCase: UpdatePostUseCase,
+ private val deletePostUsecase: DeletePostUseCase,
+ private val deleteCommentUseCase: DeleteCommentUseCase,
+ private val updateCommentUsecase: UpdateCommentUsecase,
+ private val newCommentUseCase: NewCommentUseCase,
+ private val newRecommentUseCase: NewRecommentUseCase,
+ private val reportCommentUseCase: ReportCommentUseCase
+) : BaseViewModel(AnnounceDetailState()){
+
+ private val postId: Long = savedStateHandle.get(BoardRoute.announceDetailARG)!!
+
+ fun onCommentTextChanged(commentText: String) = intent { copy(commentText = commentText) }
+ fun onSelectedCommentId(commentId: Long?) = intent {
+ if (currentState.selectedCommentId == commentId) {
+ copy(selectedCommentId = null)
+ } else {
+ copy(selectedCommentId = commentId)
+ }
+ }
+
+ fun getAnnounceDetail() {
+ viewModelScope.launch {
+ intent { copy(isLoading = true) }
+ getPostDetailUseCase(postId).onSuccess {
+ intent { copy(postDetail = it) }
+ }.onFailure {
+ postSideEffect(AnnounceDetailEffect.OnShowSnackbar(it.message ?: "게시글을 불러오지 못했어요"))
+ }
+ intent { copy(isLoading = false) }
+ }
+ }
+
+ fun onNewComment() {
+ if (currentState.commentText.isBlank()) {
+ postSideEffect(AnnounceDetailEffect.OnShowSnackbar("댓글을 입력해주세요"))
+ } else {
+ if (currentState.selectedCommentId == null) {
+ viewModelScope.launch {
+ intent { copy(isLoading = true) }
+ newCommentUseCase(
+ postId = postId,
+ content = currentState.commentText,
+ ).onSuccess {
+ postSideEffect(AnnounceDetailEffect.OnShowSnackbar("댓글이 작성되었습니다"))
+ intent { copy(commentText = "", selectedCommentId = null) }
+ getAnnounceDetail()
+ }.onFailure {
+ postSideEffect(
+ AnnounceDetailEffect.OnShowSnackbar(
+ it.message ?: "댓글 작성에 실패했어요"
+ )
+ )
+ }
+ intent { copy(isLoading = false) }
+ }
+ } else {
+ viewModelScope.launch {
+ intent { copy(isLoading = true) }
+ newRecommentUseCase(
+ postId = postId,
+ content = currentState.commentText,
+ currentState.selectedCommentId!!
+ ).onSuccess {
+ postSideEffect(AnnounceDetailEffect.OnShowSnackbar("댓글이 작성되었습니다"))
+ intent { copy(commentText = "", selectedCommentId = null)}
+ getAnnounceDetail()
+ }.onFailure {
+ postSideEffect(
+ AnnounceDetailEffect.OnShowSnackbar(
+ it.message ?: "댓글 작성에 실패했어요"
+ )
+ )
+ }
+ intent { copy(isLoading = false) }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/announce/announcelist/AnnounceListContract.kt b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/announce/announcelist/AnnounceListContract.kt
new file mode 100644
index 00000000..f2fc1cc6
--- /dev/null
+++ b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/announce/announcelist/AnnounceListContract.kt
@@ -0,0 +1,17 @@
+package com.sixkids.teacher.board.announce.announcelist
+
+import com.sixkids.model.Post
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
+sealed interface AnnounceListEffect : SideEffect{
+ data object NavigateToAnnounceDetail: AnnounceListEffect
+ data object NavigateToWriteAnnounce: AnnounceListEffect
+ data class OnShowSnackBar(val message : String) : AnnounceListEffect
+}
+
+data class AnnounceListState(
+ val isLoding: Boolean = false,
+ val classString: String = "",
+ val postList: List = emptyList(),
+): UiState
\ No newline at end of file
diff --git a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/announce/announcelist/AnnounceListScreen.kt b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/announce/announcelist/AnnounceListScreen.kt
new file mode 100644
index 00000000..196b870f
--- /dev/null
+++ b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/announce/announcelist/AnnounceListScreen.kt
@@ -0,0 +1,158 @@
+package com.sixkids.teacher.board.announce.announcelist
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.paging.compose.LazyPagingItems
+import androidx.paging.compose.collectAsLazyPagingItems
+import com.sixkids.designsystem.R
+import com.sixkids.designsystem.component.appbar.UlbanDefaultAppBar
+import com.sixkids.designsystem.component.appbar.UlbanDetailAppBar
+import com.sixkids.designsystem.component.button.EditFAB
+import com.sixkids.designsystem.component.screen.LoadingScreen
+import com.sixkids.designsystem.theme.Orange
+import com.sixkids.designsystem.theme.OrangeDark
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.designsystem.theme.Yellow
+import com.sixkids.model.Post
+import com.sixkids.teacher.board.post.postlist.component.PostItem
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.util.formatToMonthDayTimeKorean
+
+@Composable
+fun AnnounceListRoute(
+ viewModel: AnnounceListViewModel = hiltViewModel(),
+ navigateToAnnounceDetail: (postId:Long) -> Unit,
+ navigateToAnnounceWrite: () -> Unit,
+ onShowSnackBar: (SnackbarToken) -> Unit,
+ padding: PaddingValues
+) {
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ LaunchedEffect(Unit) {
+ viewModel.getAnnounceList()
+ }
+
+ LaunchedEffect(key1 = viewModel.sideEffect) {
+ viewModel.sideEffect.collect { sideEffect ->
+ when (sideEffect) {
+ AnnounceListEffect.NavigateToAnnounceDetail -> {}
+ AnnounceListEffect.NavigateToWriteAnnounce -> {}
+ is AnnounceListEffect.OnShowSnackBar -> {
+ onShowSnackBar(SnackbarToken(message = sideEffect.message))
+ }
+ }
+ }
+ }
+
+ Box(
+ modifier = Modifier
+ .padding(padding)
+ .fillMaxSize()
+ ) {
+ AnnounceListScreen(
+ announceListState = uiState,
+ postItems = viewModel.postList?.collectAsLazyPagingItems(),
+ postItemOnclick = navigateToAnnounceDetail,
+ fabClick = navigateToAnnounceWrite
+ )
+ }
+}
+
+@Composable
+fun AnnounceListScreen(
+ modifier: Modifier = Modifier,
+ announceListState: AnnounceListState = AnnounceListState(),
+ postItems: LazyPagingItems? = null,
+ postItemOnclick: (postId: Long) -> Unit = {},
+ fabClick: () -> Unit = {}
+) {
+ val listState = rememberLazyListState()
+
+ Box(
+ modifier = modifier
+ .fillMaxSize()
+ ) {
+ Column(
+
+ ) {
+
+ UlbanDefaultAppBar(
+ leftIcon = R.drawable.announce,
+ title = stringResource(id = com.sixkids.teacher.board.R.string.board_main_announce),
+ content = stringResource(id = com.sixkids.teacher.board.R.string.board_main_announce),
+ body = announceListState.classString.replace("\n", " "),
+ color = Orange
+ )
+
+ if (postItems != null){
+ if (postItems.itemCount == 0){
+ Spacer(modifier = Modifier.weight(1f))
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ text = stringResource(id = com.sixkids.teacher.board.R.string.board_announce_no_item),
+ textAlign = TextAlign.Center,
+ style = UlbanTypography.bodyLarge,
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ } else {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ state = listState,
+ ) {
+ items(postItems.itemCount) { index ->
+ postItems[index]?.let { post ->
+ PostItem(
+ title = post.title,
+ writer = post.writer,
+ dateString = post.time.formatToMonthDayTimeKorean(),
+ commentCount = post.commentCount,
+ dividerColor = OrangeDark,
+ onClick = { postItemOnclick(post.id) }
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ //FAB
+ EditFAB(
+ modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .padding(16.dp),
+ buttonColor = Orange,
+ iconColor = OrangeDark,
+ onClick = fabClick
+ )
+ if (announceListState.isLoding){
+ LoadingScreen()
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun AnnounceListScreenPreview() {
+ AnnounceListScreen()
+}
\ No newline at end of file
diff --git a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/announce/announcelist/AnnounceListViewModel.kt b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/announce/announcelist/AnnounceListViewModel.kt
new file mode 100644
index 00000000..53126b21
--- /dev/null
+++ b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/announce/announcelist/AnnounceListViewModel.kt
@@ -0,0 +1,53 @@
+package com.sixkids.teacher.board.announce.announcelist
+
+import androidx.lifecycle.viewModelScope
+import androidx.paging.PagingData
+import androidx.paging.cachedIn
+import com.sixkids.domain.usecase.organization.GetSelectedOrganizationIdUseCase
+import com.sixkids.domain.usecase.organization.LoadSelectedOrganizationNameUseCase
+import com.sixkids.domain.usecase.post.GetPostListUseCase
+import com.sixkids.model.Post
+import com.sixkids.model.PostCategory
+import com.sixkids.ui.base.BaseViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class AnnounceListViewModel @Inject constructor(
+ private val getPostListUseCase: GetPostListUseCase,
+ private val getSelectedOrganizationIdUseCase: GetSelectedOrganizationIdUseCase,
+ private val loadSelectedOrganizationNameUseCase: LoadSelectedOrganizationNameUseCase
+): BaseViewModel(AnnounceListState()){
+ private var organizationId: Int? = null
+
+ var postList: Flow>? = null
+
+ fun getAnnounceList() {
+ viewModelScope.launch {
+ intent { copy(isLoding = true) }
+
+ loadSelectedOrganizationNameUseCase().onSuccess {
+ intent { copy(classString = it) }
+ }.onFailure {
+ intent { copy(classString = "") }
+ }
+
+ if (organizationId == null){
+ organizationId = getSelectedOrganizationIdUseCase().getOrNull()
+ }
+
+ if (organizationId != null){
+ postList = getPostListUseCase(
+ organizationId = organizationId!!,
+ postCategory = PostCategory.NOTICE
+ ).cachedIn(viewModelScope)
+ } else {
+ postSideEffect(AnnounceListEffect.OnShowSnackBar("학급 정보를 불러오지 못했어요 ;("))
+ }
+
+ intent { copy(isLoding = false) }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/announce/announcewrite/AnnounceWriteContract.kt b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/announce/announcewrite/AnnounceWriteContract.kt
new file mode 100644
index 00000000..e6d9b992
--- /dev/null
+++ b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/announce/announcewrite/AnnounceWriteContract.kt
@@ -0,0 +1,19 @@
+package com.sixkids.teacher.board.announce.announcewrite
+
+import android.graphics.Bitmap
+import com.sixkids.teacher.board.post.postwrite.PostWriteEffect
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
+sealed interface AnnounceWriteEffect: SideEffect{
+ data object NavigateBack : AnnounceWriteEffect
+ data class OnShowSnackbar(val message: String) : AnnounceWriteEffect
+}
+
+data class AnnounceWriteState(
+ val isLoading: Boolean = false,
+ val title: String = "",
+ val content: String = "",
+ val anonymousChecked: Boolean = false,
+ val photo: Bitmap? = null
+): UiState
\ No newline at end of file
diff --git a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/announce/announcewrite/AnnounceWriteScreen.kt b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/announce/announcewrite/AnnounceWriteScreen.kt
new file mode 100644
index 00000000..df0b57da
--- /dev/null
+++ b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/announce/announcewrite/AnnounceWriteScreen.kt
@@ -0,0 +1,240 @@
+package com.sixkids.teacher.board.announce.announcewrite
+
+import android.graphics.ImageDecoder
+import android.os.Build
+import android.provider.MediaStore
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.PickVisualMediaRequest
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+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.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+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.wrapContentHeight
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.sixkids.designsystem.component.button.UlbanFilledButton
+import com.sixkids.designsystem.component.screen.LoadingScreen
+import com.sixkids.designsystem.theme.GrayLight
+import com.sixkids.designsystem.theme.Orange
+import com.sixkids.designsystem.theme.OrangeDark
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.teacher.board.R
+import com.sixkids.teacher.board.post.postwrite.component.PageTitle
+import com.sixkids.teacher.board.post.postwrite.saveBitmapToFile
+import com.sixkids.ui.SnackbarToken
+import java.io.IOException
+
+@Composable
+fun AnnounceWriteRoute(
+ viewModel: AnnounceWriteViewModel = hiltViewModel(),
+ padding: PaddingValues,
+ navigateBack: () -> Unit,
+ onShowSnackBar: (SnackbarToken) -> Unit
+) {
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ val context = LocalContext.current
+ val photoLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.PickVisualMedia()
+ ) { uri ->
+ uri?.let {
+ try {
+ val bitmap = if (Build.VERSION.SDK_INT < 28) {
+ MediaStore.Images.Media.getBitmap(context.contentResolver, it)
+ } else {
+ ImageDecoder.decodeBitmap(
+ ImageDecoder.createSource(
+ context.contentResolver,
+ it
+ )
+ )
+ }
+ viewModel.onAddPhoto(bitmap)
+ } catch (e: IOException) {
+ viewModel.showToast("사진 호출에 실패했습니다.")
+ }
+ }
+ }
+
+ LaunchedEffect(key1 = viewModel.sideEffect) {
+ viewModel.sideEffect.collect { sideEffect ->
+ when (sideEffect) {
+ AnnounceWriteEffect.NavigateBack -> navigateBack()
+ is AnnounceWriteEffect.OnShowSnackbar -> {
+ onShowSnackBar(SnackbarToken(message = sideEffect.message))
+ }
+ }
+ }
+ }
+
+ AnnounceWriteScreen(
+ announceWriteState = uiState,
+ cancelOnClick = { viewModel.onBack() },
+ submitOnClick = {
+ viewModel.onPostAnnounce(
+ uiState.photo?.let { saveBitmapToFile(context, it, "post_photo.jpg") }
+ )
+ },
+ titleValueChange = { viewModel.onTitleChanged(it) },
+ contentValueChange = { viewModel.onContentChanged(it) },
+ addPhotoOnClick = { photoLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) }
+ )
+}
+
+@Composable
+fun AnnounceWriteScreen(
+ modifier: Modifier = Modifier,
+ announceWriteState: AnnounceWriteState = AnnounceWriteState(),
+ cancelOnClick: () -> Unit = {},
+ submitOnClick: () -> Unit = {},
+ titleValueChange: (String) -> Unit = {},
+ contentValueChange: (String) -> Unit = {},
+ addPhotoOnClick: () -> Unit = {}
+) {
+
+ val scrollState = rememberScrollState()
+
+ LaunchedEffect(announceWriteState.content) {
+ scrollState.scrollTo(scrollState.maxValue)
+ }
+
+ Box {
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .padding(20.dp)
+ ) {
+ Spacer(modifier = Modifier.height(20.dp))
+ PageTitle(
+ title = stringResource(id = R.string.board_announce_write_title),
+ cancelOnclick = cancelOnClick
+ )
+ //title
+ OutlinedTextField(
+ value = announceWriteState.title,
+ onValueChange = titleValueChange,
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedBorderColor = Color.Transparent,
+ unfocusedBorderColor = Color.Transparent
+ ),
+ placeholder = {
+ Text(
+ text = stringResource(id = R.string.board_write_content_title),
+ style = UlbanTypography.bodyLarge.copy(
+ color = Color.Gray
+ )
+ )
+ },
+ textStyle = UlbanTypography.bodyLarge
+ )
+ HorizontalDivider(
+ thickness = 2.dp,
+ color = Color.Black
+ )
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f)
+ .verticalScroll(scrollState)
+ ) {
+ //photo
+ if (announceWriteState.photo != null) {
+ Spacer(modifier = Modifier.height(10.dp))
+ Image(
+ modifier = Modifier
+ .fillMaxWidth()
+ .aspectRatio(1f),
+ bitmap = announceWriteState.photo.asImageBitmap(),
+ contentDescription = null,
+ contentScale = ContentScale.Crop
+ )
+ }
+ //content
+ OutlinedTextField(
+ value = announceWriteState.content,
+ onValueChange = { string ->
+ contentValueChange(string)
+ },
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedBorderColor = Color.Transparent,
+ unfocusedBorderColor = Color.Transparent
+ ),
+ placeholder = {
+ Text(
+ text = stringResource(id = R.string.board_write_content_content),
+ style = UlbanTypography.bodyLarge.copy(
+ color = Color.Gray
+ )
+ )
+ },
+ textStyle = UlbanTypography.bodyLarge
+ )
+ }
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ // 이미지 추가 아이콘
+ Icon(
+ modifier = Modifier
+ .size(40.dp)
+ .clickable(onClick = addPhotoOnClick),
+ imageVector = ImageVector.vectorResource(id = com.sixkids.designsystem.R.drawable.ic_photo_camera),
+ contentDescription = null
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ // 등록 버튼
+ UlbanFilledButton(
+ text = stringResource(id = R.string.board_write_submit),
+ onClick = submitOnClick,
+ color = Orange,
+ textColor = OrangeDark
+ )
+ }
+ }
+
+ if (announceWriteState.isLoading) {
+ LoadingScreen()
+ }
+ }
+
+}
+
+@Preview(showBackground = true)
+@Composable
+fun AnnounceWriteScreenPreview() {
+ AnnounceWriteScreen()
+}
diff --git a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/announce/announcewrite/AnnounceWriteViewModel.kt b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/announce/announcewrite/AnnounceWriteViewModel.kt
new file mode 100644
index 00000000..d63d6869
--- /dev/null
+++ b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/announce/announcewrite/AnnounceWriteViewModel.kt
@@ -0,0 +1,64 @@
+package com.sixkids.teacher.board.announce.announcewrite
+
+import android.graphics.Bitmap
+import androidx.lifecycle.viewModelScope
+import com.sixkids.domain.usecase.organization.GetSelectedOrganizationIdUseCase
+import com.sixkids.domain.usecase.post.NewPostUseCase
+import com.sixkids.model.PostCategory
+import com.sixkids.teacher.board.post.postwrite.PostWriteEffect
+import com.sixkids.ui.base.BaseViewModel
+import com.sixkids.ui.util.formatToMonthDayKorean
+import com.sixkids.ui.util.formatToMonthDayTime
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import java.io.File
+import java.time.LocalDateTime
+import javax.inject.Inject
+
+@HiltViewModel
+class AnnounceWriteViewModel @Inject constructor(
+ private val newPostUseCase: NewPostUseCase,
+ private val getSelectedOrganizationIdUseCase: GetSelectedOrganizationIdUseCase
+): BaseViewModel(AnnounceWriteState(
+ title = LocalDateTime.now().formatToMonthDayKorean()
+)){
+
+ private var organizationId: Int? = null
+
+ fun onBack() = postSideEffect(AnnounceWriteEffect.NavigateBack)
+ fun onTitleChanged(title: String) = intent { copy(title = title) }
+ fun onContentChanged(content: String) = intent { copy(content = content) }
+ fun onAddPhoto(bitmap: Bitmap) = intent { copy(photo = bitmap) }
+ fun showToast(message: String) = postSideEffect(AnnounceWriteEffect.OnShowSnackbar(message))
+
+ fun onPostAnnounce(photo: File?) {
+ viewModelScope.launch {
+ intent { copy(isLoading = true) }
+
+ if (organizationId == null) {
+ organizationId = getSelectedOrganizationIdUseCase().getOrNull()
+ }
+
+ if (organizationId != null) {
+ newPostUseCase(
+ organizationId = organizationId!!.toLong(),
+ title = currentState.title,
+ content = currentState.content,
+ secretStatus = currentState.anonymousChecked,
+ postCategory = PostCategory.NOTICE,
+ file = photo
+ ).onSuccess {
+ postSideEffect(AnnounceWriteEffect.OnShowSnackbar("알림장 작성에 성공했어요 :)"))
+ postSideEffect(AnnounceWriteEffect.NavigateBack)
+ }.onFailure {
+ postSideEffect(AnnounceWriteEffect.OnShowSnackbar(it.message ?: "알림장 작성에 실패했어요 ;("))
+ }
+ } else {
+ postSideEffect(AnnounceWriteEffect.OnShowSnackbar("학급 정보를 불러오지 못했어요 ;("))
+ postSideEffect(AnnounceWriteEffect.NavigateBack)
+ }
+
+ intent { copy(isLoading = false) }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/chatting/ChattingContract.kt b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/chatting/ChattingContract.kt
new file mode 100644
index 00000000..4dbeedff
--- /dev/null
+++ b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/chatting/ChattingContract.kt
@@ -0,0 +1,18 @@
+package com.sixkids.teacher.board.chatting
+
+import com.sixkids.model.Chat
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
+sealed interface ChattingSideEffect : SideEffect{
+
+}
+
+data class ChattingState(
+ val isLoading : Boolean = false,
+ val organizationName: String = "",
+ val memberCount : Int = 0,
+ val memberId: Int = 0,
+ val message: String = "",
+ val chatList: List = emptyList()
+) : UiState
\ No newline at end of file
diff --git a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/chatting/ChattingScreen.kt b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/chatting/ChattingScreen.kt
new file mode 100644
index 00000000..317ddecc
--- /dev/null
+++ b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/chatting/ChattingScreen.kt
@@ -0,0 +1,359 @@
+package com.sixkids.teacher.board.chatting
+
+import android.annotation.SuppressLint
+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.Box
+import androidx.compose.foundation.layout.Column
+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.layout.size
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.outlined.Send
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
+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 androidx.compose.ui.unit.sp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.paging.compose.LazyPagingItems
+import androidx.paging.compose.collectAsLazyPagingItems
+import coil.compose.AsyncImage
+import com.sixkids.designsystem.component.textfield.UlbanBasicTextField
+import com.sixkids.designsystem.theme.Cream
+import com.sixkids.designsystem.theme.Gray
+import com.sixkids.designsystem.theme.GrayLight
+import com.sixkids.designsystem.theme.UlbanTheme
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.designsystem.theme.Yellow
+import com.sixkids.model.Chat
+import com.sixkids.teacher.board.R
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.extension.collectWithLifecycle
+import java.text.SimpleDateFormat
+import java.util.TimeZone
+import com.sixkids.designsystem.R as DesignSystemR
+
+private const val TAG = "D107"
+
+@Composable
+fun ChattingRoute(
+ viewModel: ChattingViewModel = hiltViewModel(),
+ onBackClick: () -> Unit,
+ onShowSnackBar: (SnackbarToken) -> Unit
+) {
+ val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
+
+ viewModel.sideEffect.collectWithLifecycle { sideEffect ->
+ when (sideEffect) {
+ else -> {}
+ }
+ }
+
+ DisposableEffect(key1 = Unit) {
+ viewModel.initStomp()
+
+ onDispose {
+ viewModel.cancelStomp()
+ }
+ }
+
+ ChattingScreen(
+ uiState = uiState,
+ onUpdateMessage = viewModel::updateMessage,
+ onBackClick = onBackClick,
+ onSendClick = viewModel::sendMessage,
+ onPhotoClick = {
+ //사진
+ },
+ chatItems = viewModel.originalChatList?.collectAsLazyPagingItems()
+ )
+}
+
+@Composable
+fun ChattingScreen(
+ uiState: ChattingState = ChattingState(),
+ onUpdateMessage: (String) -> Unit = {},
+ onBackClick: () -> Unit = {},
+ onSendClick: (String) -> Unit = {},
+ onPhotoClick: () -> Unit = {},
+ chatItems: LazyPagingItems? = null
+) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ ) {
+ Column {
+ TopSection(
+ uiState.organizationName,
+ uiState.memberCount, onBackClick
+ )
+
+ ChatSection(
+ uiState.memberId,
+ chatItems = chatItems,
+ uiState.chatList,
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxSize()
+ )
+
+ InputSection(
+ msg = uiState.message,
+ onUpdateMessage = onUpdateMessage,
+ onSendClick = onSendClick,
+ onPhotoClick = onPhotoClick
+ )
+ }
+ }
+}
+
+@Composable
+fun TopSection(
+ organizationName: String = "",
+ memberCount: Int = 0,
+ onBackClick: () -> Unit = {}
+) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(20.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Image(
+ painter = painterResource(id = DesignSystemR.drawable.ic_arrow_back),
+ contentDescription = "back button",
+ modifier = Modifier.clickable { onBackClick() }
+ )
+
+ Text(
+ text = organizationName.replace("\n", " "),
+ style = UlbanTypography.titleSmall,
+ modifier = Modifier.padding(10.dp, 0.dp)
+ )
+
+ }
+}
+
+@Composable
+fun ChatSection(
+ memberId: Int,
+ chatItems: LazyPagingItems? = null,
+ chatList: List,
+ modifier: Modifier = Modifier
+) {
+// Log.d(TAG, "ChatSection: ")
+ val scrollState = rememberLazyListState()
+
+ if (chatItems == null) {
+ Text(text = "데이터 없음")
+ } else {
+ Column(modifier = modifier) {
+ LazyColumn(
+ state = scrollState, modifier = Modifier.weight(1f),
+
+ ) {
+ items(chatItems.itemCount) { idx ->
+ if (chatItems[idx]?.memberId == memberId.toLong()) {
+ MyChat(chatItems[idx]!!)
+ } else {
+ OtherChat(chatItems[idx]!!)
+ }
+ }
+
+ items(chatList) { chat ->
+ if (chat.memberId == memberId.toLong()) {
+ MyChat(chat)
+ } else {
+ OtherChat(chat)
+ }
+ }
+
+
+ }
+ }
+ }
+ val serverChatSize = chatItems?.itemCount ?: 0
+ val socketChatSize = chatList.size
+ val totalChatSize = serverChatSize + socketChatSize
+
+ if (totalChatSize > 0) {
+ LaunchedEffect(totalChatSize) {
+ scrollState.scrollToItem(totalChatSize - 1)
+ }
+ }
+}
+
+@Composable
+fun OtherChat(chat: Chat) {
+ val screenWidthDp = with(LocalDensity.current) {
+ LocalContext.current.resources.displayMetrics.widthPixels.toDp()
+ }
+ val maxWidthDp = screenWidthDp * 0.6f
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(6.dp)
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ AsyncImage(
+ model = chat.memberImageUrl,
+ contentDescription = "profile photo",
+ modifier = Modifier
+ .size(32.dp)
+ .clip(RoundedCornerShape(4.dp)),
+ contentScale = ContentScale.Crop
+ )
+ Text(
+ text = chat.memberName,
+ style = UlbanTypography.bodySmall,
+ modifier = Modifier.padding(10.dp)
+ )
+ }
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(32.dp, 0.dp, 0.dp, 10.dp),
+ horizontalArrangement = Arrangement.Start,
+ verticalAlignment = Alignment.Bottom
+ ) {
+
+ Text(
+ text = chat.content,
+ style = UlbanTypography.bodyMedium.copy(fontWeight = FontWeight.Normal),
+ modifier = Modifier
+ .background(GrayLight, shape = RoundedCornerShape(10.dp))
+ .padding(12.dp)
+ .widthIn(max = maxWidthDp)
+ .wrapContentWidth()
+ )
+ Text(
+ text = chatTimeFormat(chat.sendDateTime),
+ style = UlbanTypography.bodySmall.copy(
+ fontSize = 8.sp,
+ lineHeight = 10.sp,
+ fontWeight = FontWeight.Normal
+ ),
+ modifier = Modifier.padding(4.dp, 0.dp, 0.dp, 6.dp)
+ )
+ }
+ }
+}
+
+@Composable
+fun MyChat(chat: Chat) {
+ val screenWidthDp = with(LocalDensity.current) {
+ LocalContext.current.resources.displayMetrics.widthPixels.toDp()
+ }
+ val maxWidthDp = screenWidthDp * 0.6f
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(6.dp),
+ horizontalArrangement = Arrangement.End,
+ verticalAlignment = Alignment.Bottom
+ ) {
+ Text(
+ text = chatTimeFormat(chat.sendDateTime),
+ style = UlbanTypography.bodySmall.copy(
+ fontSize = 8.sp,
+ lineHeight = 10.sp,
+ fontWeight = FontWeight.Normal
+ ),
+ modifier = Modifier.padding(0.dp, 0.dp, 4.dp, 6.dp)
+ )
+ Text(
+ text = chat.content,
+ style = UlbanTypography.bodyMedium.copy(fontWeight = FontWeight.Normal),
+ modifier = Modifier
+ .background(Yellow, shape = RoundedCornerShape(10.dp))
+ .padding(12.dp)
+ .widthIn(max = maxWidthDp)
+ )
+ }
+}
+
+@SuppressLint("SimpleDateFormat")
+fun chatTimeFormat(time: Long): String {
+ val formatter = SimpleDateFormat("MM/dd\na h:mm").apply {
+ timeZone = TimeZone.getTimeZone("Asia/Seoul")
+ }
+
+ return formatter.format(time)
+}
+
+@Composable
+fun InputSection(
+ msg: String = "",
+ onUpdateMessage: (String) -> Unit = {},
+ onSendClick: (String) -> Unit = {},
+ onPhotoClick: () -> Unit = {}
+) {
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(Cream)
+ .padding(6.dp),
+ verticalAlignment = Alignment.Bottom
+ ) {
+
+ UlbanBasicTextField(
+ text = msg,
+ onTextChange = onUpdateMessage,
+ modifier = Modifier
+ .padding(10.dp, 0.dp)
+ .weight(1f)
+ .wrapContentHeight(),
+ maxLines = 3,
+ textStyle = UlbanTypography.bodyMedium.copy(fontWeight = FontWeight.Normal)
+
+ )
+
+ Icon(
+ Icons.AutoMirrored.Outlined.Send,
+ contentDescription = "",
+ modifier = Modifier
+ .size(30.dp)
+ .clickable {
+ onSendClick(msg)
+ }
+ )
+
+ }
+
+
+}
+
+@Composable
+@Preview(showBackground = true)
+fun ChattingScreenPreview() {
+ UlbanTheme {
+ ChattingScreen()
+ }
+}
+
diff --git a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/chatting/ChattingViewModel.kt b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/chatting/ChattingViewModel.kt
new file mode 100644
index 00000000..59e37c99
--- /dev/null
+++ b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/chatting/ChattingViewModel.kt
@@ -0,0 +1,182 @@
+package com.sixkids.teacher.board.chatting
+
+import android.annotation.SuppressLint
+import android.util.Log
+import androidx.lifecycle.viewModelScope
+import androidx.paging.PagingData
+import androidx.paging.cachedIn
+import androidx.paging.compose.collectAsLazyPagingItems
+import com.sixkids.domain.usecase.chatting.GetChattingHistoryUseCase
+import com.sixkids.domain.usecase.organization.GetSelectedOrganizationIdUseCase
+import com.sixkids.domain.usecase.organization.LoadSelectedOrganizationNameUseCase
+import com.sixkids.domain.usecase.user.GetATKUseCase
+import com.sixkids.domain.usecase.user.GetUserInfoUseCase
+import com.sixkids.domain.usecase.user.LoadUserInfoUseCase
+import com.sixkids.model.Chat
+import com.sixkids.model.ChatMessage
+import com.sixkids.model.UserInfo
+import com.sixkids.ui.base.BaseViewModel
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.async
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.launch
+import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
+import org.hildan.krossbow.stomp.StompClient
+import org.hildan.krossbow.stomp.StompSession
+import org.hildan.krossbow.stomp.conversions.convertAndSend
+import org.hildan.krossbow.stomp.conversions.moshi.withMoshi
+import org.hildan.krossbow.stomp.frame.StompFrame
+import org.hildan.krossbow.stomp.headers.StompSendHeaders
+import org.hildan.krossbow.stomp.headers.StompSubscribeHeaders
+import org.hildan.krossbow.websocket.okhttp.OkHttpWebSocketClient
+import javax.inject.Inject
+import com.sixkids.teacher.board.BuildConfig
+
+private const val TAG = "D107"
+
+@HiltViewModel
+class ChattingViewModel @Inject constructor(
+ private val getATKUseCase: GetATKUseCase,
+ private val getSelectedOrganizationIdUseCase: GetSelectedOrganizationIdUseCase,
+ private val loadSelectedOrganizationNameUseCase: LoadSelectedOrganizationNameUseCase,
+ private val loadUserInfoUseCase: LoadUserInfoUseCase,
+ private val getChattingHistoryUseCase: GetChattingHistoryUseCase
+) : BaseViewModel(ChattingState()) {
+
+ private var roomId = 1L
+ private lateinit var tkn: String
+ private lateinit var userInfo: UserInfo
+
+ private lateinit var stompSession: StompSession
+ private val moshi: Moshi = Moshi.Builder()
+ .addLast(KotlinJsonAdapterFactory())
+ .build()
+ private lateinit var newChatMessage: Flow
+
+ private lateinit var chattingList: List
+
+ var originalChatList: Flow>? = null
+
+ init {
+ viewModelScope.launch {
+ loadSelectedOrganizationNameUseCase().onSuccess {
+ intent { copy(organizationName = it) }
+ }.onFailure {
+ intent { copy(organizationName = "채팅") }
+ }
+ }
+ }
+ @SuppressLint("CheckResult")
+ fun initStomp() {
+ viewModelScope.launch {
+ try {
+ loadLocalData()
+
+ originalChatList =
+ getChattingHistoryUseCase(roomId).cachedIn(viewModelScope)
+
+
+
+ } catch (e: Exception) {
+ Log.d(TAG, "initStomp: ${e.message}")
+ }
+ }
+ }
+
+ private fun loadLocalData() {
+ viewModelScope.launch {
+ val tknJob = async { getATKUseCase().getOrThrow() }
+ val roomIdJob = async { getSelectedOrganizationIdUseCase().getOrThrow() }
+ val userInfoJob = async { loadUserInfoUseCase().getOrThrow() }
+
+ tkn = tknJob.await()
+ roomId = roomIdJob.await().toLong()
+ userInfo = userInfoJob.await()
+
+ connectStomp()
+
+ intent { copy(memberId = userInfo.id) }
+ }
+ }
+
+ suspend fun connectStomp(){
+ viewModelScope.launch {
+ val okHttpClient = OkHttpClient.Builder()
+ .addInterceptor(
+ HttpLoggingInterceptor().apply {
+ level = HttpLoggingInterceptor.Level.BODY
+ }
+ )
+ .build()
+
+ val client = StompClient(OkHttpWebSocketClient(okHttpClient))
+
+ stompSession = client.connect(
+ BuildConfig.STOMP_ENDPOINT,
+ customStompConnectHeaders = mapOf(
+ HEADER_AUTHORIZATION to tkn,
+ HEADER_ROOM_ID to roomId.toString()
+ )
+ ).withMoshi(moshi)
+
+ newChatMessage = stompSession.subscribe(
+ StompSubscribeHeaders(
+ destination = "$SUBSCRIBE_URL$roomId",
+ customHeaders = mapOf(
+ HEADER_AUTHORIZATION to tkn
+ )
+ )
+ )
+
+
+ newChatMessage.collect {
+ val chatMessage = moshi.adapter(Chat::class.java).fromJson(it.bodyAsText)
+ intent { copy(chatList = chatList + chatMessage!!) }
+ }
+
+
+ }
+ }
+
+ fun updateMessage(message: String) {
+ intent { copy(message = message) }
+ }
+
+ fun sendMessage(message: String) {
+ viewModelScope.launch {
+ Log.d(TAG, "sendMessage: ${userInfo.photo}")
+ stompSession.withMoshi(moshi).convertAndSend(
+ StompSendHeaders(
+ destination = SEND_URL,
+ customHeaders = mapOf(
+ HEADER_AUTHORIZATION to tkn
+ )
+ ),
+ ChatMessage(roomId, userInfo.photo, message)
+ )
+ }
+ intent { copy(message = "") }
+ }
+
+ fun cancelStomp() {
+ try {
+ viewModelScope.launch {
+ stompSession.disconnect()
+ }
+ } catch (e: Exception) {
+ Log.d(TAG, "cancelStomp: ${e.message}")
+ }
+ }
+
+ companion object {
+ const val HEADER_AUTHORIZATION = "Authorization"
+ const val HEADER_ROOM_ID = "roomId"
+
+ const val SEND_URL = "/publish/chat/message"
+ const val SUBSCRIBE_URL = "/subscribe/public/"
+ }
+
+}
\ No newline at end of file
diff --git a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/main/BoardMainContract.kt b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/main/BoardMainContract.kt
index 06b60c61..ba1e938b 100644
--- a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/main/BoardMainContract.kt
+++ b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/main/BoardMainContract.kt
@@ -1,6 +1,13 @@
package com.sixkids.teacher.board.main
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
data class BoardMainState(
val isLoading: Boolean = false,
val classString: String = "",
-)
\ No newline at end of file
+): UiState
+
+sealed interface BoardMainEffect : SideEffect{
+ data object NavigateToChatting : BoardMainEffect
+}
\ No newline at end of file
diff --git a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/main/BoardMainScreen.kt b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/main/BoardMainScreen.kt
index a0ec8c83..6995c081 100644
--- a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/main/BoardMainScreen.kt
+++ b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/main/BoardMainScreen.kt
@@ -9,13 +9,21 @@ 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.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
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 androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.sixkids.designsystem.theme.Blue
+import com.sixkids.designsystem.theme.BlueText
import com.sixkids.designsystem.theme.Orange
+import com.sixkids.designsystem.theme.OrangeText
+import com.sixkids.designsystem.theme.Purple
+import com.sixkids.designsystem.theme.PurpleText
import com.sixkids.designsystem.theme.UlbanTypography
import com.sixkids.designsystem.theme.component.card.ContentAligment
import com.sixkids.designsystem.theme.component.card.ContentCard
@@ -24,21 +32,39 @@ import com.sixkids.designsystem.R as UlbanRes
@Composable
fun BoardMainRoute(
- padding: PaddingValues
+ viewModel: BoardMainViewModel = hiltViewModel(),
+ padding: PaddingValues,
+ navigateToPost: () -> Unit,
+ navigateToChatting: () -> Unit,
+ navigateToAnnounce: () -> Unit,
) {
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ LaunchedEffect(Unit) {
+ viewModel.init()
+ }
+
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
- BoardMainScreen()
+ BoardMainScreen(
+ boardMainState = uiState,
+ postCardOnClick = navigateToPost,
+ navigateToChatting = navigateToChatting,
+ announceCardOnClick = navigateToAnnounce
+ )
}
}
@Composable
fun BoardMainScreen(
modifier: Modifier = Modifier,
- boardMainState: BoardMainState = BoardMainState()
+ boardMainState: BoardMainState = BoardMainState(),
+ postCardOnClick: () -> Unit = {},
+ navigateToChatting: () -> Unit = {},
+ announceCardOnClick: () -> Unit = {}
) {
Column(
modifier = modifier
@@ -48,37 +74,49 @@ fun BoardMainScreen(
Spacer(modifier = Modifier.height(20.dp))
Text(
text = stringResource(id = R.string.board_main_title),
- style = UlbanTypography.titleLarge
+ style = UlbanTypography.titleLarge,
+ modifier = Modifier.padding(bottom = 10.dp)
)
Text(
- text = boardMainState.classString,
- style = UlbanTypography.bodySmall
+ text = boardMainState.classString.replace("\n", " "),
+ style = UlbanTypography.titleSmall
)
- Spacer(modifier = Modifier.height(20.dp))
+
+ Spacer(modifier = Modifier.weight(1f))
+
ContentCard(
modifier = Modifier.padding(start = 40.dp),
- imageModifier = Modifier.rotate(-20f),
+ imageModifier = Modifier.rotate(-20f).padding(15.dp),
contentName = stringResource(id = R.string.board_main_announce),
contentImageId = UlbanRes.drawable.announce,
cardColor = Orange,
- contentAligment = ContentAligment.ImageStart_TextEnd
+ textColor = OrangeText,
+ contentAligment = ContentAligment.ImageStart_TextEnd,
+ onclick = announceCardOnClick
)
Spacer(modifier = Modifier.height(20.dp))
ContentCard(
modifier = Modifier.padding(end = 40.dp),
+ imageModifier = Modifier.padding(15.dp),
contentName = stringResource(id = R.string.board_main_post),
contentImageId = UlbanRes.drawable.board,
cardColor = Blue,
- contentAligment = ContentAligment.ImageEnd_TextStart
+ textColor = BlueText,
+ contentAligment = ContentAligment.ImageEnd_TextStart,
+ onclick = postCardOnClick
)
Spacer(modifier = Modifier.height(20.dp))
ContentCard(
modifier = Modifier.padding(start = 40.dp),
+ imageModifier = Modifier.padding(10.dp),
contentName = stringResource(id = R.string.board_main_chat),
contentImageId = UlbanRes.drawable.chat,
- cardColor = Orange,
- contentAligment = ContentAligment.ImageStart_TextEnd
+ cardColor = Purple,
+ textColor = PurpleText,
+ contentAligment = ContentAligment.ImageStart_TextEnd,
+ onclick = navigateToChatting
)
+ Spacer(modifier = Modifier.weight(1.2f))
}
}
@@ -89,4 +127,4 @@ fun BoardMainScreenPreview() {
BoardMainScreen(
boardMainState = BoardMainState(classString = "인동초등학교 1학년 1반")
)
-}
\ No newline at end of file
+}
diff --git a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/main/BoardMainViewModel.kt b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/main/BoardMainViewModel.kt
new file mode 100644
index 00000000..f89f0c0e
--- /dev/null
+++ b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/main/BoardMainViewModel.kt
@@ -0,0 +1,26 @@
+package com.sixkids.teacher.board.main
+
+import androidx.lifecycle.viewModelScope
+import com.sixkids.domain.usecase.organization.LoadSelectedOrganizationNameUseCase
+import com.sixkids.teacher.board.chatting.ChattingSideEffect
+import com.sixkids.teacher.board.chatting.ChattingState
+import com.sixkids.ui.base.BaseViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class BoardMainViewModel @Inject constructor(
+ private val loadOrganizationNameUseCase: LoadSelectedOrganizationNameUseCase
+): BaseViewModel(BoardMainState()){
+ fun init(){
+ viewModelScope.launch {
+ loadOrganizationNameUseCase().onSuccess {
+ intent{ copy(classString = it) }
+ }.onFailure {
+ intent { copy(classString = "")}
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/navigation/BoardNavigation.kt b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/navigation/BoardNavigation.kt
index e4697167..50961d06 100644
--- a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/navigation/BoardNavigation.kt
+++ b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/navigation/BoardNavigation.kt
@@ -4,21 +4,151 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
+import androidx.navigation.NavType
import androidx.navigation.compose.composable
+import androidx.navigation.navArgument
+import com.sixkids.teacher.board.announce.announcedetail.AnnounceDetailRoute
+import com.sixkids.teacher.board.announce.announcelist.AnnounceListRoute
+import com.sixkids.teacher.board.announce.announcewrite.AnnounceWriteRoute
+import com.sixkids.teacher.board.chatting.ChattingRoute
import com.sixkids.teacher.board.main.BoardMainRoute
+import com.sixkids.teacher.board.post.postlist.PostRoute
+import com.sixkids.teacher.board.post.postdetail.PostDetailRoute
+import com.sixkids.teacher.board.post.postwrite.PostWriteRoute
+import com.sixkids.ui.SnackbarToken
fun NavController.navigateBoard(navOptions: NavOptions) {
navigate(BoardRoute.defaultRoute, navOptions)
}
+fun NavController.navigatePost() {
+ navigate(BoardRoute.postRoute)
+}
+
+fun NavController.navigatePostWrite() {
+ navigate(BoardRoute.postWriteRoute)
+}
+
+fun NavController.navigatePostDetail(postId: Long) {
+ navigate(BoardRoute.postDetailRoute(postId))
+}
+
+fun NavController.navigateChatting() {
+ navigate(BoardRoute.chattingRoute)
+}
+
+fun NavController.navigateAnnounce() {
+ navigate(BoardRoute.announceRoute)
+}
+
+fun NavController.navigateAnnounceWrite() {
+ navigate(BoardRoute.announceWriteRoute)
+}
+
+fun NavController.navigateAnnounceDetail(announceDetailId: Long) {
+ navigate(BoardRoute.announceDetailRoute(announceDetailId))
+}
+
fun NavGraphBuilder.boardNavGraph(
padding: PaddingValues,
+ navigateToPost: () -> Unit,
+ navigateToPostDetail: (Long) -> Unit,
+ navigateToPostWrite: () -> Unit,
+ onBackClick: () -> Unit,
+ onShowSnackBar: (SnackbarToken) -> Unit,
+ navigateToChatting: () -> Unit,
+ navigateToAnnounceDetail: (Long) -> Unit,
+ navigateToAnnounceWrite: () -> Unit,
+ navigateToAnnounceList: () -> Unit,
) {
- composable(route = BoardRoute.defaultRoute){
- BoardMainRoute(padding)
+ composable(route = BoardRoute.defaultRoute) {
+ BoardMainRoute(
+ padding = padding,
+ navigateToPost = navigateToPost,
+ navigateToChatting = navigateToChatting,
+ navigateToAnnounce = navigateToAnnounceList
+ )
+ }
+
+ composable(
+ route = BoardRoute.postRoute,
+ ) {
+ PostRoute(
+ padding = padding,
+ navigateToDetail = navigateToPostDetail,
+ navigateToWrite = navigateToPostWrite,
+ onShowSnackBar = onShowSnackBar
+ )
+ }
+
+ composable(BoardRoute.postWriteRoute) {
+ PostWriteRoute(
+ padding = padding,
+ navigateBack = onBackClick,
+ onShowSnackBar = onShowSnackBar
+ )
+ }
+
+ composable(
+ route = BoardRoute.postDetailRoute,
+ arguments = listOf(navArgument(BoardRoute.postDetailARG) { type = NavType.LongType })
+ ) {
+ PostDetailRoute(
+ padding = padding,
+ onShowSnackBar = onShowSnackBar
+ )
+ }
+
+ composable(route = BoardRoute.chattingRoute) {
+ ChattingRoute(
+ onBackClick = onBackClick,
+ onShowSnackBar = onShowSnackBar
+ )
+ }
+
+ composable(
+ route = BoardRoute.announceRoute,
+ ) {
+ AnnounceListRoute(
+ padding = padding,
+ navigateToAnnounceDetail = navigateToAnnounceDetail,
+ navigateToAnnounceWrite = navigateToAnnounceWrite,
+ onShowSnackBar = onShowSnackBar
+ )
+ }
+
+ composable(BoardRoute.announceWriteRoute) {
+ AnnounceWriteRoute(
+ padding = padding,
+ navigateBack = onBackClick,
+ onShowSnackBar = onShowSnackBar
+ )
+ }
+
+ composable(
+ route = BoardRoute.announceDetailRoute,
+ arguments = listOf(navArgument(BoardRoute.announceDetailARG) { type = NavType.LongType })
+ ) {
+ AnnounceDetailRoute(
+ padding = padding,
+ onShowSnackBar = onShowSnackBar
+ )
}
}
object BoardRoute {
+ const val postDetailARG = "postId"
+ const val announceDetailARG = "announceDetailId"
+
const val defaultRoute = "board"
-}
\ No newline at end of file
+ const val postRoute = "post"
+ const val postWriteRoute = "post_write"
+ const val postDetailRoute = "post_detail/{$postDetailARG}"
+ const val chattingRoute = "chatting"
+ const val announceRoute = "announce"
+ const val announceWriteRoute = "announce_write"
+ const val announceDetailRoute = "announce_detail/{$announceDetailARG}"
+
+ fun postDetailRoute(postId: Long) = "post_detail/$postId"
+ fun announceDetailRoute(announceDetailId: Long) = "announce_detail/$announceDetailId"
+}
diff --git a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postdetail/PostDetailContract.kt b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postdetail/PostDetailContract.kt
new file mode 100644
index 00000000..562a8b92
--- /dev/null
+++ b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postdetail/PostDetailContract.kt
@@ -0,0 +1,17 @@
+package com.sixkids.teacher.board.post.postdetail
+
+import com.sixkids.model.PostDetail
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
+sealed interface PostDetailEffect: SideEffect{
+ data object RefreshPostDetail: PostDetailEffect
+ data class OnShowSnackbar(val message: String) : PostDetailEffect
+}
+
+data class PostDetailState(
+ val isLoading: Boolean = false,
+ val postDetail: PostDetail = PostDetail(),
+ val commentText: String = "",
+ val selectedCommentId: Long? = null,
+) : UiState
\ No newline at end of file
diff --git a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postdetail/PostDetailScreen.kt b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postdetail/PostDetailScreen.kt
new file mode 100644
index 00000000..2400dae4
--- /dev/null
+++ b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postdetail/PostDetailScreen.kt
@@ -0,0 +1,245 @@
+package com.sixkids.teacher.board.post.postdetail
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.clickable
+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.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import coil.compose.AsyncImage
+import com.sixkids.designsystem.component.screen.LoadingScreen
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.model.Comment
+import com.sixkids.model.MemberSimple
+import com.sixkids.model.PostDetail
+import com.sixkids.model.Recomment
+import com.sixkids.teacher.board.post.postdetail.component.CommentItem
+import com.sixkids.teacher.board.post.postdetail.component.CommentTextField
+import com.sixkids.teacher.board.post.postdetail.component.PostWriterInfo
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.util.formatToMonthDayTime
+import java.time.LocalDateTime
+import com.sixkids.designsystem.R as UlbanRes
+
+@Composable
+fun PostDetailRoute(
+ viewModel: PostDetailViewModel = hiltViewModel(),
+ padding: PaddingValues,
+ onShowSnackBar: (SnackbarToken) -> Unit
+) {
+ val uiState by viewModel.uiState.collectAsState()
+
+ LaunchedEffect(Unit) {
+ viewModel.getPostDetail()
+ }
+
+ LaunchedEffect(viewModel.sideEffect) {
+ viewModel.sideEffect.collect { sideEffect ->
+ when (sideEffect) {
+ PostDetailEffect.RefreshPostDetail -> viewModel.getPostDetail()
+ is PostDetailEffect.OnShowSnackbar -> {
+ onShowSnackBar(SnackbarToken(message = sideEffect.message))
+ }
+ }
+ }
+ }
+
+ Box(modifier = Modifier.padding(padding)) {
+ PostDetailScreen(
+ postDetailState = uiState,
+ onCommentTextChanged = viewModel::onCommentTextChanged,
+ onClickComment = viewModel::onSelectedCommentId,
+ onClickSubmitComment = viewModel::onNewComment,
+ )
+ }
+}
+
+@Composable
+fun PostDetailScreen(
+ modifier: Modifier = Modifier,
+ postDetailState: PostDetailState,
+ onCommentTextChanged: (String) -> Unit = {},
+ onClickComment: (Long) -> Unit = {},
+ onClickSubmitComment: () -> Unit = {},
+ postDeleteOnclick: () -> Unit = {}
+) {
+
+ val scrollState = rememberScrollState()
+
+ BackHandler(
+ enabled = postDetailState.selectedCommentId != null,
+ onBack = {onClickComment(postDetailState.selectedCommentId?: 0)}
+ )
+
+ Box{
+ Column {
+ Column(
+ modifier = modifier
+ .weight(1f)
+ .padding(20.dp)
+ .verticalScroll(scrollState),
+ ) {
+ // 작성자 정보
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ PostWriterInfo(
+ height = 60.dp,
+ writer = postDetailState.postDetail.writeMember.name,
+ dateString = postDetailState.postDetail.createTime.formatToMonthDayTime(),
+ writerImageUrl = postDetailState.postDetail.writeMember.photo
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ Icon(
+ modifier = Modifier
+ .size(30.dp)
+ .clickable { postDeleteOnclick() },
+ imageVector = ImageVector.vectorResource(id = UlbanRes.drawable.ic_delete),
+ contentDescription = "더보기"
+ )
+ }
+
+ Spacer(modifier = Modifier.height(10.dp))
+ Text(
+ text = postDetailState.postDetail.title,
+ style = UlbanTypography.titleLarge
+ )
+ Spacer(modifier = Modifier.height(10.dp))
+ // 이미지
+ if (postDetailState.postDetail.imageUri.isNotEmpty()) {
+ AsyncImage(
+ model = postDetailState.postDetail.imageUri,
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ )
+ }
+ Spacer(modifier = Modifier.height(10.dp))
+ Text(
+ text = postDetailState.postDetail.content,
+ style = UlbanTypography.bodyLarge
+ )
+ Spacer(modifier = Modifier.height(10.dp))
+ HorizontalDivider(
+ thickness = 2.dp,
+ color = Color.Black
+ )
+ // 댓글 목록
+ for (comment in postDetailState.postDetail.comments) {
+ CommentItem(
+ selected = postDetailState.selectedCommentId == comment.id,
+ writer = comment.member.name,
+ dateString = comment.createTime.formatToMonthDayTime(),
+ writerImageUrl = comment.member.photo,
+ commentString = comment.content,
+ recommentOnclick = {
+ onClickComment(comment.id)
+ }
+ )
+ // 대댓글 목록
+ for (recomment in comment.recomments) {
+ Row {
+ Icon(
+ modifier = Modifier.padding(4.dp),
+ imageVector = ImageVector.vectorResource(id = UlbanRes.drawable.ic_recomment),
+ contentDescription = null
+ )
+ CommentItem(
+ writer = recomment.member.name,
+ dateString = recomment.createTime.formatToMonthDayTime(),
+ writerImageUrl = recomment.member.photo,
+ commentString = recomment.content,
+ isRecomment = true
+ )
+ }
+
+ }
+ }
+ }
+ CommentTextField(
+ msg = postDetailState.commentText,
+ onTextIuputChange = onCommentTextChanged,
+ onSendClick = { onClickSubmitComment() }
+ )
+ }
+
+
+ if (postDetailState.isLoading) {
+ LoadingScreen()
+ }
+ }
+
+}
+
+@Preview(showBackground = true)
+@Composable
+fun PostDetailScreenPreview() {
+ PostDetailScreen(
+ postDetailState = postDetailStateDummy
+ )
+}
+
+
+val recommentDummy = Recomment(
+ 1,
+ member = MemberSimple(
+ id = 1,
+ name = "작성자",
+ photo = "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTKe3bkhl96AgtmHyTiKW-KXRst2-5MoY6xB9-mZP74BQ&s"
+ ),
+ content = "내용내용내용내용내용내용내용내용내용내용내용내용내용내용내용",
+ createTime = LocalDateTime.now(),
+ updateTime = LocalDateTime.now(),
+ 1,
+)
+
+val commentDummy = Comment(
+ 1,
+ member = MemberSimple(
+ id = 1,
+ name = "작성자",
+ photo = "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTKe3bkhl96AgtmHyTiKW-KXRst2-5MoY6xB9-mZP74BQ&s"
+ ),
+ "내용내용내용 내용내용내용내용내용내용내용내용내용 내용내용내용내용내용내용내용내용내용",
+ LocalDateTime.now(),
+ LocalDateTime.now(),
+ listOf(recommentDummy, recommentDummy)
+)
+
+val postDetailStateDummy = PostDetailState(
+ isLoading = false,
+ postDetail = PostDetail(
+ title = "제목",
+ content = "내용내용내용내용내용내용내용내용내용내용내용내용내용",
+ writeMember = MemberSimple(
+ id = 1,
+ name = "작성자",
+ photo = "https://picsum.photos/200/300"
+ ),
+ createTime = LocalDateTime.now(),
+ imageUri = "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTKe3bkhl96AgtmHyTiKW-KXRst2-5MoY6xB9-mZP74BQ&s",
+ comments = listOf(commentDummy, commentDummy)
+ )
+)
\ No newline at end of file
diff --git a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postdetail/PostDetailViewModel.kt b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postdetail/PostDetailViewModel.kt
new file mode 100644
index 00000000..0e4a448e
--- /dev/null
+++ b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postdetail/PostDetailViewModel.kt
@@ -0,0 +1,103 @@
+package com.sixkids.teacher.board.post.postdetail
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.viewModelScope
+import com.sixkids.domain.usecase.comment.DeleteCommentUseCase
+import com.sixkids.domain.usecase.comment.NewCommentUseCase
+import com.sixkids.domain.usecase.comment.NewRecommentUseCase
+import com.sixkids.domain.usecase.comment.ReportCommentUseCase
+import com.sixkids.domain.usecase.comment.UpdateCommentUsecase
+import com.sixkids.domain.usecase.post.DeletePostUseCase
+import com.sixkids.domain.usecase.post.GetPostDetailUseCase
+import com.sixkids.domain.usecase.post.UpdatePostUseCase
+import com.sixkids.teacher.board.navigation.BoardRoute
+import com.sixkids.ui.base.BaseViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class PostDetailViewModel @Inject constructor(
+ savedStateHandle: SavedStateHandle,
+ private val getPostDetailUseCase: GetPostDetailUseCase,
+ private val updatePostUseCase: UpdatePostUseCase,
+ private val deletePostUsecase: DeletePostUseCase,
+ private val deleteCommentUseCase: DeleteCommentUseCase,
+ private val updateCommentUsecase: UpdateCommentUsecase,
+ private val newCommentUseCase: NewCommentUseCase,
+ private val newRecommentUseCase: NewRecommentUseCase,
+ private val reportCommentUseCase: ReportCommentUseCase
+) : BaseViewModel(PostDetailState()) {
+
+ private val postId: Long = savedStateHandle.get(BoardRoute.postDetailARG)!!
+
+ fun onCommentTextChanged(commentText: String) = intent { copy(commentText = commentText) }
+ fun onSelectedCommentId(commentId: Long?) = intent {
+ if (currentState.selectedCommentId == commentId) {
+ copy(selectedCommentId = null)
+ } else {
+ copy(selectedCommentId = commentId)
+ }
+ }
+
+ fun getPostDetail() {
+ viewModelScope.launch {
+ intent { copy(isLoading = true) }
+ getPostDetailUseCase(postId).onSuccess {
+ intent { copy(postDetail = it) }
+ }.onFailure {
+ postSideEffect(PostDetailEffect.OnShowSnackbar(it.message ?: "게시글을 불러오지 못했어요"))
+ }
+ intent { copy(isLoading = false) }
+ }
+ }
+
+ fun onNewComment() {
+ if (currentState.commentText.isBlank()) {
+ postSideEffect(PostDetailEffect.OnShowSnackbar("댓글을 입력해주세요"))
+ } else {
+ if (currentState.selectedCommentId == null) {
+ viewModelScope.launch {
+ intent { copy(isLoading = true) }
+ newCommentUseCase(
+ postId = postId,
+ content = currentState.commentText,
+ ).onSuccess {
+ postSideEffect(PostDetailEffect.OnShowSnackbar("댓글이 작성되었습니다"))
+ intent { copy(commentText = "", selectedCommentId = null) }
+ getPostDetail()
+ }.onFailure {
+ postSideEffect(
+ PostDetailEffect.OnShowSnackbar(
+ it.message ?: "댓글 작성에 실패했어요"
+ )
+ )
+ }
+ intent { copy(isLoading = false) }
+ }
+ } else {
+ viewModelScope.launch {
+ intent { copy(isLoading = true) }
+ newRecommentUseCase(
+ postId = postId,
+ content = currentState.commentText,
+ currentState.selectedCommentId!!
+ ).onSuccess {
+ postSideEffect(PostDetailEffect.OnShowSnackbar("댓글이 작성되었습니다"))
+ intent { copy(commentText = "", selectedCommentId = null)}
+ getPostDetail()
+ }.onFailure {
+ postSideEffect(
+ PostDetailEffect.OnShowSnackbar(
+ it.message ?: "댓글 작성에 실패했어요"
+ )
+ )
+ }
+ intent { copy(isLoading = false) }
+ }
+ }
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postdetail/component/CommentItem.kt b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postdetail/component/CommentItem.kt
new file mode 100644
index 00000000..74c9b70c
--- /dev/null
+++ b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postdetail/component/CommentItem.kt
@@ -0,0 +1,133 @@
+package com.sixkids.teacher.board.post.postdetail.component
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardColors
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+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.graphics.vector.ImageVector
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import com.sixkids.designsystem.theme.Blue
+import com.sixkids.designsystem.theme.Gray
+import com.sixkids.designsystem.theme.GrayLight
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.designsystem.R as UlbanRes
+
+@Composable
+fun CommentItem(
+ modifier: Modifier = Modifier,
+ selected: Boolean = false,
+ writer: String = "",
+ dateString: String = "00/00 00:00",
+ writerImageUrl: String = "",
+ commentString: String = "",
+ isRecomment: Boolean = false,
+ recommentOnclick: () -> Unit = {},
+ deleteOnclick: (() -> Unit)? = null
+){
+ Card(
+ colors = CardDefaults.cardColors(
+ containerColor =
+ if (selected) { Blue}
+ else if (isRecomment) {GrayLight}
+ else {Color.Transparent}
+ ),
+ ) {
+ Column(
+ modifier = modifier
+ .padding(start = 10.dp)
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ AsyncImage(
+ modifier = Modifier
+ .height(36.dp)
+ .aspectRatio(1f),
+ model = writerImageUrl,
+ contentScale = ContentScale.Crop,
+ contentDescription = "작성자 프로필 사진"
+ )
+ Spacer(modifier = Modifier.width(4.dp))
+ Text(
+ text = writer,
+ style = UlbanTypography.bodyMedium
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ if (!isRecomment) {
+ Icon(
+ modifier = Modifier.clickable{recommentOnclick()},
+ imageVector = ImageVector.vectorResource(id = UlbanRes.drawable.ic_chat_bubble_outline),
+ contentDescription = null
+ )
+ }
+ if (deleteOnclick != null) {
+ Icon(
+ modifier = Modifier.clickable{deleteOnclick()},
+ imageVector = ImageVector.vectorResource(id = UlbanRes.drawable.ic_delete),
+ contentDescription = null
+ )
+ }
+ }
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ text = commentString,
+ style = UlbanTypography.bodyMedium
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ text = dateString,
+ style = UlbanTypography.bodySmall.copy(
+ color = Gray
+ )
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun CommentItemPreview() {
+ Column {
+ CommentItem(
+ writer = "오하빈",
+ dateString = "09/01 12:00",
+ writerImageUrl = "https://www.google.com/images/branding/googlelogo/2x/googlelogo_light_color_272x92dp.png",
+ commentString = "댓글 내용",
+ deleteOnclick = {},
+ selected = true
+ )
+ CommentItem(
+ writer = "오하빈",
+ dateString = "09/01 12:00",
+ commentString = "댓글 내용",
+ writerImageUrl = "https://www.google.com/images/branding/googlelogo/2x/googlelogo_light_color_272x92dp.png",
+ )
+ CommentItem(
+ writer = "오하빈",
+ dateString = "09/01 12:00",
+ commentString = "댓글 내용",
+ writerImageUrl = "https://www.google.com/images/branding/googlelogo/2x/googlelogo_light_color_272x92dp.png",
+ isRecomment = true,
+ deleteOnclick = {}
+ )
+ }
+
+}
\ No newline at end of file
diff --git a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postdetail/component/CommentTextField.kt b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postdetail/component/CommentTextField.kt
new file mode 100644
index 00000000..16fd9449
--- /dev/null
+++ b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postdetail/component/CommentTextField.kt
@@ -0,0 +1,79 @@
+package com.sixkids.teacher.board.post.postdetail.component
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.outlined.Send
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardColors
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+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.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.sixkids.designsystem.component.textfield.UlbanBasicTextField
+import com.sixkids.designsystem.theme.Gray
+import com.sixkids.designsystem.theme.GrayLight
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.teacher.board.R
+
+@Composable
+fun CommentTextField(
+ msg: String = "",
+ onTextIuputChange: (String) -> Unit = {},
+ onSendClick: (String) -> Unit = {},
+) {
+ Card(
+ modifier = Modifier
+ .padding(6.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = GrayLight
+ )
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ UlbanBasicTextField(
+ text = msg,
+ onTextChange = onTextIuputChange,
+ modifier = Modifier
+ .padding(10.dp, 0.dp)
+ .weight(1f)
+ .wrapContentHeight(),
+ maxLines = 3,
+ textStyle = UlbanTypography.bodyMedium.copy(fontWeight = FontWeight.Normal),
+ hint = stringResource(id = R.string.board_detail_comment_hint)
+ )
+
+ Icon(
+ Icons.AutoMirrored.Outlined.Send,
+ contentDescription = "",
+ modifier = Modifier
+ .size(30.dp)
+ .padding(end = 4.dp)
+ .clickable {
+ onSendClick(msg)
+ }
+ )
+
+ }
+ }
+
+}
+
+@Preview(showBackground = true)
+@Composable
+fun CommentTextFieldPreview() {
+ CommentTextField()
+}
\ No newline at end of file
diff --git a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postdetail/component/PostWriterInfo.kt b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postdetail/component/PostWriterInfo.kt
new file mode 100644
index 00000000..5546a0e0
--- /dev/null
+++ b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postdetail/component/PostWriterInfo.kt
@@ -0,0 +1,64 @@
+package com.sixkids.teacher.board.post.postdetail.component
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxHeight
+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.layout.ContentScale
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import com.sixkids.designsystem.theme.UlbanTypography
+
+@Composable
+fun PostWriterInfo(
+ height: Dp = 60.dp,
+ writer: String = "",
+ dateString: String = "00/00 00:00",
+ writerImageUrl: String = ""
+) {
+ Row(
+ modifier = Modifier.height(height),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ AsyncImage(
+ modifier = Modifier
+ .fillMaxHeight()
+ .aspectRatio(1f),
+ model = writerImageUrl,
+ contentScale = ContentScale.Crop,
+ contentDescription = "프로필 사진"
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Column {
+ Text(
+ text = writer,
+ style = UlbanTypography.bodyLarge
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ text = dateString,
+ style = UlbanTypography.bodyMedium
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun PostWriterInfoPreview() {
+ PostWriterInfo(
+ height = 60.dp,
+ writer = "홍유준 선생님",
+ dateString = "10/10 10:10",
+ writerImageUrl = ""
+ )
+}
\ No newline at end of file
diff --git a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postlist/PostContract.kt b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postlist/PostContract.kt
new file mode 100644
index 00000000..9bad00b5
--- /dev/null
+++ b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postlist/PostContract.kt
@@ -0,0 +1,16 @@
+package com.sixkids.teacher.board.post.postlist
+
+import com.sixkids.model.Post
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
+sealed interface PostEffect : SideEffect {
+ data object NavigateToPostDetail: PostEffect
+ data object NavigateToWritePost: PostEffect
+ data class OnShowSnackBar(val message : String) : PostEffect
+}
+
+data class PostState(
+ val isLoding: Boolean = false,
+ val classString: String = "",
+): UiState
\ No newline at end of file
diff --git a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postlist/PostScreen.kt b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postlist/PostScreen.kt
new file mode 100644
index 00000000..0d24d75a
--- /dev/null
+++ b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postlist/PostScreen.kt
@@ -0,0 +1,158 @@
+package com.sixkids.teacher.board.post.postlist
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.paging.compose.LazyPagingItems
+import androidx.paging.compose.collectAsLazyPagingItems
+import com.sixkids.designsystem.component.appbar.UlbanDefaultAppBar
+import com.sixkids.designsystem.component.appbar.UlbanDetailAppBar
+import com.sixkids.designsystem.component.button.EditFAB
+import com.sixkids.designsystem.component.screen.LoadingScreen
+import com.sixkids.designsystem.theme.Blue
+import com.sixkids.designsystem.theme.BlueDark
+import com.sixkids.designsystem.theme.Orange
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.model.Post
+import com.sixkids.teacher.board.R
+import com.sixkids.teacher.board.post.postlist.component.PostItem
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.util.formatToMonthDayTimeKorean
+import com.sixkids.designsystem.R as UlbanRes
+
+@Composable
+fun PostRoute(
+ viewModel: PostViewModel = hiltViewModel(),
+ navigateToDetail: (postId:Long) -> Unit,
+ navigateToWrite: () -> Unit,
+ onShowSnackBar: (SnackbarToken) -> Unit,
+ padding: PaddingValues
+) {
+
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ LaunchedEffect(Unit) {
+ viewModel.getPostList()
+ }
+
+ LaunchedEffect(key1 = viewModel.sideEffect) {
+ viewModel.sideEffect.collect { sideEffect ->
+ when (sideEffect) {
+ PostEffect.NavigateToPostDetail -> {}
+ PostEffect.NavigateToWritePost -> {}
+ is PostEffect.OnShowSnackBar -> {
+ onShowSnackBar(SnackbarToken(message = sideEffect.message))
+ }
+ }
+ }
+ }
+
+ Box(
+ modifier = Modifier
+ .padding(padding)
+ .fillMaxSize()
+ ) {
+ PostScreen(
+ postState = uiState,
+ postItems = viewModel.postList?.collectAsLazyPagingItems(),
+ postItemOnclick = navigateToDetail,
+ fabClick = navigateToWrite
+ )
+ }
+}
+
+
+@Composable
+fun PostScreen(
+ modifier: Modifier = Modifier,
+ postState: PostState = PostState(),
+ postItems: LazyPagingItems? = null,
+ postItemOnclick: (postId: Long) -> Unit = {},
+ fabClick: () -> Unit = {}
+) {
+ val listState = rememberLazyListState()
+
+ Box(
+ modifier = modifier
+ .fillMaxSize()
+ ) {
+ Column(
+
+ ) {
+ UlbanDefaultAppBar(
+ leftIcon = UlbanRes.drawable.board,
+ title = stringResource(id = R.string.board_main_post),
+ content = stringResource(id = R.string.board_main_post),
+ body = postState.classString.replace("\n", " "),
+ color = Blue
+ )
+
+ if (postItems == null){
+ Spacer(modifier = Modifier.weight(1f))
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ text = stringResource(id = R.string.board_post_no_items),
+ textAlign = TextAlign.Center,
+ style = UlbanTypography.bodyLarge,
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ } else {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ state = listState,
+ ) {
+ items(postItems.itemCount) { index ->
+ postItems[index]?.let { post ->
+ PostItem(
+ title = post.title,
+ writer = post.writer,
+ dateString = post.time.formatToMonthDayTimeKorean(),
+ commentCount = post.commentCount,
+ onClick = { postItemOnclick(post.id) }
+ )
+ }
+ }
+ }
+ }
+ }
+ //FAB
+ EditFAB(
+ modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .padding(16.dp),
+ buttonColor = Blue,
+ iconColor = BlueDark,
+ onClick = fabClick
+ )
+ if (postState.isLoding){
+ LoadingScreen()
+ }
+ }
+}
+
+
+@Preview(showBackground = true)
+@Composable
+fun PostRoutePreview() {
+ PostScreen()
+}
\ No newline at end of file
diff --git a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postlist/PostViewModel.kt b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postlist/PostViewModel.kt
new file mode 100644
index 00000000..c929e45c
--- /dev/null
+++ b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postlist/PostViewModel.kt
@@ -0,0 +1,53 @@
+package com.sixkids.teacher.board.post.postlist
+
+import androidx.lifecycle.viewModelScope
+import androidx.paging.PagingData
+import androidx.paging.cachedIn
+import com.sixkids.domain.usecase.organization.GetSelectedOrganizationIdUseCase
+import com.sixkids.domain.usecase.organization.LoadSelectedOrganizationNameUseCase
+import com.sixkids.domain.usecase.post.GetPostListUseCase
+import com.sixkids.model.Post
+import com.sixkids.model.PostCategory
+import com.sixkids.ui.base.BaseViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class PostViewModel @Inject constructor(
+ private val getPostListUseCase: GetPostListUseCase,
+ private val getSelectedOrganizationIdUseCase: GetSelectedOrganizationIdUseCase,
+ private val loadSelectedOrganizationNameUseCase: LoadSelectedOrganizationNameUseCase
+): BaseViewModel(PostState()){
+
+ private var organizationId: Int? = null
+
+ var postList: Flow>? = null
+
+ fun getPostList() {
+ viewModelScope.launch {
+ intent { copy(isLoding = true) }
+ loadSelectedOrganizationNameUseCase().onSuccess {
+ intent { copy(classString = it) }
+ }.onFailure {
+ intent { copy(classString = "") }
+ }
+
+ if (organizationId == null){
+ organizationId = getSelectedOrganizationIdUseCase().getOrNull()
+ }
+
+ if (organizationId != null){
+ postList = getPostListUseCase(
+ organizationId = organizationId!!,
+ postCategory = PostCategory.FREE
+ ).cachedIn(viewModelScope)
+ } else {
+ postSideEffect(PostEffect.OnShowSnackBar("학급 정보를 불러오지 못했어요 ;("))
+ }
+
+ intent { copy(isLoding = false) }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postlist/component/CommentCount.kt b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postlist/component/CommentCount.kt
new file mode 100644
index 00000000..ebb33b5b
--- /dev/null
+++ b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postlist/component/CommentCount.kt
@@ -0,0 +1,38 @@
+package com.sixkids.teacher.board.post.postlist.component
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.tooling.preview.Preview
+import com.sixkids.designsystem.theme.OrangeDark
+import com.sixkids.designsystem.theme.OrangeText
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.designsystem.R as UlbanRes
+
+@Composable
+fun CommentCount(
+ count: Int
+) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(
+ imageVector = ImageVector.vectorResource(id = UlbanRes.drawable.ic_chat_bubble),
+ contentDescription = null,
+ tint = OrangeText
+ )
+ Text(
+ text = count.toString(),
+ style = UlbanTypography.bodyMedium
+ )
+ }
+}
+
+
+@Preview(showBackground = true)
+@Composable
+fun CommentCountPreview() {
+ CommentCount(10)
+}
\ No newline at end of file
diff --git a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postlist/component/PostItem.kt b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postlist/component/PostItem.kt
new file mode 100644
index 00000000..1ef08cc3
--- /dev/null
+++ b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postlist/component/PostItem.kt
@@ -0,0 +1,80 @@
+package com.sixkids.teacher.board.post.postlist.component
+
+import androidx.compose.foundation.clickable
+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.width
+import androidx.compose.material3.Divider
+import androidx.compose.material3.HorizontalDivider
+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.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.sixkids.designsystem.theme.UlbanTypography
+
+@Composable
+fun PostItem(
+ modifier: Modifier = Modifier ,
+ title: String,
+ writer: String,
+ commentCount: Int,
+ dateString: String,
+ dividerColor: Color = Color.Black,
+ onClick: () -> Unit = {}
+) {
+ Column(
+ modifier = modifier.padding(bottom = 8.dp).clickable { onClick() }
+ ) {
+ Text(
+ text = title,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = UlbanTypography.titleMedium
+ )
+ Spacer(modifier = Modifier.height(10.dp))
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ if (commentCount > 0){
+ CommentCount(count = commentCount)
+ }
+ Spacer(modifier = Modifier.width(6.dp))
+ Text(
+ text = writer,
+ style = UlbanTypography.bodyMedium
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ Text(
+ text = dateString,
+ style = UlbanTypography.bodyMedium
+ )
+ }
+ HorizontalDivider(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 4.dp),
+ thickness = 2.dp,
+ color = dividerColor
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun PostItemPreview() {
+ PostItem(
+ title = "이따 마크 할 사람~~!",
+ writer = "오하빈",
+ commentCount = 3,
+ dateString = "2024.04.16 14:30"
+ )
+}
\ No newline at end of file
diff --git a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postwrite/PostWriteContract.kt b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postwrite/PostWriteContract.kt
new file mode 100644
index 00000000..8db26a1e
--- /dev/null
+++ b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postwrite/PostWriteContract.kt
@@ -0,0 +1,18 @@
+package com.sixkids.teacher.board.post.postwrite
+
+import android.graphics.Bitmap
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
+sealed interface PostWriteEffect: SideEffect{
+ data object NavigateBack : PostWriteEffect
+ data class OnShowSnackbar(val message: String) : PostWriteEffect
+}
+
+data class PostWriteState(
+ val isLoading: Boolean = false,
+ val title: String = "",
+ val content: String = "",
+ val anonymousChecked: Boolean = false,
+ val photo: Bitmap? = null
+): UiState
\ No newline at end of file
diff --git a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postwrite/PostWriteScreen.kt b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postwrite/PostWriteScreen.kt
new file mode 100644
index 00000000..8fae0f74
--- /dev/null
+++ b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postwrite/PostWriteScreen.kt
@@ -0,0 +1,290 @@
+package com.sixkids.teacher.board.post.postwrite
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.ImageDecoder
+import android.os.Build
+import android.provider.MediaStore
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.PickVisualMediaRequest
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.gestures.ScrollableState
+import androidx.compose.foundation.gestures.scrollable
+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.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+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.layout.wrapContentHeight
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.CheckboxDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.scale
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.sixkids.designsystem.component.button.UlbanFilledButton
+import com.sixkids.designsystem.component.screen.LoadingScreen
+import com.sixkids.designsystem.theme.Blue
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.teacher.board.R
+import com.sixkids.teacher.board.post.postwrite.component.PageTitle
+import com.sixkids.ui.SnackbarToken
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+import com.sixkids.designsystem.R as UlbanRes
+
+@Composable
+fun PostWriteRoute(
+ viewModel: PostWriteViewModel = hiltViewModel(),
+ padding: PaddingValues,
+ navigateBack: () -> Unit,
+ onShowSnackBar: (SnackbarToken) -> Unit
+) {
+
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ val context = LocalContext.current
+ val photoLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.PickVisualMedia()
+ ) { uri ->
+ uri?.let {
+ try {
+ val bitmap = if (Build.VERSION.SDK_INT < 28) {
+ MediaStore.Images.Media.getBitmap(context.contentResolver, it)
+ } else {
+ ImageDecoder.decodeBitmap(
+ ImageDecoder.createSource(
+ context.contentResolver,
+ it
+ )
+ )
+ }
+ viewModel.onAddPhoto(bitmap)
+ } catch (e: IOException) {
+ viewModel.showToast("사진 호출에 실패했습니다.")
+ }
+ }
+ }
+
+ LaunchedEffect(key1 = viewModel.sideEffect) {
+ viewModel.sideEffect.collect { sideEffect ->
+ when (sideEffect) {
+ PostWriteEffect.NavigateBack -> navigateBack()
+ is PostWriteEffect.OnShowSnackbar -> {
+ onShowSnackBar(SnackbarToken(message = sideEffect.message))
+ }
+ }
+ }
+ }
+
+
+
+ PostWriteScreen(
+ postWriteState = uiState,
+ cancelOnClick = { viewModel.onBack() },
+ submitOnClick = {
+ viewModel.onPost(
+ uiState.photo?.let { saveBitmapToFile(context, it, "post_photo.jpg") }
+ )
+ },
+ titleValueChange = { viewModel.onTitleChanged(it) },
+ contentValueChange = { viewModel.onContentChanged(it) },
+ anonymousCheckedChange = { viewModel.onAnonymousChecked(it) },
+ addPhotoOnClick = { photoLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) }
+ )
+
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun PostWriteScreen(
+ modifier: Modifier = Modifier,
+ postWriteState: PostWriteState = PostWriteState(),
+ cancelOnClick: () -> Unit = {},
+ submitOnClick: () -> Unit = {},
+ titleValueChange: (String) -> Unit = {},
+ contentValueChange: (String) -> Unit = {},
+ anonymousCheckedChange: (Boolean) -> Unit = {},
+ addPhotoOnClick: () -> Unit = {}
+) {
+
+ val scrollState = rememberScrollState()
+
+ LaunchedEffect(postWriteState.content) {
+ scrollState.scrollTo(scrollState.maxValue)
+ }
+
+ Box {
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .padding(20.dp)
+ ) {
+ Spacer(modifier = Modifier.height(20.dp))
+ PageTitle(
+ title = stringResource(id = R.string.board_write_title),
+ cancelOnclick = cancelOnClick
+ )
+ //title
+ OutlinedTextField(
+ value = postWriteState.title,
+ onValueChange = titleValueChange,
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedBorderColor = Color.Transparent,
+ unfocusedBorderColor = Color.Transparent
+ ),
+ placeholder = {
+ Text(
+ text = stringResource(id = R.string.board_write_content_title),
+ style = UlbanTypography.bodyLarge
+ )
+ },
+ textStyle = UlbanTypography.bodyLarge
+ )
+ HorizontalDivider(
+ thickness = 2.dp,
+ color = Color.Black
+ )
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f)
+ .verticalScroll(scrollState)
+ ) {
+ //photo
+ if (postWriteState.photo != null) {
+ Spacer(modifier = Modifier.height(10.dp))
+ Image(
+ modifier = Modifier
+ .fillMaxWidth()
+ .aspectRatio(1f),
+ bitmap = postWriteState.photo.asImageBitmap(),
+ contentDescription = null,
+ contentScale = ContentScale.Crop
+ )
+ }
+ //content
+ OutlinedTextField(
+ value = postWriteState.content,
+ onValueChange = { string ->
+ contentValueChange(string)
+ },
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedBorderColor = Color.Transparent,
+ unfocusedBorderColor = Color.Transparent
+ ),
+ placeholder = {
+ Text(
+ text = stringResource(id = R.string.board_write_content_content),
+ style = UlbanTypography.bodyLarge
+ )
+ },
+ textStyle = UlbanTypography.bodyLarge
+ )
+ }
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ // 이미지 추가 아이콘
+ Icon(
+ modifier = Modifier
+ .size(40.dp)
+ .clickable(onClick = addPhotoOnClick),
+ imageVector = ImageVector.vectorResource(id = UlbanRes.drawable.ic_photo_camera),
+ contentDescription = null
+ )
+ Spacer(modifier = Modifier.width(10.dp))
+ // 익명 체크박스
+ Checkbox(
+ modifier = Modifier.scale(1.2f),
+ checked = postWriteState.anonymousChecked,
+ onCheckedChange = anonymousCheckedChange,
+ colors = CheckboxDefaults.colors(
+ checkedColor = Blue,
+ uncheckedColor = Color.Black
+ )
+ )
+ Text(
+ text = stringResource(id = R.string.board_write_anonymous),
+ style = UlbanTypography.bodyLarge
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ // 등록 버튼
+ UlbanFilledButton(
+ text = stringResource(id = R.string.board_write_submit),
+ onClick = submitOnClick
+ )
+ }
+ }
+
+ if (postWriteState.isLoading) {
+ LoadingScreen()
+ }
+ }
+
+}
+
+fun saveBitmapToFile(context: Context, bitmap: Bitmap?, fileName: String): File? {
+ val directory = context.getExternalFilesDir(null) ?: return null
+
+ val file = File(directory, fileName)
+ var fileOutputStream: FileOutputStream? = null
+
+ try {
+ fileOutputStream = FileOutputStream(file)
+ bitmap?.compress(Bitmap.CompressFormat.JPEG, 100, fileOutputStream)
+ fileOutputStream.flush()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return null
+ } finally {
+ try {
+ fileOutputStream?.close()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ return file
+}
+
+@Preview(showBackground = true)
+@Composable
+fun PostWriteScreenPreview() {
+ PostWriteScreen()
+}
\ No newline at end of file
diff --git a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postwrite/PostWriteViewModel.kt b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postwrite/PostWriteViewModel.kt
new file mode 100644
index 00000000..b75c1fbe
--- /dev/null
+++ b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postwrite/PostWriteViewModel.kt
@@ -0,0 +1,57 @@
+package com.sixkids.teacher.board.post.postwrite
+
+import android.graphics.Bitmap
+import androidx.lifecycle.viewModelScope
+import com.sixkids.domain.usecase.organization.GetSelectedOrganizationIdUseCase
+import com.sixkids.domain.usecase.post.NewPostUseCase
+import com.sixkids.ui.base.BaseViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import java.io.File
+import javax.inject.Inject
+
+@HiltViewModel
+class PostWriteViewModel @Inject constructor(
+ private val newPostUseCase: NewPostUseCase,
+ private val getSelectedOrganizationIdUseCase: GetSelectedOrganizationIdUseCase
+): BaseViewModel(PostWriteState()){
+
+ private var organizationId: Int? = null
+
+ fun onBack() = postSideEffect(PostWriteEffect.NavigateBack)
+ fun onTitleChanged(title: String) = intent { copy(title = title) }
+ fun onContentChanged(content: String) = intent { copy(content = content) }
+ fun onAnonymousChecked(checked: Boolean) = intent { copy(anonymousChecked = checked) }
+ fun onAddPhoto(bitmap: Bitmap) = intent { copy(photo = bitmap) }
+ fun showToast(message: String) = postSideEffect(PostWriteEffect.OnShowSnackbar(message))
+
+ fun onPost(photo: File?) {
+ viewModelScope.launch {
+ intent { copy(isLoading = true) }
+
+ if (organizationId == null) {
+ organizationId = getSelectedOrganizationIdUseCase().getOrNull()
+ }
+
+ if (organizationId != null) {
+ newPostUseCase(
+ organizationId = organizationId!!.toLong(),
+ title = currentState.title,
+ content = currentState.content,
+ secretStatus = currentState.anonymousChecked,
+ postCategory = "FREE",
+ file = photo
+ ).onSuccess {
+ postSideEffect(PostWriteEffect.OnShowSnackbar("게시글 작성에 성공했어요 :)"))
+ postSideEffect(PostWriteEffect.NavigateBack)
+ }.onFailure {
+ postSideEffect(PostWriteEffect.OnShowSnackbar(it.message ?: "게시글 작성에 실패했어요 ;("))
+ }
+ } else {
+ postSideEffect(PostWriteEffect.OnShowSnackbar("학급 정보를 불러오지 못했어요 ;("))
+ }
+
+ intent { copy(isLoading = false) }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postwrite/component/PageTitle.kt b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postwrite/component/PageTitle.kt
new file mode 100644
index 00000000..c777acdd
--- /dev/null
+++ b/android/feature/teacher/board/src/main/java/com/sixkids/teacher/board/post/postwrite/component/PageTitle.kt
@@ -0,0 +1,55 @@
+package com.sixkids.teacher.board.post.postwrite.component
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Icon
+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.vector.ImageVector
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.designsystem.R as UlbanRes
+
+@Composable
+fun PageTitle(
+ modifier: Modifier = Modifier,
+ title: String,
+ cancelOnclick: () -> Unit = {},
+) {
+ Row(
+ modifier = modifier,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ modifier = Modifier
+ .clickable { cancelOnclick() },
+ imageVector = ImageVector.vectorResource(id = UlbanRes.drawable.ic_cancel_post),
+ contentDescription = null
+ )
+ Spacer(modifier = Modifier.width(14.dp))
+ Text(
+ text = title,
+ style = UlbanTypography.titleLarge.copy(
+ fontWeight = FontWeight.Medium,
+ ),
+ )
+ }
+}
+
+
+@Preview(showBackground = true)
+@Composable
+fun PageTitlePreview() {
+ PageTitle(
+ title = "글 쓰기",
+ cancelOnclick = {}
+ )
+}
\ No newline at end of file
diff --git a/android/feature/teacher/board/src/main/res/drawable/ic_camera.xml b/android/feature/teacher/board/src/main/res/drawable/ic_camera.xml
new file mode 100644
index 00000000..7283b1a9
--- /dev/null
+++ b/android/feature/teacher/board/src/main/res/drawable/ic_camera.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/android/feature/teacher/board/src/main/res/values/strings.xml b/android/feature/teacher/board/src/main/res/values/strings.xml
index f2f4abde..f9f61225 100644
--- a/android/feature/teacher/board/src/main/res/values/strings.xml
+++ b/android/feature/teacher/board/src/main/res/values/strings.xml
@@ -4,4 +4,19 @@
알림장
자유게시판
채팅
+
+ 글쓰기
+ 제목
+ 내용을 입력하세요
+ 익명
+ 게시
+
+ 게시글이 없어요!
+
+ 댓글을 입력하세요
+
+ 알림장이 없어용!
+
+ 알림장 쓰기
+
\ No newline at end of file
diff --git a/android/feature/teacher/challenge/src/main/java/com/sixkids/teacher/challenge/create/ChallengeCreateContract.kt b/android/feature/teacher/challenge/src/main/java/com/sixkids/teacher/challenge/create/ChallengeCreateContract.kt
index 16cf1b38..d7c9d7c7 100644
--- a/android/feature/teacher/challenge/src/main/java/com/sixkids/teacher/challenge/create/ChallengeCreateContract.kt
+++ b/android/feature/teacher/challenge/src/main/java/com/sixkids/teacher/challenge/create/ChallengeCreateContract.kt
@@ -10,17 +10,22 @@ data class ChallengeCreateUiState(
val buttonEnabled: Boolean = false,
@StringRes val buttonText: Int? = null,
val step: ChallengeCreateStep = ChallengeCreateStep.INFO,
+ val organizationId: Int = 0,
) : UiState
sealed interface ChallengeCreateEffect : SideEffect {
data object NavigateUp : ChallengeCreateEffect
+ data class NavigateResult(val challengeId: Long, val title: String) : ChallengeCreateEffect
data class ShowSnackbar(val snackbarToken: SnackbarToken) : ChallengeCreateEffect
+
+ data class HandleException(val throwable: Throwable, val retry: () -> Unit) :
+ ChallengeCreateEffect
}
enum class ChallengeCreateStep {
INFO,
GROUP_TYPE,
MATCHING_TYPE,
- CREATE,
+ MATCHING_SUCCESS,
RESULT,
}
diff --git a/android/feature/teacher/challenge/src/main/java/com/sixkids/teacher/challenge/create/ChallengeCreateScreen.kt b/android/feature/teacher/challenge/src/main/java/com/sixkids/teacher/challenge/create/ChallengeCreateScreen.kt
index 5773d0f3..98ad7010 100644
--- a/android/feature/teacher/challenge/src/main/java/com/sixkids/teacher/challenge/create/ChallengeCreateScreen.kt
+++ b/android/feature/teacher/challenge/src/main/java/com/sixkids/teacher/challenge/create/ChallengeCreateScreen.kt
@@ -6,15 +6,21 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
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 androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.sixkids.designsystem.theme.UlbanTheme
+import com.sixkids.model.ChallengeGroup
import com.sixkids.teacher.challenge.create.grouptype.GroupType
import com.sixkids.teacher.challenge.create.grouptype.GroupTypeRoute
import com.sixkids.teacher.challenge.create.info.InfoContentRoute
+import com.sixkids.teacher.challenge.create.matching.GroupMatchingSettingRoute
+import com.sixkids.teacher.challenge.create.matching.GroupMatchingSuccessRoute
+import com.sixkids.teacher.challenge.create.matching.MatchingSource
+import com.sixkids.teacher.challenge.create.matching.MatchingType
import com.sixkids.ui.SnackbarToken
import com.sixkids.ui.extension.collectWithLifecycle
import java.time.LocalDateTime
@@ -23,18 +29,27 @@ import java.time.LocalDateTime
@Composable
fun ChallengeCreateRoute(
viewModel: ChallengeCreateViewModel = hiltViewModel(),
+ onNavigateResult: (Long, String) -> Unit,
onNavigateUp: () -> Unit,
+ onHandleException: (Throwable, () -> Unit) -> Unit,
onShowSnackbar: (SnackbarToken) -> Unit
) {
+
val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
viewModel.sideEffect.collectWithLifecycle {
when (it) {
is ChallengeCreateEffect.ShowSnackbar -> onShowSnackbar(it.snackbarToken)
+ is ChallengeCreateEffect.NavigateResult -> onNavigateResult(it.challengeId, it.title)
ChallengeCreateEffect.NavigateUp -> onNavigateUp()
+ is ChallengeCreateEffect.HandleException -> onHandleException(it.throwable, it.retry)
}
}
+ LaunchedEffect(Unit) {
+ viewModel.initData()
+ }
+
ChallengeCreateScreen(
uiState = uiState,
updateTitle = viewModel::updateTitle,
@@ -43,10 +58,13 @@ fun ChallengeCreateRoute(
updateEndTime = viewModel::updateEndTime,
updatePoint = viewModel::updatePoint,
updateCount = viewModel::updateCount,
+ updateMatchingMemberList = viewModel::updateMatchingMemberList,
+ updateMatchingType = viewModel::updateMatchingType,
updateGroupType = viewModel::updateGroupType,
- moveToResult = viewModel::moveToResult,
+ updateGroupList = viewModel::updateGroupList,
onMoveNextStep = viewModel::moveNextStep,
onMovePrevStep = viewModel::movePrevStep,
+ onGetMatchingGroupList = viewModel::getMatchingGroupList,
onShowSnackbar = viewModel::onShowSnackbar,
createChallenge = viewModel::createChallenge,
)
@@ -62,8 +80,11 @@ fun ChallengeCreateScreen(
updatePoint: (String) -> Unit = {},
onShowSnackbar: (SnackbarToken) -> Unit = {},
updateCount: (String) -> Unit = {},
+ updateMatchingMemberList: (List) -> Unit = {},
+ updateMatchingType: (MatchingType) -> Unit = {},
updateGroupType: (GroupType) -> Unit = {},
- moveToResult: () -> Unit = {},
+ updateGroupList: (List) -> Unit = {},
+ onGetMatchingGroupList: () -> (MatchingSource) = { MatchingSource() },
onMoveNextStep: () -> Unit = {},
onMovePrevStep: () -> Unit = {},
createChallenge: () -> Unit = {},
@@ -97,18 +118,27 @@ fun ChallengeCreateScreen(
ChallengeCreateStep.GROUP_TYPE -> GroupTypeRoute(
updateMinCount = updateCount,
updateGroupType = updateGroupType,
- moveToResult = moveToResult,
-// moveNextStep = onMoveNextStep,
- moveNextStep = {
- onShowSnackbar(SnackbarToken("그룹 지정 로직은 미구현 입니다."))
- },
+ moveNextStep = onMoveNextStep,
createChallenge = createChallenge,
onShowSnackbar = onShowSnackbar,
)
- ChallengeCreateStep.MATCHING_TYPE -> TODO()
- ChallengeCreateStep.CREATE -> TODO()
- ChallengeCreateStep.RESULT -> TODO()
+ ChallengeCreateStep.MATCHING_TYPE -> GroupMatchingSettingRoute(
+ moveNextStep = onMoveNextStep,
+ onUpdateMatchingMemberList = updateMatchingMemberList,
+ onUpdateMatchingType = updateMatchingType,
+ onShowSnackbar = onShowSnackbar,
+ )
+
+ ChallengeCreateStep.MATCHING_SUCCESS -> GroupMatchingSuccessRoute(
+ onShowSnackbar = onShowSnackbar,
+ createChallenge = createChallenge,
+ updateGroupList = updateGroupList,
+ onGetMatchingGroupList = onGetMatchingGroupList,
+ )
+ else -> {
+
+ }
}
}
diff --git a/android/feature/teacher/challenge/src/main/java/com/sixkids/teacher/challenge/create/ChallengeCreateViewModel.kt b/android/feature/teacher/challenge/src/main/java/com/sixkids/teacher/challenge/create/ChallengeCreateViewModel.kt
index f2442d3c..d29c046f 100644
--- a/android/feature/teacher/challenge/src/main/java/com/sixkids/teacher/challenge/create/ChallengeCreateViewModel.kt
+++ b/android/feature/teacher/challenge/src/main/java/com/sixkids/teacher/challenge/create/ChallengeCreateViewModel.kt
@@ -1,10 +1,13 @@
package com.sixkids.teacher.challenge.create
-import android.util.Log
import androidx.lifecycle.viewModelScope
import com.sixkids.domain.usecase.challenge.CreateChallengeUseCase
+import com.sixkids.domain.usecase.organization.GetSelectedOrganizationIdUseCase
+import com.sixkids.model.ChallengeGroup
import com.sixkids.model.GroupSimple
import com.sixkids.teacher.challenge.create.grouptype.GroupType
+import com.sixkids.teacher.challenge.create.matching.MatchingSource
+import com.sixkids.teacher.challenge.create.matching.MatchingType
import com.sixkids.ui.SnackbarToken
import com.sixkids.ui.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -14,11 +17,27 @@ import javax.inject.Inject
@HiltViewModel
class ChallengeCreateViewModel @Inject constructor(
- private val createChallengeUseCase: CreateChallengeUseCase
+ private val createChallengeUseCase: CreateChallengeUseCase,
+ private val getSelectedOrganizationIdUseCase: GetSelectedOrganizationIdUseCase
) : BaseViewModel(
ChallengeCreateUiState()
) {
+ private var isFirstVisited: Boolean = true
+ fun initData() {
+ viewModelScope.launch {
+ if (isFirstVisited.not()) return@launch
+ isFirstVisited = false
+
+ getSelectedOrganizationIdUseCase().onSuccess {
+ intent {
+ copy(organizationId = it)
+ }
+ }.onFailure {
+ postSideEffect(ChallengeCreateEffect.HandleException(it) { initData() })
+ }
+ }
+ }
private var title: String = ""
private var content: String = ""
@@ -26,24 +45,24 @@ class ChallengeCreateViewModel @Inject constructor(
private var endTime: LocalDateTime = LocalDateTime.now()
private var point: String = ""
private var headCount: String = ""
+ private var matchingMemberList: List = emptyList()
+ private var groupMatchingType: MatchingType = MatchingType.FRIENDLY
private var groupType: GroupType = GroupType.FREE
- private val groupList: List = emptyList()
+ private var groupList: List = emptyList()
fun createChallenge() {
viewModelScope.launch {
createChallengeUseCase(
- //TODO 그룹 아이디 지정 하기
- organizationId = 1,
+ organizationId = uiState.value.organizationId,
title = title,
content = content,
startTime = startTime,
endTime = endTime,
- reword = point.toInt(),
+ reward = point.toInt(),
minCount = headCount.toInt(),
groups = groupList
).onSuccess { challengeId ->
-// moveToResult(challengeId)
- Log.d("D107", "createChallenge: $challengeId")
+ postSideEffect(ChallengeCreateEffect.NavigateResult(challengeId, title))
}.onFailure {
onShowSnackbar(SnackbarToken("챌린지 생성에 실패했습니다."))
}
@@ -55,9 +74,9 @@ class ChallengeCreateViewModel @Inject constructor(
when (step) {
ChallengeCreateStep.INFO -> copy(step = ChallengeCreateStep.GROUP_TYPE)
ChallengeCreateStep.GROUP_TYPE -> copy(step = ChallengeCreateStep.MATCHING_TYPE)
- ChallengeCreateStep.MATCHING_TYPE -> copy(step = ChallengeCreateStep.CREATE)
- ChallengeCreateStep.CREATE -> copy(step = ChallengeCreateStep.RESULT)
- ChallengeCreateStep.RESULT -> copy(step = ChallengeCreateStep.INFO)
+ ChallengeCreateStep.MATCHING_TYPE -> copy(step = ChallengeCreateStep.MATCHING_SUCCESS)
+ ChallengeCreateStep.MATCHING_SUCCESS -> copy(step = ChallengeCreateStep.RESULT)
+ else -> copy()
}
}
}
@@ -72,19 +91,13 @@ class ChallengeCreateViewModel @Inject constructor(
ChallengeCreateStep.GROUP_TYPE -> copy(step = ChallengeCreateStep.INFO)
ChallengeCreateStep.MATCHING_TYPE -> copy(step = ChallengeCreateStep.GROUP_TYPE)
- ChallengeCreateStep.CREATE -> copy(step = ChallengeCreateStep.MATCHING_TYPE)
+ ChallengeCreateStep.MATCHING_SUCCESS -> copy(step = ChallengeCreateStep.MATCHING_TYPE)
else -> copy()
}
}
}
- fun moveToResult() {
- intent {
- copy(step = ChallengeCreateStep.RESULT)
- }
- }
-
fun onShowSnackbar(snackbarToken: SnackbarToken) {
postSideEffect(ChallengeCreateEffect.ShowSnackbar(snackbarToken))
}
@@ -117,5 +130,31 @@ class ChallengeCreateViewModel @Inject constructor(
this.groupType = groupType
}
+ fun updateMatchingMemberList(matchingMemberList: List