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/.gitignore b/android/app/.gitignore
index 42afabfd..2abde4aa 100644
--- a/android/app/.gitignore
+++ b/android/app/.gitignore
@@ -1 +1,2 @@
-/build
\ No newline at end of file
+/build
+/google-services.json
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index 44939ca6..d55b696a 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -1,16 +1,41 @@
+import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
+import java.util.Properties
+
plugins {
alias(libs.plugins.sixkids.android.application)
}
+fun getProperty(propertyKey: String): String =
+ gradleLocalProperties(rootDir, providers).getProperty(propertyKey)
+
android {
namespace = "com.sixkids.ulban"
defaultConfig {
applicationId = "com.sixkids.ulban"
versionCode = 1
versionName = "1.0"
+
+ val localProperties = Properties()
+ val localPropertiesFile = rootProject.file("local.properties")
+ if (localPropertiesFile.exists()) {
+ localProperties.load(localPropertiesFile.inputStream())
+ }
+
+ val nativeAppKey = localProperties.getProperty("KAKAO_NATIVE_APP_KEY") ?: ""
+ manifestPlaceholders["NATIVE_APP_KEY"] = nativeAppKey
+
+ buildConfigField("String", "KAKAO_NATIVE_APP_KEY", "\"${nativeAppKey}\"")
+ }
+
+ buildFeatures {
+ buildConfig = true
}
}
dependencies {
implementation(projects.feature.navigator)
+ implementation(projects.data)
+ implementation(libs.kakao.user)
+ implementation(platform(libs.firebase.bom))
+ implementation(libs.bundles.firebase)
}
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 06c5d6a2..dc623bcd 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -2,15 +2,41 @@
+
+
+ tools:targetApi="31">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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/java/com/sixkids/ulban/UlbanApplication.kt b/android/app/src/main/java/com/sixkids/ulban/UlbanApplication.kt
index bae5c678..510dbe6d 100644
--- a/android/app/src/main/java/com/sixkids/ulban/UlbanApplication.kt
+++ b/android/app/src/main/java/com/sixkids/ulban/UlbanApplication.kt
@@ -1,8 +1,28 @@
package com.sixkids.ulban
import android.app.Application
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
+import com.kakao.sdk.common.KakaoSdk
+import com.sixkids.ulban.UlbanFirebaseMessagingService.Companion.CHANNEL_ID
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
-class UlbanApplication: Application(){
+class UlbanApplication : Application() {
+ override fun onCreate() {
+ super.onCreate()
+ createNotificationChannel()
+
+ KakaoSdk.init(this, BuildConfig.KAKAO_NATIVE_APP_KEY)
+ }
+
+ private fun createNotificationChannel() {
+ val importance = NotificationManager.IMPORTANCE_DEFAULT
+ val channel = NotificationChannel(CHANNEL_ID, "Ulban", importance)
+
+ val notificationManager: NotificationManager =
+ getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.createNotificationChannel(channel)
+ }
}
diff --git a/android/app/src/main/java/com/sixkids/ulban/UlbanFirebaseMessagingService.kt b/android/app/src/main/java/com/sixkids/ulban/UlbanFirebaseMessagingService.kt
new file mode 100644
index 00000000..326454a0
--- /dev/null
+++ b/android/app/src/main/java/com/sixkids/ulban/UlbanFirebaseMessagingService.kt
@@ -0,0 +1,83 @@
+package com.sixkids.ulban
+
+import android.Manifest
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.Build
+import android.util.Log
+import androidx.core.app.ActivityCompat
+import androidx.core.app.NotificationCompat
+import com.google.firebase.messaging.FirebaseMessagingService
+import com.google.firebase.messaging.RemoteMessage
+import com.sixkids.designsystem.R
+import com.sixkids.feature.navigator.MainActivity
+
+private const val TAG = "D107"
+class UlbanFirebaseMessagingService : FirebaseMessagingService() {
+
+ override fun onNewToken(token: String) {
+ super.onNewToken(token)
+ Log.d(TAG, "onNewToken: $token")
+ }
+ override fun onMessageReceived(message: RemoteMessage) {
+ var messageTitle = ""
+ var messageContent = ""
+
+ message.notification?.let {
+ messageTitle = it.title.toString()
+ messageContent = it.body.toString()
+ } ?: run {
+ message.data.isNotEmpty().let {
+ messageTitle = message.data["title"].toString()
+ messageContent = message.data["body"].toString()
+ }
+ }
+
+
+ val mainIntent = Intent(this, MainActivity::class.java).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ }
+
+ val pendingIntentFlags =
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+
+ val mainPendingIntent: PendingIntent = PendingIntent.getActivity(
+ this,
+ 0,
+ mainIntent,
+ pendingIntentFlags,
+ )
+
+ val builder = NotificationCompat.Builder(this, CHANNEL_ID)
+ .setSmallIcon(R.drawable.announce)
+ .setContentTitle(messageTitle)
+ .setContentText(messageContent)
+ .setAutoCancel(true)
+ .setContentIntent(mainPendingIntent)
+
+ val notificationManager =
+ getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (ActivityCompat.checkSelfPermission(
+ this,
+ Manifest.permission.POST_NOTIFICATIONS,
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
+ return
+ }
+ }
+
+ notificationManager.notify(107, builder.build())
+
+ }
+
+ companion object {
+ const val CHANNEL_ID = "ULBAN_NOTIFICATION_CHANNEL"
+ }
+
+
+}
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-logic/convention/src/main/java/AndroidApplicationConventionPlugin.kt b/android/build-logic/convention/src/main/java/AndroidApplicationConventionPlugin.kt
index c1fb9749..f651f9fa 100644
--- a/android/build-logic/convention/src/main/java/AndroidApplicationConventionPlugin.kt
+++ b/android/build-logic/convention/src/main/java/AndroidApplicationConventionPlugin.kt
@@ -13,6 +13,7 @@ internal class AndroidApplicationConventionPlugin : Plugin{
apply("com.android.application")
apply("org.jetbrains.kotlin.android")
apply("sixkids.android.hilt")
+ apply("com.google.gms.google-services")
}
extensions.configure{
diff --git a/android/build-logic/convention/src/main/java/FeatureComposeConventionPlugin.kt b/android/build-logic/convention/src/main/java/FeatureComposeConventionPlugin.kt
index 440a0bab..f26d6642 100644
--- a/android/build-logic/convention/src/main/java/FeatureComposeConventionPlugin.kt
+++ b/android/build-logic/convention/src/main/java/FeatureComposeConventionPlugin.kt
@@ -1,3 +1,4 @@
+import com.sixkids.convention.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies
@@ -17,6 +18,8 @@ class FeatureComposeConventionPlugin: Plugin {
"implementation"(project(":core:ui"))
"implementation"(project(":core:designsystem"))
"implementation"(project(":domain"))
+ "implementation"(libs.findLibrary("coil-compose").get())
+
}
}
}
diff --git a/android/build.gradle.kts b/android/build.gradle.kts
index 8349eec4..750638f1 100644
--- a/android/build.gradle.kts
+++ b/android/build.gradle.kts
@@ -6,4 +6,13 @@ plugins {
alias(libs.plugins.androidLibrary) apply false
alias(libs.plugins.ksp) apply false
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/build.gradle.kts b/android/core/designsystem/build.gradle.kts
index 9847c821..13841a5a 100644
--- a/android/core/designsystem/build.gradle.kts
+++ b/android/core/designsystem/build.gradle.kts
@@ -10,4 +10,6 @@ android {
dependencies {
implementation(projects.core.ui)
implementation (libs.accompanist.systemuicontroller)
+ implementation(libs.coil.compose)
+ implementation(libs.lottie)
}
diff --git a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/appbar/AppBarScreenPreview.kt b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/appbar/AppBarScreenPreview.kt
index 5a856110..51c36a2d 100644
--- a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/appbar/AppBarScreenPreview.kt
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/appbar/AppBarScreenPreview.kt
@@ -73,6 +73,7 @@ fun RelayDetailAppBarPreview() {
topDescription = "04.17 15:00~",
bottomDescription = "현재 주자는 오하빈 학생입니다.",
color = Orange,
+ badgeCount = 10,
onclick = { Log.d("확인", "클릭된 ") },
expanded = !isScrolled
)
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 5bd6fc1e..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
@@ -3,17 +3,26 @@ package com.sixkids.designsystem.component.appbar
import androidx.annotation.DrawableRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Badge
+import androidx.compose.material3.ExperimentalMaterial3Api
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.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
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(
@@ -23,9 +32,10 @@ fun UlbanDetailAppBar(
content: String,
topDescription: String,
bottomDescription: String,
+ badgeCount: Int = 0,
color: Color,
expanded: Boolean = true,
- onclick: () -> Unit,
+ onclick: () -> Unit = {},
) {
BasicAppBar(
modifier = modifier,
@@ -35,7 +45,8 @@ fun UlbanDetailAppBar(
AppBarDetailInfo(
title = content,
topDescription = topDescription,
- bottomDescription = bottomDescription
+ bottomDescription = bottomDescription,
+ badgeCount = badgeCount
)
},
color = color,
@@ -44,31 +55,54 @@ fun UlbanDetailAppBar(
)
}
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppBarDetailInfo(
modifier: Modifier = Modifier,
title: String,
topDescription: String,
bottomDescription: String,
+ badgeCount: Int = 0
) {
Column(
modifier = modifier.fillMaxWidth(),
verticalArrangement = Arrangement.Center
) {
- Text(
- text = topDescription,
- style = AppBarTypography.bodySmall,
- modifier = modifier.fillMaxWidth()
- )
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.Bottom
+ ) {
+ Text(
+ text = topDescription,
+ style = AppBarTypography.bodySmall,
+ )
+ Spacer(modifier = modifier.weight(1f))
+ if (badgeCount > 0) {
+ Badge(
+ modifier = Modifier
+ .padding(top = 4.dp, end = 12.dp)
+ .size(32.dp),
+ containerColor = RedDark,
+ contentColor = Color.White
+ ) {
+ Text(
+ text = "$badgeCount",
+ style = AppBarTypography.bodyLarge.copy(fontWeight = FontWeight.Bold)
+ )
+ }
+ }
+ }
+ Spacer(modifier = modifier.height(4.dp))
Text(
text = title,
style = AppBarTypography.titleSmall,
- modifier = modifier.fillMaxWidth()
+ 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 f1d9b298..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
@@ -19,30 +20,34 @@ fun UlbanDetailWithProgressAppBar(
content: String,
topDescription: String,
bottomDescription: String,
+ badgeCount: Int = 0,
+ totalCnt: Int,
+ successCnt: Int,
color: Color,
+ progressBarColor: Color = RedDark,
expanded: Boolean = true,
onclick: () -> Unit,
- totalCnt: Int,
- successCnt: Int
) {
BasicAppBar(
modifier = modifier,
leftIcon = leftIcon,
title = title,
content = {
- Column (
+ Column(
modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start
- ){
+ ) {
AppBarDetailInfo(
title = content,
topDescription = topDescription,
- bottomDescription = bottomDescription
+ bottomDescription = bottomDescription,
+ badgeCount = badgeCount
)
StudentProgressBar(
- modifier = modifier.padding(top= 8.dp , end = 16.dp),
+ 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/button/UlbanFilledButton.kt b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/button/UlbanFilledButton.kt
new file mode 100644
index 00000000..599da145
--- /dev/null
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/button/UlbanFilledButton.kt
@@ -0,0 +1,66 @@
+package com.sixkids.designsystem.component.button
+
+import androidx.compose.foundation.layout.padding
+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.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.text.TextStyle
+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.theme.Blue
+import com.sixkids.designsystem.theme.BlueDark
+import com.sixkids.designsystem.theme.Gray
+import com.sixkids.designsystem.theme.UlbanTheme
+import com.sixkids.designsystem.theme.UlbanTypography
+
+@Composable
+fun UlbanFilledButton(
+ modifier: Modifier = Modifier,
+ text: String,
+ textStyle: TextStyle = UlbanTypography.titleSmall.copy(fontSize = 14.sp),
+ onClick: () -> Unit,
+ shape: Shape = RoundedCornerShape(12.dp),
+ color: Color = Blue,
+ elevation: Dp = 4.dp,
+ textColor: Color = BlueDark,
+ disabledTextColor: Color = Gray,
+ enabled: Boolean = true
+) {
+ Button(
+ modifier = modifier,
+ onClick = onClick,
+ shape = shape,
+ colors = ButtonDefaults.buttonColors(
+ containerColor = color
+ ),
+ elevation = ButtonDefaults.buttonElevation(
+ defaultElevation = elevation
+ ),
+ enabled = enabled
+ ) {
+ Text(
+ text = text, color = if (enabled) textColor else disabledTextColor,
+ style = textStyle,
+ modifier = Modifier.padding(4.dp)
+ )
+
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun UlbanFilledButtonPreview() {
+ UlbanTheme {
+ UlbanFilledButton(
+ text = "Button",
+ onClick = {},
+ )
+ }
+}
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 800b1b3c..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
@@ -13,6 +13,8 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -25,14 +27,17 @@ 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
import java.text.SimpleDateFormat
import java.util.Locale
@@ -94,7 +99,13 @@ fun ContentCardViewPreview() {
@Composable
fun RankCardViewPreview() {
UlbanTheme {
- RankCard()
+ ContentVerticalCard(
+ cardModifier = Modifier
+ .padding(20.dp)
+ .aspectRatio(1f),
+ imageDrawable = R.drawable.rank,
+ text = "랭킹"
+ )
}
}
@@ -107,15 +118,18 @@ enum class ContentAligment {
@Composable
fun ContentCard(
modifier: Modifier = Modifier,
+ imageModifier: Modifier = Modifier,
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
),
@@ -123,6 +137,7 @@ fun ContentCard(
defaultElevation = 4.dp,
pressedElevation = 8.dp
),
+ shape = RoundedCornerShape(24.dp),
onClick = onclick
) {
Column {
@@ -137,17 +152,21 @@ fun ContentCard(
Image(
painter = painterResource(id = contentImageId),
contentDescription = null,
- modifier = Modifier
+ modifier = imageModifier
+ .fillMaxHeight()
.aspectRatio(1f)
)
if (runningState == null) {
Text(
text = contentName,
modifier = Modifier
- .padding(20.dp),
+ .weight(1f)
+ .wrapContentHeight(),
textAlign = TextAlign.Center,
- fontSize = 30.sp,
- fontWeight = FontWeight.Black
+ style = UlbanTypography.titleLarge.copy(
+ fontSize = 26.sp,
+ color = textColor
+ )
)
} else {
RunningText(
@@ -161,10 +180,14 @@ fun ContentCard(
Text(
text = contentName,
modifier = Modifier
- .padding(20.dp),
+// .padding(20.dp)
+ .weight(1f)
+ .wrapContentHeight(),
textAlign = TextAlign.Center,
- fontSize = 30.sp,
- fontWeight = FontWeight.Black
+ style = UlbanTypography.titleLarge.copy(
+ fontSize = 26.sp,
+ color = textColor
+ )
)
} else {
RunningText(
@@ -175,7 +198,7 @@ fun ContentCard(
Image(
painter = painterResource(id = contentImageId),
contentDescription = null,
- modifier = Modifier
+ modifier = imageModifier
.fillMaxHeight()
.aspectRatio(1f)
)
@@ -219,12 +242,14 @@ fun RunningText(
Text(
text = boldText,
fontSize = 18.sp,
- fontWeight = FontWeight.Bold
+ fontWeight = FontWeight.Bold,
+ fontFamily = npsFont
)
Text(
modifier = Modifier.padding(top = 10.dp),
text = normalText,
fontSize = 14.sp,
+ fontFamily = npsFont
)
}
@@ -233,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,
@@ -258,13 +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.Black
+ 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
new file mode 100644
index 00000000..1b1fc4e0
--- /dev/null
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/card/UlbanMissionCard.kt
@@ -0,0 +1,158 @@
+package com.sixkids.designsystem.component.card
+
+import androidx.annotation.DrawableRes
+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.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
+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.platform.LocalDensity
+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 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
+
+@Composable
+fun UlbanMissionCard(
+ modifier: Modifier = Modifier,
+ title: String = "",
+ subTitle: String = "",
+ @DrawableRes imgRes: Int = R.drawable.hifive,
+ backGroundColor: Color = Red,
+ onClick: () -> Unit = {}
+) {
+ val density = LocalDensity.current
+ val cornerRadius = 16.dp // 둥근 모서리의 반경을 dp 단위로 설정
+ val heightCut = 0.8f
+ val widthCUt = 0.7f
+
+ val customShape = GenericShape { size, _ ->
+ with(density) {
+ val pxCornerRadius = cornerRadius.toPx()
+ moveTo(pxCornerRadius, 0f) // 시작 지점을 둥근 모서리의 반경만큼 오른쪽으로 이동합니다.
+ lineTo(size.width - pxCornerRadius, 0f) // 상단 가로선을 그립니다.
+ quadraticBezierTo(size.width, 0f, size.width, pxCornerRadius) // 오른쪽 상단 모서리 둥글게 처리
+ lineTo(size.width, size.height * heightCut - pxCornerRadius)
+ quadraticBezierTo(
+ size.width,
+ size.height * heightCut,
+ size.width - pxCornerRadius,
+ size.height * heightCut
+ )
+ lineTo(size.width * widthCUt + pxCornerRadius, size.height * heightCut)
+ quadraticBezierTo(
+ size.width * widthCUt,
+ size.height * heightCut,
+ size.width * widthCUt,
+ size.height * heightCut + pxCornerRadius
+ )
+ lineTo(size.width * widthCUt, size.height - pxCornerRadius)
+ quadraticBezierTo(
+ size.width * widthCUt,
+ size.height,
+ size.width * widthCUt - pxCornerRadius,
+ size.height
+ )
+ lineTo(pxCornerRadius, size.height)
+ quadraticBezierTo(0f, size.height, 0f, size.height - pxCornerRadius) // 왼쪽 하단 모서리 둥글게 처리
+ lineTo(0f, pxCornerRadius) // 왼쪽 세로선을 그립니다.
+ quadraticBezierTo(0f, 0f, pxCornerRadius, 0f) // 왼쪽 상단 모서리 둥글게 처리
+ close()
+ }
+ }
+ Box(
+ modifier = modifier.clickable {
+ onClick()
+ }
+ ) {
+ Card(
+ shape = customShape,
+ colors = CardDefaults.cardColors(
+ containerColor = backGroundColor,
+ ),
+ modifier = Modifier
+ .padding(40.dp)
+ .width(200.dp)
+ .height(300.dp)
+ ) {
+ Column {
+ Spacer(modifier = modifier.height(170.dp))
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ text = title,
+ style = UlbanTypography.titleSmall,
+ textAlign = TextAlign.Center,
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ Row(
+ modifier = Modifier.height(50.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(
+ modifier = Modifier.weight(widthCUt),
+ text = subTitle,
+ style = UlbanTypography.titleSmall.copy(fontSize = 12.sp),
+ color = Color.Gray,
+ textAlign = TextAlign.Center,
+ )
+ Spacer(modifier = Modifier.weight(1 - widthCUt))
+ }
+ }
+ }
+ Box(
+ modifier = Modifier
+ .align(Alignment.TopCenter),
+ 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)
+@Composable
+fun UlbanMissionCardPreview() {
+ UlbanTheme {
+ UlbanMissionCard(
+ title = "4월 22일 깜짝 미션",
+ subTitle = "상세 정보",
+ imgRes = R.drawable.hifive,
+ backGroundColor = Red
+ )
+ }
+}
+
+@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
new file mode 100644
index 00000000..768c9984
--- /dev/null
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/dialog/UlbanBisicDialog.kt
@@ -0,0 +1,55 @@
+package com.sixkids.designsystem.component.dialog
+
+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.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.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import com.sixkids.designsystem.theme.Cream
+import com.sixkids.designsystem.theme.UlbanTheme
+
+@Composable
+fun UlbanBasicDialog(
+ modifier: Modifier = Modifier,
+ onDismiss: () -> Unit = {},
+ backGroundColor: Color = Cream,
+ content: @Composable () -> Unit
+) {
+ Dialog(onDismissRequest = onDismiss) {
+ Card(
+ modifier = modifier,
+ colors = CardDefaults.cardColors(containerColor = backGroundColor),
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ Column(
+ modifier = modifier
+ .padding(16.dp),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ content()
+ }
+ }
+ }
+}
+
+
+@Preview(showBackground = true)
+@Composable
+fun UlbalDialogPreview() {
+ UlbanTheme {
+ UlbanBasicDialog {
+ Text(text = "Hello World")
+ }
+ }
+}
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
new file mode 100644
index 00000000..f8bc2300
--- /dev/null
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/dialog/UlbanDatePickerDialog.kt
@@ -0,0 +1,104 @@
+package com.sixkids.designsystem.component.dialog
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.material3.Button
+import androidx.compose.material3.DatePicker
+import androidx.compose.material3.DatePickerDefaults
+import androidx.compose.material3.DatePickerDialog
+import androidx.compose.material3.DisplayMode
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.rememberDatePickerState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import com.sixkids.designsystem.R
+import com.sixkids.designsystem.theme.Cream
+import com.sixkids.designsystem.theme.UlbanTheme
+import java.time.Instant
+import java.time.LocalDate
+import java.time.ZoneId
+
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun UlbanDatePickerDialog(
+ selectedDate: LocalDate = LocalDate.now(),
+ onDismiss: () -> Unit,
+ onClickConfirm: (date: LocalDate) -> Unit
+) {
+
+ val datePickerState = rememberDatePickerState(
+ initialDisplayMode = DisplayMode.Picker,
+ initialSelectedDateMillis = selectedDate.plusDays(1)
+ .atStartOfDay(ZoneId.systemDefault()).toInstant()
+ .toEpochMilli()
+ )
+
+
+ DatePickerDialog(
+ onDismissRequest = { onDismiss() }, confirmButton = {
+ TextButton(
+ onClick = {
+ onClickConfirm(datePickerState.selectedDateMillis?.let {
+ Instant.ofEpochMilli(it).atZone(ZoneId.systemDefault()).toLocalDate()
+ } ?: LocalDate.now())
+ },
+ ) {
+ Text(stringResource(R.string.confirm))
+ }
+ }, dismissButton = {
+ TextButton(onClick = {
+ onDismiss()
+ }) {
+ Text(stringResource(R.string.cancel))
+ }
+ }, colors = DatePickerDefaults.colors(
+ containerColor = Cream
+ )
+ ) {
+ DatePicker(
+ state = datePickerState,
+ )
+ }
+}
+
+
+@Preview
+@Composable
+fun CustomDatePickerDialogPreview() {
+ UlbanTheme {
+ var selectedDate by remember { mutableStateOf(LocalDate.now()) }
+ var showDialog by remember { mutableStateOf(false) }
+ Column {
+ Text(
+ text = selectedDate.toString(),
+ )
+ if (showDialog) {
+ UlbanDatePickerDialog(
+ selectedDate = selectedDate,
+ onDismiss = {
+ showDialog = false
+ },
+ onClickConfirm = {
+ selectedDate = it
+ showDialog = false
+ }
+ )
+ }
+ Button(
+ onClick = {
+ showDialog = true
+ }
+ ) {
+ Text("날짜 선택")
+ }
+ }
+ }
+
+}
diff --git a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/dialog/UlbanTimePickerDialog.kt b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/dialog/UlbanTimePickerDialog.kt
new file mode 100644
index 00000000..502db34f
--- /dev/null
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/dialog/UlbanTimePickerDialog.kt
@@ -0,0 +1,87 @@
+package com.sixkids.designsystem.component.dialog
+
+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.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.TimePicker
+import androidx.compose.material3.rememberTimePickerState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import com.sixkids.designsystem.R
+import com.sixkids.designsystem.theme.UlbanTheme
+import java.time.LocalTime
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun UlbanTimePickerDialog(
+ modifier: Modifier = Modifier,
+ onDismiss: () -> Unit = {},
+ onClickConfirm: (time: LocalTime) -> Unit,
+ selectedTime: LocalTime = LocalTime.now(),
+) {
+ UlbanBasicDialog(
+ modifier = modifier,
+ onDismiss = onDismiss,
+ ) {
+ val timePickerState = rememberTimePickerState(
+ initialHour = selectedTime.hour,
+ initialMinute = selectedTime.minute
+ )
+
+ Column(
+ modifier = modifier,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ TimePicker(state = timePickerState)
+ Row(
+ modifier = modifier
+ .fillMaxWidth(),
+ horizontalArrangement = Arrangement.End
+ ) {
+ TextButton(onClick = onDismiss) {
+ Text(stringResource(id = R.string.cancel))
+ }
+ TextButton(
+ onClick = {
+ onClickConfirm(LocalTime.of(timePickerState.hour, timePickerState.minute))
+ }
+ ) {
+ Text(stringResource(id = R.string.confirm))
+ }
+ }
+ }
+
+ }
+}
+
+
+@Preview(showBackground = true)
+@Composable
+fun UlbanTimePickerDialogPreview() {
+
+ UlbanTheme {
+ var selectedTime by remember { mutableStateOf(LocalTime.now()) }
+ var showDialog by remember { mutableStateOf(false) }
+
+ UlbanTimePickerDialog(
+ selectedTime = selectedTime,
+ onDismiss = {
+ showDialog = false
+ },
+ onClickConfirm = {
+ selectedTime = it
+ showDialog = false
+ }
+ )
+ }
+}
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
new file mode 100644
index 00000000..c86dfe98
--- /dev/null
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/item/MemberSimpleItem.kt
@@ -0,0 +1,71 @@
+package com.sixkids.designsystem.component.item
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.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
+import com.sixkids.designsystem.theme.UlbanTypography
+
+
+@Composable
+fun MemberSimpleItem(
+ modifier: Modifier = Modifier,
+ member: DisplayableMember,
+) {
+ Column(
+ modifier = modifier,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ AsyncImage(
+ modifier = Modifier
+ .size(60.dp)
+ .clip(CircleShape),
+ model = member.photo,
+ contentScale = ContentScale.Crop,
+ contentDescription = "프로필 사진"
+ )
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ if (member.isLeader) {
+ Image(
+ painter = painterResource(id = R.drawable.crown),
+ contentDescription = "리더 아이콘",
+ modifier = Modifier.size(16.dp)
+ )
+ }
+ Text(text = member.name, style = UlbanTypography.bodySmall)
+ }
+ }
+}
+
+interface DisplayableMember {
+ val name: String
+ 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
new file mode 100644
index 00000000..5cee18b7
--- /dev/null
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/item/StudentSimpleCardItem.kt
@@ -0,0 +1,99 @@
+package com.sixkids.designsystem.component.item
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+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.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
+import com.sixkids.designsystem.theme.Cream
+import com.sixkids.designsystem.theme.UlbanTypography
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun StudentSimpleCardItem(
+ modifier: Modifier = Modifier,
+ id: Long = 0,
+ name: String = "",
+ photo: String = "",
+ score: Int? = null,
+ onClick: (Long) -> Unit = {},
+ isCount: Boolean = false
+) {
+ Card(
+ modifier = modifier,
+ colors = CardDefaults.cardColors(
+ containerColor = Cream
+ ),
+ elevation = CardDefaults.cardElevation(
+ defaultElevation = 4.dp,
+ pressedElevation = 8.dp
+ ),
+ onClick = {onClick(id)}
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .padding(8.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ AsyncImage(
+ modifier = Modifier
+ .fillMaxWidth()
+ .aspectRatio(1f)
+ .padding(8.dp)
+ .clip(RoundedCornerShape(16.dp)),
+ model = photo,
+
+ contentScale = ContentScale.Crop,
+ contentDescription = "프로필 사진"
+ )
+ Text(
+ text = name,
+ style = UlbanTypography.bodyMedium,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ modifier = Modifier.padding(bottom = 4.dp)
+ )
+ if (score != null) {
+ Text(
+ text = if (isCount)"${score}회" else "${score}점",
+ style = UlbanTypography.bodySmall
+ )
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+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",
+ score = 97
+ )
+ }
+ }
+}
diff --git a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/item/UlbanChallengeItem.kt b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/item/UlbanChallengeItem.kt
new file mode 100644
index 00000000..65c16671
--- /dev/null
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/item/UlbanChallengeItem.kt
@@ -0,0 +1,103 @@
+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.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.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+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 UlbanChallengeItem(
+ modifier: Modifier = Modifier,
+ padding: PaddingValues = PaddingValues(
+ horizontal = 16.dp,
+ vertical = 16.dp
+ ),
+ title: String,
+ description: String,
+ startDate: LocalDateTime,
+ endDate: LocalDateTime,
+ userCount: Int,
+ color: Color = Cream,
+ 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 = title,
+ style = UlbanTypography.titleSmall
+ )
+ Spacer(modifier = modifier.padding(8.dp))
+ Text(
+ text = description,
+ style = UlbanTypography.bodySmall
+ )
+ Spacer(modifier = modifier.padding(8.dp))
+ Row(
+ modifier = modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ modifier = modifier.size(32.dp),
+ painter = painterResource(id = R.drawable.member),
+ contentDescription = null,
+ tint = Color.Unspecified
+ )
+ Text(text = stringResource(id = R.string.hifive_user_count, userCount))
+ }
+ Spacer(modifier = modifier.padding(4.dp))
+ Row {
+ Text(
+ text = startDate.formatToMonthDayTime(),
+ style = UlbanTypography.bodySmall
+ )
+ Text(
+ text = " ~ ",
+ style = UlbanTypography.bodySmall
+ )
+ Text(
+ text = endDate.formatToMonthDayTime(),
+ style = UlbanTypography.bodySmall
+ )
+ }
+
+ }
+
+ }
+}
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
new file mode 100644
index 00000000..0db54da4
--- /dev/null
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/item/UlbanReportItem.kt
@@ -0,0 +1,174 @@
+package com.sixkids.designsystem.component.item
+
+import androidx.compose.foundation.layout.Arrangement
+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.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+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.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import com.sixkids.designsystem.theme.Blue
+import com.sixkids.designsystem.theme.BlueDark
+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.ui.util.formatToMonthDayTime
+import java.time.LocalDateTime
+
+@Composable
+fun UlbanReportItem(
+ modifier: Modifier = Modifier,
+ padding: PaddingValues = PaddingValues(16.dp),
+ startDate: LocalDateTime = LocalDateTime.now(),
+ endDate: LocalDateTime = LocalDateTime.now(),
+ content: String = "",
+ file: String = "",
+ accepted: Boolean = false,
+ memberList: List = emptyList(),
+ onReject: () -> Unit = {},
+ onAccept: () -> Unit = {},
+) {
+ Card(
+ modifier = modifier
+ .fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = Cream
+ ),
+ elevation = CardDefaults.cardElevation(
+ defaultElevation = 8.dp
+ ),
+ shape = CardDefaults.outlinedShape
+ ) {
+ Column(
+ modifier = modifier
+ .padding(padding)
+ ) {
+ Spacer(modifier = modifier.padding(4.dp))
+ Row {
+ Text(
+ text = startDate.formatToMonthDayTime(),
+ style = UlbanTypography.bodySmall
+ )
+ Text(
+ text = " ~ ",
+ style = UlbanTypography.bodySmall
+ )
+ Text(
+ text = endDate.formatToMonthDayTime(),
+ style = UlbanTypography.bodySmall
+ )
+ }
+ AsyncImage(
+ model = file,
+ contentDescription = "과제 사진",
+ modifier = modifier
+ .fillMaxWidth()
+ .height(240.dp)
+ .padding(vertical = 8.dp)
+ )
+ MemberSimpleList(
+ modifier = modifier.padding(vertical = 8.dp),
+ memberList = memberList
+ )
+ Text(text = content, style = UlbanTypography.bodySmall)
+ if (accepted.not()) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(top = 16.dp)
+ ) {
+ Button(
+ modifier = modifier.weight(1f),
+ colors = ButtonDefaults.buttonColors(containerColor = Red),
+ onClick = { onReject() }) {
+ Text(text = "X", style = UlbanTypography.bodyLarge, color = RedDark)
+ }
+ Spacer(modifier = modifier.width(8.dp))
+ Button(
+ modifier = modifier.weight(1f),
+ colors = ButtonDefaults.buttonColors(containerColor = Blue),
+ onClick = { onAccept() }) {
+ Text(text = "O", style = UlbanTypography.bodyLarge, color = BlueDark)
+ }
+ }
+ }
+ }
+
+ }
+}
+
+@Composable
+fun MemberSimpleList(
+ modifier: Modifier = Modifier,
+ memberList: List = emptyList(),
+) {
+ LazyRow(
+ modifier = modifier,
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ items(memberList) { member ->
+ MemberSimpleItem(
+ member = member,
+ )
+ }
+
+ }
+}
+
+
+@Preview(showBackground = true, showSystemUi = true)
+@Composable
+fun UlbanReportItemPreview() {
+
+ UlbanTheme {
+ UlbanReportItem(
+ content = "4명 다 모여서 쿵푸팬더 4 다같이 봤어요!!",
+ startDate = LocalDateTime.now(),
+ endDate = LocalDateTime.now(),
+ accepted = false,
+ file = "https://file2.nocutnews.co.kr/newsroom/image/2024/04/05/202404052218304873_0.jpg",
+ memberList = listOf(
+ object : DisplayableMember {
+ override val name: String = "김규리"
+ override val photo: String =
+ "https://encrypted-tbn3.gstatic.com/images?q=tbn:ANd9GcSGfpQ3m-QWiXgCBJJbrcUFdNdWAhj7rcUqjeNUC6eKcXZDAtWm"
+ override val isLeader: Boolean = true
+ },
+ object : DisplayableMember {
+ override val name: String = "오하빈"
+ override val photo: String =
+ "https://health.chosun.com/site/data/img_dir/2023/07/17/2023071701753_0.jpg"
+ override val isLeader: Boolean = false
+ },
+ object : DisplayableMember {
+ override val name: String = "차성원"
+ override val photo: String =
+ "https://ichef.bbci.co.uk/ace/ws/800/cpsprodpb/E172/production/_126241775_getty_cats.png"
+ override val isLeader: Boolean = false
+ },
+ object : DisplayableMember {
+ override val name: String = "정철주"
+ override val photo: String =
+ "https://image.newsis.com/2023/07/12/NISI20230712_0001313626_web.jpg?rnd=20230712163021"
+ override val isLeader: Boolean = false
+ }
+ )
+ )
+ }
+}
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/LoadingScreen.kt b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/screen/LoadingScreen.kt
new file mode 100644
index 00000000..3a54283e
--- /dev/null
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/screen/LoadingScreen.kt
@@ -0,0 +1,46 @@
+package com.sixkids.designsystem.component.screen
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+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.unit.dp
+import com.airbnb.lottie.compose.LottieAnimation
+import com.airbnb.lottie.compose.LottieCompositionSpec
+import com.airbnb.lottie.compose.rememberLottieAnimatable
+import com.airbnb.lottie.compose.rememberLottieComposition
+import com.sixkids.designsystem.R
+import com.sixkids.designsystem.theme.LoadingBackground
+
+@Composable
+fun LoadingScreen() {
+ val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.lodaing))
+ val lottieAnimatable = rememberLottieAnimatable()
+
+ LaunchedEffect(composition) {
+ lottieAnimatable.animate(
+ composition = composition,
+ initialProgress = 0f
+ )
+ }
+
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(LoadingBackground),
+ contentAlignment = Alignment.Center,
+ ) {
+ LottieAnimation(
+ composition = composition,
+ modifier = Modifier.size(150.dp).aspectRatio(1f),
+ iterations = Int.MAX_VALUE
+ )
+ }
+}
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/screen/UlbanTopSectionScreen.kt b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/screen/UlbanTopSectionScreen.kt
new file mode 100644
index 00000000..6586c290
--- /dev/null
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/screen/UlbanTopSectionScreen.kt
@@ -0,0 +1,35 @@
+package com.sixkids.designsystem.component.screen
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import com.sixkids.designsystem.R
+import com.sixkids.designsystem.theme.UlbanTypography
+
+@Composable
+fun UlbanTopSection(text: String = "", onBackClick: () -> Unit = {}) {
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.Start,
+ ) {
+ Image(
+ painter = painterResource(id = R.drawable.ic_arrow_back),
+ contentDescription = "back button",
+ modifier = Modifier.clickable { onBackClick() }
+ )
+
+ Text(
+ text = text,
+ style = UlbanTypography.titleMedium,
+ modifier = Modifier.padding(top = 20.dp)
+ )
+ }
+}
\ No newline at end of file
diff --git a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/snackbar/UlbanSnackbar.kt b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/snackbar/UlbanSnackbar.kt
new file mode 100644
index 00000000..0a58f895
--- /dev/null
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/snackbar/UlbanSnackbar.kt
@@ -0,0 +1,134 @@
+package com.sixkids.designsystem.component.snackbar
+
+import androidx.annotation.DrawableRes
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+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.shape.RoundedCornerShape
+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.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.sixkids.designsystem.R
+import com.sixkids.designsystem.theme.Cream
+import com.sixkids.designsystem.theme.RedDark
+import com.sixkids.designsystem.theme.UlbanTheme
+import com.sixkids.designsystem.theme.UlbanTypography
+
+@Composable
+fun UlbanSnackbar(
+ modifier: Modifier = Modifier,
+ visible: Boolean,
+ message: String,
+ @DrawableRes actionIconId: Int? = null,
+ actionButtonText: String? = null,
+ onClickActionButton: () -> Unit = {},
+) {
+ AnimatedVisibility(
+ visible = visible,
+ enter = fadeIn(),
+ exit = fadeOut(),
+ ) {
+ Box(
+ modifier = modifier.fillMaxSize(),
+ contentAlignment = Alignment.BottomCenter,
+ ) {
+ UlbanSnackbarContent(
+ modifier = Modifier,
+ message = message,
+ actionIconId = actionIconId,
+ actionButtonText = actionButtonText,
+ onClickActionButton = onClickActionButton,
+ )
+ }
+ }
+}
+
+@Composable
+private fun UlbanSnackbarContent(
+ modifier: Modifier = Modifier,
+ message: String,
+ @DrawableRes actionIconId: Int? = null,
+ actionButtonText: String? = null,
+ onClickActionButton: () -> Unit = {},
+) {
+ Row(
+ modifier = modifier
+ .padding(8.dp)
+ .fillMaxWidth()
+ .background(color = Cream, shape = RoundedCornerShape(4.dp))
+ .padding(12.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center
+ ) {
+ Text(
+ modifier = modifier.weight(1f), text = message,
+ style = UlbanTypography.bodyMedium
+ )
+
+ if (actionIconId != null) {
+ Icon(
+ painter = painterResource(id = actionIconId),
+ contentDescription = null,
+ modifier = Modifier
+ .size(24.dp)
+ .clickable { onClickActionButton() },
+ tint = RedDark
+ )
+ }
+
+ if (actionButtonText != null) {
+ Text(
+ modifier = Modifier
+ .clickable(
+ onClick = onClickActionButton,
+ ),
+ text = actionButtonText,
+ style = UlbanTypography.bodySmall.copy(color = RedDark)
+ )
+ }
+ }
+}
+
+
+@Preview
+@Composable
+fun UlbanSnackbarPreview() {
+ UlbanTheme {
+ Column {
+ UlbanSnackbarContent(message = "This is a snackbar")
+ UlbanSnackbarContent(message = "This is a snackbar This is a snackbar This is a snackbar ")
+
+ UlbanSnackbarContent(message = "This is a snackbar", actionButtonText = "Action")
+
+ UlbanSnackbarContent(
+ message = "This is a snackbar",
+ actionIconId = R.drawable.ic_arrow_back
+ )
+
+ UlbanSnackbarContent(
+ message = "This is a snackbar This is a snackbar This is a snackbar ",
+ actionButtonText = "Action"
+ )
+
+ UlbanSnackbarContent(
+ message = "This is a snackbar This is a snackbar This is a snackbar ",
+ actionIconId = R.drawable.ic_arrow_back
+ )
+ }
+ }
+}
diff --git a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/textfield/NumberVisualTransformation.kt b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/textfield/NumberVisualTransformation.kt
new file mode 100644
index 00000000..96cefa47
--- /dev/null
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/textfield/NumberVisualTransformation.kt
@@ -0,0 +1,50 @@
+package com.sixkids.designsystem.component.textfield
+
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.input.OffsetMapping
+import androidx.compose.ui.text.input.TransformedText
+import androidx.compose.ui.text.input.VisualTransformation
+import java.text.DecimalFormat
+
+class NumberVisualTransformation(
+ private val postfix: String,
+) : VisualTransformation {
+ override fun filter(text: AnnotatedString): TransformedText {
+ val amount = text.text
+
+ if(amount.isEmpty()) {
+ return TransformedText(AnnotatedString(""), OffsetMapping.Identity)
+ }
+ val formatAmount = DecimalFormat("#,###").format(amount.toBigDecimal())
+
+ return TransformedText(
+ text = AnnotatedString(formatAmount + postfix),
+ offsetMapping = object : OffsetMapping{
+ override fun originalToTransformed(offset: Int): Int {
+ if (offset <= 1) return offset
+
+ val entireCommaCount = if (amount.length % 3 == 0) amount.length / 3 - 1 else amount.length / 3
+ val sliceUntil = if (offset + entireCommaCount <= formatAmount.length) offset + entireCommaCount else formatAmount.length
+ val commaBeforeOffsetCount = formatAmount.substring(0 until sliceUntil).count { it == ',' }
+
+ return offset + commaBeforeOffsetCount
+ }
+
+ override fun transformedToOriginal(offset: Int): Int {
+ return when (offset) {
+ in 0..1 -> offset
+ in 2 until formatAmount.length -> {
+ val entireCommaCount = if (amount.length % 3 == 0) amount.length / 3 - 1 else amount.length / 3
+ val sliceUntil = if (offset + entireCommaCount <= formatAmount.length) offset + entireCommaCount else formatAmount.length
+ val commaBeforeOffsetCount = formatAmount.substring(0 until sliceUntil).count { it == ',' }
+ offset - commaBeforeOffsetCount
+ }
+
+ else -> amount.length
+ }
+ }
+
+ }
+ )
+ }
+}
diff --git a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/textfield/UlbanBasicTextField.kt b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/textfield/UlbanBasicTextField.kt
new file mode 100644
index 00000000..b5610990
--- /dev/null
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/textfield/UlbanBasicTextField.kt
@@ -0,0 +1,76 @@
+package com.sixkids.designsystem.component.textfield
+
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.sixkids.designsystem.theme.Gray
+import com.sixkids.designsystem.theme.UlbanTheme
+import com.sixkids.designsystem.theme.UlbanTypography
+
+
+@Composable
+fun UlbanBasicTextField(
+ modifier: Modifier = Modifier,
+ text: String = "",
+ maxLines: Int = 1,
+ minLines: Int = 1,
+ onTextChange: (String) -> Unit = {},
+ hint: String = "",
+ textStyle: TextStyle = UlbanTypography.bodyLarge,
+ keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
+ keyboardActions: KeyboardActions = KeyboardActions.Default,
+ visualTransformation: VisualTransformation = VisualTransformation.None,
+) {
+ BasicTextField(
+ modifier = modifier,
+ value = text,
+ onValueChange = onTextChange,
+ textStyle = textStyle,
+ maxLines = maxLines,
+ minLines = minLines,
+ keyboardOptions = keyboardOptions,
+ keyboardActions = keyboardActions,
+ visualTransformation = visualTransformation,
+ ) { innerTextField ->
+ Box(
+ modifier = Modifier
+ .padding(8.dp)
+ ) {
+ if (text.isEmpty()) {
+ Text(
+ text = hint,
+ style = textStyle.copy(color = Gray),
+ )
+ }
+ innerTextField()
+ }
+ }
+}
+
+
+@Preview
+@Composable
+fun UlbanBasicTextFieldPreview() {
+ UlbanTheme {
+ var text by remember { mutableStateOf("") }
+ UlbanBasicTextField(
+ text = text,
+ onTextChange = { text = it },
+ hint = "Hint",
+ )
+ }
+}
diff --git a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/textfield/UlbanNumberTextField.kt b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/textfield/UlbanNumberTextField.kt
new file mode 100644
index 00000000..f650b4f4
--- /dev/null
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/textfield/UlbanNumberTextField.kt
@@ -0,0 +1,60 @@
+package com.sixkids.designsystem.component.textfield
+
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.tooling.preview.Preview
+import com.sixkids.designsystem.theme.UlbanTheme
+import com.sixkids.designsystem.theme.UlbanTypography
+
+
+@Composable
+fun UlbanNumberTextField(
+ modifier: Modifier = Modifier,
+ text: String = "",
+ maxLines: Int = 1,
+ minLines: Int = 1,
+ onTextChange: (String) -> Unit = {},
+ hint: String = "",
+ textStyle: TextStyle = UlbanTypography.bodyLarge,
+ keyboardActions: KeyboardActions = KeyboardActions.Default,
+ postfix: String = "",
+) {
+ UlbanBasicTextField(
+ modifier = modifier,
+ text = text,
+ hint = hint,
+ textStyle = textStyle,
+ onTextChange = onTextChange,
+ maxLines = maxLines,
+ minLines = minLines,
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Number,
+ imeAction = ImeAction.Done
+ ),
+ keyboardActions = keyboardActions,
+ visualTransformation = NumberVisualTransformation(postfix),
+ )
+}
+
+
+@Preview
+@Composable
+fun UlbanPointTextFieldPreview() {
+ UlbanTheme {
+ var price by remember { mutableStateOf("") }
+ UlbanNumberTextField(
+ text = price,
+ onTextChange = { price = it },
+ hint = "Hint",
+ )
+ }
+}
diff --git a/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/textfield/UlbanUnderLineIconInputField.kt b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/textfield/UlbanUnderLineIconInputField.kt
new file mode 100644
index 00000000..9d2ee3b7
--- /dev/null
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/textfield/UlbanUnderLineIconInputField.kt
@@ -0,0 +1,85 @@
+package com.sixkids.designsystem.component.textfield
+
+import androidx.annotation.DrawableRes
+import androidx.compose.foundation.clickable
+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.foundation.layout.size
+import androidx.compose.material3.Divider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.sixkids.designsystem.R
+import com.sixkids.designsystem.theme.Blue
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.ui.util.formatToDayMonthYear
+import java.time.LocalDate
+
+@Composable
+fun UlbanUnderLineIconInputField(
+ modifier: Modifier = Modifier,
+ text: String = "",
+ onIconClick: () -> Unit = {},
+ @DrawableRes iconResource: Int,
+ textStyle: TextStyle = UlbanTypography.bodyMedium
+) {
+ Column(
+ modifier = modifier
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = text,
+ style = textStyle,
+ modifier = modifier
+ .weight(1f)
+ .padding(8.dp)
+ )
+ Icon(
+ painter = painterResource(iconResource),
+ contentDescription = null,
+ modifier = Modifier
+ .clickable { onIconClick() }
+ .padding(horizontal = 8.dp)
+ .size(24.dp)
+ )
+ }
+ Divider(
+ modifier = Modifier.fillMaxWidth(),
+ color = Blue,
+ thickness = 2.dp
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun UlbanUnderLineDateFieldPreview() {
+ val date by remember { mutableStateOf(LocalDate.now()) }
+
+ Column {
+ Text(
+ text = "날짜를 선택해 주세요",
+ style = UlbanTypography.titleSmall,
+ modifier = Modifier.padding(8.dp)
+ )
+ UlbanUnderLineIconInputField(
+ text = date.formatToDayMonthYear(),
+ iconResource = R.drawable.ic_calendar,
+ )
+
+ }
+}
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
new file mode 100644
index 00000000..5acd589c
--- /dev/null
+++ b/android/core/designsystem/src/main/java/com/sixkids/designsystem/component/textfield/UlbanUnderLineTextField.kt
@@ -0,0 +1,189 @@
+package com.sixkids.designsystem.component.textfield
+
+import androidx.compose.foundation.clickable
+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.foundation.layout.size
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.Divider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+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.TextStyle
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.sixkids.designsystem.R
+import com.sixkids.designsystem.theme.Blue
+import com.sixkids.designsystem.theme.UlbanTypography
+
+@Composable
+fun UlbanUnderLineTextField(
+ modifier: Modifier = Modifier,
+ text: String = "",
+ onTextChange: (String) -> Unit = {},
+ hint: String = "",
+ onIconClick: () -> Unit = {},
+ inputTextType: InputTextType = InputTextType.TEXT,
+ textStyle: TextStyle = UlbanTypography.bodyMedium,
+ keyboardActions: KeyboardActions = KeyboardActions.Default,
+ keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
+ visualTransformation: VisualTransformation = VisualTransformation.None,
+) {
+
+ Column(
+ modifier = modifier,
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ when (inputTextType) {
+ InputTextType.TEXT -> {
+ UlbanBasicTextField(
+ modifier = modifier
+ .weight(1f),
+ text = text,
+ onTextChange = onTextChange,
+ hint = hint,
+ textStyle = textStyle,
+ keyboardActions = keyboardActions,
+ keyboardOptions = keyboardOptions,
+ visualTransformation = visualTransformation
+ )
+ }
+
+ InputTextType.POINT -> {
+ UlbanNumberTextField(
+ modifier = Modifier
+ .weight(1f),
+ text = text,
+ onTextChange = onTextChange,
+ hint = hint,
+ textStyle = textStyle,
+ keyboardActions = keyboardActions,
+ postfix = stringResource(id = R.string.point_postfix)
+ )
+ }
+
+ InputTextType.PEOPLE -> {
+ UlbanNumberTextField(
+ modifier = Modifier
+ .weight(1f),
+ text = text,
+ onTextChange = onTextChange,
+ hint = hint,
+ textStyle = textStyle,
+ keyboardActions = keyboardActions,
+ postfix = stringResource(id = R.string.people_postfix)
+ )
+ }
+
+ InputTextType.GRADE -> {
+ UlbanNumberTextField(
+ modifier = Modifier
+ .weight(1f),
+ text = text,
+ onTextChange = onTextChange,
+ hint = hint,
+ textStyle = textStyle,
+ keyboardActions = keyboardActions,
+ postfix = stringResource(id = R.string.grade_postfix)
+ )
+ }
+ InputTextType.CLASS -> {
+ UlbanNumberTextField(
+ modifier = Modifier
+ .weight(1f),
+ text = text,
+ onTextChange = onTextChange,
+ hint = hint,
+ textStyle = textStyle,
+ keyboardActions = keyboardActions,
+ postfix = stringResource(id = R.string.class_postfix)
+ )
+ }
+ }
+ if (text.isNotEmpty()) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_cancel),
+ contentDescription = null,
+ modifier = Modifier
+ .clickable { onIconClick() }
+ .padding(horizontal = 8.dp)
+ .size(24.dp)
+ )
+ }
+ }
+ Divider(
+ modifier = Modifier.fillMaxWidth(),
+ color = Blue,
+ thickness = 2.dp
+ )
+ }
+}
+
+
+enum class InputTextType {
+ TEXT,
+ POINT,
+ PEOPLE,
+ GRADE,
+ CLASS
+}
+
+@Preview(showBackground = true)
+@Composable
+fun UlbanUnderLineTextFieldPreview() {
+ var text by remember { mutableStateOf("4월 22일 함께 달리기") }
+ var point by remember { mutableStateOf("1200") }
+ var peopleCnt by remember { mutableStateOf("") }
+ Column(
+// verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ Text(text = "제목을 입력해 주세요", style = UlbanTypography.titleSmall)
+ UlbanUnderLineTextField(
+ text = text,
+ onTextChange = { text = it },
+ hint = "hint",
+ onIconClick = {
+ text = ""
+ },
+ inputTextType = InputTextType.TEXT
+ )
+
+ Text(text = "점수를 입력해 주세요", style = UlbanTypography.titleSmall)
+ UlbanUnderLineTextField(
+ text = point,
+ onTextChange = { point = it },
+ hint = "hint",
+ onIconClick = {
+ point = ""
+ },
+ inputTextType = InputTextType.POINT
+ )
+
+ Text(text = "그룹 최소 인원을 설정해 주세요", style = UlbanTypography.titleSmall)
+ UlbanUnderLineTextField(
+ text = peopleCnt,
+ onTextChange = { peopleCnt = it },
+ hint = "hint",
+ onIconClick = {
+ peopleCnt = ""
+ },
+ inputTextType = InputTextType.PEOPLE
+ )
+ }
+}
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 6c8e98c0..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,4 +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/announce.png b/android/core/designsystem/src/main/res/drawable/announce.png
index 0ff24915..4f95a517 100644
Binary files a/android/core/designsystem/src/main/res/drawable/announce.png and b/android/core/designsystem/src/main/res/drawable/announce.png differ
diff --git a/android/core/designsystem/src/main/res/drawable/board.png b/android/core/designsystem/src/main/res/drawable/board.png
index a9815fce..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/chat.png b/android/core/designsystem/src/main/res/drawable/chat.png
index c8d3d301..7d7c1057 100644
Binary files a/android/core/designsystem/src/main/res/drawable/chat.png and b/android/core/designsystem/src/main/res/drawable/chat.png differ
diff --git a/android/core/designsystem/src/main/res/drawable/hifive.png b/android/core/designsystem/src/main/res/drawable/hifive.png
index fc18eb74..29bc1c3b 100644
Binary files a/android/core/designsystem/src/main/res/drawable/hifive.png and b/android/core/designsystem/src/main/res/drawable/hifive.png differ
diff --git a/android/core/designsystem/src/main/res/drawable/ic_arrow_back.xml b/android/core/designsystem/src/main/res/drawable/ic_arrow_back.xml
new file mode 100644
index 00000000..d73d39d2
--- /dev/null
+++ b/android/core/designsystem/src/main/res/drawable/ic_arrow_back.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/android/core/designsystem/src/main/res/drawable/ic_calendar.xml b/android/core/designsystem/src/main/res/drawable/ic_calendar.xml
new file mode 100644
index 00000000..452a3c28
--- /dev/null
+++ b/android/core/designsystem/src/main/res/drawable/ic_calendar.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/android/core/designsystem/src/main/res/drawable/ic_cancel.xml b/android/core/designsystem/src/main/res/drawable/ic_cancel.xml
new file mode 100644
index 00000000..c3d36647
--- /dev/null
+++ b/android/core/designsystem/src/main/res/drawable/ic_cancel.xml
@@ -0,0 +1,9 @@
+
+
+
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_filter_alt.xml b/android/core/designsystem/src/main/res/drawable/ic_filter_alt.xml
new file mode 100644
index 00000000..abe03146
--- /dev/null
+++ b/android/core/designsystem/src/main/res/drawable/ic_filter_alt.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/ic_time.xml b/android/core/designsystem/src/main/res/drawable/ic_time.xml
new file mode 100644
index 00000000..ff137565
--- /dev/null
+++ b/android/core/designsystem/src/main/res/drawable/ic_time.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/android/core/designsystem/src/main/res/drawable/paint.png b/android/core/designsystem/src/main/res/drawable/paint.png
new file mode 100644
index 00000000..5dd22d87
Binary files /dev/null and b/android/core/designsystem/src/main/res/drawable/paint.png differ
diff --git a/android/core/designsystem/src/main/res/drawable/pencil.png b/android/core/designsystem/src/main/res/drawable/pencil.png
new file mode 100644
index 00000000..a0410423
Binary files /dev/null and b/android/core/designsystem/src/main/res/drawable/pencil.png differ
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_seuccess.png b/android/core/designsystem/src/main/res/drawable/relay_seuccess.png
deleted file mode 100644
index d12befb0..00000000
Binary files a/android/core/designsystem/src/main/res/drawable/relay_seuccess.png and /dev/null differ
diff --git a/android/core/designsystem/src/main/res/drawable/relay_success.png b/android/core/designsystem/src/main/res/drawable/relay_success.png
new file mode 100644
index 00000000..58d9a66a
Binary files /dev/null and b/android/core/designsystem/src/main/res/drawable/relay_success.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/drawable/setting.png b/android/core/designsystem/src/main/res/drawable/setting.png
index d74ab317..b463adcd 100644
Binary files a/android/core/designsystem/src/main/res/drawable/setting.png and b/android/core/designsystem/src/main/res/drawable/setting.png differ
diff --git a/android/core/designsystem/src/main/res/drawable/statistics.png b/android/core/designsystem/src/main/res/drawable/statistics.png
index 218e429d..5b59ffd5 100644
Binary files a/android/core/designsystem/src/main/res/drawable/statistics.png and b/android/core/designsystem/src/main/res/drawable/statistics.png differ
diff --git a/android/core/designsystem/src/main/res/raw/lodaing.json b/android/core/designsystem/src/main/res/raw/lodaing.json
new file mode 100644
index 00000000..cafcccf1
--- /dev/null
+++ b/android/core/designsystem/src/main/res/raw/lodaing.json
@@ -0,0 +1 @@
+{"v":"4.6.8","fr":29.9700012207031,"ip":0,"op":40.0000016292334,"w":256,"h":256,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 3","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":20,"s":[208.6,127.969,0],"e":[208.6,88,0],"to":[0,-6.66145849227905,0],"ti":[0,-0.00520833348855,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":30,"s":[208.6,88,0],"e":[208.6,128,0],"to":[0,0.00520833348855,0],"ti":[0,-6.66666650772095,0]},{"t":40.0000016292334}]},"a":{"a":0,"k":[-70,-0.5,0]},"s":{"a":0,"k":[75,75,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[33.75,34.5]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":0,"k":[0.9843137,0.5490196,0,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[-70.125,-0.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":300.00001221925,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 2","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":15,"s":[168.6,128,0],"e":[168.6,88,0],"to":[0,-6.66666650772095,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":25,"s":[168.6,88,0],"e":[168.6,128,0],"to":[0,0,0],"ti":[0,-6.66666650772095,0]},{"t":35.0000014255792}]},"a":{"a":0,"k":[-70,-0.5,0]},"s":{"a":0,"k":[75,75,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[33.75,34.5]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":0,"k":[0.9921569,0.8470588,0.2078431,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[-70.125,-0.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":300.00001221925,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 1","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":10,"s":[128.594,127.969,0],"e":[128.594,88,0],"to":[0,-6.66145849227905,0],"ti":[0,-0.00520833348855,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":20,"s":[128.594,88,0],"e":[128.594,128,0],"to":[0,0.00520833348855,0],"ti":[0,-6.66666650772095,0]},{"t":30.0000012219251}]},"a":{"a":0,"k":[-70,-0.5,0]},"s":{"a":0,"k":[75,75,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[33.75,34.5]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":0,"k":[0.2627451,0.627451,0.2784314,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[-70.125,-0.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":300.00001221925,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 4","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":5,"s":[88.6,127.969,0],"e":[88.6,88,0],"to":[0,-6.66145849227905,0],"ti":[0,-0.00520833348855,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":15,"s":[88.6,88,0],"e":[88.6,128,0],"to":[0,0.00520833348855,0],"ti":[0,-6.66666650772095,0]},{"t":25.0000010182709}]},"a":{"a":0,"k":[-70,-0.5,0]},"s":{"a":0,"k":[75,75,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[33.75,34.5]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":0,"k":[0.1176471,0.5333334,0.8980392,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[-70.125,-0.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":300.00001221925,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":5,"ty":4,"nm":"Shape Layer 5","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":0,"s":[48.6,127.969,0],"e":[48.6,88,0],"to":[0,-6.66145849227905,0],"ti":[0,-0.00520833348855,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":10,"s":[48.6,88,0],"e":[48.6,128,0],"to":[0,0.00520833348855,0],"ti":[0,-6.66666650772095,0]},{"t":20.0000008146167}]},"a":{"a":0,"k":[-70,-0.5,0]},"s":{"a":0,"k":[75,75,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[33.75,34.5]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":0,"k":[0.8980392,0.2235294,0.2078431,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[-70.125,-0.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":300.00001221925,"st":0,"bm":0,"sr":1}]}
\ No newline at end of file
diff --git a/android/core/designsystem/src/main/res/values/strings.xml b/android/core/designsystem/src/main/res/values/strings.xml
new file mode 100644
index 00000000..3e68bcb5
--- /dev/null
+++ b/android/core/designsystem/src/main/res/values/strings.xml
@@ -0,0 +1,30 @@
+
+
+ %d명 참여
+ " 점"
+ 명
+ " 학년"
+ " 반"
+ 확인
+ 취소
+ 이어 달리기
+ %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
new file mode 100644
index 00000000..23ff1ef0
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/Challenge.kt
@@ -0,0 +1,14 @@
+package com.sixkids.model
+
+import java.time.LocalDateTime
+
+data class Challenge(
+ 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
new file mode 100644
index 00000000..0cc138aa
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/ChallengeDetail.kt
@@ -0,0 +1,14 @@
+package com.sixkids.model
+
+import java.time.LocalDateTime
+
+data class ChallengeDetail(
+ val id: Long = 0,
+ val title: String = "",
+ 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/Group.kt b/android/core/model/src/main/java/com/sixkids/model/Group.kt
new file mode 100644
index 00000000..feb8f4d2
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/Group.kt
@@ -0,0 +1,7 @@
+package com.sixkids.model
+
+data class Group(
+ val id: Long = 0,
+ val leaderId: Long = 0,
+ val studentList : List = emptyList(),
+)
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
new file mode 100644
index 00000000..497ab35b
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/GroupSimple.kt
@@ -0,0 +1,7 @@
+package com.sixkids.model
+
+data class GroupSimple(
+ val headCount: 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/JwtToken.kt b/android/core/model/src/main/java/com/sixkids/model/JwtToken.kt
new file mode 100644
index 00000000..18160eaa
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/JwtToken.kt
@@ -0,0 +1,7 @@
+package com.sixkids.model
+
+
+data class JwtToken(
+ val accessToken : String,
+ val refreshToken : String
+)
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
new file mode 100644
index 00000000..7b005fac
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/MemberSimple.kt
@@ -0,0 +1,10 @@
+package com.sixkids.model
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class MemberSimple(
+ 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/Organization.kt b/android/core/model/src/main/java/com/sixkids/model/Organization.kt
new file mode 100644
index 00000000..5f2cbb2e
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/Organization.kt
@@ -0,0 +1,7 @@
+package com.sixkids.model
+
+data class Organization(
+ val id: Int,
+ val name: String,
+ val memberCount: Int,
+)
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
new file mode 100644
index 00000000..dab6342e
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/Report.kt
@@ -0,0 +1,13 @@
+package com.sixkids.model
+
+import java.time.LocalDateTime
+
+data class Report(
+ val id: Long = 0,
+ val group: Group = Group(),
+ val startTime: LocalDateTime = LocalDateTime.now(),
+ val endTime: LocalDateTime = LocalDateTime.now(),
+ val file : String = "",
+ val content: String = "",
+ 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
new file mode 100644
index 00000000..d0ecb6dd
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/RunningChallenge.kt
@@ -0,0 +1,14 @@
+package com.sixkids.model
+
+import java.time.LocalDateTime
+
+data class RunningChallenge(
+ val id: Long = 0,
+ val title: String = "",
+ val content: String = "",
+ val totalMemberCount: Int = 0,
+ val doneMemberCount: Int = 0,
+ val startTime: LocalDateTime = LocalDateTime.now(),
+ val endTime: LocalDateTime = LocalDateTime.now(),
+ val waitingCount: 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/model/src/main/java/com/sixkids/model/UserInfo.kt b/android/core/model/src/main/java/com/sixkids/model/UserInfo.kt
new file mode 100644
index 00000000..bec59f95
--- /dev/null
+++ b/android/core/model/src/main/java/com/sixkids/model/UserInfo.kt
@@ -0,0 +1,9 @@
+package com.sixkids.model
+
+data class UserInfo(
+ val id: Int,
+ val name: String,
+ val email: String,
+ val photo: String,
+ val role: String
+)
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/SnackbarToken.kt b/android/core/ui/src/main/java/com/sixkids/ui/SnackbarToken.kt
new file mode 100644
index 00000000..7a105af6
--- /dev/null
+++ b/android/core/ui/src/main/java/com/sixkids/ui/SnackbarToken.kt
@@ -0,0 +1,10 @@
+package com.sixkids.ui
+
+import androidx.annotation.DrawableRes
+
+data class SnackbarToken(
+ val message: String = "",
+ @DrawableRes val actionIcon: Int? = null,
+ val actionButtonText: String? = null,
+ val onClickActionButton: () -> Unit = {},
+)
diff --git a/android/core/ui/src/main/java/com/sixkids/ui/extension/Flow.kt b/android/core/ui/src/main/java/com/sixkids/ui/extension/Flow.kt
new file mode 100644
index 00000000..c457486c
--- /dev/null
+++ b/android/core/ui/src/main/java/com/sixkids/ui/extension/Flow.kt
@@ -0,0 +1,22 @@
+package com.sixkids.ui.extension
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import kotlinx.coroutines.flow.Flow
+
+@Composable
+inline fun Flow.collectWithLifecycle(
+ minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
+ noinline action: suspend (T) -> Unit,
+) {
+ val lifecycleOwner = LocalLifecycleOwner.current
+
+ LaunchedEffect(this, lifecycleOwner) {
+ lifecycleOwner.lifecycle.repeatOnLifecycle(minActiveState) {
+ this@collectWithLifecycle.collect { action(it) }
+ }
+ }
+}
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
new file mode 100644
index 00000000..5d5fe69c
--- /dev/null
+++ b/android/core/ui/src/main/java/com/sixkids/ui/util/Date.kt
@@ -0,0 +1,31 @@
+package com.sixkids.ui.util
+
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.LocalTime
+import java.time.format.DateTimeFormatter
+
+fun LocalDateTime.formatToMonthDayTime(): String {
+ val formatter = DateTimeFormatter.ofPattern("MM.dd HH:mm")
+ 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)
+}
+
+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/build.gradle.kts b/android/data/build.gradle.kts
index d079fe21..4b33d17a 100644
--- a/android/data/build.gradle.kts
+++ b/android/data/build.gradle.kts
@@ -13,6 +13,7 @@ dependencies {
implementation(libs.bundles.retrofit)
implementation(libs.datastore)
+ implementation(libs.paging)
testImplementation(libs.junit)
}
diff --git a/android/data/src/main/AndroidManifest.xml b/android/data/src/main/AndroidManifest.xml
index 8bdb7e14..891017a8 100644
--- a/android/data/src/main/AndroidManifest.xml
+++ b/android/data/src/main/AndroidManifest.xml
@@ -1,4 +1,5 @@
+
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
new file mode 100644
index 00000000..ebf683a3
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/api/ChallengeService.kt
@@ -0,0 +1,60 @@
+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 {
+
+
+ @POST("challenges")
+ suspend fun createChallenge(
+ @Body challengeCreateRequest: ChallengeCreateRequest
+ ): ApiResult>
+
+ @GET("challenges")
+ suspend fun getChallengeHistory(
+ @Query("orgId") organizationId: Int,
+ @Query("memberId") memberId: Int? = null,
+ @Query("page") page: Int,
+ @Query("size") size: Int,
+ ): ApiResult>
+
+ @GET("challenges/running")
+ suspend fun getRunningChallenge(
+ @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
new file mode 100644
index 00000000..9558279a
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/api/MemberService.kt
@@ -0,0 +1,44 @@
+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
+import okhttp3.MultipartBody
+import okhttp3.RequestBody
+import retrofit2.http.Body
+import retrofit2.http.GET
+import retrofit2.http.Multipart
+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(
+ @Part file: MultipartBody.Part?,
+ @PartMap data: HashMap
+ ): ApiResult>
+
+ @PATCH("members/token")
+ suspend fun autoSignIn() : ApiResult>
+
+ @POST("members/fcm")
+ suspend fun updateFCMToken(
+ @Body fcmToken: FcmRequest
+ ): ApiResult>
+}
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
new file mode 100644
index 00000000..dd814144
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/api/OrganizationService.kt
@@ -0,0 +1,57 @@
+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")
+ suspend fun getOrganizationList(): ApiResult>>
+
+ @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/api/SignInService.kt b/android/data/src/main/java/com/sixkids/data/api/SignInService.kt
new file mode 100644
index 00000000..305ac0dd
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/api/SignInService.kt
@@ -0,0 +1,28 @@
+package com.sixkids.data.api
+
+import com.sixkids.data.model.request.SignInRequest
+import com.sixkids.data.model.response.ApiResponse
+import com.sixkids.data.model.response.SignInResponse
+import com.sixkids.data.network.ApiResult
+import okhttp3.MultipartBody
+import okhttp3.RequestBody
+import retrofit2.http.Body
+import retrofit2.http.Multipart
+import retrofit2.http.POST
+import retrofit2.http.Part
+import retrofit2.http.PartMap
+
+interface SignInService {
+
+ @POST("members/sign-in")
+ suspend fun signIn(
+ @Body signInRequest: SignInRequest
+ ): ApiResult>
+
+ @Multipart
+ @POST("members/")
+ suspend fun signUp(
+ @Part file: MultipartBody.Part?,
+ @PartMap data: HashMap
+ ): ApiResult>
+}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/api/TokenService.kt b/android/data/src/main/java/com/sixkids/data/api/TokenService.kt
index 56a4497d..db9e0095 100644
--- a/android/data/src/main/java/com/sixkids/data/api/TokenService.kt
+++ b/android/data/src/main/java/com/sixkids/data/api/TokenService.kt
@@ -1,14 +1,11 @@
package com.sixkids.data.api
-import com.sixkids.data.model.request.RefreshTokenRequest
+import com.sixkids.data.model.response.ApiResponse
import com.sixkids.data.model.response.TokenResponse
import com.sixkids.data.network.ApiResult
-import retrofit2.http.Body
import retrofit2.http.POST
interface TokenService {
@POST("members/token")
- suspend fun refreshToken(
- @Body refreshTokenRequest: RefreshTokenRequest
- ): ApiResult
+ suspend fun refreshToken(): 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
new file mode 100644
index 00000000..24464180
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/di/DataSourceModule.kt
@@ -0,0 +1,87 @@
+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 dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+
+@Module
+@InstallIn(SingletonComponent::class)
+abstract class DataSourceModule {
+ @Binds
+ abstract fun bindUserDataSource(
+ userDataSource: UserRemoteDataSourceImpl
+ ): UserRemoteDataSource
+
+ @Binds
+ abstract fun bindLocalUserDataSource(
+ userLocalDataSource: UserLocalDataSourceImpl
+ ): UserLocalDataSource
+
+ @Binds
+ abstract fun bindChallengeDataSource(
+ challengeRemoteDataSource: ChallengeRemoteDataSourceImpl
+ ): ChallengeRemoteDataSource
+
+ @Binds
+ abstract fun bindOrganizationRemoteDataSource(
+ organizationRemoteDataSource: OrganizationRemoteDataSourceImpl
+ ): OrganizationRemoteDataSource
+
+ @Binds
+ abstract fun bindLocalOrganizationDataSource(
+ 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/DataStoreModule.kt b/android/data/src/main/java/com/sixkids/data/di/DataStoreModule.kt
index 89afd9d6..db319b9b 100644
--- a/android/data/src/main/java/com/sixkids/data/di/DataStoreModule.kt
+++ b/android/data/src/main/java/com/sixkids/data/di/DataStoreModule.kt
@@ -18,8 +18,9 @@ object DataStoreModule {
private const val USER_PREFERENCES = "user_preferences"
- @Provides
+
@Singleton
+ @Provides
fun provideUserPreferenceDataStore(@ApplicationContext context: Context)
: DataStore {
return PreferenceDataStoreFactory.create(
diff --git a/android/data/src/main/java/com/sixkids/data/di/NetworkModule.kt b/android/data/src/main/java/com/sixkids/data/di/NetworkModule.kt
index 96e426e5..21e2704b 100644
--- a/android/data/src/main/java/com/sixkids/data/di/NetworkModule.kt
+++ b/android/data/src/main/java/com/sixkids/data/di/NetworkModule.kt
@@ -4,7 +4,11 @@ import com.sixkids.data.network.ApiResultCallAdapterFactory
import com.sixkids.data.network.RefreshTokenInterceptor
import com.sixkids.data.network.TokenAuthenticator
import com.sixkids.data.network.TokenInterceptor
+import com.sixkids.data.util.LocalDateAdapter
+import com.sixkids.data.util.LocalDateTimeAdapter
+import com.sixkids.data.util.UnitJsonAdapter
import com.squareup.moshi.Moshi
+import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -21,7 +25,8 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
object NetworkModule {
- private const val BASE_URL = "base_url"
+ private const val BASE_URL = "https://k10d107.p.ssafy.io/api/"
+
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class PublicOkHttpClient
@@ -49,9 +54,18 @@ object NetworkModule {
@Provides
@Singleton
- fun moshi(): Moshi {
- return Moshi.Builder().build()
- }
+ fun moshi(): Moshi =
+ Moshi.Builder()
+ .add(LocalDateTimeAdapter())
+ .add(LocalDateAdapter())
+ .add(UnitJsonAdapter())
+ .addLast(KotlinJsonAdapterFactory())
+ .build()
+
+ @Provides
+ @Singleton
+ fun provideMoshiConverterFactory(moshi: Moshi): MoshiConverterFactory =
+ MoshiConverterFactory.create(moshi)
@Provides
@Singleton
@@ -107,13 +121,13 @@ object NetworkModule {
@Singleton
@PublicRetrofit
fun providePublicRetrofit(
- moshi: Moshi,
+ moshiConverterFactory: MoshiConverterFactory,
@PublicOkHttpClient okHttpClient: OkHttpClient
): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addCallAdapterFactory(ApiResultCallAdapterFactory())
- .addConverterFactory(MoshiConverterFactory.create(moshi))
+ .addConverterFactory(moshiConverterFactory)
.client(okHttpClient)
.build()
}
@@ -122,13 +136,13 @@ object NetworkModule {
@Singleton
@AuthRetrofit
fun provideAuthRetrofit(
- moshi: Moshi,
+ moshiConverterFactory: MoshiConverterFactory,
@AuthOkHttpClient okHttpClient: OkHttpClient
): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addCallAdapterFactory(ApiResultCallAdapterFactory())
- .addConverterFactory(MoshiConverterFactory.create(moshi))
+ .addConverterFactory(moshiConverterFactory)
.client(okHttpClient)
.build()
}
@@ -137,15 +151,15 @@ object NetworkModule {
@Singleton
@RefreshRetrofit
fun provideRefreshRetrofit(
- moshi: Moshi,
+ moshiConverterFactory: MoshiConverterFactory,
@RefreshOkHttpClient okHttpClient: OkHttpClient
): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addCallAdapterFactory(ApiResultCallAdapterFactory())
- .addConverterFactory(MoshiConverterFactory.create(moshi))
+ .addConverterFactory(moshiConverterFactory)
.client(okHttpClient)
.build()
}
-}
\ No newline at end of file
+}
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 d1403bb0..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
@@ -1,18 +1,93 @@
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.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
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
+ @Singleton
@Binds
abstract fun bindTokenRepository(
tokenRepository: TokenRepositoryImpl
): TokenRepository
-}
\ No newline at end of file
+
+ @Singleton
+ @Binds
+ abstract fun bindUserRepository(
+ userRepository: UserRepositoryImpl
+ ): UserRepository
+
+ @Singleton
+ @Binds
+ abstract fun bindChallengeRepository(
+ challengeRepository: ChallengeRepositoryImpl
+ ): ChallengeRepository
+
+ @Singleton
+ @Binds
+ 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 b3a37da6..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,5 +1,16 @@
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
import dagger.Provides
@@ -19,4 +30,93 @@ object ServiceModule {
): TokenService {
return retrofit.create(TokenService::class.java)
}
-}
\ No newline at end of file
+
+ @Singleton
+ @Provides
+ fun provideSignInService(
+ @NetworkModule.PublicRetrofit retrofit: Retrofit
+ ): SignInService {
+ return retrofit.create(SignInService::class.java)
+ }
+
+ @Singleton
+ @Provides
+ fun provideChallengeService(
+ @NetworkModule.AuthRetrofit retrofit: Retrofit
+ ): ChallengeService {
+ return retrofit.create(ChallengeService::class.java)
+ }
+
+ @Singleton
+ @Provides
+ fun provideMemberService(
+ @NetworkModule.AuthRetrofit retrofit: Retrofit
+ ): MemberService {
+ return retrofit.create(MemberService::class.java)
+ }
+
+ @Singleton
+ @Provides
+ fun provideOrganizationService(
+ @NetworkModule.AuthRetrofit retrofit: Retrofit
+ ): OrganizationService {
+ 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
new file mode 100644
index 00000000..32b12f97
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/request/ChallengeCreateRequest.kt
@@ -0,0 +1,20 @@
+package com.sixkids.data.model.request
+
+import java.time.LocalDateTime
+
+data class ChallengeCreateRequest(
+ val organizationId: Int,
+ val title: String,
+ val content: String,
+ val startTime: LocalDateTime,
+ val endTime: LocalDateTime,
+ val minCount: Int,
+ val reward: Int,
+ val groups: List
+)
+
+data class GroupRequest(
+ val headCount: 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/FcmRequest.kt b/android/data/src/main/java/com/sixkids/data/model/request/FcmRequest.kt
new file mode 100644
index 00000000..b872b399
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/request/FcmRequest.kt
@@ -0,0 +1,5 @@
+package com.sixkids.data.model.request
+
+data class FcmRequest(
+ val fcmToken: String,
+)
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/NewOrganizationRequest.kt b/android/data/src/main/java/com/sixkids/data/model/request/NewOrganizationRequest.kt
new file mode 100644
index 00000000..fcac6884
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/request/NewOrganizationRequest.kt
@@ -0,0 +1,5 @@
+package com.sixkids.data.model.request
+
+data class NewOrganizationRequest(
+ val name: 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/RefreshTokenRequest.kt b/android/data/src/main/java/com/sixkids/data/model/request/RefreshTokenRequest.kt
deleted file mode 100644
index a6bd0a4c..00000000
--- a/android/data/src/main/java/com/sixkids/data/model/request/RefreshTokenRequest.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.sixkids.data.model.request
-
-data class RefreshTokenRequest(
- val accessToken: String,
- val refreshToken: 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/request/SignInRequest.kt b/android/data/src/main/java/com/sixkids/data/model/request/SignInRequest.kt
new file mode 100644
index 00000000..3482b3b0
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/request/SignInRequest.kt
@@ -0,0 +1,5 @@
+package com.sixkids.data.model.request
+
+data class SignInRequest(
+ val idToken: String
+)
diff --git a/android/data/src/main/java/com/sixkids/data/model/request/SignUpRequest.kt b/android/data/src/main/java/com/sixkids/data/model/request/SignUpRequest.kt
new file mode 100644
index 00000000..de550522
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/request/SignUpRequest.kt
@@ -0,0 +1,10 @@
+package com.sixkids.data.model.request
+
+import okhttp3.MultipartBody
+
+data class SignUpRequest(
+ val idToken: String,
+ val file: MultipartBody.Part?,
+ val defaultImage: Int,
+ val role: String
+)
diff --git a/android/data/src/main/java/com/sixkids/data/model/response/ApiResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/ApiResponse.kt
new file mode 100644
index 00000000..45d12ebe
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/ApiResponse.kt
@@ -0,0 +1,7 @@
+package com.sixkids.data.model.response
+
+data class ApiResponse(
+ val message: String,
+ val status: String,
+ val data: T,
+)
\ No newline at end of file
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
new file mode 100644
index 00000000..602fa0c9
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/ChallengeHistoryResponse.kt
@@ -0,0 +1,31 @@
+package com.sixkids.data.model.response
+
+import com.sixkids.model.Challenge
+import java.time.LocalDateTime
+
+data class ChallengeHistoryResponse(
+ val page: Int,
+ val size: Int,
+ val last: Boolean,
+ val totalCount : Int,
+ val challenges: List
+)
+
+data class ChallengeResponse(
+ val id: Long = 0,
+ val title: String = "",
+ val content: String = "",
+ val headCount: Int = 0,
+ val startTime: LocalDateTime = LocalDateTime.now(),
+ val endTime: LocalDateTime = LocalDateTime.now(),
+)
+
+internal fun ChallengeResponse.toModel(totalCount: Int = 0) = Challenge(
+ id = id,
+ title = title,
+ content = content,
+ headCount = headCount,
+ startTime = startTime,
+ 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
new file mode 100644
index 00000000..2b3c3375
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/MemberInfoResponse.kt
@@ -0,0 +1,36 @@
+package com.sixkids.data.model.response
+
+import com.sixkids.model.MemberSimple
+import com.sixkids.model.UserInfo
+
+data class MemberInfoResponse(
+ val id: Int,
+ val name: String,
+ val email: String,
+ val photo: String,
+ val role: String
+)
+
+data class MemberSimpleInfoResponse(
+ val id: Long,
+ val name: String,
+ val photo: String?
+)
+
+internal fun MemberInfoResponse.toModel(): UserInfo {
+ return UserInfo(
+ id = id,
+ name = name,
+ email = email,
+ 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/OrganizationResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/OrganizationResponse.kt
new file mode 100644
index 00000000..d9469e91
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/OrganizationResponse.kt
@@ -0,0 +1,17 @@
+package com.sixkids.data.model.response
+
+import com.sixkids.model.Organization
+import java.time.LocalDate
+
+data class OrganizationResponse(
+ val id: Int,
+ val name: String,
+ val memberCount: Int,
+ val createTime: LocalDate = LocalDate.now(),
+)
+
+internal fun OrganizationResponse.toModel() = Organization(
+ id = id,
+ name = name,
+ memberCount = memberCount
+)
\ No newline at end of file
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/RunningChallengeResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/RunningChallengeResponse.kt
new file mode 100644
index 00000000..51502fa9
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/RunningChallengeResponse.kt
@@ -0,0 +1,25 @@
+package com.sixkids.data.model.response
+
+import com.sixkids.model.RunningChallenge
+import com.squareup.moshi.Json
+
+data class RunningChallengeResponse(
+ @Json(name = "challengeSimpleDTO")
+ val challenge: ChallengeResponse,
+ val waitingCount: Int,
+ val doneMemberCount: Int
+)
+
+internal fun RunningChallengeResponse.toModel(): RunningChallenge {
+ val challenge = challenge.toModel()
+ return RunningChallenge(
+ id = challenge.id,
+ title = challenge.title,
+ content = challenge.content,
+ totalMemberCount = challenge.headCount,
+ doneMemberCount = doneMemberCount,
+ startTime = challenge.startTime,
+ endTime = challenge.endTime,
+ waitingCount = waitingCount
+ )
+}
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/SignInResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/SignInResponse.kt
new file mode 100644
index 00000000..0ef61a92
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/SignInResponse.kt
@@ -0,0 +1,14 @@
+package com.sixkids.data.model.response
+
+import com.sixkids.model.JwtToken
+
+data class SignInResponse(
+ val accessToken : String,
+ val refreshToken : String,
+ val role : String
+)
+
+internal fun SignInResponse.toModel() = JwtToken(
+ accessToken = accessToken,
+ refreshToken = refreshToken
+)
diff --git a/android/data/src/main/java/com/sixkids/data/model/response/SignUpResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/SignUpResponse.kt
new file mode 100644
index 00000000..39c52824
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/SignUpResponse.kt
@@ -0,0 +1,14 @@
+package com.sixkids.data.model.response
+
+import com.sixkids.model.JwtToken
+
+data class SignUpResponse (
+ val accessToken: String,
+ val refreshToken: String,
+ val role: String
+)
+
+internal fun SignUpResponse.toModel() = JwtToken(
+ accessToken = accessToken,
+ refreshToken = refreshToken
+)
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/model/response/TokenResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/TokenResponse.kt
index 2900fb29..4a5596f1 100644
--- a/android/data/src/main/java/com/sixkids/data/model/response/TokenResponse.kt
+++ b/android/data/src/main/java/com/sixkids/data/model/response/TokenResponse.kt
@@ -4,3 +4,8 @@ data class TokenResponse(
val accessToken: String,
val refreshToken: String
)
+
+internal fun TokenResponse.toModel() = com.sixkids.model.JwtToken(
+ accessToken = accessToken,
+ refreshToken = refreshToken
+)
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/model/response/UpdateProfilePhotoResponse.kt b/android/data/src/main/java/com/sixkids/data/model/response/UpdateProfilePhotoResponse.kt
new file mode 100644
index 00000000..146e3249
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/model/response/UpdateProfilePhotoResponse.kt
@@ -0,0 +1,5 @@
+package com.sixkids.data.model.response
+
+data class UpdateProfilePhotoResponse (
+ val photoImageUrl: String
+)
diff --git a/android/data/src/main/java/com/sixkids/data/network/ApiResponse.kt b/android/data/src/main/java/com/sixkids/data/network/ApiResponse.kt
new file mode 100644
index 00000000..7648655a
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/network/ApiResponse.kt
@@ -0,0 +1,10 @@
+package com.sixkids.data.network
+
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class ApiResponse(
+ val message: String,
+ val status: String,
+ val data: T,
+)
diff --git a/android/data/src/main/java/com/sixkids/data/network/ApiResult.kt b/android/data/src/main/java/com/sixkids/data/network/ApiResult.kt
index 157c92a8..d22b8f57 100644
--- a/android/data/src/main/java/com/sixkids/data/network/ApiResult.kt
+++ b/android/data/src/main/java/com/sixkids/data/network/ApiResult.kt
@@ -56,4 +56,4 @@ private fun handleHttpError(httpError: ApiResult.Failure.HttpError): Exception =
404 -> NotFoundException()
500, 501, 502, 503, 504, 505 -> NetworkException()
else -> UnknownException()
-}
\ No newline at end of file
+}
diff --git a/android/data/src/main/java/com/sixkids/data/network/ApiResultCallAdapter.kt b/android/data/src/main/java/com/sixkids/data/network/ApiResultCallAdapter.kt
index 64597c47..4d3db04d 100644
--- a/android/data/src/main/java/com/sixkids/data/network/ApiResultCallAdapter.kt
+++ b/android/data/src/main/java/com/sixkids/data/network/ApiResultCallAdapter.kt
@@ -13,22 +13,21 @@ import java.lang.reflect.Type
private const val TAG = "ApiResultCallAdapter_hong"
-class ApiResultCallAdapter(
- private val successType: Type
+internal class ApiResultCallAdapter(
+ private val successType: Type,
) : CallAdapter>> {
+ override fun adapt(call: Call): Call> = ApiResultCall(call, successType)
override fun responseType(): Type = successType
-
- override fun adapt(call: Call): Call> =
- ApiResultCall(call, successType)
}
+
private class ApiResultCall(
private val delegate: Call,
private val successType: Type,
) : Call> {
- override fun enqueue(callback: Callback>) {
- delegate.enqueue(object : Callback {
+ override fun enqueue(callback: Callback>) = delegate.enqueue(
+ object : Callback {
override fun onResponse(call: Call, response: Response) {
callback.onResponse(this@ApiResultCall, Response.success(response.toApiResult()))
}
@@ -49,13 +48,9 @@ private class ApiResultCall(
@Suppress("UNCHECKED_CAST")
(ApiResult.successOf(Unit as R))
} else {
- Log.d(
- TAG,
- "toApiResult: successType이 Unit이 아닌 값입니다. 확인하세요. successType: $successType"
- )
ApiResult.Failure.UnknownApiError(
IllegalStateException(
- "successType이 Unit이 아닌 값입니다. 확인하세요. successType: $successType",
+ "Body가 존재하지 않지만, Unit 이외의 타입으로 정의했습니다. ApiResult로 정의하세요.",
),
)
}
@@ -69,24 +64,21 @@ private class ApiResultCall(
}
callback.onResponse(this@ApiResultCall, Response.success(error))
}
- })
- }
+ },
+ )
override fun clone(): Call> = ApiResultCall(delegate.clone(), successType)
- override fun execute(): Response> {
- val response = delegate.execute()
- return if (response.isSuccessful && response.body() != null) {
- Response.success(ApiResult.Success(response.body()!!))
- } else {
- Response.error(response.code(), response.errorBody()!!)
- }
- }
+ override fun execute(): Response> =
+ throw UnsupportedOperationException("This adapter doesn't support sync execution")
override fun isExecuted(): Boolean = delegate.isExecuted
+
override fun cancel() = delegate.cancel()
+
override fun isCanceled(): Boolean = delegate.isCanceled
+
override fun request(): Request = delegate.request()
- override fun timeout(): Timeout = delegate.timeout()
-}
\ No newline at end of file
+ override fun timeout(): Timeout = delegate.timeout()
+}
diff --git a/android/data/src/main/java/com/sixkids/data/network/ApiResultCallAdapterFactory.kt b/android/data/src/main/java/com/sixkids/data/network/ApiResultCallAdapterFactory.kt
index efd085bf..54a539a9 100644
--- a/android/data/src/main/java/com/sixkids/data/network/ApiResultCallAdapterFactory.kt
+++ b/android/data/src/main/java/com/sixkids/data/network/ApiResultCallAdapterFactory.kt
@@ -1,5 +1,6 @@
package com.sixkids.data.network
+import retrofit2.Call
import retrofit2.CallAdapter
import retrofit2.Retrofit
import java.lang.reflect.ParameterizedType
@@ -9,14 +10,14 @@ class ApiResultCallAdapterFactory : CallAdapter.Factory() {
override fun get(
returnType: Type,
annotations: Array,
- retrofit: Retrofit
+ retrofit: Retrofit,
): CallAdapter<*, *>? {
val wrapperType = getParameterUpperBound(0, returnType as ParameterizedType)
- if (getRawType(returnType) != ApiResult::class.java) {
+ if (getRawType(returnType) != Call::class.java) {
return null
}
val bodyType = getParameterUpperBound(0, wrapperType as ParameterizedType)
return ApiResultCallAdapter(bodyType)
}
-}
\ No newline at end of file
+}
diff --git a/android/data/src/main/java/com/sixkids/data/network/RefreshTokenInterceptor.kt b/android/data/src/main/java/com/sixkids/data/network/RefreshTokenInterceptor.kt
index 460a7d5b..9b94340d 100644
--- a/android/data/src/main/java/com/sixkids/data/network/RefreshTokenInterceptor.kt
+++ b/android/data/src/main/java/com/sixkids/data/network/RefreshTokenInterceptor.kt
@@ -11,15 +11,20 @@ class RefreshTokenInterceptor @Inject constructor(
) : Interceptor {
companion object {
const val AUTHORIZATION_HEADER = "Authorization"
+ const val REFRESH_HEADER = "refreshToken"
const val TOKEN_TYPE = "Bearer"
}
override fun intercept(chain: Interceptor.Chain): Response {
- val token = runBlocking {
+ val atk = runBlocking {
+ tokenRepository.getAccessToken()
+ }
+ val rtk = runBlocking {
tokenRepository.getRefreshToken()
}
val request = chain.request().newBuilder().apply {
- addHeader(AUTHORIZATION_HEADER, "$TOKEN_TYPE $token")
+ addHeader(AUTHORIZATION_HEADER, "$TOKEN_TYPE $atk")
+ addHeader(AUTHORIZATION_HEADER, "$REFRESH_HEADER $rtk")
}
return chain.proceed(request.build())
diff --git a/android/data/src/main/java/com/sixkids/data/network/TokenAuthenticator.kt b/android/data/src/main/java/com/sixkids/data/network/TokenAuthenticator.kt
index 8bbbec2e..37980770 100644
--- a/android/data/src/main/java/com/sixkids/data/network/TokenAuthenticator.kt
+++ b/android/data/src/main/java/com/sixkids/data/network/TokenAuthenticator.kt
@@ -1,7 +1,6 @@
package com.sixkids.data.network
import com.sixkids.data.api.TokenService
-import com.sixkids.data.model.request.RefreshTokenRequest
import com.sixkids.domain.repository.TokenRepository
import kotlinx.coroutines.runBlocking
import okhttp3.Authenticator
@@ -20,29 +19,22 @@ class TokenAuthenticator @Inject constructor(
const val TOKEN_TYPE = "Bearer"
}
override fun authenticate(route: Route?, response: Response): Request? {
- val refreshToken = runBlocking {
- tokenRepository.getRefreshToken()
- }
- val accessToken = runBlocking {
- tokenRepository.getAccessToken()
- }
-
return runBlocking {
- val tokenResponse = tokenService.refreshToken(
- RefreshTokenRequest(accessToken = accessToken, refreshToken = refreshToken),
- )
+ val tokenResponse = tokenService.refreshToken()
// 2-1. 정상적으로 받지 못하면 request token 까지 만료된 것.
- if(tokenResponse.isFailure || tokenResponse.getOrNull == null) {
+ if(tokenResponse.isFailure || tokenResponse.getOrNull() == null) {
tokenRepository.clearTokens()
null
} else {
+ tokenRepository.saveIdToken(tokenResponse.getOrNull()!!.data.accessToken)
+ tokenRepository.saveRefreshToken(tokenResponse.getOrNull()!!.data.refreshToken)
// 3. 헤더에 토큰을 교체한 request 생성
response.request.newBuilder()
- .header(AUTHORIZATION_HEADER, "$TOKEN_TYPE ${tokenResponse.getOrNull.accessToken}")
+ .header(AUTHORIZATION_HEADER, "$TOKEN_TYPE ${tokenResponse.getOrNull()!!.data.accessToken}")
.build()
}
}
}
-}
\ No newline at end of file
+}
diff --git a/android/data/src/main/java/com/sixkids/data/network/TokenInterceptor.kt b/android/data/src/main/java/com/sixkids/data/network/TokenInterceptor.kt
index e2faf278..5dc18195 100644
--- a/android/data/src/main/java/com/sixkids/data/network/TokenInterceptor.kt
+++ b/android/data/src/main/java/com/sixkids/data/network/TokenInterceptor.kt
@@ -25,4 +25,4 @@ class TokenInterceptor @Inject constructor(
return chain.proceed(request.build())
}
-}
\ No newline at end of file
+}
diff --git a/android/data/src/main/java/com/sixkids/data/repository/TokenRepositoryImpl.kt b/android/data/src/main/java/com/sixkids/data/repository/TokenRepositoryImpl.kt
index 05e6b69b..71260f0f 100644
--- a/android/data/src/main/java/com/sixkids/data/repository/TokenRepositoryImpl.kt
+++ b/android/data/src/main/java/com/sixkids/data/repository/TokenRepositoryImpl.kt
@@ -16,6 +16,7 @@ class TokenRepositoryImpl @Inject constructor(
companion object {
val ACCESS_TOKEN_KEY = stringPreferencesKey("access_token")
val REFRESH_TOKEN_KEY = stringPreferencesKey("refresh_token")
+ val ID_TOKEN_KEY = stringPreferencesKey("id_token")
}
override suspend fun getAccessToken(): String =
dataStore.data.map { preferences ->
@@ -40,10 +41,29 @@ class TokenRepositoryImpl @Inject constructor(
}
}
+ override suspend fun saveIdToken(token: String) {
+ dataStore.edit { preferences ->
+ preferences[ID_TOKEN_KEY] = token
+ }
+ }
+
+ override suspend fun getIdToken(): String =
+ dataStore.data.map { preferences ->
+ preferences[ID_TOKEN_KEY] ?: ""
+ }.first()
+
+ override suspend fun deleteIdToken() {
+ dataStore.edit { preferences ->
+ preferences.remove(ID_TOKEN_KEY)
+ }
+ }
+
+
override suspend fun clearTokens() {
dataStore.edit { preferences ->
preferences.remove(ACCESS_TOKEN_KEY)
preferences.remove(REFRESH_TOKEN_KEY)
+ preferences.remove(ID_TOKEN_KEY)
}
}
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
new file mode 100644
index 00000000..69b90ca2
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/challenge/ChallengeRepositoryImpl.kt
@@ -0,0 +1,81 @@
+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
+
+class ChallengeRepositoryImpl @Inject constructor(
+ private val challengeService: ChallengeService,
+ private val challengeRemoteDataSourceImpl: ChallengeRemoteDataSourceImpl,
+) : ChallengeRepository {
+ override suspend fun getChallengeHistory(
+ organizationId: Int,
+ memberId: Int?,
+ ): Flow> =
+ Pager(
+ config = PagingConfig(DEFAULT_SIZE),
+ pagingSourceFactory = {
+ ChallengeHistoryPagingSource(
+ challengeService,
+ organizationId,
+ memberId,
+ )
+ }
+ ).flow
+
+ 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,
+ reward: Int,
+ minCount: Int,
+ groups: List
+ ): Long = challengeRemoteDataSourceImpl.createChallenge(
+ organizationId,
+ title,
+ content,
+ startTime,
+ endTime,
+ 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
new file mode 100644
index 00000000..ac10c0ad
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/challenge/remote/ChallengeHistoryPagingSource.kt
@@ -0,0 +1,51 @@
+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
+import com.sixkids.data.model.response.toModel
+import com.sixkids.model.Challenge
+import javax.inject.Inject
+
+class ChallengeHistoryPagingSource @Inject constructor(
+ private val challengeService: ChallengeService,
+ 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 = challengeService.getChallengeHistory(
+ organizationId,
+ memberId,
+ page,
+ DEFAULT_SIZE
+ )
+ val challengeHistory = response.getOrThrow().data.let { challengeResponse ->
+ challengeResponse.challenges.map { it.toModel(challengeResponse.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
+ }
+
+}
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
new file mode 100644
index 00000000..f6868748
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/challenge/remote/ChallengeRemoteDataSource.kt
@@ -0,0 +1,37 @@
+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
+
+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,
+ 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
new file mode 100644
index 00000000..a4461fac
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/challenge/remote/ChallengeRemoteDataSourceImpl.kt
@@ -0,0 +1,67 @@
+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
+
+class ChallengeRemoteDataSourceImpl @Inject constructor(
+ private val challengeService: ChallengeService,
+) : ChallengeRemoteDataSource {
+ 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,
+ reward: Int,
+ minCount: Int,
+ groups: List,
+ ): Long = challengeService.createChallenge(
+ ChallengeCreateRequest(
+ organizationId = organizationId,
+ title = title,
+ content = content,
+ startTime = startTime,
+ endTime = endTime,
+ reward = reward,
+ minCount = minCount,
+ groups = groups.map {
+ GroupRequest(
+ headCount = it.headCount,
+ leaderId = it.leaderId,
+ students = it.students
+ )
+ }
+ )
+ ).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
new file mode 100644
index 00000000..dd6a6e75
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/organization/OrganizationRepositoryImpl.kt
@@ -0,0 +1,91 @@
+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(
+ private val organizationRemoteDataSource: OrganizationRemoteDataSource,
+ private val organizationLocalDataSource: OrganizationLocalDataSource
+) : OrganizationRepository {
+ override suspend fun getClassList(): List {
+ return organizationRemoteDataSource.getClassList()
+ }
+
+ override suspend fun saveSelectedOrganizationId(organizationId: Int) {
+ organizationLocalDataSource.saveSelectedOrganizationId(organizationId)
+ }
+
+ override suspend fun getSelectedOrganizationId(): Int {
+ return organizationLocalDataSource.getSelectedOrganizationId()
+ }
+
+ override suspend fun newOrganization(name: String): Long {
+ return organizationRemoteDataSource.newOrganization(name)
+ }
+
+ 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
new file mode 100644
index 00000000..2298ee06
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/organization/local/OrganizationLocalDataSource.kt
@@ -0,0 +1,8 @@
+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
new file mode 100644
index 00000000..62cff7b0
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/organization/local/OrganizationLocalDataSourceImpl.kt
@@ -0,0 +1,44 @@
+package com.sixkids.data.repository.organization.local
+
+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
+
+class OrganizationLocalDataSourceImpl @Inject constructor(
+ private val dataStore: DataStore
+) : OrganizationLocalDataSource {
+ override suspend fun getSelectedOrganizationId(): Int {
+ return dataStore.data.map { preference ->
+ preference[SELECTED_ORGANIZATION_ID_KEY] ?: 0
+ }.first()
+ }
+
+ override suspend fun saveSelectedOrganizationId(organizationId: Int) {
+ dataStore.edit { preference ->
+ preference[SELECTED_ORGANIZATION_ID_KEY] = organizationId
+ }
+ }
+
+ 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
new file mode 100644
index 00000000..a25d5bc3
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/organization/remote/OrganizationRemoteDataSource.kt
@@ -0,0 +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
new file mode 100644
index 00000000..084e9b54
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/organization/remote/OrganizationRemoteDataSourceImpl.kt
@@ -0,0 +1,80 @@
+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,
+ private val memberOrgService: MemberOrgService
+) : OrganizationRemoteDataSource {
+ override suspend fun getClassList(): List {
+ return organizationService.getOrganizationList().getOrThrow().data.map { it.toModel() }
+ }
+
+ 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
new file mode 100644
index 00000000..f5054b0a
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/user/UserRepositoryImpl.kt
@@ -0,0 +1,97 @@
+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
+
+class UserRepositoryImpl @Inject constructor(
+ private val userRemoteDataSource: UserRemoteDataSource,
+ private val userLocalDataSource: UserLocalDataSource,
+ private val tokenRepository: TokenRepository
+) : UserRepository {
+ override suspend fun signIn(idToken: String): JwtToken {
+ try {
+ val response = userRemoteDataSource.signIn(idToken)
+
+ tokenRepository.saveAccessToken(response.accessToken)
+ tokenRepository.saveRefreshToken(response.refreshToken)
+
+ return response
+ } catch (e: Exception) {
+ tokenRepository.saveIdToken(idToken)
+ throw e
+ }
+ }
+
+ override suspend fun signUp(
+ file: File?,
+ defaultImage: Int,
+ role: String
+ ): JwtToken {
+ try {
+ val idToken = tokenRepository.getIdToken()
+ val response = userRemoteDataSource.signUp(file, idToken, defaultImage, role)
+
+ tokenRepository.saveAccessToken(response.accessToken)
+ tokenRepository.saveRefreshToken(response.refreshToken)
+
+ tokenRepository.deleteIdToken()
+
+ return response
+ } catch (e: Exception) {
+ throw e
+ }
+ }
+
+ override suspend fun getRole(): String {
+ return userLocalDataSource.getRole()
+ }
+
+ override suspend fun getMemberInfo(): UserInfo {
+ 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)
+ }
+
+ override suspend fun signOut(): Boolean {
+ return userLocalDataSource.signOut()
+ }
+
+ override suspend fun updateFCMToken(fcmToken: String) =
+ userRemoteDataSource.updateFCMToken(fcmToken)
+
+ override suspend fun autoSignIn(): JwtToken {
+ try {
+ val response = userRemoteDataSource.autoSignIn()
+
+ tokenRepository.saveAccessToken(response.accessToken)
+ tokenRepository.saveRefreshToken(response.refreshToken)
+
+ return response
+ }catch (e: Exception){
+ 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/local/UserLocalDataSource.kt b/android/data/src/main/java/com/sixkids/data/repository/user/local/UserLocalDataSource.kt
new file mode 100644
index 00000000..053c7019
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/user/local/UserLocalDataSource.kt
@@ -0,0 +1,22 @@
+package com.sixkids.data.repository.user.local
+
+import com.sixkids.model.UserInfo
+
+interface UserLocalDataSource {
+ suspend fun getRole() : String
+ suspend fun saveRole(role: String)
+
+ suspend fun getUserId() : Int
+ suspend fun saveUserId(userId: Int)
+
+ suspend fun getUserName() : String
+ suspend fun saveUserName(userName: String)
+
+ suspend fun getUserProfileImage() : String
+ suspend fun saveUserProfileImage(image: String)
+
+ suspend fun getUserInfo() : UserInfo
+ suspend fun saveUserInfo(id: Int, name: String, email: String, photo: String, role: String)
+
+ suspend fun signOut() : Boolean
+}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/repository/user/local/UserLocalDataSourceImpl.kt b/android/data/src/main/java/com/sixkids/data/repository/user/local/UserLocalDataSourceImpl.kt
new file mode 100644
index 00000000..4640bed7
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/user/local/UserLocalDataSourceImpl.kt
@@ -0,0 +1,110 @@
+package com.sixkids.data.repository.user.local
+
+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 com.sixkids.model.UserInfo
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import javax.inject.Inject
+
+class UserLocalDataSourceImpl @Inject constructor(
+ private val dataStore: DataStore
+) : UserLocalDataSource {
+ override suspend fun getRole(): String {
+ return dataStore.data.map { preferences ->
+ preferences[ROLE_KEY] ?: ""
+ }.first()
+ }
+
+ override suspend fun saveRole(role: String) {
+ dataStore.edit { preferences ->
+ preferences[ROLE_KEY] = role
+ }
+ }
+
+ override suspend fun getUserId(): Int {
+ return dataStore.data.map { preferences ->
+ preferences[ID_KEY] ?: 0
+ }.first()
+ }
+
+ override suspend fun saveUserId(userId: Int) {
+ dataStore.edit { preferences ->
+ preferences[ID_KEY] = userId
+ }
+ }
+
+ override suspend fun getUserName(): String {
+ return dataStore.data.map { preferences ->
+ preferences[NAME_KEY] ?: ""
+ }.first()
+ }
+
+ override suspend fun saveUserName(userName: String) {
+ dataStore.edit { preferences ->
+ preferences[NAME_KEY] = userName
+ }
+ }
+
+ override suspend fun getUserProfileImage(): String {
+ return dataStore.data.map { preferences ->
+ preferences[IMAGE_KEY] ?: ""
+ }.first()
+ }
+
+ override suspend fun saveUserProfileImage(image: String) {
+ dataStore.edit { preferences ->
+ preferences[IMAGE_KEY] = image
+ }
+ }
+
+ override suspend fun getUserInfo(): UserInfo {
+ return dataStore.data.map { preferences ->
+ UserInfo(
+ id = preferences[ID_KEY] ?: 0,
+ name = preferences[NAME_KEY] ?: "",
+ email = preferences[EMAIL_KEY] ?: "",
+ photo = preferences[IMAGE_KEY] ?: "",
+ role = preferences[ROLE_KEY] ?: ""
+ )
+ }.first()
+ }
+
+ override suspend fun saveUserInfo(
+ id: Int,
+ name: String,
+ email: String,
+ photo: String,
+ role: String
+ ) {
+ dataStore.edit { preferences ->
+ preferences[ID_KEY] = id
+ preferences[NAME_KEY] = name
+ preferences[EMAIL_KEY] = email
+ preferences[IMAGE_KEY] = photo
+ preferences[ROLE_KEY] = role
+ }
+ }
+
+ override suspend fun signOut(): Boolean {
+ return try {
+ dataStore.edit { preferences ->
+ preferences.clear()
+ }
+ true
+ }catch (e: Exception){
+ false
+ }
+ }
+
+ companion object {
+ val ROLE_KEY = stringPreferencesKey("role")
+ val ID_KEY = intPreferencesKey("id")
+ val NAME_KEY = stringPreferencesKey("name")
+ val IMAGE_KEY = stringPreferencesKey("image")
+ val EMAIL_KEY = stringPreferencesKey("email")
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..a4dff1ad
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/user/remote/UserRemoteDataSource.kt
@@ -0,0 +1,25 @@
+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
+
+interface UserRemoteDataSource {
+ suspend fun signIn(idToken: String): JwtToken
+
+ suspend fun signUp(file: File?, idToken: String, defaultImage: Int, role: String): JwtToken
+
+ 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
new file mode 100644
index 00000000..af989e6b
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/repository/user/remote/UserRemoteDataSourceImpl.kt
@@ -0,0 +1,117 @@
+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
+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
+import okhttp3.RequestBody
+import okhttp3.RequestBody.Companion.asRequestBody
+import java.io.File
+import javax.inject.Inject
+
+class UserRemoteDataSourceImpl @Inject constructor(
+ private val signInService: SignInService,
+ private val memberService: MemberService,
+ private val userLocalDataSource: UserLocalDataSource,
+ private val memberOrgService: MemberOrgService
+) : UserRemoteDataSource{
+ override suspend fun signIn(idToken: String): JwtToken {
+ val response = signInService.signIn(SignInRequest(idToken))
+ if (response.getOrNull() != null) {
+ userLocalDataSource.saveRole(response.getOrNull()?.data?.role ?: "error")
+ }
+ return response.getOrThrow().data.toModel()
+ }
+
+ override suspend fun signUp(
+ file: File?,
+ idToken: String,
+ defaultImage: Int,
+ role: String
+ ): JwtToken {
+ val data = HashMap()
+
+ var multipartBody : MultipartBody.Part? = null
+
+ if (file != null) {
+ val image = file.asRequestBody("image/*".toMediaTypeOrNull())
+ multipartBody = MultipartBody.Part.createFormData("file", file.name, image)
+ }
+
+ val _idToken = RequestBody.create("text/plain".toMediaTypeOrNull(), idToken)
+ val _defaultImage = RequestBody.create("text/plain".toMediaTypeOrNull(), defaultImage.toString())
+ val _role = RequestBody.create("text/plain".toMediaTypeOrNull(), role)
+
+ data["idToken"] = _idToken
+ data["defaultImage"] = _defaultImage
+ data["role"] = _role
+
+ val response = signInService.signUp(multipartBody, data)
+ if (response.getOrNull() != null) {
+ userLocalDataSource.saveRole(response.getOrNull()?.data?.role ?: "error")
+ }
+ return response.getOrThrow().data.toModel()
+ }
+
+ override suspend fun getMemberInfo(): UserInfo {
+ val response = memberService.getMemberInfo()
+ if (response.getOrNull() != null) {
+ userLocalDataSource.saveUserId(response.getOrNull()?.data?.id ?: 0)
+ userLocalDataSource.saveUserName(response.getOrNull()?.data?.name ?: "")
+ userLocalDataSource.saveUserProfileImage(response.getOrNull()?.data?.photo ?: "")
+ userLocalDataSource.saveUserInfo(
+ response.getOrNull()?.data?.id ?: 0,
+ response.getOrNull()?.data?.name ?: "",
+ response.getOrNull()?.data?.email ?: "",
+ response.getOrNull()?.data?.photo ?: "",
+ response.getOrNull()?.data?.role ?: ""
+ )
+ }
+ 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()
+
+ var multipartBody : MultipartBody.Part? = null
+
+ if (file != null) {
+ val image = file.asRequestBody("image/*".toMediaTypeOrNull())
+ multipartBody = MultipartBody.Part.createFormData("file", file.name, image)
+ }
+
+ val _defaultImage = RequestBody.create("text/plain".toMediaTypeOrNull(), defaultImage.toString())
+ data["defaultImage"] = _defaultImage
+
+ val response = memberService.updateMemberProfilePhoto(multipartBody, data)
+
+ if (response.getOrNull() != null) {
+ userLocalDataSource.saveUserProfileImage(response.getOrNull()?.data?.photoImageUrl ?: "")
+ }
+
+ return response.getOrThrow().data.photoImageUrl
+ }
+
+ override suspend fun updateFCMToken(fcmToken: String) = memberService.updateFCMToken(FcmRequest(fcmToken)).getOrThrow().data
+
+ override suspend fun autoSignIn(): JwtToken {
+ val response = memberService.autoSignIn()
+ if (response.getOrNull() != null) {
+ userLocalDataSource.saveRole(response.getOrNull()?.data?.role ?: "error")
+ }
+ return response.getOrThrow().data.toModel()
+ }
+
+ override suspend fun getStudentHomeInfo(organizationId: Long) = memberOrgService.getStudentHomeInfo(organizationId).getOrThrow().data
+}
diff --git a/android/data/src/main/java/com/sixkids/data/util/LocalDateAdapter.kt b/android/data/src/main/java/com/sixkids/data/util/LocalDateAdapter.kt
new file mode 100644
index 00000000..3e19c346
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/util/LocalDateAdapter.kt
@@ -0,0 +1,22 @@
+package com.sixkids.data.util
+
+import com.squareup.moshi.FromJson
+import com.squareup.moshi.ToJson
+import java.time.LocalDate
+import java.time.format.DateTimeFormatter
+
+class LocalDateAdapter {
+ @ToJson
+ fun toJson(value: LocalDate): String {
+ return FORMATTER.format(value)
+ }
+
+ @FromJson
+ fun fromJson(value: String): LocalDate {
+ return LocalDate.parse(value, FORMATTER)
+ }
+
+ companion object {
+ private val FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE
+ }
+}
\ No newline at end of file
diff --git a/android/data/src/main/java/com/sixkids/data/util/LocalDateTimeAdapter.kt b/android/data/src/main/java/com/sixkids/data/util/LocalDateTimeAdapter.kt
new file mode 100644
index 00000000..a977bc81
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/util/LocalDateTimeAdapter.kt
@@ -0,0 +1,22 @@
+package com.sixkids.data.util
+
+import com.squareup.moshi.FromJson
+import com.squareup.moshi.ToJson
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
+
+class LocalDateTimeAdapter {
+ @ToJson
+ fun toJson(value: LocalDateTime): String {
+ return FORMATTER.format(value)
+ }
+
+ @FromJson
+ fun fromJson(value: String): LocalDateTime {
+ return LocalDateTime.parse(value, FORMATTER)
+ }
+
+ companion object {
+ private val FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME
+ }
+}
diff --git a/android/data/src/main/java/com/sixkids/data/util/UnitJsonAdapter.kt b/android/data/src/main/java/com/sixkids/data/util/UnitJsonAdapter.kt
new file mode 100644
index 00000000..84c0d8de
--- /dev/null
+++ b/android/data/src/main/java/com/sixkids/data/util/UnitJsonAdapter.kt
@@ -0,0 +1,21 @@
+package com.sixkids.data.util
+
+import com.squareup.moshi.FromJson
+import com.squareup.moshi.JsonAdapter
+import com.squareup.moshi.JsonReader
+import com.squareup.moshi.JsonWriter
+import com.squareup.moshi.ToJson
+
+class UnitJsonAdapter : JsonAdapter() {
+ @ToJson
+ override fun toJson(writer: JsonWriter, value: Unit?) {
+ writer.nullValue()
+ }
+
+ @FromJson
+ override fun fromJson(reader: JsonReader): Unit? {
+ reader.skipValue()
+ return Unit
+ }
+
+}
diff --git a/android/domain/build.gradle.kts b/android/domain/build.gradle.kts
index 10600601..af30b201 100644
--- a/android/domain/build.gradle.kts
+++ b/android/domain/build.gradle.kts
@@ -4,4 +4,6 @@ plugins {
dependencies {
implementation(projects.core.model)
implementation(libs.javax.inject)
+ implementation(libs.paging.common)
+ implementation(libs.kotlinx.coroutines.core)
}
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
new file mode 100644
index 00000000..9df6f72b
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/repository/ChallengeRepository.kt
@@ -0,0 +1,41 @@
+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
+
+interface ChallengeRepository {
+ suspend fun getChallengeHistory(
+ organizationId: Int,
+ memberId: Int?
+ ): Flow>
+
+ 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,
+ 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
new file mode 100644
index 00000000..c0d2ffe7
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/repository/OrganizationRepository.kt
@@ -0,0 +1,42 @@
+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
+
+ suspend fun saveSelectedOrganizationId(organizationId: Int)
+ 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/TokenRepository.kt b/android/domain/src/main/java/com/sixkids/domain/repository/TokenRepository.kt
index 8288c3b3..cafc174b 100644
--- a/android/domain/src/main/java/com/sixkids/domain/repository/TokenRepository.kt
+++ b/android/domain/src/main/java/com/sixkids/domain/repository/TokenRepository.kt
@@ -5,5 +5,8 @@ interface TokenRepository {
suspend fun saveAccessToken(token: String)
suspend fun getRefreshToken(): String
suspend fun saveRefreshToken(token: String)
+ suspend fun saveIdToken(token: String)
+ suspend fun getIdToken(): String
+ suspend fun deleteIdToken()
suspend fun clearTokens()
}
\ 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
new file mode 100644
index 00000000..23418399
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/repository/UserRepository.kt
@@ -0,0 +1,31 @@
+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
+
+interface UserRepository {
+ suspend fun signIn(idToken: String): JwtToken
+
+ suspend fun signUp(file: File?, defaultImage: Int, role: String): JwtToken
+
+ suspend fun getRole(): String
+
+ suspend fun getMemberInfo(): UserInfo
+
+ suspend fun getMemberSimpleInfo(id: Long): MemberSimple
+
+ suspend fun updateMemberProfilePhoto(file: File?, defaultImage: Int): String
+
+ suspend fun signOut() : Boolean
+
+ 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
new file mode 100644
index 00000000..242f9455
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/challenge/CreateChallengeUseCase.kt
@@ -0,0 +1,32 @@
+package com.sixkids.domain.usecase.challenge
+
+import com.sixkids.domain.repository.ChallengeRepository
+import com.sixkids.model.GroupSimple
+import java.time.LocalDateTime
+import javax.inject.Inject
+
+class CreateChallengeUseCase @Inject constructor(
+ private val challengeRepository: ChallengeRepository
+) {
+ suspend operator fun invoke(
+ organizationId: Int,
+ title: String,
+ content: String,
+ startTime: LocalDateTime,
+ endTime: LocalDateTime,
+ reward: Int,
+ minCount: Int,
+ groups: List = emptyList(),
+ ) = runCatching {
+ challengeRepository.createChallenge(
+ organizationId = organizationId,
+ title = title,
+ content = content,
+ startTime = startTime,
+ endTime = endTime,
+ 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/GetChallengeHistoryUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/challenge/GetChallengeHistoryUseCase.kt
new file mode 100644
index 00000000..9caeb373
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/challenge/GetChallengeHistoryUseCase.kt
@@ -0,0 +1,13 @@
+package com.sixkids.domain.usecase.challenge
+
+import com.sixkids.domain.repository.ChallengeRepository
+import javax.inject.Inject
+
+class GetChallengeHistoryUseCase @Inject constructor(
+ private val challengeRepository: ChallengeRepository
+) {
+ suspend operator fun invoke(
+ organizationId: Int,
+ memberId: Int? = null
+ ) = challengeRepository.getChallengeHistory(organizationId, memberId)
+}
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/GetRunningChallengeUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/challenge/GetRunningChallengeUseCase.kt
new file mode 100644
index 00000000..194f1bd2
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/challenge/GetRunningChallengeUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.challenge
+
+import com.sixkids.domain.repository.ChallengeRepository
+import javax.inject.Inject
+
+class GetRunningChallengeUseCase @Inject constructor(
+ private val challengeRepository: ChallengeRepository
+) {
+ suspend operator fun invoke(organizationId: Int) = runCatching {
+ challengeRepository.getRunningChallenge(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/GetOrganizationListUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/organization/GetOrganizationListUseCase.kt
new file mode 100644
index 00000000..829467dd
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/organization/GetOrganizationListUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.organization
+
+import com.sixkids.domain.repository.OrganizationRepository
+import javax.inject.Inject
+
+class GetOrganizationListUseCase @Inject constructor(
+ private val organizationRepository: OrganizationRepository
+){
+ suspend operator fun invoke() = runCatching {
+ organizationRepository.getClassList()
+ }
+}
\ 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
new file mode 100644
index 00000000..063c4209
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/organization/GetSelectedOrganizationIdUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.organization
+
+import com.sixkids.domain.repository.OrganizationRepository
+import javax.inject.Inject
+
+class GetSelectedOrganizationIdUseCase @Inject constructor(
+ private val organizationRepository: OrganizationRepository
+) {
+ suspend operator fun invoke() = runCatching {
+ organizationRepository.getSelectedOrganizationId()
+ }
+}
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/NewOrganizationUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/organization/NewOrganizationUseCase.kt
new file mode 100644
index 00000000..afb88a90
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/organization/NewOrganizationUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.organization
+
+import com.sixkids.domain.repository.OrganizationRepository
+import javax.inject.Inject
+
+class NewOrganizationUseCase @Inject constructor(
+ private val organizationRepository: OrganizationRepository
+) {
+ suspend operator fun invoke(name: String) = runCatching {
+ organizationRepository.newOrganization(name)
+ }
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/organization/SaveSelectedOrganizationIdUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/organization/SaveSelectedOrganizationIdUseCase.kt
new file mode 100644
index 00000000..abd52af8
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/organization/SaveSelectedOrganizationIdUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.organization
+
+import com.sixkids.domain.repository.OrganizationRepository
+import javax.inject.Inject
+
+class SaveSelectedOrganizationIdUseCase @Inject constructor(
+ private val organizationRepository: OrganizationRepository
+){
+ suspend operator fun invoke(organizationId: Int) {
+ organizationRepository.saveSelectedOrganizationId(organizationId)
+ }
+}
\ 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/AutoSignInUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/user/AutoSignInUseCase.kt
new file mode 100644
index 00000000..bff34251
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/user/AutoSignInUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.user
+
+import com.sixkids.domain.repository.UserRepository
+import javax.inject.Inject
+
+class AutoSignInUseCase @Inject constructor(
+ private val userRepository: UserRepository
+){
+ suspend operator fun invoke() = runCatching {
+ userRepository.autoSignIn()
+ }
+}
\ 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/GetRoleUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/user/GetRoleUseCase.kt
new file mode 100644
index 00000000..de284de9
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/user/GetRoleUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.user
+
+import com.sixkids.domain.repository.UserRepository
+import javax.inject.Inject
+
+class GetRoleUseCase @Inject constructor(
+ private val userRepository: UserRepository
+) {
+ suspend operator fun invoke() = runCatching {
+ userRepository.getRole()
+ }
+}
\ No newline at end of file
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/GetUserInfoUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/user/GetUserInfoUseCase.kt
new file mode 100644
index 00000000..bf468f72
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/user/GetUserInfoUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.user
+
+import com.sixkids.domain.repository.UserRepository
+import javax.inject.Inject
+
+class GetUserInfoUseCase @Inject constructor(
+ private val userRepository: UserRepository
+){
+ suspend operator fun invoke() = runCatching {
+ userRepository.getMemberInfo()
+ }
+}
\ 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/domain/src/main/java/com/sixkids/domain/usecase/user/SignInUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/user/SignInUseCase.kt
new file mode 100644
index 00000000..257e035b
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/user/SignInUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.user
+
+import com.sixkids.domain.repository.UserRepository
+import javax.inject.Inject
+
+class SignInUseCase @Inject constructor(
+ private val userRepository: UserRepository
+){
+ suspend operator fun invoke(idToken: String) = runCatching {
+ userRepository.signIn(idToken)
+ }
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/user/SignOutUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/user/SignOutUseCase.kt
new file mode 100644
index 00000000..7f41c1fa
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/user/SignOutUseCase.kt
@@ -0,0 +1,10 @@
+package com.sixkids.domain.usecase.user
+
+import com.sixkids.domain.repository.UserRepository
+import javax.inject.Inject
+
+class SignOutUseCase @Inject constructor(
+ private val userRepository: UserRepository
+){
+ suspend operator fun invoke() = userRepository.signOut()
+}
\ No newline at end of file
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/user/SignUpUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/user/SignUpUseCase.kt
new file mode 100644
index 00000000..4af74dea
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/user/SignUpUseCase.kt
@@ -0,0 +1,13 @@
+package com.sixkids.domain.usecase.user
+
+import com.sixkids.domain.repository.UserRepository
+import java.io.File
+import javax.inject.Inject
+
+class SignUpUseCase @Inject constructor(
+ private val userRepository: UserRepository
+){
+ suspend operator fun invoke(file: File?, defaultImage: Int, role: String) = runCatching {
+ userRepository.signUp(file, defaultImage, role)
+ }
+}
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/user/UpdateFCMTokenUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/user/UpdateFCMTokenUseCase.kt
new file mode 100644
index 00000000..c5ec4dfd
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/user/UpdateFCMTokenUseCase.kt
@@ -0,0 +1,12 @@
+package com.sixkids.domain.usecase.user
+
+import com.sixkids.domain.repository.UserRepository
+import javax.inject.Inject
+
+class UpdateFCMTokenUseCase @Inject constructor(
+ private val userRepository: UserRepository
+) {
+ suspend operator fun invoke(token: String) = runCatching {
+ userRepository.updateFCMToken(token)
+ }
+}
diff --git a/android/domain/src/main/java/com/sixkids/domain/usecase/user/UpdateUserProfilePhotoUseCase.kt b/android/domain/src/main/java/com/sixkids/domain/usecase/user/UpdateUserProfilePhotoUseCase.kt
new file mode 100644
index 00000000..fef65e83
--- /dev/null
+++ b/android/domain/src/main/java/com/sixkids/domain/usecase/user/UpdateUserProfilePhotoUseCase.kt
@@ -0,0 +1,13 @@
+package com.sixkids.domain.usecase.user
+
+import com.sixkids.domain.repository.UserRepository
+import java.io.File
+import javax.inject.Inject
+
+class UpdateUserProfilePhotoUseCase @Inject constructor(
+ private val userRepository: UserRepository
+){
+ suspend operator fun invoke(file: File?, defaultImage: Int) = runCatching {
+ userRepository.updateMemberProfilePhoto(file, defaultImage)
+ }
+}
\ No newline at end of file
diff --git a/android/feature/navigator/build.gradle.kts b/android/feature/navigator/build.gradle.kts
index b269b498..4f4dc89b 100644
--- a/android/feature/navigator/build.gradle.kts
+++ b/android/feature/navigator/build.gradle.kts
@@ -9,4 +9,17 @@ android {
dependencies {
implementation(projects.feature.teacher.home)
implementation(projects.feature.teacher.board)
+ implementation(projects.feature.teacher.manageclass)
+ implementation(projects.feature.teacher.challenge)
+ 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/AndroidManifest.xml b/android/feature/navigator/src/main/AndroidManifest.xml
index 999fd910..42e0c67a 100644
--- a/android/feature/navigator/src/main/AndroidManifest.xml
+++ b/android/feature/navigator/src/main/AndroidManifest.xml
@@ -1,9 +1,13 @@
+
+
-
+ android:theme="@style/Theme.Ulban"
+ android:windowSoftInputMode="adjustResize">
diff --git a/android/feature/navigator/src/main/java/com/sixkids/feature/navigator/MainActivity.kt b/android/feature/navigator/src/main/java/com/sixkids/feature/navigator/MainActivity.kt
index 2f0bfd1b..cc0713e9 100644
--- a/android/feature/navigator/src/main/java/com/sixkids/feature/navigator/MainActivity.kt
+++ b/android/feature/navigator/src/main/java/com/sixkids/feature/navigator/MainActivity.kt
@@ -12,8 +12,8 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
setContent {
UlbanTheme {
- MainScreen()
+ MainScreen()
}
}
}
-}
\ No newline at end of file
+}
diff --git a/android/feature/navigator/src/main/java/com/sixkids/feature/navigator/MainContract.kt b/android/feature/navigator/src/main/java/com/sixkids/feature/navigator/MainContract.kt
new file mode 100644
index 00000000..77e5c589
--- /dev/null
+++ b/android/feature/navigator/src/main/java/com/sixkids/feature/navigator/MainContract.kt
@@ -0,0 +1,14 @@
+package com.sixkids.feature.navigator
+
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
+data class MainState(
+ val snackbarToken: SnackbarToken = SnackbarToken(),
+ val snackbarVisible: Boolean = false,
+) : UiState
+
+sealed interface MainSideEffect : SideEffect {
+ data object ShowSnackbar : MainSideEffect
+}
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 8dcb4c0d..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,8 +8,14 @@ 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
+import com.sixkids.teacher.managestudent.navigation.ManageStudentRoute
enum class MainNavigationTab(
@@ -25,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,
@@ -34,13 +40,37 @@ enum class MainNavigationTab(
iconId = R.drawable.manage_student,
iconTint = Purple,
labelId = R.string.bottom_navigation_tab_label_manage_student,
- route = "StatisticsRoute.route",
+ route = ManageStudentRoute.defaultRoute
),
MANAGE_CLASS(
iconId = R.drawable.manage_class,
iconTint = Green,
labelId = R.string.bottom_navigation_tab_label_manage_class,
- route = "CommunityRoute.route",
+ 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,
)
;
@@ -53,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 2bb18042..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
@@ -8,15 +10,77 @@ import androidx.navigation.NavOptions
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navOptions
+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
+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,
) {
- val startDestination = HomeRoute.defaultRoute
+ var bottomTabItems: State>? = null
+ val startDestination = SignInRoute.defaultRoute
private val currentDestination: NavDestination?
@Composable get() = navController
.currentBackStackEntryAsState().value?.destination
@@ -26,7 +90,7 @@ class MainNavigator(
?.let { MainNavigationTab.find(it) }
fun navigate(tab: MainNavigationTab) {
- val navOptions = navOptions {
+ val teacherNavOptions = navOptions {
popUpTo(HomeRoute.defaultRoute) {
saveState = true
}
@@ -34,29 +98,328 @@ 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 -> {}
- MainNavigationTab.MANAGE_CLASS -> {}
+ // 선생님 바텀 네비게이션 탭
+ 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)
+ }
+ }
+
+ /**
+ * 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(navOptions: NavOptions) {
- navController.navigate(HomeRoute.defaultRoute){
- popUpTo(navController.graph.id){
+ fun navigateHome() {
+ bottomTabItems = teacherTab() // 바텀 네비게이션 탭 초기화
+ navController.navigate(HomeRoute.defaultRoute)
+ }
+
+ fun navigateRank() {
+ navController.navigateRank()
+ }
+
+ /**
+ * Manage Class Navigation
+ */
+ fun navigateManageClass(navOptions: NavOptions) {
+ navController.navigate(ManageClassRoute.defaultRoute) {
+ popUpTo(navController.graph.id) {
inclusive = true
}
}
}
- fun navigateRank(){
+ 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) {
+ 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() {
+ navController.navigateSignIn()
+ }
+
+ fun navigateSignUp() {
+ navController.navigateSignUp()
+ }
+
+ 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: Long, groupId: Long?) {
+ navController.navigateChallengeDetail(challengeId, groupId)
+ }
+
+ fun navigatePopupToHistory() {
+ navController.navigatePopupToHistory()
+ }
+
+ fun navigateCreateChallenge() {
+ navController.navigateCreateChallenge()
+ }
+
+ 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() {
+ navController.navigateNewOrganization()
+ }
+
+ 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
@@ -69,4 +432,26 @@ internal fun rememberMainNavigator(
navController: NavHostController = rememberNavController(),
): MainNavigator = remember(navController) {
MainNavigator(navController)
-}
\ No newline at end of file
+}
+
+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 5bdde665..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
@@ -1,13 +1,15 @@
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
import androidx.compose.animation.slideOut
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
@@ -17,6 +19,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -24,17 +27,51 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.IntOffset
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.NavHost
+import com.google.accompanist.permissions.ExperimentalPermissionsApi
+import com.google.accompanist.permissions.isGranted
+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
fun MainScreen(
modifier: Modifier = Modifier,
- // TODO viewmodel: MainViewModel,
+ viewModel: MainViewModel = hiltViewModel(),
navigator: MainNavigator = rememberMainNavigator()
) {
+
+ val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
+
+ viewModel.sideEffect.collectWithLifecycle {
+ when (it) {
+ is MainSideEffect.ShowSnackbar -> {
+ viewModel.onShowSnackbar(uiState.snackbarToken)
+ }
+ }
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ RequestNotificationPermission()
+ }
+
Scaffold(
bottomBar = {
BottomNav(
@@ -42,23 +79,159 @@ 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(
+ navigateChallengeDetail = navigator::navigateChallengeDetail,
+ navigateCreateChallenge = navigator::navigateCreateChallenge,
+ navigateChallengeCreatedResult = navigator::navigateChallengeCreatedResult,
+ navigateChallengeHistory = navigator::navigatePopupToHistory,
+ handleException = viewModel::handleException,
+ showSnackbar = viewModel::onShowSnackbar,
+ navigateUp = navigator::popBackStack,
+ )
+
+ 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(
+ navigateToSignUp = navigator::navigateSignUp,
+ navigateSignUpPhoto = navigator::navigateSignUpPhoto,
+ navigateToHome = navigator::navigateHome,
+ onShowSnackBar = viewModel::onShowSnackbar,
+ onBackClick = navigator::popBackStack,
+ navigateToTeacherOrganizationList = navigator::navigateTeacherOrganizationList,
+ navigateToStudentOrganizationList = navigator::navigateStudentOrganizationList,
+ )
+
+ teacherOrganizationListNavGraph(
+ navigateToNewOrganization = navigator::navigateNewOrganization,
+ navigateToProfile = navigator::navigateProfile,
+ navigateToHome = navigator::navigateHome,
+ onShowSnackBar = viewModel::onShowSnackbar,
+ onBackClick = navigator::popBackStack,
+ 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(
+ modifier = Modifier.padding(innerPadding),
+ visible = snackbarVisible,
+ message = snackbarToken.message,
+ actionIconId = snackbarToken.actionIcon,
+ actionButtonText = snackbarToken.actionButtonText,
+ onClickActionButton = snackbarToken.onClickActionButton,
)
}
}
@@ -71,6 +244,7 @@ fun BottomNav(
modifier: Modifier,
itemClick: (MainNavigationTab) -> Unit = {},
selectedTab: MainNavigationTab,
+ bottomTavItems: List? = null
) {
val selectedItem = rememberUpdatedState(newValue = selectedTab)
@@ -88,7 +262,7 @@ fun BottomNav(
modifier = modifier,
containerColor = Cream,
) {
- MainNavigationTab.entries.forEach { item ->
+ bottomTavItems?.forEach { item ->
NavigationBarItem(
icon = {
Icon(
@@ -113,4 +287,20 @@ fun BottomNav(
}
}
}
-}
\ No newline at end of file
+}
+
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+@OptIn(ExperimentalPermissionsApi::class)
+@Composable
+fun RequestNotificationPermission() {
+ val notificationPermissionState = rememberPermissionState(
+ permission = android.Manifest.permission.POST_NOTIFICATIONS
+ )
+
+ LaunchedEffect(Unit) {
+ 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
new file mode 100644
index 00000000..745bf33a
--- /dev/null
+++ b/android/feature/navigator/src/main/java/com/sixkids/feature/navigator/MainViewModel.kt
@@ -0,0 +1,47 @@
+package com.sixkids.feature.navigator
+
+import androidx.lifecycle.viewModelScope
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.base.BaseViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import javax.inject.Inject
+
+@HiltViewModel
+class MainViewModel @Inject constructor(
+) : BaseViewModel(MainState()) {
+ private val mutex = Mutex()
+
+ fun onShowSnackbar(snackbarToken: SnackbarToken) {
+ viewModelScope.launch {
+ mutex.withLock {
+ intent { copy(snackbarToken = snackbarToken, snackbarVisible = true) }
+ delay(SHOW_TOAST_LENGTH)
+ intent { copy(snackbarVisible = false) }
+ }
+ }
+ }
+
+ fun onCloseSnackbar() {
+ viewModelScope.launch {
+ intent { copy(snackbarVisible = false) }
+ }
+ }
+
+ fun handleException(throwable: Throwable, retry: () -> Unit) {
+ onShowSnackbar(
+ SnackbarToken(
+ message = throwable.message ?: "알 수 없는 에러 입니다.",
+ actionButtonText = "재시도",
+ onClickActionButton = retry
+ )
+ )
+ }
+
+ companion object {
+ private const val SHOW_TOAST_LENGTH = 2000L
+ }
+}
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/build.gradle.kts b/android/feature/signin/build.gradle.kts
index f285a3fd..8f8d3730 100644
--- a/android/feature/signin/build.gradle.kts
+++ b/android/feature/signin/build.gradle.kts
@@ -7,4 +7,6 @@ android {
}
dependencies {
+ implementation(libs.kakao.user)
+
}
diff --git a/android/feature/signin/src/main/AndroidManifest.xml b/android/feature/signin/src/main/AndroidManifest.xml
index a5918e68..cecf573e 100644
--- a/android/feature/signin/src/main/AndroidManifest.xml
+++ b/android/feature/signin/src/main/AndroidManifest.xml
@@ -1,4 +1,5 @@
-
+
+
\ No newline at end of file
diff --git a/android/feature/signin/src/main/java/com/sixkids/feature/signin/login/KakaoManager.kt b/android/feature/signin/src/main/java/com/sixkids/feature/signin/login/KakaoManager.kt
new file mode 100644
index 00000000..a2e36643
--- /dev/null
+++ b/android/feature/signin/src/main/java/com/sixkids/feature/signin/login/KakaoManager.kt
@@ -0,0 +1,69 @@
+package com.sixkids.feature.signin.login
+
+import android.content.Context
+import android.util.Log
+import com.kakao.sdk.common.model.AuthError
+import com.kakao.sdk.common.model.ClientError
+import com.kakao.sdk.common.model.ClientErrorCause
+import com.kakao.sdk.user.UserApiClient
+
+private const val TAG = "D107"
+
+object KakaoManager {
+ fun login(
+ context: Context,
+ onSuccess: (String) -> Unit,
+ onFailed: (Throwable?) -> Unit,
+ ) {
+ if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) {
+ loginWithKakaoTalk(
+ onSuccess = onSuccess,
+ onFailed = { error ->
+ if (error is AuthError || (error is ClientError && error.reason != ClientErrorCause.Cancelled)) {
+ loginWithKakaoAccount(
+ onSuccess = onSuccess,
+ onFailed = onFailed,
+ context = context,
+ )
+ }
+ },
+ context = context,
+ )
+ } else {
+ loginWithKakaoAccount(
+ onSuccess = onSuccess,
+ onFailed = onFailed,
+ context = context,
+ )
+ }
+ }
+
+ private fun loginWithKakaoTalk(
+ context: Context,
+ onSuccess: (String) -> Unit,
+ onFailed: (Throwable?) -> Unit,
+ ) {
+ UserApiClient.instance.loginWithKakaoTalk(context) { token, error ->
+ Log.d(TAG, "loginWithKakaoTalk:$token")
+ if (token != null && token.idToken != null){
+ onSuccess(token.idToken!!)
+ } else {
+ onFailed(error)
+ }
+ }
+ }
+
+ private fun loginWithKakaoAccount(
+ context: Context,
+ onSuccess: (String) -> Unit,
+ onFailed: (Throwable?) -> Unit,
+ ) {
+ UserApiClient.instance.loginWithKakaoAccount(context) { token, error ->
+ if (token != null && token.idToken != null){
+ onSuccess(token.idToken!!)
+ } else {
+ onFailed(error)
+ }
+ }
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..82cac746
--- /dev/null
+++ b/android/feature/signin/src/main/java/com/sixkids/feature/signin/login/LoginContract.kt
@@ -0,0 +1,17 @@
+package com.sixkids.feature.signin.login
+
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
+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
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
new file mode 100644
index 00000000..dfbe44ce
--- /dev/null
+++ b/android/feature/signin/src/main/java/com/sixkids/feature/signin/login/LoginScreen.kt
@@ -0,0 +1,267 @@
+package com.sixkids.feature.signin.login
+
+import android.util.Log
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.slideInHorizontally
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.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.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.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.draw.rotate
+import androidx.compose.ui.graphics.Color
+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.TextAlign
+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 coil.compose.AsyncImage
+import com.sixkids.designsystem.component.screen.LoadingScreen
+import com.sixkids.designsystem.theme.UlbanTheme
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.signin.R
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.extension.collectWithLifecycle
+import kotlinx.coroutines.delay
+
+private const val TAG = "HONG"
+@Composable
+fun LoginRoute(
+ viewModel: LoginViewModel = hiltViewModel(),
+ navigateToHome: () -> Unit,
+ navigateToSignUp: () -> Unit,
+ navigateToTeacherOrganizationList: () -> Unit,
+ navigateToStudentOrganizationList: () -> Unit,
+ onShowSnackBar: (SnackbarToken) -> Unit
+) {
+ val context = LocalContext.current
+
+ val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
+
+ LaunchedEffect(key1 = Unit) {
+ viewModel.autoSignIn()
+ }
+
+ viewModel.sideEffect.collectWithLifecycle { sideEffect ->
+ when (sideEffect) {
+ is LoginEffect.NavigateToSignUp -> navigateToSignUp()
+ LoginEffect.NavigateToHome -> navigateToHome()
+ is LoginEffect.OnShowSnackBar -> onShowSnackBar(sideEffect.tkn)
+ LoginEffect.NavigateToTeacherOrganizationList -> navigateToTeacherOrganizationList()
+ LoginEffect.NavigateToStudentOrganizationList -> navigateToStudentOrganizationList()
+ }
+ }
+
+ LoginScreen(
+ uiState = uiState,
+ onLoginClick = {
+ KakaoManager.login(
+ context = context,
+ onSuccess = { idToken ->
+ viewModel.login(idToken)
+ },
+ onFailed = {
+ Log.d(TAG, "카카오 에러!")
+ }
+ )
+ }
+
+ )
+}
+
+@Composable
+fun LoginScreen(
+ uiState: LoginState = LoginState(),
+ onLoginClick: () -> Unit = {},
+) {
+ var animationStart by remember {
+ mutableStateOf(false)
+ }
+ LaunchedEffect(Unit) {
+ delay(300)
+ animationStart = true
+ }
+
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.White)
+ ) {
+ Image(
+ painter = painterResource(id = R.drawable.main_background),
+ contentDescription = "Home Background",
+ alignment = Alignment.TopCenter,
+ contentScale = ContentScale.Crop,
+ modifier = Modifier
+ .fillMaxWidth()
+ )
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .align(Alignment.TopCenter)
+ .fillMaxWidth()
+ .padding(0.dp, 20.dp, 0.dp, 0.dp)
+ ) {
+ AnimatedVisibility(
+ visible = animationStart,
+ enter = slideInHorizontally(
+ initialOffsetX = { -it },
+ animationSpec = tween(1000)
+ ),
+ modifier = Modifier.weight(1f)
+ ) {
+ Column(
+ horizontalAlignment = Alignment.Start,
+ ) {
+ AsyncImage(
+ model = com.sixkids.designsystem.R.drawable.hifive,
+ contentDescription = "Rocket",
+ modifier = Modifier.fillMaxWidth(),
+
+ )
+
+ AsyncImage(
+ model = com.sixkids.designsystem.R.drawable.paint,
+ contentDescription = "Paint",
+ modifier = Modifier
+ .padding(50.dp, 0.dp, 0.dp, 0.dp)
+ .rotate(15f)
+ )
+ }
+ }
+
+ AnimatedVisibility(
+ visible = animationStart,
+ enter = slideInHorizontally(
+ initialOffsetX = { it },
+ animationSpec = tween(1000)
+ ),
+ modifier = Modifier
+ .weight(1f)
+ .align(Alignment.Top),
+ ) {
+ AsyncImage(
+ model = com.sixkids.designsystem.R.drawable.pencil,
+ contentDescription = "Pencil",
+ contentScale = ContentScale.FillHeight,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(280.dp)
+ .rotate(18f)
+ )
+ }
+ }
+
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.SpaceBetween,
+ modifier = Modifier
+ .fillMaxWidth()
+ ) {
+ Spacer(modifier = Modifier.weight(1f))
+
+ Text(
+ text = stringResource(id = R.string.signin_subtitle),
+ style = UlbanTypography.bodyMedium.copy(
+ fontSize = 16.sp
+ ),
+ modifier = Modifier
+ .align(Alignment.End)
+ .padding(0.dp, 100.dp, 20.dp, 0.dp)
+ )
+
+ Spacer(modifier = Modifier.height(5.dp))
+
+ Text(
+ text = stringResource(id = R.string.signin_title),
+ style = UlbanTypography.titleLarge.copy(
+ fontSize = 48.sp
+ ),
+ modifier = Modifier
+ .align(Alignment.End)
+ .padding(0.dp, 0.dp, 20.dp, 0.dp)
+ )
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ LogInButton(onLoginClick)
+ }
+
+ if (uiState.isLoading){
+ LoadingScreen()
+ }
+ }
+}
+
+@Composable
+fun LogInButton(
+ onClick: () -> Unit,
+){
+ Button(
+ onClick = { onClick() },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(10.dp, 40.dp)
+ .height(50.dp),
+ shape = RoundedCornerShape(12.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Color(0xFFFEE500)
+ )
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center
+ ) {
+ Image(
+ painter = painterResource(id = R.drawable.kakao),
+ contentDescription = "Icon",
+ modifier = Modifier.size(24.dp)
+ )
+ Spacer(Modifier.width(8.dp))
+ Text(
+ text = stringResource(id = R.string.signin_kakao),
+ style = UlbanTypography.bodyMedium.copy(),
+ color = Color.Black,
+ textAlign = TextAlign.Center
+ )
+ }
+ }
+}
+
+
+@Preview(showBackground = true)
+@Composable
+fun LoginScreenPreview() {
+ UlbanTheme {
+ LoginScreen()
+ }
+
+}
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
new file mode 100644
index 00000000..47a450ec
--- /dev/null
+++ b/android/feature/signin/src/main/java/com/sixkids/feature/signin/login/LoginViewModel.kt
@@ -0,0 +1,72 @@
+package com.sixkids.feature.signin.login
+
+import android.util.Log
+import androidx.lifecycle.viewModelScope
+import com.sixkids.domain.usecase.user.AutoSignInUseCase
+import com.sixkids.domain.usecase.user.GetRoleUseCase
+import com.sixkids.domain.usecase.user.SignInUseCase
+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
+
+private const val TAG = "D107"
+@HiltViewModel
+class LoginViewModel @Inject constructor(
+ private val autoSignInUseCase: AutoSignInUseCase,
+ private val signInUseCase: SignInUseCase,
+ private val getRoleUseCase: GetRoleUseCase
+) : BaseViewModel(LoginState()){
+
+ fun autoSignIn(){
+ viewModelScope.launch {
+ autoSignInUseCase()
+ .onSuccess {
+ getRoleUseCase()
+ .onSuccess {
+ when(it){
+ "TEACHER" -> postSideEffect(LoginEffect.NavigateToTeacherOrganizationList)
+ "STUDENT" -> {
+ Log.d(TAG, "autoSignIn: STUDENT")
+ postSideEffect(LoginEffect.NavigateToStudentOrganizationList)}
+ }
+ }.onFailure {
+ Log.d(TAG, "autoSignIn: ${it.message}")
+ }
+ }.onFailure {
+ Log.d(TAG, "autoSignIn: ${it.message}")
+ }
+ }
+ }
+
+ fun login(idToken: String){
+ viewModelScope.launch {
+ intent { copy(isLoading = true)}
+ signInUseCase(idToken)
+ .onSuccess {
+ getRoleUseCase()
+ .onSuccess {
+ when(it){
+ "TEACHER" -> postSideEffect(LoginEffect.NavigateToTeacherOrganizationList)
+ "STUDENT" -> {postSideEffect(LoginEffect.NavigateToStudentOrganizationList)}
+ }
+ }.onFailure {
+ postSideEffect(LoginEffect.OnShowSnackBar(SnackbarToken("로그인에 실패했습니다")))
+ }
+ }.onFailure {
+ when(it){
+ is NotFoundException -> {
+ postSideEffect(LoginEffect.OnShowSnackBar(SnackbarToken("회원가입을 진행 해주세요")))
+ postSideEffect(LoginEffect.NavigateToSignUp)
+ }
+ else -> {
+ postSideEffect(LoginEffect.OnShowSnackBar(SnackbarToken(it.message?:"로그인에 실패했습니다")))
+ }
+ }
+ }
+ intent { copy(isLoading = false)}
+ }
+ }
+}
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
new file mode 100644
index 00000000..fa543d6b
--- /dev/null
+++ b/android/feature/signin/src/main/java/com/sixkids/feature/signin/navigation/SignInNavigation.kt
@@ -0,0 +1,77 @@
+package com.sixkids.feature.signin.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.feature.signin.login.LoginRoute
+import com.sixkids.feature.signin.signup.SignUpPhotoRoute
+import com.sixkids.feature.signin.signup.SignUpRoute
+import com.sixkids.ui.SnackbarToken
+
+
+fun NavController.navigateSignIn() {
+ navigate(SignInRoute.defaultRoute)
+}
+
+fun NavController.navigateSignUp() {
+ navigate(SignInRoute.signUpRoute)
+}
+
+fun NavController.navigateSignUpPhoto(isTeacher: Boolean) {
+ navigate(SignInRoute.signUpPhotoRoute(isTeacher))
+}
+
+
+fun NavGraphBuilder.signInNavGraph(
+ navigateToSignUp: () -> Unit,
+ navigateSignUpPhoto: (Boolean) -> Unit,
+ navigateToHome: () -> Unit,
+ navigateToTeacherOrganizationList: () -> Unit,
+ navigateToStudentOrganizationList: () -> Unit,
+ onShowSnackBar: (SnackbarToken) -> Unit,
+ onBackClick : () -> Unit
+) {
+ composable(route = SignInRoute.defaultRoute){
+ LoginRoute(
+ navigateToSignUp = navigateToSignUp,
+ navigateToHome = navigateToHome,
+ onShowSnackBar = onShowSnackBar,
+ navigateToTeacherOrganizationList = navigateToTeacherOrganizationList,
+ navigateToStudentOrganizationList = navigateToStudentOrganizationList
+ )
+ }
+
+ composable(route = SignInRoute.signUpRoute){
+ SignUpRoute(
+ navigateToSignUpPhoto = { isTeacher ->
+ navigateSignUpPhoto(isTeacher)
+ },
+ onBackClick = onBackClick
+ )
+ }
+
+ composable(
+ route = SignInRoute.signUpPhotoRoute,
+ arguments = listOf(navArgument(SignInRoute.SIGN_UP_TEACHER) { type = NavType.BoolType })
+ ){
+ SignUpPhotoRoute(
+ onShowSnackBar = onShowSnackBar,
+ navigateToTeacherOrganizationList = navigateToTeacherOrganizationList,
+ navigateToStudentOrganizationList = navigateToStudentOrganizationList,
+ onBackClick = onBackClick
+ )
+ }
+}
+
+object SignInRoute{
+ const val SIGN_UP_TEACHER = "isTeacher"
+
+ const val defaultRoute = "signIn"
+ const val signUpRoute = "signUp"
+ const val signUpPhotoRoute = "sign-up-photo/{$SIGN_UP_TEACHER}"
+
+ fun signUpPhotoRoute(isTeacher: Boolean) = "sign-up-photo/$isTeacher"
+
+}
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
new file mode 100644
index 00000000..7b0722e2
--- /dev/null
+++ b/android/feature/signin/src/main/java/com/sixkids/feature/signin/signup/SignUpContract.kt
@@ -0,0 +1,38 @@
+package com.sixkids.feature.signin.signup
+
+import android.graphics.Bitmap
+import com.sixkids.ui.SnackbarToken
+import com.sixkids.ui.base.SideEffect
+import com.sixkids.ui.base.UiState
+
+sealed interface SignUpRoleEffect : SideEffect {
+ data class NavigateToSignUpPhoto(val isTeacher: Boolean) : SignUpRoleEffect
+}
+
+data class SignUpRoleState(
+ val role : Role = Role.TEACHER,
+) : UiState
+
+enum class Role{
+ TEACHER,
+ STUDENT
+}
+
+
+sealed interface SignUpPhotoEffect : SideEffect {
+ data object NavigateToTeacherOrganizationList : SignUpPhotoEffect
+ data object NavigateToStudentOrganizationList : SignUpPhotoEffect
+ data class OnShowSnackBar(val tkn : SnackbarToken) : SignUpPhotoEffect
+}
+
+data class SignUpPhotoState(
+ val isLoading: Boolean = false,
+ val gender: Gender? = null,
+ val profileDefaultPhoto: Int? = null,
+ val profileUserPhoto: Bitmap? = null
+) : UiState
+
+enum class Gender{
+ MAN,
+ WOMAN
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..2cc2f451
--- /dev/null
+++ b/android/feature/signin/src/main/java/com/sixkids/feature/signin/signup/SignUpPhotoScreen.kt
@@ -0,0 +1,303 @@
+package com.sixkids.feature.signin.signup
+
+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.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+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.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.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 com.sixkids.designsystem.R
+import com.sixkids.designsystem.component.screen.LoadingScreen
+import com.sixkids.designsystem.component.screen.UlbanTopSection
+import com.sixkids.designsystem.theme.Blue
+import com.sixkids.designsystem.theme.BlueDark
+import com.sixkids.designsystem.theme.Cream
+import com.sixkids.designsystem.theme.UlbanTheme
+import com.sixkids.designsystem.theme.UlbanTypography
+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 SignUpPhotoRoute(
+ viewModel: SignUpPhotoViewModel = hiltViewModel(),
+ navigateToTeacherOrganizationList: () -> Unit,
+ navigateToStudentOrganizationList: () -> Unit,
+ onShowSnackBar: (SnackbarToken) -> Unit,
+ onBackClick: () -> Unit
+) {
+ val context = LocalContext.current
+ val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
+
+ 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) {
+ is SignUpPhotoEffect.OnShowSnackBar -> onShowSnackBar(it.tkn)
+ SignUpPhotoEffect.NavigateToTeacherOrganizationList -> navigateToTeacherOrganizationList()
+ SignUpPhotoEffect.NavigateToStudentOrganizationList -> navigateToStudentOrganizationList()
+ }
+ }
+
+ SignUpPhotoScreen(
+ uiState = uiState,
+ isTeacher = viewModel.isTeacher,
+ onClickPhoto = { resId ->
+ when(resId){
+ R.drawable.camera ->
+ launcher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
+ R.drawable.teacher_man ->
+ viewModel.onProfileDefaultPhotoSelected(resId, Gender.MAN)
+ R.drawable.student_boy ->
+ viewModel.onProfileDefaultPhotoSelected(resId, Gender.MAN)
+ R.drawable.teacher_woman ->
+ viewModel.onProfileDefaultPhotoSelected(resId, Gender.WOMAN)
+ R.drawable.student_girl ->
+ viewModel.onProfileDefaultPhotoSelected(resId, Gender.WOMAN)
+ }
+ },
+ onDoneClick = {
+ viewModel.signUp(
+ saveBitmapToFile(context, uiState.profileUserPhoto, "profile.jpg")
+ )
+ },
+ onBackClick = {
+ onBackClick()
+ }
+ )
+}
+@Composable
+fun SignUpPhotoScreen(
+ uiState: SignUpPhotoState = SignUpPhotoState(),
+ isTeacher: Boolean = true,
+ onClickPhoto: (Int) -> Unit = {},
+ onDoneClick : () -> Unit = {},
+ onBackClick : () -> Unit = {}
+) {
+ val imageMan = if (isTeacher) R.drawable.teacher_man else R.drawable.student_boy
+ val imageWoman = if (isTeacher) R.drawable.teacher_woman else R.drawable.student_girl
+ Box(modifier = Modifier.fillMaxSize()){
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(21.dp)
+ ) {
+ UlbanTopSection(stringResource(id = com.sixkids.signin.R.string.signup_photo_title), onBackClick)
+
+ Spacer(modifier = Modifier.height(60.dp))
+
+ SelectedPhotoCard(
+ uiState.profileDefaultPhoto ?: imageMan,
+ uiState.profileUserPhoto,
+ 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 = R.drawable.camera,
+ onClickPhoto = onClickPhoto
+ )
+ }
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ DoneButton(onDoneClick)
+ }
+ if (uiState.isLoading){
+ LoadingScreen()
+ }
+ }
+}
+
+@Composable
+fun DoneButton(
+ onDoneClick: () -> Unit
+) {
+ Button(
+ onClick = { onDoneClick() },
+ modifier = Modifier
+ .fillMaxWidth(),
+ shape = RoundedCornerShape(12.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Blue
+ )
+ )
+ {
+ Text(
+ text = "완료",
+ style = UlbanTypography.titleSmall.copy(
+ fontSize = 14.sp,
+ color = BlueDark
+ ),
+ modifier = Modifier.padding(5.dp)
+ )
+ }
+}
+
+@Composable
+fun SelectedPhotoCard(defaultImage: Int, 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 (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
+
+ 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 SignUpPhotoScreenPreview() {
+ UlbanTheme {
+ SignUpPhotoScreen()
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..1e538d86
--- /dev/null
+++ b/android/feature/signin/src/main/java/com/sixkids/feature/signin/signup/SignUpPhotoViewModel.kt
@@ -0,0 +1,86 @@
+package com.sixkids.feature.signin.signup
+
+import android.graphics.Bitmap
+import androidx.annotation.DrawableRes
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.viewModelScope
+import com.sixkids.domain.usecase.user.GetRoleUseCase
+import com.sixkids.domain.usecase.user.SignUpUseCase
+import com.sixkids.feature.signin.navigation.SignInRoute.SIGN_UP_TEACHER
+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
+
+
+@HiltViewModel
+class SignUpPhotoViewModel @Inject constructor(
+ savedStateHandle: SavedStateHandle,
+ private val signUpUseCase: SignUpUseCase,
+ private val getRoleUseCase: GetRoleUseCase
+) : BaseViewModel(SignUpPhotoState()) {
+
+ val isTeacher = savedStateHandle.get(SIGN_UP_TEACHER)!!
+
+ fun onProfilePhotoSelected(bitmap: Bitmap) {
+ intent {
+ copy(
+ profileUserPhoto = bitmap,
+ profileDefaultPhoto = null,
+ gender = null
+ )
+ }
+ }
+
+ fun onProfileDefaultPhotoSelected(@DrawableRes photo: Int, gender: Gender) {
+ intent {
+ copy(
+ profileDefaultPhoto = photo,
+ profileUserPhoto = null,
+ gender = gender
+ )
+ }
+ }
+
+ fun signUp(file: File?) {
+ viewModelScope.launch {
+ intent { copy(isLoading = true)}
+ val defaultImage = when (file) {
+ null -> {
+ when (uiState.value.gender) {
+ null -> 0
+ Gender.MAN -> if (isTeacher) 1 else 3
+ Gender.WOMAN -> if (isTeacher) 2 else 4
+ }
+ }
+ else -> 0
+ }
+
+ signUpUseCase(
+ file = file,
+ defaultImage = defaultImage,
+ role = if (isTeacher) "TEACHER" else "STUDENT"
+ ).onSuccess {
+ postSideEffect(SignUpPhotoEffect.OnShowSnackBar(SnackbarToken(
+ message = "환영합니다."
+ )))
+ getRoleUseCase()
+ .onSuccess {
+ when(it){
+ "TEACHER" -> postSideEffect(SignUpPhotoEffect.NavigateToTeacherOrganizationList)
+ "STUDENT" -> postSideEffect(SignUpPhotoEffect.NavigateToStudentOrganizationList)
+ }
+ }.onFailure {
+
+ }
+ }.onFailure {
+ postSideEffect(SignUpPhotoEffect.OnShowSnackBar(SnackbarToken(
+ message = it.message ?: "알 수 없는 에러 입니다."
+ )))
+ }
+ intent { copy(isLoading = false)}
+ }
+ }
+}
diff --git a/android/feature/signin/src/main/java/com/sixkids/feature/signin/signup/SignUpRoleScreen.kt b/android/feature/signin/src/main/java/com/sixkids/feature/signin/signup/SignUpRoleScreen.kt
new file mode 100644
index 00000000..c5ac5b86
--- /dev/null
+++ b/android/feature/signin/src/main/java/com/sixkids/feature/signin/signup/SignUpRoleScreen.kt
@@ -0,0 +1,176 @@
+package com.sixkids.feature.signin.signup
+
+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.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.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.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+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.compose.ui.unit.sp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.sixkids.designsystem.R
+import com.sixkids.designsystem.component.screen.UlbanTopSection
+import com.sixkids.designsystem.theme.Green
+import com.sixkids.designsystem.theme.Red
+import com.sixkids.designsystem.theme.UlbanTheme
+import com.sixkids.designsystem.theme.UlbanTypography
+import com.sixkids.ui.extension.collectWithLifecycle
+
+@Composable
+fun SignUpRoute(
+ viewModel: SignUpRoleViewModel = hiltViewModel(),
+ navigateToSignUpPhoto: (Boolean) -> Unit,
+ onBackClick: () -> Unit
+) {
+ viewModel.sideEffect.collectWithLifecycle { sideEffect ->
+ when (sideEffect) {
+ is SignUpRoleEffect.NavigateToSignUpPhoto -> navigateToSignUpPhoto(sideEffect.isTeacher)
+ }
+ }
+
+ SignUpScreen(
+ onTeacherClick = {
+ viewModel.onTeacherClick(it)
+ },
+ onBackClick = {
+ onBackClick()
+ }
+ )
+}
+
+@Composable
+fun SignUpScreen(
+ onTeacherClick: (Boolean) -> Unit = {},
+ onBackClick : () -> Unit = {}
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(21.dp)
+ ) {
+ UlbanTopSection(stringResource(id = com.sixkids.signin.R.string.signup_role_title), onBackClick = onBackClick)
+ BodySection(
+ onCardClick = onTeacherClick,
+ modifier = Modifier
+ .weight(1f)
+ .padding(0.dp, 0.dp, 0.dp, 40.dp)
+ )
+ }
+}
+
+@Composable
+fun BodySection(onCardClick: (Boolean) -> Unit, modifier: Modifier = Modifier) {
+ Column(
+ modifier = modifier,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Card(
+ modifier = Modifier
+ .padding(20.dp)
+ .fillMaxWidth()
+ .clickable {
+ onCardClick(true)
+ },
+ shape = RoundedCornerShape(20.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = Red
+ ),
+ elevation = CardDefaults.cardElevation(
+ defaultElevation = 4.dp,
+ pressedElevation = 8.dp
+ ),
+ ) {
+ Row(
+ modifier = Modifier
+ .padding(40.dp, 10.dp)
+ .fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+
+ Image(
+ painter = painterResource(id = R.drawable.teacher_woman),
+ contentDescription = "teacher",
+ modifier = Modifier.size(120.dp)
+ )
+
+ Spacer(modifier = Modifier.size(10.dp))
+
+ Text(
+ text = "선생님",
+ style = UlbanTypography.titleLarge.copy(
+ fontSize = 28.sp
+ ),
+ )
+
+ }
+ }
+
+ Card(
+ modifier = Modifier
+ .padding(20.dp)
+ .fillMaxWidth()
+ .clickable {
+ onCardClick(false)
+ },
+ shape = RoundedCornerShape(20.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = Green
+ ),
+ elevation = CardDefaults.cardElevation(
+ defaultElevation = 4.dp,
+ pressedElevation = 8.dp
+ ),
+ ) {
+ Row(
+ modifier = Modifier
+ .padding(40.dp, 10.dp)
+ .fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.End
+ ) {
+ Text(
+ text = "학생",
+ style = UlbanTypography.titleLarge.copy(
+ fontSize = 28.sp
+ ),
+ )
+
+ Spacer(modifier = Modifier.size(10.dp))
+
+ Image(
+ painter = painterResource(id = R.drawable.student_girl),
+ contentDescription = "student",
+ modifier = Modifier.size(120.dp)
+ )
+
+ }
+ }
+ }
+}
+
+@Composable
+@Preview(showBackground = true)
+fun SignUpScreenPreview() {
+ UlbanTheme {
+ SignUpScreen()
+ }
+}
\ No newline at end of file
diff --git a/android/feature/signin/src/main/java/com/sixkids/feature/signin/signup/SignUpRoleViewModel.kt b/android/feature/signin/src/main/java/com/sixkids/feature/signin/signup/SignUpRoleViewModel.kt
new file mode 100644
index 00000000..079fbc4f
--- /dev/null
+++ b/android/feature/signin/src/main/java/com/sixkids/feature/signin/signup/SignUpRoleViewModel.kt
@@ -0,0 +1,16 @@
+package com.sixkids.feature.signin.signup
+
+import android.graphics.Bitmap
+import com.sixkids.ui.base.BaseViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+
+@HiltViewModel
+class SignUpRoleViewModel @Inject constructor(
+
+) : BaseViewModel(SignUpRoleState()){
+
+ fun onTeacherClick(isTeacher: Boolean){
+ postSideEffect(SignUpRoleEffect.NavigateToSignUpPhoto(isTeacher))
+ }
+}
\ No newline at end of file
diff --git a/android/feature/signin/src/main/res/drawable/kakao.xml b/android/feature/signin/src/main/res/drawable/kakao.xml
new file mode 100644
index 00000000..a768edff
--- /dev/null
+++ b/android/feature/signin/src/main/res/drawable/kakao.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/android/feature/signin/src/main/res/drawable/main_background.png b/android/feature/signin/src/main/res/drawable/main_background.png
new file mode 100644
index 00000000..d0be43bc
Binary files /dev/null and b/android/feature/signin/src/main/res/drawable/main_background.png differ
diff --git a/android/feature/signin/src/main/res/values/strings.xml b/android/feature/signin/src/main/res/values/strings.xml
new file mode 100644
index 00000000..75c00df8
--- /dev/null
+++ b/android/feature/signin/src/main/res/values/strings.xml
@@ -0,0 +1,13 @@
+
+
+
+ 울반
+ 슬기로운 학급생활 메이트
+ 5초 만에 카카오로 시작하기
+
+
+ 어떤 회원으로 가입할까요?
+
+
+ 프로필 사진을 선택하세요
+
\ No newline at end of file
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/data/src/androidTest/java/com/sixkids/data/ExampleInstrumentedTest.kt b/android/feature/student/main/src/androidTest/java/com/sixkids/student/main/ExampleInstrumentedTest.kt
similarity index 83%
rename from android/data/src/androidTest/java/com/sixkids/data/ExampleInstrumentedTest.kt
rename to android/feature/student/main/src/androidTest/java/com/sixkids/student/main/ExampleInstrumentedTest.kt
index 781e9e19..3f1c064f 100644
--- a/android/data/src/androidTest/java/com/sixkids/data/ExampleInstrumentedTest.kt
+++ b/android/feature/student/main/src/androidTest/java/com/sixkids/student/main/ExampleInstrumentedTest.kt
@@ -1,4 +1,4 @@
-package com.sixkids.data
+package com.sixkids.student.main
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -19,6 +19,6 @@ class ExampleInstrumentedTest {
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
- assertEquals("com.sixkids.data.test", appContext.packageName)
+ 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