diff --git a/.github/workflows/android-cd.yml b/.github/workflows/android-cd.yml
new file mode 100644
index 00000000..461419fe
--- /dev/null
+++ b/.github/workflows/android-cd.yml
@@ -0,0 +1,58 @@
+name: android-cd
+on:
+ push:
+ branches:
+ - master
+ workflow_dispatch:
+
+jobs:
+ build:
+ runs-on: macos-latest
+ environment: Android CI/CD
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Create Google Services JSON File
+ env:
+ GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }}
+ run: |
+ echo "$GOOGLE_SERVICES_JSON" > google-services.json.b64
+ base64 -d -i google-services.json.b64 > ./app/google-services.json
+
+ - name: Create Firebase Service Credentials file
+ env:
+ FIREBASE_CREDENTIALS: ${{ secrets.FIREBASE_CREDENTIALS }}
+ run: |
+ echo "$FIREBASE_CREDENTIALS" > firebase_credentials.json.b64
+ base64 -d -i firebase_credentials.json.b64 > firebase_credentials.json
+
+ - name: Create LocalProperites
+ env:
+ CREDENTIAL_WEB_CLIENT_ID: ${{ secrets.CREDENTIAL_WEB_CLIENT_ID }}
+ MISSION_MATE_BASE_URL: ${{ secrets.MISSION_MATE_BASE_URL }}
+ SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
+ SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
+ SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
+ run: |
+ echo CREDENTIAL_WEB_CLIENT_ID=\"$CREDENTIAL_WEB_CLIENT_ID\" > ./local.properties
+ echo MISSION_MATE_BASE_URL=\"$MISSION_MATE_BASE_URL\" >> ./local.properties
+ echo SIGNING_STORE_PASSWORD=$SIGNING_STORE_PASSWORD >> ./local.properties
+ echo SIGNING_KEY_PASSWORD=$SIGNING_KEY_PASSWORD >> ./local.properties
+ echo SIGNING_KEY_ALIAS=$SIGNING_KEY_ALIAS >> ./local.properties
+
+ - name: Generate Keystore file from Github Secrets
+ env:
+ KEYSTORE: ${{ secrets.KEYSTORE_BASE64 }}
+ run: |
+ echo "$KEYSTORE" > ./mission-mate-keystore.b64
+ base64 -d -i ./mission-mate-keystore.b64 > ./app/mission-mate-keystore.jks
+
+ - name: build release aab
+ run: ./gradlew app:bundleRelease
+
+ - name: Upload .aab as artifact
+ uses: actions/upload-artifact@v3
+ with:
+ name: app-bundle
+ path: app/build/outputs/bundle/release/app-release.aab
diff --git a/.github/workflows/android-ci.yml b/.github/workflows/android-ci.yml
new file mode 100644
index 00000000..99453e78
--- /dev/null
+++ b/.github/workflows/android-ci.yml
@@ -0,0 +1,40 @@
+name: android-ci
+on:
+ pull_request:
+ branches:
+ - master
+ - 'dev'
+ workflow_dispatch:
+
+jobs:
+ build:
+ runs-on: macos-latest
+ environment: Android CI/CD
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Create Google Services JSON File
+ env:
+ GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }}
+ run: |
+ echo "$GOOGLE_SERVICES_JSON" > google-services.json.b64
+ base64 -d -i google-services.json.b64 > ./app/google-services.json
+
+ - name: Create LocalProperites
+ env:
+ CREDENTIAL_WEB_CLIENT_ID: ${{ secrets.CREDENTIAL_WEB_CLIENT_ID }}
+ MISSION_MATE_BASE_URL: ${{ secrets.MISSION_MATE_BASE_URL }}
+ run: |
+ echo CREDENTIAL_WEB_CLIENT_ID=\"$CREDENTIAL_WEB_CLIENT_ID\" > ./local.properties
+ echo MISSION_MATE_BASE_URL=\"$MISSION_MATE_BASE_URL\" >> ./local.properties
+
+ - name: Generate Keystore file from Github Secrets
+ env:
+ KEYSTORE: ${{ secrets.KEYSTORE_BASE64 }}
+ run: |
+ echo "$KEYSTORE" > ./mission-mate-keystore.b64
+ base64 -d -i ./mission-mate-keystore.b64 > ./app/mission-mate-keystore.jks
+
+ - name: Build with Gradle
+ run: fastlane test
diff --git a/.gitignore b/.gitignore
index 9b55656f..bfabf4d2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -56,6 +56,7 @@ build/
# Local configuration file (sdk path, etc)
local.properties
+keystore.properties
# Proguard folder generated by Eclipse
proguard/
@@ -164,4 +165,11 @@ google-services.json
# End of https://www.gitignore.io/api/kotlin,androidstudio
-/app/keystore
\ No newline at end of file
+/app/keystore
+/app/mission-mate-keystore.jks
+/.idea/kotlinc.xml
+
+firebase_credentials.json
+/.idea/inspectionProfiles/Project_Default.xml
+/.idea/deploymentTargetSelector.xml
+/.idea/other.xml
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 00000000..cdd3a6b3
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,6 @@
+source "https://rubygems.org"
+
+gem "fastlane"
+
+plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
+eval_gemfile(plugins_path) if File.exist?(plugins_path)
diff --git a/Gemfile.lock b/Gemfile.lock
new file mode 100644
index 00000000..af43b978
--- /dev/null
+++ b/Gemfile.lock
@@ -0,0 +1,228 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ CFPropertyList (3.0.7)
+ base64
+ nkf
+ rexml
+ addressable (2.8.7)
+ public_suffix (>= 2.0.2, < 7.0)
+ artifactory (3.0.17)
+ atomos (0.1.3)
+ aws-eventstream (1.3.0)
+ aws-partitions (1.958.0)
+ aws-sdk-core (3.201.3)
+ aws-eventstream (~> 1, >= 1.3.0)
+ aws-partitions (~> 1, >= 1.651.0)
+ aws-sigv4 (~> 1.8)
+ jmespath (~> 1, >= 1.6.1)
+ aws-sdk-kms (1.88.0)
+ aws-sdk-core (~> 3, >= 3.201.0)
+ aws-sigv4 (~> 1.5)
+ aws-sdk-s3 (1.156.0)
+ aws-sdk-core (~> 3, >= 3.201.0)
+ aws-sdk-kms (~> 1)
+ aws-sigv4 (~> 1.5)
+ aws-sigv4 (1.9.0)
+ aws-eventstream (~> 1, >= 1.0.2)
+ babosa (1.0.4)
+ base64 (0.2.0)
+ claide (1.1.0)
+ colored (1.2)
+ colored2 (3.1.2)
+ commander (4.6.0)
+ highline (~> 2.0.0)
+ declarative (0.0.20)
+ digest-crc (0.6.5)
+ rake (>= 12.0.0, < 14.0.0)
+ domain_name (0.6.20240107)
+ dotenv (2.8.1)
+ emoji_regex (3.2.3)
+ excon (0.111.0)
+ faraday (1.10.3)
+ faraday-em_http (~> 1.0)
+ faraday-em_synchrony (~> 1.0)
+ faraday-excon (~> 1.1)
+ faraday-httpclient (~> 1.0)
+ faraday-multipart (~> 1.0)
+ faraday-net_http (~> 1.0)
+ faraday-net_http_persistent (~> 1.0)
+ faraday-patron (~> 1.0)
+ faraday-rack (~> 1.0)
+ faraday-retry (~> 1.0)
+ ruby2_keywords (>= 0.0.4)
+ faraday-cookie_jar (0.0.7)
+ faraday (>= 0.8.0)
+ http-cookie (~> 1.0.0)
+ faraday-em_http (1.0.0)
+ faraday-em_synchrony (1.0.0)
+ faraday-excon (1.1.0)
+ faraday-httpclient (1.0.1)
+ faraday-multipart (1.0.4)
+ multipart-post (~> 2)
+ faraday-net_http (1.0.2)
+ faraday-net_http_persistent (1.2.0)
+ faraday-patron (1.0.0)
+ faraday-rack (1.0.0)
+ faraday-retry (1.0.3)
+ faraday_middleware (1.2.0)
+ faraday (~> 1.0)
+ fastimage (2.3.1)
+ fastlane (2.221.1)
+ CFPropertyList (>= 2.3, < 4.0.0)
+ addressable (>= 2.8, < 3.0.0)
+ artifactory (~> 3.0)
+ aws-sdk-s3 (~> 1.0)
+ babosa (>= 1.0.3, < 2.0.0)
+ bundler (>= 1.12.0, < 3.0.0)
+ colored (~> 1.2)
+ commander (~> 4.6)
+ dotenv (>= 2.1.1, < 3.0.0)
+ emoji_regex (>= 0.1, < 4.0)
+ excon (>= 0.71.0, < 1.0.0)
+ faraday (~> 1.0)
+ faraday-cookie_jar (~> 0.0.6)
+ faraday_middleware (~> 1.0)
+ fastimage (>= 2.1.0, < 3.0.0)
+ gh_inspector (>= 1.1.2, < 2.0.0)
+ google-apis-androidpublisher_v3 (~> 0.3)
+ google-apis-playcustomapp_v1 (~> 0.1)
+ google-cloud-env (>= 1.6.0, < 2.0.0)
+ google-cloud-storage (~> 1.31)
+ highline (~> 2.0)
+ http-cookie (~> 1.0.5)
+ json (< 3.0.0)
+ jwt (>= 2.1.0, < 3)
+ mini_magick (>= 4.9.4, < 5.0.0)
+ multipart-post (>= 2.0.0, < 3.0.0)
+ naturally (~> 2.2)
+ optparse (>= 0.1.1, < 1.0.0)
+ plist (>= 3.1.0, < 4.0.0)
+ rubyzip (>= 2.0.0, < 3.0.0)
+ security (= 0.1.5)
+ simctl (~> 1.6.3)
+ terminal-notifier (>= 2.0.0, < 3.0.0)
+ terminal-table (~> 3)
+ tty-screen (>= 0.6.3, < 1.0.0)
+ tty-spinner (>= 0.8.0, < 1.0.0)
+ word_wrap (~> 1.0.0)
+ xcodeproj (>= 1.13.0, < 2.0.0)
+ xcpretty (~> 0.3.0)
+ xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
+ fastlane-plugin-firebase_app_distribution (0.9.1)
+ google-apis-firebaseappdistribution_v1 (~> 0.3.0)
+ google-apis-firebaseappdistribution_v1alpha (~> 0.2.0)
+ gh_inspector (1.1.3)
+ google-apis-androidpublisher_v3 (0.54.0)
+ google-apis-core (>= 0.11.0, < 2.a)
+ google-apis-core (0.11.3)
+ addressable (~> 2.5, >= 2.5.1)
+ googleauth (>= 0.16.2, < 2.a)
+ httpclient (>= 2.8.1, < 3.a)
+ mini_mime (~> 1.0)
+ representable (~> 3.0)
+ retriable (>= 2.0, < 4.a)
+ rexml
+ google-apis-firebaseappdistribution_v1 (0.3.0)
+ google-apis-core (>= 0.11.0, < 2.a)
+ google-apis-firebaseappdistribution_v1alpha (0.2.0)
+ google-apis-core (>= 0.11.0, < 2.a)
+ google-apis-iamcredentials_v1 (0.17.0)
+ google-apis-core (>= 0.11.0, < 2.a)
+ google-apis-playcustomapp_v1 (0.13.0)
+ google-apis-core (>= 0.11.0, < 2.a)
+ google-apis-storage_v1 (0.31.0)
+ google-apis-core (>= 0.11.0, < 2.a)
+ google-cloud-core (1.7.0)
+ google-cloud-env (>= 1.0, < 3.a)
+ google-cloud-errors (~> 1.0)
+ google-cloud-env (1.6.0)
+ faraday (>= 0.17.3, < 3.0)
+ google-cloud-errors (1.4.0)
+ google-cloud-storage (1.47.0)
+ addressable (~> 2.8)
+ digest-crc (~> 0.4)
+ google-apis-iamcredentials_v1 (~> 0.1)
+ google-apis-storage_v1 (~> 0.31.0)
+ google-cloud-core (~> 1.6)
+ googleauth (>= 0.16.2, < 2.a)
+ mini_mime (~> 1.0)
+ googleauth (1.8.1)
+ faraday (>= 0.17.3, < 3.a)
+ jwt (>= 1.4, < 3.0)
+ multi_json (~> 1.11)
+ os (>= 0.9, < 2.0)
+ signet (>= 0.16, < 2.a)
+ highline (2.0.3)
+ http-cookie (1.0.6)
+ domain_name (~> 0.5)
+ httpclient (2.8.3)
+ jmespath (1.6.2)
+ json (2.7.2)
+ jwt (2.8.2)
+ base64
+ mini_magick (4.13.2)
+ mini_mime (1.1.5)
+ multi_json (1.15.0)
+ multipart-post (2.4.1)
+ nanaimo (0.3.0)
+ naturally (2.2.1)
+ nkf (0.2.0)
+ optparse (0.5.0)
+ os (1.1.4)
+ plist (3.7.1)
+ public_suffix (6.0.1)
+ rake (13.2.1)
+ representable (3.2.0)
+ declarative (< 0.1.0)
+ trailblazer-option (>= 0.1.1, < 0.2.0)
+ uber (< 0.2.0)
+ retriable (3.1.2)
+ rexml (3.2.9)
+ strscan
+ rouge (2.0.7)
+ ruby2_keywords (0.0.5)
+ rubyzip (2.3.2)
+ security (0.1.5)
+ signet (0.19.0)
+ addressable (~> 2.8)
+ faraday (>= 0.17.5, < 3.a)
+ jwt (>= 1.5, < 3.0)
+ multi_json (~> 1.10)
+ simctl (1.6.10)
+ CFPropertyList
+ naturally
+ strscan (3.1.0)
+ terminal-notifier (2.0.0)
+ terminal-table (3.0.2)
+ unicode-display_width (>= 1.1.1, < 3)
+ trailblazer-option (0.1.2)
+ tty-cursor (0.7.1)
+ tty-screen (0.8.2)
+ tty-spinner (0.9.3)
+ tty-cursor (~> 0.7)
+ uber (0.1.0)
+ unicode-display_width (2.5.0)
+ word_wrap (1.0.0)
+ xcodeproj (1.24.0)
+ CFPropertyList (>= 2.3.3, < 4.0)
+ atomos (~> 0.1.3)
+ claide (>= 1.0.2, < 2.0)
+ colored2 (~> 3.1)
+ nanaimo (~> 0.3.0)
+ rexml (~> 3.2.4)
+ xcpretty (0.3.0)
+ rouge (~> 2.0.7)
+ xcpretty-travis-formatter (1.0.1)
+ xcpretty (~> 0.2, >= 0.0.7)
+
+PLATFORMS
+ arm64-darwin-23
+ ruby
+
+DEPENDENCIES
+ fastlane
+ fastlane-plugin-firebase_app_distribution
+
+BUNDLED WITH
+ 2.5.14
diff --git a/app/.gitignore b/app/.gitignore
index 9b55656f..f9e0c6a6 100644
--- a/app/.gitignore
+++ b/app/.gitignore
@@ -164,4 +164,6 @@ google-services.json
# End of https://www.gitignore.io/api/kotlin,androidstudio
-/app/keystore
\ No newline at end of file
+/app/keystore
+
+firebase_credentials.json
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 8568fa9e..c827b090 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,3 +1,4 @@
+import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
@@ -15,8 +16,8 @@ android {
applicationId = "com.goalpanzi.mission_mate"
minSdk = 26
targetSdk = 34
- versionCode = 1
- versionName = "1.0"
+ versionCode = 3
+ versionName = "1.0.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -24,15 +25,30 @@ android {
}
}
+ signingConfigs {
+ create("release") {
+ storeFile = file("./mission-mate-keystore.jks")
+ storePassword = gradleLocalProperties(rootDir, providers).getProperty("SIGNING_STORE_PASSWORD")
+ keyAlias = gradleLocalProperties(rootDir, providers).getProperty("SIGNING_KEY_ALIAS")
+ keyPassword = gradleLocalProperties(rootDir, providers).getProperty("SIGNING_KEY_PASSWORD")
+ }
+ }
+
buildTypes {
release {
- isMinifyEnabled = false
+ signingConfig = signingConfigs.getByName("release")
+ isMinifyEnabled = true
+ isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
+ isDebuggable = false
}
}
+ buildFeatures {
+ buildConfig = true
+ }
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
@@ -53,8 +69,10 @@ dependencies {
ksp(libs.hilt.compiler)
implementation(libs.hilt.android)
+ implementation(platform(libs.firebase.bom))
implementation(project(":feature:main"))
implementation(project(":feature:login"))
implementation(project(":core:designsystem"))
-}
\ No newline at end of file
+ implementation(project(":core:data"))
+}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 481bb434..109525fd 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -18,4 +18,12 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
-#-renamesourcefileattribute SourceFile
\ No newline at end of file
+#-renamesourcefileattribute SourceFile
+
+-keep class * { *; }
+-keep interface * { *; }
+
+# Keep Dependency Injection Framework related classes and methods
+-keep class dagger.hilt.** { *; }
+-keep class javax.inject.** { *; }
+-keep class javax.annotation.** { *; }
diff --git a/app/src/androidTest/java/com/goalpanzi/mission_mate/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/goalpanzi/mission_mate/ExampleInstrumentedTest.kt
index 2a450a19..08256afe 100644
--- a/app/src/androidTest/java/com/goalpanzi/mission_mate/ExampleInstrumentedTest.kt
+++ b/app/src/androidTest/java/com/goalpanzi/mission_mate/ExampleInstrumentedTest.kt
@@ -1,24 +1,11 @@
package com.goalpanzi.mission_mate
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.ext.junit.runners.AndroidJUnit4
-
-import org.junit.Test
-import org.junit.runner.RunWith
-
-import org.junit.Assert.*
-
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
-@RunWith(AndroidJUnit4::class)
+
class ExampleInstrumentedTest {
- @Test
- fun useAppContext() {
- // Context of the app under test.
- val appContext = InstrumentationRegistry.getInstrumentation().targetContext
- assertEquals("com.goalpanzi.mission_mate", appContext.packageName)
- }
+
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 33341792..691447c2 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,6 +2,10 @@
+
+
+
-
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png
new file mode 100644
index 00000000..2ae86dd0
Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
deleted file mode 100644
index 07d5da9c..00000000
--- a/app/src/main/res/drawable/ic_launcher_background.xml
+++ /dev/null
@@ -1,170 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
deleted file mode 100644
index 2b068d11..00000000
--- a/app/src/main/res/drawable/ic_launcher_foreground.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000..036d09bc
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000..036d09bc
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml
deleted file mode 100644
index 6f3b755b..00000000
--- a/app/src/main/res/mipmap-anydpi/ic_launcher.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
deleted file mode 100644
index 6f3b755b..00000000
--- a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
index c209e78e..50d5eff3 100644
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
new file mode 100644
index 00000000..b29a5799
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
index b2dfe3d1..8e89ef8d 100644
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
index 4f0f1d64..b090cd2a 100644
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
new file mode 100644
index 00000000..d1cdc357
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
index 62b611da..735c9535 100644
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
index 948a3070..c4e4f36d 100644
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
new file mode 100644
index 00000000..1ea5453d
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
index 1b9a6956..1eb10227 100644
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
index 28d4b77f..b55f4207 100644
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
new file mode 100644
index 00000000..91602f58
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
index 9287f508..ac92f957 100644
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
index aa7d6427..942289b5 100644
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
new file mode 100644
index 00000000..e98830a6
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
index 9126ae37..9c13adf6 100644
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 00000000..336b4b63
--- /dev/null
+++ b/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #FF5732
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml
new file mode 100644
index 00000000..872512f7
--- /dev/null
+++ b/app/src/main/res/xml/network_security_config.xml
@@ -0,0 +1,6 @@
+
+
+
+ 223.130.130.31
+
+
\ No newline at end of file
diff --git a/app/src/test/java/com/goalpanzi/mission_mate/ExampleUnitTest.kt b/app/src/test/java/com/goalpanzi/mission_mate/ExampleUnitTest.kt
index 3d08c0ac..1c19bfb1 100644
--- a/app/src/test/java/com/goalpanzi/mission_mate/ExampleUnitTest.kt
+++ b/app/src/test/java/com/goalpanzi/mission_mate/ExampleUnitTest.kt
@@ -1,8 +1,5 @@
package com.goalpanzi.mission_mate
-import org.junit.Test
-
-import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
@@ -10,8 +7,5 @@ import org.junit.Assert.*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
- @Test
- fun addition_isCorrect() {
- assertEquals(4, 2 + 2)
- }
+
}
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index ee715b43..344707f2 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -8,4 +8,6 @@ plugins {
alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.hilt.android) apply false
alias(libs.plugins.kotlin.ksp) apply false
+ alias(libs.plugins.kotlin.parcelize) apply false
+ alias(libs.plugins.google.service) apply false
}
\ No newline at end of file
diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts
index c6fba51a..e7be6c14 100644
--- a/core/data/build.gradle.kts
+++ b/core/data/build.gradle.kts
@@ -15,12 +15,10 @@ android {
minSdk = 26
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
- isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
@@ -42,7 +40,12 @@ dependencies {
implementation(libs.bundles.test)
implementation(libs.bundles.coroutines)
+ implementation(libs.retrofit)
ksp(libs.hilt.compiler)
implementation(libs.hilt.android)
+
+ implementation(project(":core:domain"))
+ implementation(project(":core:model"))
+ implementation(project(":core:network"))
}
\ No newline at end of file
diff --git a/core/data/proguard-rules.pro b/core/data/proguard-rules.pro
index 481bb434..109525fd 100644
--- a/core/data/proguard-rules.pro
+++ b/core/data/proguard-rules.pro
@@ -18,4 +18,12 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
-#-renamesourcefileattribute SourceFile
\ No newline at end of file
+#-renamesourcefileattribute SourceFile
+
+-keep class * { *; }
+-keep interface * { *; }
+
+# Keep Dependency Injection Framework related classes and methods
+-keep class dagger.hilt.** { *; }
+-keep class javax.inject.** { *; }
+-keep class javax.annotation.** { *; }
diff --git a/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/Data.kt b/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/Data.kt
deleted file mode 100644
index 4b970b65..00000000
--- a/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/Data.kt
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.goalpanzi.mission_mate.core.data
-
-class Data {
-}
\ No newline at end of file
diff --git a/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/di/DataModule.kt b/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/di/DataModule.kt
new file mode 100644
index 00000000..0e891ec1
--- /dev/null
+++ b/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/di/DataModule.kt
@@ -0,0 +1,31 @@
+package com.goalpanzi.mission_mate.core.data.di
+
+import com.goalpanzi.mission_mate.core.data.repository.AuthRepositoryImpl
+import com.goalpanzi.mission_mate.core.data.repository.MissionRepositoryImpl
+import com.goalpanzi.mission_mate.core.data.repository.OnboardingRepositoryImpl
+import com.goalpanzi.mission_mate.core.data.repository.ProfileRepositoryImpl
+import com.goalpanzi.mission_mate.core.domain.repository.AuthRepository
+import com.goalpanzi.mission_mate.core.domain.repository.MissionRepository
+import com.goalpanzi.mission_mate.core.domain.repository.OnboardingRepository
+import com.goalpanzi.mission_mate.core.domain.repository.ProfileRepository
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+
+@Module
+@InstallIn(SingletonComponent::class)
+internal abstract class DataModule {
+
+ @Binds
+ abstract fun bindLoginRepository(impl: AuthRepositoryImpl): AuthRepository
+
+ @Binds
+ abstract fun bindProfileRepository(impl: ProfileRepositoryImpl): ProfileRepository
+
+ @Binds
+ abstract fun bindOnboardingRepository(impl: OnboardingRepositoryImpl): OnboardingRepository
+
+ @Binds
+ abstract fun bindMissionRepository(impl: MissionRepositoryImpl): MissionRepository
+}
\ No newline at end of file
diff --git a/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/di/DispatchersModule.kt b/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/di/DispatchersModule.kt
new file mode 100644
index 00000000..2b129a1b
--- /dev/null
+++ b/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/di/DispatchersModule.kt
@@ -0,0 +1,27 @@
+package com.goalpanzi.mission_mate.core.data.di
+
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import javax.inject.Qualifier
+
+@Qualifier
+@Retention(AnnotationRetention.RUNTIME)
+annotation class Dispatcher(val dispatchers: MissionMateDispatcher)
+
+enum class MissionMateDispatcher {
+ IO
+}
+
+@Module
+@InstallIn(SingletonComponent::class)
+interface DispatchersModule {
+
+ @Provides
+ @Dispatcher(MissionMateDispatcher.IO)
+ fun providesIODispatcher(): CoroutineDispatcher = Dispatchers.IO
+
+}
\ No newline at end of file
diff --git a/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/repository/AuthRepositoryImpl.kt b/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/repository/AuthRepositoryImpl.kt
new file mode 100644
index 00000000..bbcc7411
--- /dev/null
+++ b/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/repository/AuthRepositoryImpl.kt
@@ -0,0 +1,25 @@
+package com.goalpanzi.mission_mate.core.data.repository
+
+import com.goalpanzi.mission_mate.core.domain.repository.AuthRepository
+import com.goalpanzi.mission_mate.core.network.service.LoginService
+import com.goalpanzi.core.model.base.NetworkResult
+import com.goalpanzi.core.model.request.GoogleLoginRequest
+import javax.inject.Inject
+
+class AuthRepositoryImpl @Inject constructor(
+ private val loginService: LoginService
+): AuthRepository {
+
+ override suspend fun requestGoogleLogin(email: String) = handleResult {
+ val request = GoogleLoginRequest(email = email)
+ loginService.requestGoogleLogin(request)
+ }
+
+ override suspend fun requestLogout(): NetworkResult = handleResult {
+ loginService.requestLogout()
+ }
+
+ override suspend fun requestAccountDelete(): NetworkResult = handleResult {
+ loginService.requestDeleteAccount()
+ }
+}
\ No newline at end of file
diff --git a/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/repository/MissionRepositoryImpl.kt b/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/repository/MissionRepositoryImpl.kt
new file mode 100644
index 00000000..7927dd7c
--- /dev/null
+++ b/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/repository/MissionRepositoryImpl.kt
@@ -0,0 +1,61 @@
+package com.goalpanzi.mission_mate.core.data.repository
+
+import com.goalpanzi.mission_mate.core.domain.repository.MissionRepository
+import com.goalpanzi.mission_mate.core.network.service.MissionService
+import com.goalpanzi.core.model.base.NetworkResult
+import com.goalpanzi.core.model.response.MissionBoardsResponse
+import com.goalpanzi.core.model.response.MissionDetailResponse
+import com.goalpanzi.core.model.response.MissionRankResponse
+import com.goalpanzi.core.model.response.MissionVerificationResponse
+import com.goalpanzi.core.model.response.MissionVerificationsResponse
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
+import okhttp3.MultipartBody
+import okhttp3.RequestBody.Companion.asRequestBody
+import java.io.File
+import javax.inject.Inject
+
+class MissionRepositoryImpl @Inject constructor(
+ private val missionService: MissionService,
+) : MissionRepository {
+ override suspend fun getMissionBoards(missionId: Long): NetworkResult =
+ handleResult {
+ missionService.getMissionBoards(missionId)
+ }
+
+ override suspend fun getMission(missionId: Long): NetworkResult =
+ handleResult {
+ missionService.getMission(missionId)
+ }
+
+ override suspend fun getMissionVerifications(missionId: Long): NetworkResult =
+ handleResult {
+ missionService.getMissionVerifications(missionId)
+ }
+
+ override suspend fun deleteMission(missionId: Long): NetworkResult =
+ handleResult {
+ missionService.deleteMission(missionId)
+ }
+
+ override suspend fun getMissionRank(missionId: Long): NetworkResult =
+ handleResult {
+ missionService.getMissionRank(missionId)
+ }
+
+ override suspend fun verifyMission(missionId: Long, image: File): NetworkResult =
+ handleResult {
+ val requestFile = MultipartBody.Part.createFormData(
+ "imageFile",
+ image.name,
+ image.asRequestBody("image/*".toMediaTypeOrNull())
+ )
+ missionService.verifyMission(missionId, requestFile)
+ }
+
+ override suspend fun getMyMissionVerification(
+ missionId: Long,
+ number: Int
+ ): NetworkResult = handleResult {
+ missionService.getMyMissionVerification(missionId,number)
+ }
+}
\ No newline at end of file
diff --git a/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/repository/OnboardingRepositoryImpl.kt b/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/repository/OnboardingRepositoryImpl.kt
new file mode 100644
index 00000000..eab9c56d
--- /dev/null
+++ b/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/repository/OnboardingRepositoryImpl.kt
@@ -0,0 +1,30 @@
+package com.goalpanzi.mission_mate.core.data.repository
+
+import com.goalpanzi.mission_mate.core.domain.repository.OnboardingRepository
+import com.goalpanzi.mission_mate.core.network.service.OnboardingService
+import com.goalpanzi.core.model.base.NetworkResult
+import com.goalpanzi.core.model.request.CreateMissionRequest
+import com.goalpanzi.core.model.request.JoinMissionRequest
+import com.goalpanzi.core.model.response.MissionDetailResponse
+import com.goalpanzi.core.model.response.MissionsResponse
+import javax.inject.Inject
+
+class OnboardingRepositoryImpl @Inject constructor(
+ private val onboardingService: OnboardingService
+) : OnboardingRepository {
+ override suspend fun createMission(missionRequest: CreateMissionRequest): NetworkResult = handleResult {
+ onboardingService.createMission(missionRequest)
+ }
+
+ override suspend fun getMissionByInvitationCode(invitationCode: String): NetworkResult = handleResult{
+ onboardingService.getMissionByInvitationCode(invitationCode)
+ }
+
+ override suspend fun joinMission(invitationCode: String): NetworkResult = handleResult {
+ onboardingService.joinMission(JoinMissionRequest(invitationCode))
+ }
+
+ override suspend fun getJoinedMissions(): NetworkResult = handleResult {
+ onboardingService.getJoinedMissions()
+ }
+}
\ No newline at end of file
diff --git a/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/repository/ProfileRepositoryImpl.kt b/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/repository/ProfileRepositoryImpl.kt
new file mode 100644
index 00000000..0bfcbc60
--- /dev/null
+++ b/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/repository/ProfileRepositoryImpl.kt
@@ -0,0 +1,21 @@
+package com.goalpanzi.mission_mate.core.data.repository
+
+import com.goalpanzi.mission_mate.core.domain.repository.ProfileRepository
+import com.goalpanzi.mission_mate.core.network.service.ProfileService
+import com.goalpanzi.core.model.CharacterType
+import com.goalpanzi.core.model.base.NetworkResult
+import com.goalpanzi.core.model.request.SaveProfileRequest
+import javax.inject.Inject
+
+class ProfileRepositoryImpl @Inject constructor(
+ private val profileService: ProfileService
+) : ProfileRepository {
+ override suspend fun saveProfile(
+ nickname: String,
+ type: CharacterType,
+ isEqualNickname: Boolean
+ ): NetworkResult = handleResult {
+ val request = SaveProfileRequest.createRequest(if(isEqualNickname)null else nickname, type)
+ profileService.saveProfile(request)
+ }
+}
\ No newline at end of file
diff --git a/core/datastore/build.gradle.kts b/core/datastore/build.gradle.kts
index e9bdfd03..85ef0949 100644
--- a/core/datastore/build.gradle.kts
+++ b/core/datastore/build.gradle.kts
@@ -15,12 +15,10 @@ android {
minSdk = 26
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
- isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
@@ -47,4 +45,6 @@ dependencies {
ksp(libs.hilt.compiler)
implementation(libs.hilt.android)
+
+ implementation(project(":core:model"))
}
\ No newline at end of file
diff --git a/core/datastore/consumer-rules.pro b/core/datastore/consumer-rules.pro
deleted file mode 100644
index e69de29b..00000000
diff --git a/core/datastore/proguard-rules.pro b/core/datastore/proguard-rules.pro
index 481bb434..109525fd 100644
--- a/core/datastore/proguard-rules.pro
+++ b/core/datastore/proguard-rules.pro
@@ -18,4 +18,12 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
-#-renamesourcefileattribute SourceFile
\ No newline at end of file
+#-renamesourcefileattribute SourceFile
+
+-keep class * { *; }
+-keep interface * { *; }
+
+# Keep Dependency Injection Framework related classes and methods
+-keep class dagger.hilt.** { *; }
+-keep class javax.inject.** { *; }
+-keep class javax.annotation.** { *; }
diff --git a/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/DataStore.kt b/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/DataStore.kt
deleted file mode 100644
index f688f9d7..00000000
--- a/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/DataStore.kt
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.goalpanzi.mission_mate.core.datastore
-
-class DataStore {
-}
\ No newline at end of file
diff --git a/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/datasource/AuthDataSource.kt b/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/datasource/AuthDataSource.kt
new file mode 100644
index 00000000..0b1054ff
--- /dev/null
+++ b/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/datasource/AuthDataSource.kt
@@ -0,0 +1,11 @@
+package com.goalpanzi.mission_mate.core.datastore.datasource
+
+import kotlinx.coroutines.flow.Flow
+
+interface AuthDataSource {
+ fun getAccessToken() : Flow
+ fun getRefreshToken() : Flow
+
+ fun setAccessToken(accessToken : String) : Flow
+ fun setRefreshToken(refreshToken : String) : Flow
+}
\ No newline at end of file
diff --git a/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/datasource/AuthDataSourceImpl.kt b/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/datasource/AuthDataSourceImpl.kt
new file mode 100644
index 00000000..babebe84
--- /dev/null
+++ b/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/datasource/AuthDataSourceImpl.kt
@@ -0,0 +1,43 @@
+package com.goalpanzi.mission_mate.core.datastore.datasource
+
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.stringPreferencesKey
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.map
+import javax.inject.Inject
+
+class AuthDataSourceImpl @Inject constructor(
+ private val dataStore: DataStore
+) : AuthDataSource {
+ object PreferencesKey {
+ val ACCESS_TOKEN = stringPreferencesKey("ACCESS_TOKEN")
+ val REFRESH_TOKEN = stringPreferencesKey("REFRESH_TOKEN")
+
+ }
+ override fun getAccessToken(): Flow =
+ dataStore.data.map { preferences ->
+ preferences[PreferencesKey.ACCESS_TOKEN]
+ }
+
+ override fun getRefreshToken(): Flow =
+ dataStore.data.map { preferences ->
+ preferences[PreferencesKey.REFRESH_TOKEN]
+ }
+
+ override fun setAccessToken(accessToken: String): Flow = flow {
+ dataStore.edit { preferences ->
+ preferences[PreferencesKey.ACCESS_TOKEN] = accessToken
+ }
+ emit(Unit)
+ }
+
+ override fun setRefreshToken(refreshToken: String): Flow = flow {
+ dataStore.edit { preferences ->
+ preferences[PreferencesKey.REFRESH_TOKEN] = refreshToken
+ }
+ emit(Unit)
+ }
+}
\ No newline at end of file
diff --git a/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/datasource/DefaultDataSource.kt b/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/datasource/DefaultDataSource.kt
new file mode 100644
index 00000000..8296af73
--- /dev/null
+++ b/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/datasource/DefaultDataSource.kt
@@ -0,0 +1,14 @@
+package com.goalpanzi.mission_mate.core.datastore.datasource
+
+import com.goalpanzi.core.model.UserProfile
+import kotlinx.coroutines.flow.Flow
+
+interface DefaultDataSource {
+ fun clearUserData() : Flow
+ fun setUserProfile(data: UserProfile) : Flow
+ fun getUserProfile() : Flow
+ fun getViewedTooltip() : Flow
+ fun setViewedTooltip() : Flow
+ fun setMemberId(data: Long) : Flow
+ fun getMemberId() : Flow
+}
\ No newline at end of file
diff --git a/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/datasource/DefaultDataSourceImpl.kt b/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/datasource/DefaultDataSourceImpl.kt
new file mode 100644
index 00000000..a197f128
--- /dev/null
+++ b/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/datasource/DefaultDataSourceImpl.kt
@@ -0,0 +1,74 @@
+package com.goalpanzi.mission_mate.core.datastore.datasource
+
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.booleanPreferencesKey
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.longPreferencesKey
+import androidx.datastore.preferences.core.stringPreferencesKey
+import com.goalpanzi.core.model.CharacterType
+import com.goalpanzi.core.model.UserProfile
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.map
+import javax.inject.Inject
+
+class DefaultDataSourceImpl @Inject constructor(
+ private val dataStore: DataStore
+) : DefaultDataSource {
+
+ object PreferencesKey {
+ val USER_NICKNAME = stringPreferencesKey("USER_NICKNAME")
+ val USER_CHARACTER = stringPreferencesKey("USER_CHARACTER")
+ val VIEWED_TOOLTIP = booleanPreferencesKey("VIEWED_TOOLTIP")
+ val MEMBER_ID = longPreferencesKey("MEMBER_ID")
+ }
+
+ override fun clearUserData(): Flow = flow {
+ dataStore.edit { preferences ->
+ preferences.clear()
+ }
+ emit(Unit)
+ }
+
+ override fun setUserProfile(data: UserProfile): Flow = flow {
+ dataStore.edit { preferences ->
+ preferences[PreferencesKey.USER_NICKNAME] = data.nickname
+ preferences[PreferencesKey.USER_CHARACTER] = data.characterType.name.uppercase()
+ }
+ emit(Unit)
+ }
+
+ override fun getUserProfile(): Flow = dataStore.data.map { preferences ->
+ val nickname = preferences[PreferencesKey.USER_NICKNAME]
+ val character = preferences[PreferencesKey.USER_CHARACTER]
+ if (nickname != null && character != null) {
+ UserProfile(nickname, CharacterType.valueOf(character))
+ } else {
+ null
+ }
+ }
+
+ override fun getViewedTooltip(): Flow = dataStore.data.map { preferences ->
+ val viewed = preferences[PreferencesKey.VIEWED_TOOLTIP]
+ viewed ?: false
+ }
+
+ override fun setViewedTooltip(): Flow = flow {
+ dataStore.edit { preferences ->
+ preferences[PreferencesKey.VIEWED_TOOLTIP] = true
+ }
+ emit(Unit)
+ }
+
+ override fun setMemberId(data: Long): Flow = flow {
+ dataStore.edit { preferences ->
+ preferences[PreferencesKey.MEMBER_ID] = data
+ }
+ emit(Unit)
+ }
+
+ override fun getMemberId(): Flow = dataStore.data.map { preferences ->
+ preferences[PreferencesKey.MEMBER_ID]
+ }
+}
\ No newline at end of file
diff --git a/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/datasource/MissionDataSource.kt b/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/datasource/MissionDataSource.kt
new file mode 100644
index 00000000..75129ab4
--- /dev/null
+++ b/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/datasource/MissionDataSource.kt
@@ -0,0 +1,9 @@
+package com.goalpanzi.mission_mate.core.datastore.datasource
+
+import kotlinx.coroutines.flow.Flow
+
+interface MissionDataSource {
+ fun clearMissionData() : Flow
+ fun setIsMissionJoined(data: Boolean) : Flow
+ fun getIsMissionJoined() : Flow
+}
\ No newline at end of file
diff --git a/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/datasource/MissionDataSourceImpl.kt b/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/datasource/MissionDataSourceImpl.kt
new file mode 100644
index 00000000..43a55e36
--- /dev/null
+++ b/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/datasource/MissionDataSourceImpl.kt
@@ -0,0 +1,37 @@
+package com.goalpanzi.mission_mate.core.datastore.datasource
+
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.booleanPreferencesKey
+import androidx.datastore.preferences.core.edit
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.map
+import javax.inject.Inject
+
+class MissionDataSourceImpl @Inject constructor(
+ private val dataStore: DataStore
+) : MissionDataSource {
+
+ object PreferencesKey {
+ val MISSION_IS_JOINED = booleanPreferencesKey("MISSION_IS_JOINED")
+ }
+
+ override fun clearMissionData(): Flow = flow {
+ dataStore.edit { preferences ->
+ preferences.clear()
+ }
+ emit(Unit)
+ }
+
+ override fun setIsMissionJoined(data: Boolean): Flow = flow {
+ dataStore.edit { preferences ->
+ preferences[PreferencesKey.MISSION_IS_JOINED] = data
+ }
+ emit(Unit)
+ }
+
+ override fun getIsMissionJoined(): Flow = dataStore.data.map { preferences ->
+ preferences[PreferencesKey.MISSION_IS_JOINED]
+ }
+}
\ No newline at end of file
diff --git a/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/di/DataSourceModule.kt b/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/di/DataSourceModule.kt
new file mode 100644
index 00000000..ef3cda7d
--- /dev/null
+++ b/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/di/DataSourceModule.kt
@@ -0,0 +1,32 @@
+package com.goalpanzi.mission_mate.core.datastore.di
+
+import com.goalpanzi.mission_mate.core.datastore.datasource.AuthDataSource
+import com.goalpanzi.mission_mate.core.datastore.datasource.AuthDataSourceImpl
+import com.goalpanzi.mission_mate.core.datastore.datasource.DefaultDataSource
+import com.goalpanzi.mission_mate.core.datastore.datasource.DefaultDataSourceImpl
+import com.goalpanzi.mission_mate.core.datastore.datasource.MissionDataSource
+import com.goalpanzi.mission_mate.core.datastore.datasource.MissionDataSourceImpl
+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 bindAuthDataSource(
+ authDataSource: AuthDataSourceImpl
+ ): AuthDataSource
+
+ @Binds
+ abstract fun bindDefaultDataSource(
+ defaultDataSource: DefaultDataSourceImpl
+ ): DefaultDataSource
+
+ @Binds
+ abstract fun bindMissionDataSource(
+ missionDataSource: MissionDataSourceImpl
+ ): MissionDataSource
+}
\ No newline at end of file
diff --git a/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/di/DataStoreModule.kt b/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/di/DataStoreModule.kt
new file mode 100644
index 00000000..c879f392
--- /dev/null
+++ b/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/di/DataStoreModule.kt
@@ -0,0 +1,29 @@
+package com.goalpanzi.mission_mate.core.datastore.di
+
+import android.content.Context
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.PreferenceDataStoreFactory
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.preferencesDataStoreFile
+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
+
+private const val AUTH_PREFERENCES = "auth_preferences"
+
+@InstallIn(SingletonComponent::class)
+@Module
+object DataStoreModule {
+ @Singleton
+ @Provides
+ fun provideAuthPreferencesDataStore(
+ @ApplicationContext context: Context
+ ): DataStore {
+ return PreferenceDataStoreFactory.create(
+ produceFile = { context.preferencesDataStoreFile(AUTH_PREFERENCES) }
+ )
+ }
+}
\ No newline at end of file
diff --git a/core/designsystem/build.gradle.kts b/core/designsystem/build.gradle.kts
index 11513e74..589e2760 100644
--- a/core/designsystem/build.gradle.kts
+++ b/core/designsystem/build.gradle.kts
@@ -14,12 +14,10 @@ android {
minSdk = 26
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
- isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
@@ -54,4 +52,6 @@ dependencies {
androidTestImplementation(platform(libs.androidx.compose.bom))
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
+
+ implementation(libs.lottie.compose)
}
\ No newline at end of file
diff --git a/core/designsystem/consumer-rules.pro b/core/designsystem/consumer-rules.pro
deleted file mode 100644
index e69de29b..00000000
diff --git a/core/designsystem/proguard-rules.pro b/core/designsystem/proguard-rules.pro
index 481bb434..109525fd 100644
--- a/core/designsystem/proguard-rules.pro
+++ b/core/designsystem/proguard-rules.pro
@@ -18,4 +18,12 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
-#-renamesourcefileattribute SourceFile
\ No newline at end of file
+#-renamesourcefileattribute SourceFile
+
+-keep class * { *; }
+-keep interface * { *; }
+
+# Keep Dependency Injection Framework related classes and methods
+-keep class dagger.hilt.** { *; }
+-keep class javax.inject.** { *; }
+-keep class javax.annotation.** { *; }
diff --git a/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/component/Button.kt b/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/component/Button.kt
new file mode 100644
index 00000000..fabe6d7c
--- /dev/null
+++ b/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/component/Button.kt
@@ -0,0 +1,113 @@
+package com.goalpanzi.mission_mate.core.designsystem.component
+
+import androidx.annotation.StringRes
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonColors
+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.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.goalpanzi.mission_mate.core.designsystem.R
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorDisabled_FFB3B3B3
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorOrange_FFFF5732
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF
+import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography
+
+enum class MissionMateButtonType(
+ val containerColor : Color,
+ val contentColor : Color = ColorWhite_FFFFFFFF,
+) {
+ ACTIVE(containerColor = ColorOrange_FFFF5732),
+ SECONDARY(containerColor = ColorGray1_FF404249),
+ DISABLED(containerColor = ColorDisabled_FFB3B3B3)
+}
+
+@Composable
+fun MissionMateTextButton(
+ @StringRes textId: Int,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ buttonType: MissionMateButtonType = MissionMateButtonType.ACTIVE,
+ textColor: Color = Color(0xFFFFFFFF),
+ textStyle: TextStyle = MissionMateTypography.body_lg_bold
+) {
+ MissionMateButton(
+ modifier = modifier,
+ buttonType = buttonType,
+ onClick = onClick
+ ) {
+ Text(
+ text = stringResource(id = textId),
+ style = textStyle,
+ color = textColor
+ )
+ }
+}
+
+@Composable
+fun MissionMateButton(
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ buttonType: MissionMateButtonType = MissionMateButtonType.ACTIVE,
+ shape: Shape = RoundedCornerShape(30.dp),
+ contentPadding: PaddingValues = PaddingValues(vertical = 18.dp, horizontal = 30.dp),
+ content: @Composable () -> Unit
+) {
+ Button(
+ modifier = modifier,
+ enabled = buttonType != MissionMateButtonType.DISABLED,
+ shape = shape,
+ colors = ButtonColors(
+ containerColor = buttonType.containerColor,
+ disabledContainerColor = MissionMateButtonType.DISABLED.containerColor,
+ contentColor = buttonType.contentColor,
+ disabledContentColor = MissionMateButtonType.DISABLED.contentColor
+ ),
+ contentPadding = contentPadding,
+ onClick = onClick
+ ) {
+ content()
+ }
+}
+
+@Preview
+@Composable
+fun PreviewTextButton(){
+ MissionMateTextButton(
+ modifier = Modifier.fillMaxWidth(),
+ textId = R.string.app_name,
+ onClick = {}
+ )
+}
+
+@Preview
+@Composable
+fun PreviewSecondaryTextButton(){
+ MissionMateTextButton(
+ modifier = Modifier.fillMaxWidth(),
+ textId = R.string.app_name,
+ buttonType = MissionMateButtonType.SECONDARY,
+ onClick = {}
+ )
+}
+
+
+@Preview
+@Composable
+fun PreviewTextButtonDisabled(){
+ MissionMateTextButton(
+ modifier = Modifier.fillMaxWidth(),
+ textId = R.string.app_name,
+ buttonType = MissionMateButtonType.DISABLED,
+ onClick = {}
+ )
+}
\ No newline at end of file
diff --git a/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/component/Dialog.kt b/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/component/Dialog.kt
new file mode 100644
index 00000000..da7ded9c
--- /dev/null
+++ b/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/component/Dialog.kt
@@ -0,0 +1,239 @@
+package com.goalpanzi.mission_mate.core.designsystem.component
+
+import android.annotation.SuppressLint
+import androidx.annotation.StringRes
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.PaddingValues
+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.padding
+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.Shape
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.DialogProperties
+import com.goalpanzi.mission_mate.core.designsystem.R
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorBlack_FF000000
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray2_FF4F505C
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray3_FF727484
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF
+import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography
+
+@SuppressLint("UnrememberedMutableInteractionSource")
+@Composable
+fun MissionMateDialog(
+ @StringRes titleId: Int,
+ onDismissRequest: () -> Unit,
+ onClickOk: () -> Unit,
+ modifier: Modifier = Modifier,
+ @StringRes descriptionId: Int? = null,
+ @StringRes okTextId: Int? = null,
+ @StringRes cancelTextId: Int? = null,
+ titleStyle: TextStyle = MissionMateTypography.title_xl_bold,
+ descriptionStyle: TextStyle = MissionMateTypography.body_lg_regular,
+ okTextStyle: TextStyle = MissionMateTypography.body_lg_bold,
+ cancelTextStyle: TextStyle = MissionMateTypography.body_lg_bold,
+ shape: Shape = RoundedCornerShape(20.dp),
+ dialogInnerPadding: PaddingValues = PaddingValues(
+ top = 40.dp,
+ bottom = 34.dp,
+ start = 24.dp,
+ end = 24.dp
+ ),
+ dialogProperties: DialogProperties = DialogProperties(
+ usePlatformDefaultWidth = false
+ )
+) {
+ Dialog(
+ properties = dialogProperties,
+ onDismissRequest = onDismissRequest,
+ ) {
+ Column(
+ modifier = modifier
+ .padding(horizontal = 20.dp)
+ .clip(shape)
+ .background(ColorWhite_FFFFFFFF)
+ .padding(dialogInnerPadding),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = stringResource(id = titleId),
+ style = titleStyle,
+ textAlign = TextAlign.Center,
+ color = ColorGray1_FF404249
+ )
+ if (descriptionId != null) {
+ Text(
+ modifier = Modifier.padding(top = 12.dp),
+ text = stringResource(id = descriptionId),
+ style = descriptionStyle,
+ textAlign = TextAlign.Center,
+ color = ColorGray2_FF4F505C
+ )
+ }
+ if (okTextId != null) {
+ MissionMateTextButton(
+ modifier = Modifier
+ .padding(top = 32.dp)
+ .fillMaxWidth(),
+ textId = okTextId,
+ textStyle = okTextStyle,
+ onClick = onClickOk
+ )
+ }
+
+ if (cancelTextId != null) {
+ Text(
+ modifier = Modifier
+ .padding(top = 20.dp)
+ .clickable(
+ interactionSource = MutableInteractionSource(),
+ indication = null,
+ onClick = onDismissRequest
+ ),
+ text = stringResource(id = cancelTextId),
+ style = cancelTextStyle,
+ textAlign = TextAlign.Center,
+ color = ColorGray3_FF727484
+ )
+ }
+ }
+ }
+}
+
+
+@SuppressLint("UnrememberedMutableInteractionSource")
+@Composable
+fun MissionMateDialog(
+ onDismissRequest: () -> Unit,
+ onClickOk: () -> Unit,
+ modifier: Modifier = Modifier,
+ @StringRes okTextId: Int? = null,
+ @StringRes cancelTextId: Int? = null,
+ okTextStyle: TextStyle = MissionMateTypography.body_lg_bold,
+ cancelTextStyle: TextStyle = MissionMateTypography.body_lg_bold,
+ shape: Shape = RoundedCornerShape(20.dp),
+ dialogInnerPadding: PaddingValues = PaddingValues(
+ top = 40.dp,
+ bottom = 34.dp,
+ start = 24.dp,
+ end = 24.dp
+ ),
+ dialogProperties: DialogProperties = DialogProperties(
+ usePlatformDefaultWidth = false
+ ),
+ content: @Composable ColumnScope.() -> Unit
+) {
+ Dialog(
+ properties = dialogProperties,
+ onDismissRequest = onDismissRequest,
+ ) {
+ Column(
+ modifier = modifier
+ .padding(horizontal = 20.dp)
+ .clip(shape)
+ .background(ColorWhite_FFFFFFFF)
+ .padding(dialogInnerPadding),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ content()
+
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ if (okTextId != null) {
+ MissionMateTextButton(
+ modifier = Modifier
+ .fillMaxWidth(),
+ textId = okTextId,
+ textStyle = okTextStyle,
+ onClick = onClickOk
+ )
+ }
+
+ if (cancelTextId != null) {
+ Text(
+ modifier = Modifier
+ .padding(top = 20.dp)
+ .clickable(
+ interactionSource = MutableInteractionSource(),
+ indication = null,
+ onClick = onDismissRequest
+ ),
+ text = stringResource(id = cancelTextId),
+ style = cancelTextStyle,
+ textAlign = TextAlign.Center,
+ color = ColorGray3_FF727484
+ )
+ }
+ }
+
+ }
+ }
+}
+
+
+@Preview
+@Composable
+fun PreviewMissionMateDialog() {
+ Box(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ MissionMateDialog(
+ modifier = Modifier.fillMaxWidth(),
+ titleId = R.string.app_name,
+ descriptionId = R.string.app_name,
+ okTextId = R.string.app_name,
+ cancelTextId = R.string.app_name,
+ onClickOk = {},
+ onDismissRequest = {}
+ )
+ }
+
+}
+
+@Preview
+@Composable
+fun PreviewMissionMateContentDialog() {
+ Box(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ MissionMateDialog(
+ modifier = Modifier.fillMaxWidth(),
+ okTextId = R.string.app_name,
+ cancelTextId = R.string.app_name,
+ onDismissRequest = {},
+ onClickOk = {}
+ ) {
+ Spacer(
+ modifier = Modifier
+ .padding(bottom = 20.dp)
+ .fillMaxWidth()
+ .aspectRatio(1f)
+ .border(
+ 1.dp,
+ ColorBlack_FF000000
+ )
+ )
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/component/LottieImage.kt b/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/component/LottieImage.kt
new file mode 100644
index 00000000..ac109415
--- /dev/null
+++ b/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/component/LottieImage.kt
@@ -0,0 +1,24 @@
+package com.goalpanzi.mission_mate.core.designsystem.component
+
+import androidx.annotation.RawRes
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import com.airbnb.lottie.compose.LottieAnimation
+import com.airbnb.lottie.compose.LottieCompositionSpec
+import com.airbnb.lottie.compose.animateLottieCompositionAsState
+import com.airbnb.lottie.compose.rememberLottieComposition
+
+@Composable
+fun LottieImage(
+ @RawRes lottieRes : Int,
+ modifier: Modifier = Modifier
+) {
+ val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(lottieRes))
+ val progress by animateLottieCompositionAsState(composition)
+ LottieAnimation(
+ modifier = modifier,
+ composition = composition,
+ progress = { progress },
+ )
+}
\ No newline at end of file
diff --git a/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/component/TextField.kt b/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/component/TextField.kt
new file mode 100644
index 00000000..ea87518e
--- /dev/null
+++ b/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/component/TextField.kt
@@ -0,0 +1,225 @@
+package com.goalpanzi.mission_mate.core.designsystem.component
+
+import androidx.annotation.StringRes
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+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.fillMaxWidth
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+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.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+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.goalpanzi.mission_mate.core.designsystem.R
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray2_FF4F505C
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray3_FF727484
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray4_FFE5E5E5
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray5_FFF5F6F9
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorRed_FFFF5858
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF
+import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography
+
+@Composable
+fun MissionMateTextField(
+ text: String,
+ onValueChange: (String) -> Unit,
+ modifier: Modifier = Modifier,
+ @StringRes hintId : Int? = null,
+ @StringRes guidanceId : Int? = null,
+ isError : Boolean = false,
+ useMaxLength : Boolean = false,
+ textStyle: TextStyle = MissionMateTypography.body_lg_regular,
+ hintStyle: TextStyle = MissionMateTypography.body_lg_regular,
+ textColor: Color = ColorGray1_FF404249,
+ hintColor: Color = ColorGray3_FF727484,
+ guidanceColor : Color = Color(0xFF4F505C),
+ errorColor : Color = Color(0xFFFF6464),
+ containerColor: Color = ColorWhite_FFFFFFFF,
+ unfocusedContainerColor: Color = ColorGray5_FFF5F6F9,
+ unfocusedHintColor: Color = ColorGray3_FF727484,
+ borderStroke: BorderStroke = BorderStroke(1.dp, ColorGray5_FFF5F6F9),
+ focusedBorderStroke: BorderStroke = BorderStroke(1.dp, ColorGray4_FFE5E5E5),
+ errorBorderStroke: BorderStroke = BorderStroke(2.dp, ColorRed_FFFF5858),
+ shape: Shape = RoundedCornerShape(12.dp),
+ maxLength : Int = Int.MAX_VALUE,
+ isSingleLine: Boolean = true,
+ visualTransformation: VisualTransformation = VisualTransformation.None,
+ keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
+ keyboardActions: KeyboardActions = KeyboardActions.Default,
+ textAlign : Alignment = Alignment.CenterStart,
+ contentPadding : PaddingValues = PaddingValues(horizontal = 16.dp)
+) {
+ var isFocused by remember { mutableStateOf(false) }
+ BasicTextField(
+ modifier = modifier
+ .onFocusChanged {
+ isFocused = it.isFocused
+ },
+ value = text,
+ singleLine = isSingleLine,
+ textStyle = textStyle.copy(
+ color = textColor
+ ),
+ visualTransformation = visualTransformation,
+ keyboardActions = keyboardActions,
+ keyboardOptions = keyboardOptions,
+ onValueChange = onValueChange,
+ decorationBox = { innerTextField ->
+ Column(
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .heightIn(min = 60.dp)
+ .clip(shape)
+ .border(
+ border = if (isError) errorBorderStroke
+ else if (isFocused) focusedBorderStroke
+ else borderStroke,
+ shape = shape
+ )
+ .background(
+ if(text.isNotEmpty()) containerColor
+ else if (!isFocused) unfocusedContainerColor
+ else containerColor
+ )
+ .padding(contentPadding),
+ contentAlignment = textAlign
+ ) {
+ if(text.isBlank()){
+ Text(
+ text = hintId?.let { stringResource(id = it) } ?: "",
+ style = hintStyle,
+ color = if(isFocused) hintColor else unfocusedHintColor
+ )
+ }
+ innerTextField()
+ }
+ if(guidanceId != null){
+ Text(
+ text = stringResource(id = guidanceId) + if(useMaxLength) "(${text.length}/$maxLength)" else "",
+ style = MissionMateTypography.body_md_regular,
+ color = if(isError) errorColor else guidanceColor
+ )
+ }
+ }
+ }
+ )
+}
+
+@Composable
+fun MissionMateTextFieldGroup(
+ text: String,
+ onValueChange: (String) -> Unit,
+ modifier: Modifier = Modifier,
+ useMaxLength : Boolean = false,
+ @StringRes titleId : Int? = null,
+ @StringRes hintId : Int? = null,
+ @StringRes guidanceId : Int? = null,
+ maxLength : Int = Int.MAX_VALUE,
+ isError : Boolean = false,
+ titleColor : Color = Color(0xFF4F505C),
+ guidanceColor : Color = Color(0xFF4F505C),
+ errorColor : Color = Color(0xFFFF6464),
+ keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
+ keyboardActions: KeyboardActions = KeyboardActions.Default,
+){
+ Column(
+ modifier = modifier,
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ if(titleId != null){
+ Text(
+ text = stringResource(id = titleId),
+ style = MissionMateTypography.body_md_bold,
+ color = titleColor
+ )
+ }
+ MissionMateTextField(
+ text = text,
+ onValueChange = onValueChange,
+ modifier = Modifier.fillMaxWidth(),
+ hintId = hintId,
+ isError = isError,
+ useMaxLength = useMaxLength,
+ guidanceId = guidanceId,
+ maxLength = maxLength,
+ guidanceColor = guidanceColor,
+ errorColor = errorColor,
+ keyboardOptions = keyboardOptions,
+ keyboardActions = keyboardActions
+ )
+ }
+}
+
+@Composable
+@Preview
+fun PreviewMissionMateTextField(){
+ MissionMateTextField(
+ modifier = Modifier.fillMaxWidth(),
+ text = "",
+ hintId = R.string.app_name,
+ onValueChange = {}
+ )
+}
+
+@Composable
+@Preview
+fun PreviewMissionMateTextFieldGroup(){
+ MissionMateTextFieldGroup(
+ text = "Goalpanzi",
+ onValueChange = {},
+ titleId = R.string.app_name,
+ guidanceId = R.string.app_name,
+ )
+}
+
+@Composable
+@Preview
+fun PreviewMissionMateTextFieldGroupWithMaxLength(){
+ MissionMateTextFieldGroup(
+ text = "Goalpanzi",
+ onValueChange = {},
+ titleId = R.string.app_name,
+ guidanceId = R.string.app_name,
+ useMaxLength = true,
+ maxLength = 12
+ )
+}
+
+@Composable
+@Preview
+fun PreviewMissionMateTextFieldGroupError(){
+ MissionMateTextFieldGroup(
+ text = "Goalpanzi",
+ onValueChange = {},
+ titleId = R.string.app_name,
+ guidanceId = R.string.app_name,
+ isError = true
+ )
+}
+
diff --git a/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/ext/Modifier.kt b/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/ext/Modifier.kt
new file mode 100644
index 00000000..be858e39
--- /dev/null
+++ b/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/ext/Modifier.kt
@@ -0,0 +1,57 @@
+package com.goalpanzi.mission_mate.core.designsystem.ext
+
+import android.graphics.BlurMaskFilter
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Paint
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.drawOutline
+import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+fun Modifier.dropShadow(
+ shape: Shape,
+ color: Color = Color.Black.copy(0.1f),
+ blur: Dp = 4.dp,
+ offsetY: Dp = 0.dp,
+ offsetX: Dp = 0.dp,
+ spread: Dp = 0.dp
+) = this.drawBehind {
+
+ val shadowSize = Size(size.width + spread.toPx(), size.height + spread.toPx())
+ val shadowOutline = shape.createOutline(shadowSize, layoutDirection, this)
+
+ val paint = Paint()
+ paint.color = color
+
+ if (blur.toPx() > 0) {
+ paint.asFrameworkPaint().apply {
+ maskFilter = BlurMaskFilter(blur.toPx(), BlurMaskFilter.Blur.NORMAL)
+ }
+ }
+
+ drawIntoCanvas { canvas ->
+ canvas.save()
+ canvas.translate(offsetX.toPx(), offsetY.toPx())
+ canvas.drawOutline(shadowOutline, paint)
+ canvas.restore()
+ }
+}
+
+fun Modifier.clickableWithoutRipple(
+ onClick : () -> Unit,
+) : Modifier {
+ return then(
+ Modifier.clickable(
+ interactionSource = MutableInteractionSource(),
+ indication = null,
+ onClick = onClick
+ )
+ )
+
+}
\ No newline at end of file
diff --git a/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/theme/Color.kt b/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/theme/Color.kt
index 10bade95..850fcb6e 100644
--- a/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/theme/Color.kt
+++ b/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/theme/Color.kt
@@ -1,11 +1,37 @@
package com.goalpanzi.mission_mate.core.designsystem.theme
+import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
-val Purple80 = Color(0xFFD0BCFF)
-val PurpleGrey80 = Color(0xFFCCC2DC)
-val Pink80 = Color(0xFFEFB8C8)
+val ColorWhite_FFFFFFFF = Color(0xFFFFFFFF)
+val ColorBlack_FF000000 = Color(0xFF000000)
+val ColorGray1_FF404249 = Color(0xFF404249)
+val ColorGray2_FF4F505C = Color(0xFF4F505C)
+val ColorGray3_FF727484 = Color(0xFF727484)
-val Purple40 = Color(0xFF6650a4)
-val PurpleGrey40 = Color(0xFF625b71)
-val Pink40 = Color(0xFF7D5260)
\ No newline at end of file
+val ColorGray4_FFE5E5E5 = Color(0xFFE5E5E5)
+val ColorGray5_FFF5F6F9 = Color(0xFFF5F6F9)
+val ColorGray5_80F5F6F9_opacity_50 = Color(0x80F5F6F9)
+val ColorDisabled_FFB3B3B3 = Color(0xFFB3B3B3)
+
+val ColorRed_FFFF5858 = Color(0xFFFF5858)
+val ColorOrange_FFFF5732 = Color(0xFFFF5732)
+val ColorPink_FFFFE4E4 = Color(0xFFFFE4E4)
+val ColorLightYellow_FFFFE59A = Color(0xFFFFE59A)
+val ColorYellow_FFFBBC05 = Color(0xFFFBBC05)
+
+val ColorLightGreen_FFC2E792 = Color(0xFFC2E792)
+val ColorLightBlue_FFBCE7FF = Color(0xFFBCE7FF)
+val ColorBlue_FFBFD7FF = Color(0xFFBFD7FF)
+val ColorLightBrown_FFF7D8B3 = Color(0xFFF7D8B3)
+
+val ColorFFF5EDEA = Color(0xFFF5EDEA)
+
+val ColorFFFF5F3C = Color(0xFFFF5F3C)
+val ColorFFFFAE50 = Color(0xFFFFAE50)
+
+val OrangeGradient_FFFF5F3C_FFFFAE50 = Brush.verticalGradient(
+ listOf(ColorFFFF5F3C, ColorFFFFAE50)
+)
+
+val Color_FFFF5632 = Color(0xFFFF5632)
\ No newline at end of file
diff --git a/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/theme/Font.kt b/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/theme/Font.kt
new file mode 100644
index 00000000..d6040f24
--- /dev/null
+++ b/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/theme/Font.kt
@@ -0,0 +1,12 @@
+package com.goalpanzi.mission_mate.core.designsystem.theme
+
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontWeight
+import com.goalpanzi.mission_mate.core.designsystem.R
+
+val pretendardFamily = FontFamily(
+ Font(R.font.pretendard_bold, FontWeight.Bold, FontStyle.Normal),
+ Font(R.font.pretendard_regular, FontWeight.Normal, FontStyle.Normal)
+)
\ No newline at end of file
diff --git a/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/theme/Theme.kt b/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/theme/Theme.kt
index a29f23ca..b106399a 100644
--- a/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/theme/Theme.kt
+++ b/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/theme/Theme.kt
@@ -1,6 +1,5 @@
package com.goalpanzi.mission_mate.core.designsystem.theme
-import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
@@ -12,25 +11,15 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
- primary = Purple80,
- secondary = PurpleGrey80,
- tertiary = Pink80
+ primary = ColorOrange_FFFF5732,
+ secondary = ColorYellow_FFFBBC05,
+ tertiary = ColorBlue_FFBFD7FF
)
private val LightColorScheme = lightColorScheme(
- primary = Purple40,
- secondary = PurpleGrey40,
- tertiary = Pink40
-
- /* Other default colors to override
- background = Color(0xFFFFFBFE),
- surface = Color(0xFFFFFBFE),
- onPrimary = Color.White,
- onSecondary = Color.White,
- onTertiary = Color.White,
- onBackground = Color(0xFF1C1B1F),
- onSurface = Color(0xFF1C1B1F),
- */
+ primary = ColorOrange_FFFF5732,
+ secondary = ColorYellow_FFFBBC05,
+ tertiary = ColorBlue_FFBFD7FF
)
@Composable
@@ -52,7 +41,6 @@ fun MissionmateTheme(
MaterialTheme(
colorScheme = colorScheme,
- typography = Typography,
content = content
)
}
\ No newline at end of file
diff --git a/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/theme/Type.kt b/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/theme/Type.kt
index 742189b3..18f6b60a 100644
--- a/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/theme/Type.kt
+++ b/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/theme/Type.kt
@@ -1,34 +1,150 @@
package com.goalpanzi.mission_mate.core.designsystem.theme
-import androidx.compose.material3.Typography
+import androidx.compose.ui.text.PlatformTextStyle
import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.unit.sp
-// Set of Material typography styles to start with
-val Typography = Typography(
- bodyLarge = TextStyle(
- fontFamily = FontFamily.Default,
+object MissionMateTypography {
+ private val DefaultTextStyle = TextStyle(
+ fontFamily = pretendardFamily,
+ platformStyle = PlatformTextStyle(
+ includeFontPadding = false
+ ),
+ lineHeightStyle = LineHeightStyle(
+ alignment = LineHeightStyle.Alignment.Center,
+ trim = LineHeightStyle.Trim.None
+ )
+ )
+
+ private val heading_xl = DefaultTextStyle.copy(
+ fontSize = 60.sp,
+ lineHeight = 78.sp
+ )
+
+ val heading_xl_bold = heading_xl.copy(
+ fontWeight = FontWeight.Bold,
+ )
+
+ val heading_xl_regular = heading_xl.copy(
fontWeight = FontWeight.Normal,
+ )
+
+ private val heading_lg = DefaultTextStyle.copy(
+ fontSize = 48.sp,
+ lineHeight = 64.sp
+ )
+
+ val heading_lg_bold = heading_lg.copy(
+ fontWeight = FontWeight.Bold
+ )
+
+ val heading_lg_regular = heading_lg.copy(
+ fontWeight = FontWeight.Normal
+ )
+
+ private val heading_md = DefaultTextStyle.copy(
+ fontSize = 34.sp,
+ lineHeight = 48.sp
+ )
+
+ val heading_md_bold = heading_md.copy(
+ fontWeight = FontWeight.Bold
+ )
+
+ val heading_md_regular = heading_md.copy(
+ fontWeight = FontWeight.Normal
+ )
+
+ private val heading_sm = DefaultTextStyle.copy(
+ fontSize = 30.sp,
+ lineHeight = 40.sp
+ )
+
+ val heading_sm_bold = heading_sm.copy(
+ fontWeight = FontWeight.Bold
+ )
+
+ val heading_sm_regular = heading_sm.copy(
+ fontWeight = FontWeight.Normal
+ )
+
+ private val title_xl = DefaultTextStyle.copy(
+ fontSize = 24.sp,
+ lineHeight = 34.sp
+ )
+
+ val title_xl_bold = title_xl.copy(
+ fontWeight = FontWeight.Bold
+ )
+
+ val title_xl_regular = title_xl.copy(
+ fontWeight = FontWeight.Normal
+ )
+
+ private val title_lg = DefaultTextStyle.copy(
+ fontSize = 20.sp,
+ lineHeight = 30.sp
+ )
+
+ val title_lg_bold = title_lg.copy(
+ fontWeight = FontWeight.Bold
+ )
+
+ val title_lg_regular = title_lg.copy(
+ fontWeight = FontWeight.Normal
+ )
+
+ private val body_xl = DefaultTextStyle.copy(
+ fontSize = 18.sp,
+ lineHeight = 28.sp
+ )
+
+ val body_xl_bold = body_xl.copy(
+ fontWeight = FontWeight.Bold
+ )
+
+ val body_xl_regular = body_xl.copy(
+ fontWeight = FontWeight.Normal
+ )
+
+ private val body_lg = DefaultTextStyle.copy(
fontSize = 16.sp,
- lineHeight = 24.sp,
- letterSpacing = 0.5.sp
+ lineHeight = 24.sp
)
- /* Other default text styles to override
- titleLarge = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Normal,
- fontSize = 22.sp,
- lineHeight = 28.sp,
- letterSpacing = 0.sp
- ),
- labelSmall = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Medium,
- fontSize = 11.sp,
- lineHeight = 16.sp,
- letterSpacing = 0.5.sp
- )
- */
-)
\ No newline at end of file
+
+ val body_lg_bold = body_lg.copy(
+ fontWeight = FontWeight.Bold
+ )
+
+ val body_lg_regular = body_lg.copy(
+ fontWeight = FontWeight.Normal
+ )
+
+ private val body_md = DefaultTextStyle.copy(
+ fontSize = 14.sp,
+ lineHeight = 20.sp
+ )
+
+ val body_md_bold = body_md.copy(
+ fontWeight = FontWeight.Bold
+ )
+
+ val body_md_regular = body_md.copy(
+ fontWeight = FontWeight.Normal
+ )
+
+ private val body_sm = DefaultTextStyle.copy(
+ fontSize = 12.sp,
+ lineHeight = 18.sp
+ )
+
+ val body_sm_bold = body_sm.copy(
+ fontWeight = FontWeight.Bold
+ )
+
+ val body_sm_regular = body_sm.copy(
+ fontWeight = FontWeight.Normal
+ )
+}
\ No newline at end of file
diff --git a/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/theme/component/AppTopBar.kt b/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/theme/component/AppTopBar.kt
new file mode 100644
index 00000000..276fb54e
--- /dev/null
+++ b/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/theme/component/AppTopBar.kt
@@ -0,0 +1,130 @@
+package com.goalpanzi.mission_mate.core.designsystem.theme.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+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 com.goalpanzi.mission_mate.core.designsystem.R
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray3_FF727484
+import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography
+
+enum class NavigationType {
+ BACK, NONE
+}
+
+@Composable
+fun MissionMateTopAppBar(
+ modifier: Modifier = Modifier,
+ title: String? = null,
+ navigationType: NavigationType,
+ onNavigationClick: () -> Unit = {},
+ containerColor: Color = MaterialTheme.colorScheme.surfaceDim,
+ contentColor: Color = MaterialTheme.colorScheme.onSurface,
+ leftActionButtons: @Composable (() -> Unit)? = null,
+ rightActionButtons: @Composable () -> Unit = {},
+) {
+ CompositionLocalProvider(LocalContentColor provides contentColor) {
+ val icon: @Composable (Modifier, imageVector: ImageVector) -> Unit =
+ { modifier, imageVector ->
+ IconButton(
+ onClick = onNavigationClick,
+ modifier = modifier.wrapContentSize()
+ ) {
+ Icon(
+ imageVector = imageVector,
+ contentDescription = "",
+ tint = ColorGray3_FF727484
+ )
+ }
+ }
+
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(56.dp)
+ .background(containerColor)
+ .padding(horizontal = 12.dp),
+ contentAlignment = Alignment.CenterStart
+ ) {
+ when (navigationType) {
+ NavigationType.BACK -> {
+ icon(
+ Modifier.align(Alignment.CenterStart),
+ ImageVector.vectorResource(id = R.drawable.ic_arrow_left)
+ )
+ }
+
+ NavigationType.NONE -> {
+ leftActionButtons?.invoke()
+ Box(
+ modifier = modifier.height(48.dp)
+ )
+ }
+ }
+ title?.let {
+ Text(
+ text = it,
+ modifier = Modifier.align(Alignment.Center),
+ style = MissionMateTypography.title_lg_bold,
+ color = ColorGray1_FF404249
+ )
+ }
+ Box(
+ modifier = Modifier
+ .align(Alignment.CenterEnd)
+ ) {
+ rightActionButtons()
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+fun AppTopBarBackPreview() {
+ MissionMateTopAppBar(
+ navigationType = NavigationType.BACK,
+ onNavigationClick = {},
+ containerColor = Color.White,
+ title = stringResource(id = R.string.app_name)
+ )
+}
+
+@Preview
+@Composable
+fun AppTopBarNonePreview() {
+ MissionMateTopAppBar(
+ navigationType = NavigationType.NONE,
+ onNavigationClick = {},
+ containerColor = Color.White,
+ rightActionButtons = {
+ IconButton(
+ onClick = {},
+ modifier = Modifier.wrapContentSize()
+ ) {
+ Icon(
+ imageVector = ImageVector.vectorResource(id = R.drawable.ic_setting),
+ contentDescription = "",
+ )
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/core/designsystem/src/main/res/drawable/background_bear.png b/core/designsystem/src/main/res/drawable/background_bear.png
new file mode 100644
index 00000000..966017e9
Binary files /dev/null and b/core/designsystem/src/main/res/drawable/background_bear.png differ
diff --git a/core/designsystem/src/main/res/drawable/background_bird.png b/core/designsystem/src/main/res/drawable/background_bird.png
new file mode 100644
index 00000000..d8ae931d
Binary files /dev/null and b/core/designsystem/src/main/res/drawable/background_bird.png differ
diff --git a/core/designsystem/src/main/res/drawable/background_cat.png b/core/designsystem/src/main/res/drawable/background_cat.png
new file mode 100644
index 00000000..ea702be8
Binary files /dev/null and b/core/designsystem/src/main/res/drawable/background_cat.png differ
diff --git a/core/designsystem/src/main/res/drawable/background_dog.png b/core/designsystem/src/main/res/drawable/background_dog.png
new file mode 100644
index 00000000..f5dd367f
Binary files /dev/null and b/core/designsystem/src/main/res/drawable/background_dog.png differ
diff --git a/core/designsystem/src/main/res/drawable/background_jeju.png b/core/designsystem/src/main/res/drawable/background_jeju.png
new file mode 100644
index 00000000..b9c2f1b4
Binary files /dev/null and b/core/designsystem/src/main/res/drawable/background_jeju.png differ
diff --git a/core/designsystem/src/main/res/drawable/background_jeju_full.png b/core/designsystem/src/main/res/drawable/background_jeju_full.png
new file mode 100644
index 00000000..71717c1c
Binary files /dev/null and b/core/designsystem/src/main/res/drawable/background_jeju_full.png differ
diff --git a/core/designsystem/src/main/res/drawable/background_panda.png b/core/designsystem/src/main/res/drawable/background_panda.png
new file mode 100644
index 00000000..ca4f56c1
Binary files /dev/null and b/core/designsystem/src/main/res/drawable/background_panda.png differ
diff --git a/core/designsystem/src/main/res/drawable/background_rabbit.png b/core/designsystem/src/main/res/drawable/background_rabbit.png
new file mode 100644
index 00000000..f61b8949
Binary files /dev/null and b/core/designsystem/src/main/res/drawable/background_rabbit.png differ
diff --git a/core/designsystem/src/main/res/drawable/ic_add_user.xml b/core/designsystem/src/main/res/drawable/ic_add_user.xml
new file mode 100644
index 00000000..cb4434b4
--- /dev/null
+++ b/core/designsystem/src/main/res/drawable/ic_add_user.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
diff --git a/core/designsystem/src/main/res/drawable/ic_arrow_left.xml b/core/designsystem/src/main/res/drawable/ic_arrow_left.xml
new file mode 100644
index 00000000..0ab79f80
--- /dev/null
+++ b/core/designsystem/src/main/res/drawable/ic_arrow_left.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/core/designsystem/src/main/res/drawable/ic_arrow_right.xml b/core/designsystem/src/main/res/drawable/ic_arrow_right.xml
new file mode 100644
index 00000000..c2730f6f
--- /dev/null
+++ b/core/designsystem/src/main/res/drawable/ic_arrow_right.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/core/designsystem/src/main/res/drawable/ic_close.xml b/core/designsystem/src/main/res/drawable/ic_close.xml
new file mode 100644
index 00000000..aebe3377
--- /dev/null
+++ b/core/designsystem/src/main/res/drawable/ic_close.xml
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/core/designsystem/src/main/res/drawable/ic_creating_board.xml b/core/designsystem/src/main/res/drawable/ic_creating_board.xml
new file mode 100644
index 00000000..57d2aa51
--- /dev/null
+++ b/core/designsystem/src/main/res/drawable/ic_creating_board.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/core/designsystem/src/main/res/drawable/ic_flag.xml b/core/designsystem/src/main/res/drawable/ic_flag.xml
new file mode 100644
index 00000000..2830301a
--- /dev/null
+++ b/core/designsystem/src/main/res/drawable/ic_flag.xml
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/core/designsystem/src/main/res/drawable/ic_invitation_friend.xml b/core/designsystem/src/main/res/drawable/ic_invitation_friend.xml
new file mode 100644
index 00000000..7ce3efb8
--- /dev/null
+++ b/core/designsystem/src/main/res/drawable/ic_invitation_friend.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
diff --git a/core/designsystem/src/main/res/drawable/ic_setting.xml b/core/designsystem/src/main/res/drawable/ic_setting.xml
new file mode 100644
index 00000000..cbe860e1
--- /dev/null
+++ b/core/designsystem/src/main/res/drawable/ic_setting.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/core/designsystem/src/main/res/drawable/ic_time.xml b/core/designsystem/src/main/res/drawable/ic_time.xml
new file mode 100644
index 00000000..6aa3accc
--- /dev/null
+++ b/core/designsystem/src/main/res/drawable/ic_time.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/core/designsystem/src/main/res/drawable/image_jeju_success.png b/core/designsystem/src/main/res/drawable/image_jeju_success.png
new file mode 100644
index 00000000..48b1602d
Binary files /dev/null and b/core/designsystem/src/main/res/drawable/image_jeju_success.png differ
diff --git a/core/designsystem/src/main/res/drawable/image_onboarding_jeju.png b/core/designsystem/src/main/res/drawable/image_onboarding_jeju.png
new file mode 100644
index 00000000..71af63aa
Binary files /dev/null and b/core/designsystem/src/main/res/drawable/image_onboarding_jeju.png differ
diff --git a/core/designsystem/src/main/res/drawable/img_app_logo.png b/core/designsystem/src/main/res/drawable/img_app_logo.png
new file mode 100644
index 00000000..fdc4fc94
Binary files /dev/null and b/core/designsystem/src/main/res/drawable/img_app_logo.png differ
diff --git a/core/designsystem/src/main/res/drawable/img_app_title.png b/core/designsystem/src/main/res/drawable/img_app_title.png
new file mode 100644
index 00000000..98b64573
Binary files /dev/null and b/core/designsystem/src/main/res/drawable/img_app_title.png differ
diff --git a/core/designsystem/src/main/res/drawable/img_bear_default.png b/core/designsystem/src/main/res/drawable/img_bear_default.png
new file mode 100644
index 00000000..dbdedbf6
Binary files /dev/null and b/core/designsystem/src/main/res/drawable/img_bear_default.png differ
diff --git a/core/designsystem/src/main/res/drawable/img_bear_selected.png b/core/designsystem/src/main/res/drawable/img_bear_selected.png
new file mode 100644
index 00000000..f60e3cee
Binary files /dev/null and b/core/designsystem/src/main/res/drawable/img_bear_selected.png differ
diff --git a/core/designsystem/src/main/res/drawable/img_bird_default.png b/core/designsystem/src/main/res/drawable/img_bird_default.png
new file mode 100644
index 00000000..04bd7534
Binary files /dev/null and b/core/designsystem/src/main/res/drawable/img_bird_default.png differ
diff --git a/core/designsystem/src/main/res/drawable/img_bird_selected.png b/core/designsystem/src/main/res/drawable/img_bird_selected.png
new file mode 100644
index 00000000..81187b03
Binary files /dev/null and b/core/designsystem/src/main/res/drawable/img_bird_selected.png differ
diff --git a/core/designsystem/src/main/res/drawable/img_cat_default.png b/core/designsystem/src/main/res/drawable/img_cat_default.png
new file mode 100644
index 00000000..a03016c3
Binary files /dev/null and b/core/designsystem/src/main/res/drawable/img_cat_default.png differ
diff --git a/core/designsystem/src/main/res/drawable/img_cat_selected.png b/core/designsystem/src/main/res/drawable/img_cat_selected.png
new file mode 100644
index 00000000..17e844eb
Binary files /dev/null and b/core/designsystem/src/main/res/drawable/img_cat_selected.png differ
diff --git a/core/designsystem/src/main/res/drawable/img_dog_default.png b/core/designsystem/src/main/res/drawable/img_dog_default.png
new file mode 100644
index 00000000..812a9d91
Binary files /dev/null and b/core/designsystem/src/main/res/drawable/img_dog_default.png differ
diff --git a/core/designsystem/src/main/res/drawable/img_dog_selected.png b/core/designsystem/src/main/res/drawable/img_dog_selected.png
new file mode 100644
index 00000000..63428f3c
Binary files /dev/null and b/core/designsystem/src/main/res/drawable/img_dog_selected.png differ
diff --git a/core/designsystem/src/main/res/drawable/img_jeju_theme.png b/core/designsystem/src/main/res/drawable/img_jeju_theme.png
new file mode 100644
index 00000000..a1fd5147
Binary files /dev/null and b/core/designsystem/src/main/res/drawable/img_jeju_theme.png differ
diff --git a/core/designsystem/src/main/res/drawable/img_login_bottom_animals.png b/core/designsystem/src/main/res/drawable/img_login_bottom_animals.png
new file mode 100644
index 00000000..6022232c
Binary files /dev/null and b/core/designsystem/src/main/res/drawable/img_login_bottom_animals.png differ
diff --git a/core/designsystem/src/main/res/drawable/img_panda_default.png b/core/designsystem/src/main/res/drawable/img_panda_default.png
new file mode 100644
index 00000000..1e23946a
Binary files /dev/null and b/core/designsystem/src/main/res/drawable/img_panda_default.png differ
diff --git a/core/designsystem/src/main/res/drawable/img_panda_selected.png b/core/designsystem/src/main/res/drawable/img_panda_selected.png
new file mode 100644
index 00000000..0e9846e1
Binary files /dev/null and b/core/designsystem/src/main/res/drawable/img_panda_selected.png differ
diff --git a/core/designsystem/src/main/res/drawable/img_rabbit_default.png b/core/designsystem/src/main/res/drawable/img_rabbit_default.png
new file mode 100644
index 00000000..94ed5e2a
Binary files /dev/null and b/core/designsystem/src/main/res/drawable/img_rabbit_default.png differ
diff --git a/core/designsystem/src/main/res/drawable/img_rabbit_selected.png b/core/designsystem/src/main/res/drawable/img_rabbit_selected.png
new file mode 100644
index 00000000..952d1804
Binary files /dev/null and b/core/designsystem/src/main/res/drawable/img_rabbit_selected.png differ
diff --git a/core/designsystem/src/main/res/font/pretendard_bold.otf b/core/designsystem/src/main/res/font/pretendard_bold.otf
new file mode 100644
index 00000000..8e5e30a2
Binary files /dev/null and b/core/designsystem/src/main/res/font/pretendard_bold.otf differ
diff --git a/core/designsystem/src/main/res/font/pretendard_regular.otf b/core/designsystem/src/main/res/font/pretendard_regular.otf
new file mode 100644
index 00000000..08bf4cfc
Binary files /dev/null and b/core/designsystem/src/main/res/font/pretendard_regular.otf differ
diff --git a/core/designsystem/src/main/res/raw/animation_celebration.json b/core/designsystem/src/main/res/raw/animation_celebration.json
new file mode 100644
index 00000000..23444f15
--- /dev/null
+++ b/core/designsystem/src/main/res/raw/animation_celebration.json
@@ -0,0 +1 @@
+{"v":"5.6.6","fr":60,"ip":0,"op":141,"w":940,"h":752,"nm":"Explode_Export","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 6","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[187.5,368,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":1500,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Fill 94","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[-5.546,-348.328,0],"to":[0,15.417,0],"ti":[0,-30.417,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":34.15,"s":[-5.546,-255.828,0],"to":[0,30.417,0],"ti":[0,-25,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":70.928,"s":[-5.546,-165.828,0],"to":[0,25,0],"ti":[0,-30,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":102.451,"s":[-5.546,-105.828,0],"to":[0,30,0],"ti":[0,-61.667,0]},{"i":{"x":0.667,"y":0.407},"o":{"x":0.333,"y":0},"t":135.727,"s":[-5.546,14.172,0],"to":[0,40.458,0],"ti":[0,-59.475,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0.775},"t":153,"s":[-5.546,135.369,0],"to":[0,31.177,0],"ti":[0,-26.94,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":177,"s":[-5.546,264.172,0],"to":[0,78.333,0],"ti":[0,-53.333,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":204.027,"s":[-5.546,484.172,0],"to":[0,53.333,0],"ti":[13.333,-30,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":230.297,"s":[-5.546,584.172,0],"to":[-13.333,30,0],"ti":[13.333,-31.667,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":257.441,"s":[-85.546,664.172,0],"to":[-13.333,31.667,0],"ti":[0,-46.667,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":294.219,"s":[-85.546,774.172,0],"to":[0,46.667,0],"ti":[0,-46.667,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":348.51,"s":[-85.546,944.172,0],"to":[0,46.667,0],"ti":[0,-46.667,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":387.039,"s":[-85.546,1054.172,0],"to":[0,46.667,0],"ti":[0,-50,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":423.816,"s":[-85.546,1224.172,0],"to":[0,50,0],"ti":[0,-43.333,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":467.598,"s":[-85.546,1354.172,0],"to":[0,43.333,0],"ti":[0,-55,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":508.754,"s":[-85.546,1484.172,0],"to":[0,55,0],"ti":[0,-40,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":539.402,"s":[-85.546,1684.172,0],"to":[0,40,0],"ti":[0,-33.333,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":569.174,"s":[-85.546,1724.172,0],"to":[0,33.333,0],"ti":[15,-33.333,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":621.713,"s":[-85.546,1884.172,0],"to":[-15,33.333,0],"ti":[15,-21.667,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":661.992,"s":[-175.546,1924.172,0],"to":[-15,21.667,0],"ti":[0,-46.667,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":688.264,"s":[-175.546,2014.172,0],"to":[0,46.667,0],"ti":[0,-51.667,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":727.668,"s":[-175.546,2204.172,0],"to":[0,51.667,0],"ti":[0,-48.333,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":751.311,"s":[-175.546,2324.172,0],"to":[0,48.333,0],"ti":[0,-45,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":785.461,"s":[-175.546,2494.172,0],"to":[0,45,0],"ti":[-10,-28.333,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":809.979,"s":[-175.546,2594.172,0],"to":[10,28.333,0],"ti":[-10,-11.667,0]},{"t":838,"s":[-115.546,2664.172,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[65,65,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.01,0]],"o":[[0,0]],"v":[[4.118,-8.704]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[-0.18,0.47],[0,0],[0,0],[0.41,0.34],[0,0],[0,0],[0.41,-0.29],[0,0],[0,0],[-0.13,-0.51],[0,0],[0,0],[-0.52,-0.01],[0,0]],"o":[[-0.29,-0.42],[0,0],[0,0],[-0.5,0.16],[0,0],[0,0],[-0.01,0.5],[0,0],[0,0],[0.5,0.17],[0,0],[0,0],[0.3,-0.41],[0,0],[0,0]],"v":[[11.007,-1.504],[10.837,-2.924],[15.087,-14.204],[3.887,-10.584],[2.427,-10.864],[-6.643,-18.204],[-7.023,-6.144],[-7.703,-4.894],[-17.643,1.886],[-6.603,5.666],[-5.593,6.756],[-2.633,18.206],[4.547,8.566],[5.847,7.936],[17.647,8.326]],"c":true},"ix":2},"nm":"Path 7","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.97647100687,0.776471018791,0.02352900058,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":0,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":38.527,"s":[-101,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":84.062,"s":[-101,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":137.477,"s":[100,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":181.26,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":226.793,"s":[104,0]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":261.82,"s":[104,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":295.971,"s":[104,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":329.246,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":363.396,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":404.551,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":441.328,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":480.734,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":510.506,"s":[-100,0]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":538.525,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":573.553,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":603.324,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":636.6,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":662.869,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":692.641,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":725.916,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":753.061,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":783.709,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":816.107,"s":[100,-100]},{"t":838,"s":[-100,-100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[25]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":236.428,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":431.697,"s":[25]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":612.957,"s":[15]},{"t":838,"s":[25]}],"ix":4},"sa":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":838,"s":[5760]}],"ix":5},"nm":"Transform"}],"nm":"Fill 17","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"Null 3","parent":1,"sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[0,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":98.949,"s":[120,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":183.887,"s":[0,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":259.193,"s":[240,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":339.754,"s":[40,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":373.029,"s":[160,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":416.811,"s":[0,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":491.242,"s":[160,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":514.883,"s":[80,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":573.553,"s":[240,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":629.594,"s":[160,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":661.994,"s":[280,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":710.154,"s":[0,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":753.938,"s":[200,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":784.586,"s":[280,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"t":838,"s":[-40,-14,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":1549,"st":0,"bm":0}]},{"id":"comp_1","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 6","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[187.5,368,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":1500,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Fill 97","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.562,"y":1},"o":{"x":0.198,"y":0},"t":0,"s":[-148.874,-341.028,0],"to":[0,24.201,0],"ti":[0,-48.268,0]},{"i":{"x":0.653,"y":1},"o":{"x":0.31,"y":0},"t":34,"s":[-178.874,-229.131,0],"to":[0,26.371,0],"ti":[0,-30.606,0]},{"i":{"x":0.676,"y":1},"o":{"x":0.335,"y":0},"t":67.422,"s":[-208.874,-143.145,0],"to":[0,30.58,0],"ti":[0,-32.729,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.356,"y":0},"t":96.736,"s":[-208.874,-47.662,0],"to":[0,47.125,0],"ti":[0,-46.326,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.44,"y":0},"t":131.211,"s":[-208.874,94.065,0],"to":[0,111.388,0],"ti":[0,-45.734,0]},{"i":{"x":0.544,"y":0.637},"o":{"x":0.196,"y":0},"t":185,"s":[-178.874,351.302,0],"to":[0,14.751,0],"ti":[7.083,-74.622,0]},{"i":{"x":0.636,"y":1},"o":{"x":0.3,"y":0.265},"t":215,"s":[-116.508,489.887,0],"to":[-1.475,15.536,0],"ti":[0,-17.658,0]},{"i":{"x":0.488,"y":1},"o":{"x":0.167,"y":0},"t":237.93,"s":[-208.874,539.719,0],"to":[0,40.921,0],"ti":[0,-50.858,0]},{"i":{"x":0.595,"y":1},"o":{"x":0.372,"y":0},"t":276.418,"s":[-208.874,677.896,0],"to":[0,57.581,0],"ti":[0,-67.539,0]},{"i":{"x":0.68,"y":1},"o":{"x":0.366,"y":0},"t":328.902,"s":[-208.874,866.313,0],"to":[0,52.132,0],"ti":[0,-56.507,0]},{"i":{"x":0.737,"y":1},"o":{"x":0.292,"y":0},"t":377.889,"s":[-208.874,1029.61,0],"to":[0,56.974,0],"ti":[0,-60.036,0]},{"i":{"x":0.614,"y":0.63},"o":{"x":0.206,"y":0},"t":428.621,"s":[-208.874,1205.472,0],"to":[0,44.588,0],"ti":[0,-45.608,0]},{"i":{"x":0.76,"y":1},"o":{"x":0.388,"y":0.423},"t":465,"s":[-118.874,1340.909,0],"to":[0,26.971,0],"ti":[0,-27.169,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.522,"y":0},"t":490.729,"s":[-178.874,1422.148,0],"to":[0,54.309,0],"ti":[0,-54.395,0]},{"i":{"x":0.586,"y":1},"o":{"x":0.167,"y":0},"t":533.592,"s":[-208.874,1585.44,0],"to":[0,65.449,0],"ti":[0,-64.072,0]},{"i":{"x":0.236,"y":1},"o":{"x":0.167,"y":0},"t":586.949,"s":[-208.874,1780.132,0],"to":[0,58.702,0],"ti":[0,-56.228,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":641.184,"s":[-208.874,1952.841,0],"to":[0,69.785,0],"ti":[0,-63.798,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.685,"y":0},"t":689.295,"s":[-208.874,2153.817,0],"to":[0,55.302,0],"ti":[0,-49.112,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":735.654,"s":[-208.874,2310.83,0],"to":[0,57.546,0],"ti":[0,-46.707,0]},{"i":{"x":0.511,"y":1},"o":{"x":0.281,"y":0},"t":781,"s":[-148.874,2467.84,0],"to":[0,28.636,0],"ti":[0,-13.385,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":809,"s":[-178.874,2590.308,0],"to":[0,63.954,0],"ti":[0,0,0]},{"t":838,"s":[-208.874,2690.802,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[65,65,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-4.722,-2.013],[-0.688,5.385],[3.87,2.274],[0.581,-5.397]],"o":[[3.92,1.671],[0.662,-5.187],[-4.388,-2.579],[-0.514,4.772]],"v":[[-5.961,4.675],[3.474,-2.806],[-2.929,-16.147],[-13.338,-9.733]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9176470588235294,0,0.2627450980392157,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":0,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":34,"s":[-100,0]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":66,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":106,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":140,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":178,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":208,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":242,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":280,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":311,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":346,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":383,"s":[100,-100]},{"i":{"x":[0.833,0.833],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0,0]},"t":408,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":453,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":497,"s":[-101,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":549,"s":[-101,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":610,"s":[100,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":660,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":712,"s":[104,0]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":752,"s":[104,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":791,"s":[104,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":829,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":868,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":915,"s":[100,-100]},{"t":957,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":102,"s":[25]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":383,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":750,"s":[25]},{"t":957,"s":[25]}],"ix":4},"sa":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":957,"s":[5760]}],"ix":5},"nm":"Transform"}],"nm":"Fill 91","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":982,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"Null 3","parent":1,"sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[0,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":111.229,"s":[120,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":206.707,"s":[0,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":291.361,"s":[240,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":381.92,"s":[40,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":419.324,"s":[160,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":468.539,"s":[0,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":552.207,"s":[160,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":578.783,"s":[80,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":644.734,"s":[240,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":707.729,"s":[160,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":744.15,"s":[280,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":798.287,"s":[0,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":847.506,"s":[200,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":881.957,"s":[280,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"t":942,"s":[-40,-14,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":1549,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":3,"nm":"Null 3","parent":1,"sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[0,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":98.949,"s":[120,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":183.887,"s":[0,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":259.193,"s":[240,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":339.754,"s":[40,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":373.029,"s":[160,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":416.811,"s":[0,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":491.242,"s":[160,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":514.883,"s":[80,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":573.553,"s":[240,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":629.594,"s":[160,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":661.994,"s":[280,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":710.154,"s":[0,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":753.938,"s":[200,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":784.586,"s":[280,-14,0],"to":[0,0,0],"ti":[0,0,0]},{"t":838,"s":[-40,-14,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":1549,"st":0,"bm":0}]},{"id":"comp_2","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Fill 96","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.562,"y":1},"o":{"x":0.198,"y":0},"t":0,"s":[202.626,13.222,0],"to":[0,24.201,0],"ti":[0,-48.268,0]},{"i":{"x":0.653,"y":1},"o":{"x":0.31,"y":0},"t":34,"s":[172.626,125.119,0],"to":[0,26.371,0],"ti":[0,-30.606,0]},{"i":{"x":0.676,"y":1},"o":{"x":0.335,"y":0},"t":67.422,"s":[142.626,211.105,0],"to":[0,30.58,0],"ti":[0,-32.729,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.356,"y":0},"t":96.736,"s":[142.626,306.588,0],"to":[0,47.125,0],"ti":[0,-46.326,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.44,"y":0},"t":131.211,"s":[142.626,448.315,0],"to":[0,111.388,0],"ti":[0,-45.734,0]},{"i":{"x":0.544,"y":0.637},"o":{"x":0.196,"y":0},"t":185,"s":[172.626,705.552,0],"to":[0,14.751,0],"ti":[7.083,-74.622,0]},{"i":{"x":0.636,"y":1},"o":{"x":0.3,"y":0.265},"t":215,"s":[234.992,844.137,0],"to":[-1.475,15.536,0],"ti":[0,-17.658,0]},{"i":{"x":0.488,"y":1},"o":{"x":0.167,"y":0},"t":237.93,"s":[142.626,893.969,0],"to":[0,40.921,0],"ti":[0,-50.858,0]},{"i":{"x":0.595,"y":1},"o":{"x":0.372,"y":0},"t":276.418,"s":[142.626,1032.146,0],"to":[0,57.581,0],"ti":[0,-67.539,0]},{"i":{"x":0.68,"y":1},"o":{"x":0.366,"y":0},"t":328.902,"s":[142.626,1220.563,0],"to":[0,52.132,0],"ti":[0,-56.507,0]},{"i":{"x":0.737,"y":1},"o":{"x":0.292,"y":0},"t":377.889,"s":[142.626,1383.86,0],"to":[0,56.974,0],"ti":[0,-60.036,0]},{"i":{"x":0.614,"y":0.63},"o":{"x":0.206,"y":0},"t":428.621,"s":[142.626,1559.722,0],"to":[0,44.588,0],"ti":[0,-45.608,0]},{"i":{"x":0.76,"y":1},"o":{"x":0.388,"y":0.423},"t":465,"s":[232.626,1695.159,0],"to":[0,26.971,0],"ti":[0,-27.169,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.522,"y":0},"t":490.729,"s":[172.626,1776.398,0],"to":[0,54.309,0],"ti":[0,-54.395,0]},{"i":{"x":0.586,"y":1},"o":{"x":0.167,"y":0},"t":533.592,"s":[142.626,1939.69,0],"to":[0,65.449,0],"ti":[0,-64.072,0]},{"i":{"x":0.236,"y":1},"o":{"x":0.167,"y":0},"t":586.949,"s":[142.626,2134.382,0],"to":[0,58.702,0],"ti":[0,-56.228,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":641.184,"s":[142.626,2307.091,0],"to":[0,69.785,0],"ti":[0,-63.798,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.685,"y":0},"t":689.295,"s":[142.626,2508.067,0],"to":[0,55.302,0],"ti":[0,-49.112,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":735.654,"s":[142.626,2665.08,0],"to":[0,57.546,0],"ti":[0,-46.707,0]},{"i":{"x":0.511,"y":1},"o":{"x":0.281,"y":0},"t":781,"s":[202.626,2822.09,0],"to":[0,28.636,0],"ti":[0,-13.385,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":809,"s":[172.626,2944.558,0],"to":[0,63.954,0],"ti":[0,0,0]},{"t":838,"s":[142.626,3045.052,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[65,65,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-9.584,3.802],[9.734,-6.562],[5.318,-19.023],[-14.005,-8.222]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9176470588235294,0,0.2627450980392157,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":0,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":34,"s":[-100,0]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":66,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":106,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":140,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":178,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":208,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":242,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":280,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":311,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":346,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":383,"s":[100,-100]},{"i":{"x":[0.833,0.833],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0,0]},"t":408,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":453,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":497,"s":[-101,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":549,"s":[-101,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":610,"s":[100,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":660,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":712,"s":[104,0]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":752,"s":[104,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":791,"s":[104,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":829,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":868,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":915,"s":[100,-100]},{"t":957,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":102,"s":[25]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":383,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":750,"s":[25]},{"t":957,"s":[25]}],"ix":4},"sa":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":957,"s":[5760]}],"ix":5},"nm":"Transform"}],"nm":"Fill 91","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":982,"st":0,"bm":0}]},{"id":"comp_3","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Fill 95","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.562,"y":1},"o":{"x":0.198,"y":0},"t":0,"s":[202.626,13.222,0],"to":[0,24.201,0],"ti":[0,-48.268,0]},{"i":{"x":0.653,"y":1},"o":{"x":0.31,"y":0},"t":34,"s":[172.626,125.119,0],"to":[0,26.371,0],"ti":[0,-30.606,0]},{"i":{"x":0.676,"y":1},"o":{"x":0.335,"y":0},"t":67.422,"s":[142.626,211.105,0],"to":[0,30.58,0],"ti":[0,-32.729,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.356,"y":0},"t":96.736,"s":[142.626,306.588,0],"to":[0,47.125,0],"ti":[0,-46.326,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.44,"y":0},"t":131.211,"s":[142.626,448.315,0],"to":[0,111.388,0],"ti":[0,-45.734,0]},{"i":{"x":0.544,"y":0.637},"o":{"x":0.196,"y":0},"t":185,"s":[172.626,705.552,0],"to":[0,14.751,0],"ti":[7.083,-74.622,0]},{"i":{"x":0.636,"y":1},"o":{"x":0.3,"y":0.265},"t":215,"s":[234.992,844.137,0],"to":[-1.475,15.536,0],"ti":[0,-17.658,0]},{"i":{"x":0.488,"y":1},"o":{"x":0.167,"y":0},"t":237.93,"s":[142.626,893.969,0],"to":[0,40.921,0],"ti":[0,-50.858,0]},{"i":{"x":0.595,"y":1},"o":{"x":0.372,"y":0},"t":276.418,"s":[142.626,1032.146,0],"to":[0,57.581,0],"ti":[0,-67.539,0]},{"i":{"x":0.68,"y":1},"o":{"x":0.366,"y":0},"t":328.902,"s":[142.626,1220.563,0],"to":[0,52.132,0],"ti":[0,-56.507,0]},{"i":{"x":0.737,"y":1},"o":{"x":0.292,"y":0},"t":377.889,"s":[142.626,1383.86,0],"to":[0,56.974,0],"ti":[0,-60.036,0]},{"i":{"x":0.614,"y":0.63},"o":{"x":0.206,"y":0},"t":428.621,"s":[142.626,1559.722,0],"to":[0,44.588,0],"ti":[0,-45.608,0]},{"i":{"x":0.76,"y":1},"o":{"x":0.388,"y":0.423},"t":465,"s":[232.626,1695.159,0],"to":[0,26.971,0],"ti":[0,-27.169,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.522,"y":0},"t":490.729,"s":[172.626,1776.398,0],"to":[0,54.309,0],"ti":[0,-54.395,0]},{"i":{"x":0.586,"y":1},"o":{"x":0.167,"y":0},"t":533.592,"s":[142.626,1939.69,0],"to":[0,65.449,0],"ti":[0,-64.072,0]},{"i":{"x":0.236,"y":1},"o":{"x":0.167,"y":0},"t":586.949,"s":[142.626,2134.382,0],"to":[0,58.702,0],"ti":[0,-56.228,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":641.184,"s":[142.626,2307.091,0],"to":[0,69.785,0],"ti":[0,-63.798,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.685,"y":0},"t":689.295,"s":[142.626,2508.067,0],"to":[0,55.302,0],"ti":[0,-49.112,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":735.654,"s":[142.626,2665.08,0],"to":[0,57.546,0],"ti":[0,-46.707,0]},{"i":{"x":0.511,"y":1},"o":{"x":0.281,"y":0},"t":781,"s":[202.626,2822.09,0],"to":[0,28.636,0],"ti":[0,-13.385,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":809,"s":[172.626,2944.558,0],"to":[0,63.954,0],"ti":[0,0,0]},{"t":838,"s":[142.626,3045.052,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[65,65,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-3.155,7.985],[11.583,-1.952],[5.318,-19.023],[-10.536,-10.298]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.8274509803921568,0.5725490196078431,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":0,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":34,"s":[-100,0]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":66,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":106,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":140,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":178,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":208,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":242,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":280,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":311,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":346,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":383,"s":[100,-100]},{"i":{"x":[0.833,0.833],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0,0]},"t":408,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":453,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":497,"s":[-101,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":549,"s":[-101,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":610,"s":[100,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":660,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":712,"s":[104,0]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":752,"s":[104,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":791,"s":[104,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":829,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":868,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":915,"s":[100,-100]},{"t":957,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":102,"s":[25]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":383,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":750,"s":[25]},{"t":957,"s":[25]}],"ix":4},"sa":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":957,"s":[5760]}],"ix":5},"nm":"Transform"}],"nm":"Fill 91","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":982,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":3,"nm":"Null 3","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[187.5,354,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":111.229,"s":[307.5,354,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":206.707,"s":[187.5,354,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":291.361,"s":[427.5,354,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":381.92,"s":[227.5,354,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":419.324,"s":[347.5,354,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":468.539,"s":[187.5,354,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":552.207,"s":[347.5,354,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":578.783,"s":[267.5,354,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":644.734,"s":[427.5,354,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":707.729,"s":[347.5,354,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":744.15,"s":[467.5,354,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":798.287,"s":[187.5,354,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":847.506,"s":[387.5,354,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":881.957,"s":[467.5,354,0],"to":[0,0,0],"ti":[0,0,0]},{"t":942,"s":[147.5,354,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":1549,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":2,"ty":4,"nm":"Star 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":-2,"s":[468.954,392.843,0],"to":[24.333,-78.833,0],"ti":[-100.333,-67.167,0]},{"t":55,"s":[782.954,309.843,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[65,65,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-0.18,0.47],[0,0],[0,0],[0.41,0.34],[0,0],[0,0],[0.41,-0.29],[0,0],[0,0],[-0.13,-0.51],[0,0],[0,0],[-0.52,-0.01],[0,0]],"o":[[-0.29,-0.42],[0,0],[0,0],[-0.5,0.16],[0,0],[0,0],[-0.01,0.5],[0,0],[0,0],[0.5,0.17],[0,0],[0,0],[0.3,-0.41],[0,0],[0,0]],"v":[[11.007,-1.504],[10.837,-2.924],[15.087,-14.204],[3.887,-10.584],[2.427,-10.864],[-6.643,-18.204],[-7.023,-6.144],[-7.703,-4.894],[-17.643,1.886],[-6.603,5.666],[-5.593,6.756],[-2.633,18.206],[4.547,8.566],[5.847,7.936],[17.647,8.326]],"c":true},"ix":2},"nm":"Path 7","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.97647100687,0.776471018791,0.02352900058,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-99,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-55,"s":[-101,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-3,"s":[-101,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":58,"s":[100,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":108,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":160,"s":[104,0]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":200,"s":[104,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":239,"s":[104,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":277,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":316,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":363,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":405,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":450,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":484,"s":[-100,0]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":516,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":556,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":590,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":628,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":658,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":692,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":730,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":761,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":796,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":833,"s":[100,-100]},{"t":858,"s":[-100,-100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-99,"s":[25]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":171,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":394,"s":[25]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":601,"s":[15]},{"t":858,"s":[25]}],"ix":4},"sa":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-99,"s":[0]},{"t":858,"s":[5760]}],"ix":5},"nm":"Transform"}],"nm":"Fill 17","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":8,"st":-6,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"Star","refId":"comp_0","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":110,"s":[100]},{"t":140,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[538,662,0],"to":[35,0,0],"ti":[-35,0,0]},{"t":27,"s":[748,662,0]}],"ix":2},"a":{"a":0,"k":[187.5,1500,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":375,"h":3000,"ip":8,"op":141,"st":-284,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Star 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":-2,"s":[466.954,395.843,0],"to":[-15.333,-186.5,0],"ti":[2.333,-186.5,0]},{"t":55,"s":[125.954,342.843,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[65,65,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-0.18,0.47],[0,0],[0,0],[0.41,0.34],[0,0],[0,0],[0.41,-0.29],[0,0],[0,0],[-0.13,-0.51],[0,0],[0,0],[-0.52,-0.01],[0,0]],"o":[[-0.29,-0.42],[0,0],[0,0],[-0.5,0.16],[0,0],[0,0],[-0.01,0.5],[0,0],[0,0],[0.5,0.17],[0,0],[0,0],[0.3,-0.41],[0,0],[0,0]],"v":[[11.007,-1.504],[10.837,-2.924],[15.087,-14.204],[3.887,-10.584],[2.427,-10.864],[-6.643,-18.204],[-7.023,-6.144],[-7.703,-4.894],[-17.643,1.886],[-6.603,5.666],[-5.593,6.756],[-2.633,18.206],[4.547,8.566],[5.847,7.936],[17.647,8.326]],"c":true},"ix":2},"nm":"Path 7","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.97647100687,0.776471018791,0.02352900058,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-346,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-302,"s":[-101,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-250,"s":[-101,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":-189,"s":[100,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":-139,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-87,"s":[104,0]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-47,"s":[104,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-8,"s":[104,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":30,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":69,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":116,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":158,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":203,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":237,"s":[-100,0]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":269,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":309,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":343,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":381,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":411,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":445,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":483,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":514,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":549,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":586,"s":[100,-100]},{"t":611,"s":[-100,-100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-346,"s":[25]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-76,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":147,"s":[25]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":354,"s":[15]},{"t":611,"s":[25]}],"ix":4},"sa":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-346,"s":[0]},{"t":611,"s":[5760]}],"ix":5},"nm":"Transform"}],"nm":"Fill 17","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":14,"st":-6,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"Star","refId":"comp_0","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":110,"s":[100]},{"t":140,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[204.818,543,0],"ix":2},"a":{"a":0,"k":[187.5,1500,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":375,"h":3000,"ip":14,"op":141,"st":-303,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Star 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":-2,"s":[464.954,392.843,0],"to":[8.5,-240.5,0],"ti":[-5.5,-140.5,0]},{"t":55,"s":[551.954,233.843,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[65,65,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-0.18,0.47],[0,0],[0,0],[0.41,0.34],[0,0],[0,0],[0.41,-0.29],[0,0],[0,0],[-0.13,-0.51],[0,0],[0,0],[-0.52,-0.01],[0,0]],"o":[[-0.29,-0.42],[0,0],[0,0],[-0.5,0.16],[0,0],[0,0],[-0.01,0.5],[0,0],[0,0],[0.5,0.17],[0,0],[0,0],[0.3,-0.41],[0,0],[0,0]],"v":[[11.007,-1.504],[10.837,-2.924],[15.087,-14.204],[3.887,-10.584],[2.427,-10.864],[-6.643,-18.204],[-7.023,-6.144],[-7.703,-4.894],[-17.643,1.886],[-6.603,5.666],[-5.593,6.756],[-2.633,18.206],[4.547,8.566],[5.847,7.936],[17.647,8.326]],"c":true},"ix":2},"nm":"Path 7","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.97647100687,0.776471018791,0.02352900058,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-421,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-377,"s":[-101,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-325,"s":[-101,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":-264,"s":[100,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":-214,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-162,"s":[104,0]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-122,"s":[104,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-83,"s":[104,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-45,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":-6,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":41,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":83,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":128,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":162,"s":[-100,0]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":194,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":234,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":268,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":306,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":336,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":370,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":408,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":439,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":474,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":511,"s":[100,-100]},{"t":536,"s":[-100,-100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-421,"s":[25]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-151,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":72,"s":[25]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":279,"s":[15]},{"t":536,"s":[25]}],"ix":4},"sa":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-421,"s":[0]},{"t":536,"s":[5760]}],"ix":5},"nm":"Transform"}],"nm":"Fill 17","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":36,"st":-6,"bm":0},{"ddd":0,"ind":7,"ty":0,"nm":"Star","refId":"comp_0","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":110,"s":[100]},{"t":140,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":36,"s":[518.5,-301,0],"to":[-10,0,0],"ti":[10,0,0]},{"t":61,"s":[458.5,-301,0]}],"ix":2},"a":{"a":0,"k":[187.5,1500,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":375,"h":3000,"ip":36,"op":141,"st":-499,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Star","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":-2,"s":[466.954,395.843,0],"to":[-12.833,-251.833,0],"ti":[1.833,-151.167,0]},{"t":55,"s":[251.954,192.843,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[65,65,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-0.18,0.47],[0,0],[0,0],[0.41,0.34],[0,0],[0,0],[0.41,-0.29],[0,0],[0,0],[-0.13,-0.51],[0,0],[0,0],[-0.52,-0.01],[0,0]],"o":[[-0.29,-0.42],[0,0],[0,0],[-0.5,0.16],[0,0],[0,0],[-0.01,0.5],[0,0],[0,0],[0.5,0.17],[0,0],[0,0],[0.3,-0.41],[0,0],[0,0]],"v":[[11.007,-1.504],[10.837,-2.924],[15.087,-14.204],[3.887,-10.584],[2.427,-10.864],[-6.643,-18.204],[-7.023,-6.144],[-7.703,-4.894],[-17.643,1.886],[-6.603,5.666],[-5.593,6.756],[-2.633,18.206],[4.547,8.566],[5.847,7.936],[17.647,8.326]],"c":true},"ix":2},"nm":"Path 7","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.97647100687,0.776471018791,0.02352900058,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-142,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-98,"s":[-101,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-46,"s":[-101,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":15,"s":[100,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":65,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":117,"s":[104,0]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":157,"s":[104,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":196,"s":[104,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":234,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":273,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":320,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":362,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":407,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":441,"s":[-100,0]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":473,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":513,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":547,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":585,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":615,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":649,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":687,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":718,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":753,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":790,"s":[100,-100]},{"t":815,"s":[-100,-100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-142,"s":[25]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":128,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":351,"s":[25]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":558,"s":[15]},{"t":815,"s":[25]}],"ix":4},"sa":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-142,"s":[0]},{"t":815,"s":[5760]}],"ix":5},"nm":"Transform"}],"nm":"Fill 17","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":29,"st":-6,"bm":0},{"ddd":0,"ind":9,"ty":0,"nm":"Star","refId":"comp_0","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":110,"s":[100]},{"t":140,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[170,1319,0],"ix":2},"a":{"a":0,"k":[187.5,1500,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":375,"h":3000,"ip":29,"op":141,"st":-96,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Circle 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":-2,"s":[461.681,401.958,0],"to":[12.333,-218.667,0],"ti":[-2.333,-103.333,0]},{"t":55,"s":[680.681,240.958,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[65,65,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-4.722,-2.013],[-0.688,5.385],[3.87,2.274],[0.581,-5.397]],"o":[[3.92,1.671],[0.662,-5.187],[-4.388,-2.579],[-0.514,4.772]],"v":[[-5.961,4.675],[3.474,-2.806],[-2.929,-16.147],[-13.338,-9.733]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9176470588235294,0,0.2627450980392157,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-600,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-566,"s":[-100,0]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-534,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-494,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-460,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-422,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-392,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-358,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-320,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-289,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-254,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-217,"s":[100,-100]},{"i":{"x":[0.833,0.833],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0,0]},"t":-192,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-147,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-103,"s":[-101,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-51,"s":[-101,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":10,"s":[100,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":60,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":112,"s":[104,0]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":152,"s":[104,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":191,"s":[104,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":229,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":268,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":315,"s":[100,-100]},{"t":357,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-600,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-498,"s":[25]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-217,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":150,"s":[25]},{"t":357,"s":[25]}],"ix":4},"sa":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-600,"s":[0]},{"t":357,"s":[5760]}],"ix":5},"nm":"Transform"}],"nm":"Fill 91","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":28,"st":-6,"bm":0},{"ddd":0,"ind":11,"ty":0,"nm":"Dot","refId":"comp_1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":110,"s":[100]},{"t":140,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[796.5,1218.75,0],"ix":2},"a":{"a":0,"k":[187.5,1500,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":375,"h":3000,"ip":28,"op":141,"st":-123,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"Circle 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":-2,"s":[465.681,404.958,0],"to":[-3.667,-183.333,0],"ti":[4.667,-176.667,0]},{"t":55,"s":[230.681,260.958,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[65,65,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-4.722,-2.013],[-0.688,5.385],[3.87,2.274],[0.581,-5.397]],"o":[[3.92,1.671],[0.662,-5.187],[-4.388,-2.579],[-0.514,4.772]],"v":[[-5.961,4.675],[3.474,-2.806],[-2.929,-16.147],[-13.338,-9.733]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9176470588235294,0,0.2627450980392157,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-353,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-319,"s":[-100,0]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-287,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-247,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-213,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-175,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-145,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-111,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-73,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-42,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-7,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":30,"s":[100,-100]},{"i":{"x":[0.833,0.833],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0,0]},"t":55,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":100,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":144,"s":[-101,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":196,"s":[-101,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":257,"s":[100,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":307,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":359,"s":[104,0]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":399,"s":[104,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":438,"s":[104,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":476,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":515,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":562,"s":[100,-100]},{"t":604,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-353,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-251,"s":[25]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":397,"s":[25]},{"t":604,"s":[25]}],"ix":4},"sa":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-353,"s":[0]},{"t":604,"s":[5760]}],"ix":5},"nm":"Transform"}],"nm":"Fill 91","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":31,"st":-6,"bm":0},{"ddd":0,"ind":13,"ty":0,"nm":"Dot","refId":"comp_1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":110,"s":[100]},{"t":140,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[384,844,0],"ix":2},"a":{"a":0,"k":[187.5,1500,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":375,"h":3000,"ip":30,"op":141,"st":-203,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"Circle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":-2,"s":[466.681,399.958,0],"to":[-12.833,-34.833,0],"ti":[20.667,-158,0]},{"t":55,"s":[389.681,190.958,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[65,65,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-4.722,-2.013],[-0.688,5.385],[3.87,2.274],[0.581,-5.397]],"o":[[3.92,1.671],[0.662,-5.187],[-4.388,-2.579],[-0.514,4.772]],"v":[[-5.961,4.675],[3.474,-2.806],[-2.929,-16.147],[-13.338,-9.733]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9176470588235294,0,0.2627450980392157,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-368,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-334,"s":[-100,0]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-302,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-262,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-228,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-190,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-160,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-126,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-88,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-57,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-22,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":15,"s":[100,-100]},{"i":{"x":[0.833,0.833],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0,0]},"t":40,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":85,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":129,"s":[-101,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":181,"s":[-101,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":242,"s":[100,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":292,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":344,"s":[104,0]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":384,"s":[104,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":423,"s":[104,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":461,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":500,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":547,"s":[100,-100]},{"t":589,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-368,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-266,"s":[25]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":382,"s":[25]},{"t":589,"s":[25]}],"ix":4},"sa":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-368,"s":[0]},{"t":589,"s":[5760]}],"ix":5},"nm":"Transform"}],"nm":"Fill 91","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":25,"st":-6,"bm":0},{"ddd":0,"ind":15,"ty":0,"nm":"Dot","refId":"comp_1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":110,"s":[100]},{"t":140,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[529.5,1448,0],"ix":2},"a":{"a":0,"k":[187.5,1500,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":375,"h":3000,"ip":25,"op":141,"st":-38,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":"Rectangle 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":-2,"s":[457.681,392.958,0],"to":[4.5,-184.833,0],"ti":[-1.5,-100.167,0]},{"t":55,"s":[616.681,165.958,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[65,65,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-9.584,3.802],[9.734,-6.562],[5.318,-19.023],[-14.005,-8.222]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9176470588235294,0,0.2627450980392157,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-199,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-165,"s":[-100,0]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-133,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-93,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-59,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-21,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":9,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":43,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":81,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":112,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":147,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":184,"s":[100,-100]},{"i":{"x":[0.833,0.833],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0,0]},"t":209,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":254,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":298,"s":[-101,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":350,"s":[-101,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":411,"s":[100,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":461,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":513,"s":[104,0]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":553,"s":[104,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":592,"s":[104,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":630,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":669,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":716,"s":[100,-100]},{"t":758,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-199,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-97,"s":[25]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":184,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":551,"s":[25]},{"t":758,"s":[25]}],"ix":4},"sa":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-199,"s":[0]},{"t":758,"s":[5760]}],"ix":5},"nm":"Transform"}],"nm":"Fill 91","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":42,"st":-6,"bm":0},{"ddd":0,"ind":17,"ty":0,"nm":"Rectangle","refId":"comp_2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":110,"s":[100]},{"t":140,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[625.5,971.75,0],"ix":2},"a":{"a":0,"k":[187.5,1500,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":375,"h":3000,"ip":42,"op":141,"st":-140,"bm":0},{"ddd":0,"ind":18,"ty":4,"nm":"Rectangle 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":-2,"s":[461.681,401.958,0],"to":[-3,-192.5,0],"ti":[8,-141.5,0]},{"t":55,"s":[191.681,218.958,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[65,65,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-9.584,3.802],[9.734,-6.562],[5.318,-19.023],[-14.005,-8.222]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9176470588235294,0,0.2627450980392157,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-167,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-133,"s":[-100,0]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-101,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-61,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-27,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":11,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":41,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":75,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":113,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":144,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":179,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":216,"s":[100,-100]},{"i":{"x":[0.833,0.833],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0,0]},"t":241,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":286,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":330,"s":[-101,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":382,"s":[-101,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":443,"s":[100,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":493,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":545,"s":[104,0]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":585,"s":[104,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":624,"s":[104,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":662,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":701,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":748,"s":[100,-100]},{"t":790,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-167,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-65,"s":[25]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":216,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":583,"s":[25]},{"t":790,"s":[25]}],"ix":4},"sa":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-167,"s":[0]},{"t":790,"s":[5760]}],"ix":5},"nm":"Transform"}],"nm":"Fill 91","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":33,"st":-6,"bm":0},{"ddd":0,"ind":19,"ty":0,"nm":"Rectangle","refId":"comp_2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":110,"s":[100]},{"t":140,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[241.5,1235.25,0],"ix":2},"a":{"a":0,"k":[187.5,1500,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":375,"h":3000,"ip":33,"op":141,"st":-107,"bm":0},{"ddd":0,"ind":20,"ty":4,"nm":"Rectangle 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":-2,"s":[462.681,406.958,0],"to":[5.833,-282.167,0],"ti":[-3.833,-109.833,0]},{"t":55,"s":[491.681,165.958,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[65,65,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-9.584,3.802],[9.734,-6.562],[5.318,-19.023],[-14.005,-8.222]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9176470588235294,0,0.2627450980392157,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-6,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":28,"s":[-100,0]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":60,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":100,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":134,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":172,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":202,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":236,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":274,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":305,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":340,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":377,"s":[100,-100]},{"i":{"x":[0.833,0.833],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0,0]},"t":402,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":447,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":491,"s":[-101,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":543,"s":[-101,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":604,"s":[100,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":654,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":706,"s":[104,0]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":746,"s":[104,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":785,"s":[104,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":823,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":862,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":909,"s":[100,-100]},{"t":951,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-6,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":96,"s":[25]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":377,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":744,"s":[25]},{"t":951,"s":[25]}],"ix":4},"sa":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-6,"s":[0]},{"t":951,"s":[5760]}],"ix":5},"nm":"Transform"}],"nm":"Fill 91","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":26,"st":-6,"bm":0},{"ddd":0,"ind":21,"ty":0,"nm":"Rectangle","refId":"comp_2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":110,"s":[100]},{"t":140,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[537,1208.25,0],"ix":2},"a":{"a":0,"k":[187.5,1500,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":375,"h":3000,"ip":26,"op":141,"st":-96,"bm":0},{"ddd":0,"ind":22,"ty":4,"nm":"Rectangle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":-2,"s":[462.681,407.958,0],"to":[7.333,-130.667,0],"ti":[-0.333,-144.333,0]},{"t":55,"s":[722.681,211.958,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[65,65,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-9.584,3.802],[9.734,-6.562],[5.318,-19.023],[-14.005,-8.222]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9176470588235294,0,0.2627450980392157,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-60,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-26,"s":[-100,0]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":6,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":46,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":80,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":118,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":148,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":182,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":220,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":251,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":286,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":323,"s":[100,-100]},{"i":{"x":[0.833,0.833],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0,0]},"t":348,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":393,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":437,"s":[-101,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":489,"s":[-101,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":550,"s":[100,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":600,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":652,"s":[104,0]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":692,"s":[104,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":731,"s":[104,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":769,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":808,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":855,"s":[100,-100]},{"t":897,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-60,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":42,"s":[25]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":323,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":690,"s":[25]},{"t":897,"s":[25]}],"ix":4},"sa":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-60,"s":[0]},{"t":897,"s":[5760]}],"ix":5},"nm":"Transform"}],"nm":"Fill 91","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":26,"st":-6,"bm":0},{"ddd":0,"ind":23,"ty":0,"nm":"Rectangle","refId":"comp_2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":110,"s":[100]},{"t":140,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[754,1398.75,0],"ix":2},"a":{"a":0,"k":[187.5,1500,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":375,"h":3000,"ip":26,"op":141,"st":-59,"bm":0},{"ddd":0,"ind":24,"ty":4,"nm":"Square 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":-2,"s":[464.681,409.958,0],"to":[-3.167,-234.5,0],"ti":[3.167,-135.5,0]},{"t":55,"s":[331.681,274.958,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[65,65,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-3.155,7.985],[11.583,-1.952],[5.318,-19.023],[-10.536,-10.298]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.8274509803921568,0.5725490196078431,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-24,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":10,"s":[-100,0]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":42,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":82,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":116,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":154,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":184,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":218,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":256,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":287,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":322,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":359,"s":[100,-100]},{"i":{"x":[0.833,0.833],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0,0]},"t":384,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":429,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":473,"s":[-101,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":525,"s":[-101,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":586,"s":[100,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":636,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":688,"s":[104,0]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":728,"s":[104,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":767,"s":[104,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":805,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":844,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":891,"s":[100,-100]},{"t":933,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-24,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":78,"s":[25]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":359,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":726,"s":[25]},{"t":933,"s":[25]}],"ix":4},"sa":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-24,"s":[0]},{"t":933,"s":[5760]}],"ix":5},"nm":"Transform"}],"nm":"Fill 91","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":27,"st":-6,"bm":0},{"ddd":0,"ind":25,"ty":0,"nm":"Square","refId":"comp_3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":110,"s":[100]},{"t":140,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[310.317,985.151,0],"ix":2},"a":{"a":0,"k":[187.5,1500,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":375,"h":3000,"ip":27,"op":141,"st":-172,"bm":0},{"ddd":0,"ind":26,"ty":4,"nm":"Square 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":-2,"s":[466.681,408.958,0],"to":[5.333,-269,0],"ti":[-6.333,-197,0]},{"t":55,"s":[642.681,294.958,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[65,65,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-3.155,7.985],[11.583,-1.952],[5.318,-19.023],[-10.536,-10.298]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.8274509803921568,0.5725490196078431,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-475,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-441,"s":[-100,0]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-409,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-369,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-335,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-297,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-267,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-233,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-195,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-164,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-129,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-92,"s":[100,-100]},{"i":{"x":[0.833,0.833],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0,0]},"t":-67,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-22,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":22,"s":[-101,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":74,"s":[-101,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":135,"s":[100,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":185,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":237,"s":[104,0]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":277,"s":[104,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":316,"s":[104,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":354,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":393,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":440,"s":[100,-100]},{"t":482,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-475,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-373,"s":[25]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-92,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":275,"s":[25]},{"t":482,"s":[25]}],"ix":4},"sa":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-475,"s":[0]},{"t":482,"s":[5760]}],"ix":5},"nm":"Transform"}],"nm":"Fill 91","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":18,"st":-6,"bm":0},{"ddd":0,"ind":27,"ty":0,"nm":"Square","refId":"comp_3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":110,"s":[100]},{"t":140,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[656.317,1122.151,0],"ix":2},"a":{"a":0,"k":[187.5,1500,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":375,"h":3000,"ip":18,"op":141,"st":-147,"bm":0},{"ddd":0,"ind":28,"ty":4,"nm":"Square 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":-2,"s":[465.681,408.958,0],"to":[-2,-270.833,0],"ti":[5,-82.167,0]},{"t":55,"s":[321.681,181.958,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[65,65,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-3.155,7.985],[11.583,-1.952],[5.318,-19.023],[-10.536,-10.298]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.8274509803921568,0.5725490196078431,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-138,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-104,"s":[-100,0]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-72,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-32,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":2,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":40,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":70,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":104,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":142,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":173,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":208,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":245,"s":[100,-100]},{"i":{"x":[0.833,0.833],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0,0]},"t":270,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":315,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":359,"s":[-101,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":411,"s":[-101,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":472,"s":[100,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":522,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":574,"s":[104,0]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":614,"s":[104,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":653,"s":[104,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":691,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":730,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":777,"s":[100,-100]},{"t":819,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-138,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-36,"s":[25]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":245,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":612,"s":[25]},{"t":819,"s":[25]}],"ix":4},"sa":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-138,"s":[0]},{"t":819,"s":[5760]}],"ix":5},"nm":"Transform"}],"nm":"Fill 91","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":32,"st":-6,"bm":0},{"ddd":0,"ind":29,"ty":0,"nm":"Square","refId":"comp_3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":110,"s":[100]},{"t":140,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[371.317,1320.151,0],"ix":2},"a":{"a":0,"k":[187.5,1500,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":375,"h":3000,"ip":32,"op":141,"st":-75,"bm":0},{"ddd":0,"ind":30,"ty":4,"nm":"Square","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":-2,"s":[464.681,410.958,0],"to":[9.333,-292.667,0],"ti":[-18.333,-191.333,0]},{"t":55,"s":[706.681,298.958,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[65,65,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-3.155,7.985],[11.583,-1.952],[5.318,-19.023],[-10.536,-10.298]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.8274509803921568,0.5725490196078431,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-332,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-298,"s":[-100,0]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-266,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-226,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-192,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-154,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-124,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-90,"s":[100,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":-52,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":-21,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":14,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":51,"s":[100,-100]},{"i":{"x":[0.833,0.833],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0,0]},"t":76,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":121,"s":[100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":165,"s":[-101,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":217,"s":[-101,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":278,"s":[100,-106]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":328,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":380,"s":[104,0]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":420,"s":[104,100]},{"i":{"x":[0.27,0.27],"y":[1.55,1]},"o":{"x":[0.68,0.68],"y":[-0.55,0]},"t":459,"s":[104,-100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":497,"s":[-100,-100]},{"i":{"x":[0.27,0.27],"y":[1.55,1.55]},"o":{"x":[0.68,0.68],"y":[-0.55,-0.55]},"t":536,"s":[-100,100]},{"i":{"x":[0.27,0.27],"y":[1,1.55]},"o":{"x":[0.68,0.68],"y":[0,-0.55]},"t":583,"s":[100,-100]},{"t":625,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-332,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-230,"s":[25]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":51,"s":[15]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":418,"s":[25]},{"t":625,"s":[25]}],"ix":4},"sa":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-332,"s":[0]},{"t":625,"s":[5760]}],"ix":5},"nm":"Transform"}],"nm":"Fill 91","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":27,"st":-6,"bm":0},{"ddd":0,"ind":31,"ty":0,"nm":"Square","refId":"comp_3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":110,"s":[100]},{"t":140,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.469,"y":1},"o":{"x":0.167,"y":0.167},"t":27,"s":[742,1554,0],"to":[1.833,1.667,0],"ti":[-1.833,-1.667,0]},{"t":34,"s":[753,1564,0]}],"ix":2},"a":{"a":0,"k":[187.5,1500,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":375,"h":3000,"ip":27,"op":141,"st":-30,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/core/designsystem/src/main/res/values/colors.xml b/core/designsystem/src/main/res/values/colors.xml
index f8c6127d..400b3520 100644
--- a/core/designsystem/src/main/res/values/colors.xml
+++ b/core/designsystem/src/main/res/values/colors.xml
@@ -7,4 +7,11 @@
#FF018786
#FF000000
#FFFFFFFF
+
+ #FFFFE4E4
+ #FFBFD7FF
+ #FFFFE59A
+ #FFC2E792
+ #FFF7D8B3
+ #FFBCE7FF
\ No newline at end of file
diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml
index 30298fab..3b8c080d 100644
--- a/core/designsystem/src/main/res/values/strings.xml
+++ b/core/designsystem/src/main/res/values/strings.xml
@@ -1,3 +1,12 @@
- mission-mate
+ 미션메이트
+
+ 월
+ 화
+ 수
+ 목
+ 금
+ 토
+ 일
+
\ No newline at end of file
diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts
index d3dd2cc9..b3882872 100644
--- a/core/domain/build.gradle.kts
+++ b/core/domain/build.gradle.kts
@@ -15,12 +15,10 @@ android {
minSdk = 26
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
- isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
@@ -45,4 +43,8 @@ dependencies {
ksp(libs.hilt.compiler)
implementation(libs.hilt.android)
+
+ implementation(project(":core:model"))
+ implementation(project(":core:datastore"))
+ implementation(project(":core:network"))
}
\ No newline at end of file
diff --git a/core/domain/consumer-rules.pro b/core/domain/consumer-rules.pro
deleted file mode 100644
index e69de29b..00000000
diff --git a/core/domain/proguard-rules.pro b/core/domain/proguard-rules.pro
index 481bb434..109525fd 100644
--- a/core/domain/proguard-rules.pro
+++ b/core/domain/proguard-rules.pro
@@ -18,4 +18,12 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
-#-renamesourcefileattribute SourceFile
\ No newline at end of file
+#-renamesourcefileattribute SourceFile
+
+-keep class * { *; }
+-keep interface * { *; }
+
+# Keep Dependency Injection Framework related classes and methods
+-keep class dagger.hilt.** { *; }
+-keep class javax.inject.** { *; }
+-keep class javax.annotation.** { *; }
diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/Domain.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/Domain.kt
deleted file mode 100644
index 17c80efa..00000000
--- a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/Domain.kt
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.goalpanzi.mission_mate.core.domain
-
-class Domain {
-}
\ No newline at end of file
diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/di/ResourceProvider.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/di/ResourceProvider.kt
new file mode 100644
index 00000000..fe1d04c1
--- /dev/null
+++ b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/di/ResourceProvider.kt
@@ -0,0 +1,30 @@
+package com.goalpanzi.mission_mate.core.domain.di
+
+import android.content.Context
+import android.content.res.TypedArray
+import androidx.annotation.ArrayRes
+import androidx.annotation.StringRes
+import dagger.hilt.android.qualifiers.ApplicationContext
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class ResourceProvider @Inject constructor(
+ @ApplicationContext private val context: Context
+) {
+ fun getString(@StringRes stringResId: Int): String {
+ return context.getString(stringResId)
+ }
+
+ fun getIntArray(@ArrayRes arrayResId: Int): Array {
+ return context.resources.getIntArray(arrayResId).toTypedArray()
+ }
+
+ fun getDrawableArray(@ArrayRes arrayResId: Int): TypedArray {
+ return context.resources.obtainTypedArray(arrayResId)
+ }
+
+ fun getStringArray(@ArrayRes arrayResId: Int): Array {
+ return context.resources.getStringArray(arrayResId)
+ }
+}
\ No newline at end of file
diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/repository/AuthRepository.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/repository/AuthRepository.kt
new file mode 100644
index 00000000..2f7da289
--- /dev/null
+++ b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/repository/AuthRepository.kt
@@ -0,0 +1,11 @@
+package com.goalpanzi.mission_mate.core.domain.repository
+
+import com.goalpanzi.mission_mate.core.network.ResultHandler
+import com.goalpanzi.core.model.response.GoogleLogin
+import com.goalpanzi.core.model.base.NetworkResult
+
+interface AuthRepository : ResultHandler {
+ suspend fun requestGoogleLogin(email: String): NetworkResult
+ suspend fun requestLogout(): NetworkResult
+ suspend fun requestAccountDelete(): NetworkResult
+}
\ No newline at end of file
diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/repository/MissionRepository.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/repository/MissionRepository.kt
new file mode 100644
index 00000000..3e2c3bdb
--- /dev/null
+++ b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/repository/MissionRepository.kt
@@ -0,0 +1,26 @@
+package com.goalpanzi.mission_mate.core.domain.repository
+
+import com.goalpanzi.mission_mate.core.network.ResultHandler
+import com.goalpanzi.core.model.base.NetworkResult
+import com.goalpanzi.core.model.response.MissionBoardsResponse
+import com.goalpanzi.core.model.response.MissionDetailResponse
+import com.goalpanzi.core.model.response.MissionRankResponse
+import com.goalpanzi.core.model.response.MissionVerificationResponse
+import com.goalpanzi.core.model.response.MissionVerificationsResponse
+import java.io.File
+
+interface MissionRepository : ResultHandler {
+ suspend fun getMissionBoards(missionId : Long) : NetworkResult
+
+ suspend fun getMission(missionId : Long) : NetworkResult
+
+ suspend fun getMissionVerifications(missionId: Long) : NetworkResult
+
+ suspend fun deleteMission(missionId : Long) : NetworkResult
+
+ suspend fun getMissionRank(missionId: Long) : NetworkResult
+
+ suspend fun verifyMission(missionId: Long, image: File) : NetworkResult
+
+ suspend fun getMyMissionVerification(missionId: Long, number : Int) : NetworkResult
+}
\ No newline at end of file
diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/repository/OnboardingRepository.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/repository/OnboardingRepository.kt
new file mode 100644
index 00000000..8d7a8554
--- /dev/null
+++ b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/repository/OnboardingRepository.kt
@@ -0,0 +1,23 @@
+package com.goalpanzi.mission_mate.core.domain.repository
+
+import com.goalpanzi.mission_mate.core.network.ResultHandler
+import com.goalpanzi.core.model.base.NetworkResult
+import com.goalpanzi.core.model.request.CreateMissionRequest
+import com.goalpanzi.core.model.response.MissionDetailResponse
+import com.goalpanzi.core.model.response.MissionsResponse
+
+interface OnboardingRepository : ResultHandler {
+ suspend fun createMission(
+ missionRequest: CreateMissionRequest
+ ): NetworkResult
+
+ suspend fun getMissionByInvitationCode(
+ invitationCode : String
+ ) : NetworkResult
+
+ suspend fun joinMission(
+ invitationCode: String
+ ) : NetworkResult
+
+ suspend fun getJoinedMissions() : NetworkResult
+}
\ No newline at end of file
diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/repository/ProfileRepository.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/repository/ProfileRepository.kt
new file mode 100644
index 00000000..3ecd5a34
--- /dev/null
+++ b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/repository/ProfileRepository.kt
@@ -0,0 +1,9 @@
+package com.goalpanzi.mission_mate.core.domain.repository
+
+import com.goalpanzi.mission_mate.core.network.ResultHandler
+import com.goalpanzi.core.model.CharacterType
+import com.goalpanzi.core.model.base.NetworkResult
+
+interface ProfileRepository: ResultHandler {
+ suspend fun saveProfile(nickname: String, type: CharacterType, isEqualNickname : Boolean): NetworkResult
+}
\ No newline at end of file
diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/AccountDeleteUseCase.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/AccountDeleteUseCase.kt
new file mode 100644
index 00000000..88a90a19
--- /dev/null
+++ b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/AccountDeleteUseCase.kt
@@ -0,0 +1,18 @@
+package com.goalpanzi.mission_mate.core.domain.usecase
+
+import com.goalpanzi.mission_mate.core.datastore.datasource.DefaultDataSource
+import com.goalpanzi.mission_mate.core.domain.repository.AuthRepository
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flow
+import javax.inject.Inject
+
+class AccountDeleteUseCase @Inject constructor(
+ private val authRepository: AuthRepository,
+ private val defaultDataSource: DefaultDataSource
+) {
+ operator fun invoke() = flow {
+ authRepository.requestAccountDelete()
+ defaultDataSource.clearUserData().first()
+ emit(Unit)
+ }
+}
\ No newline at end of file
diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/CreateMissionUseCase.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/CreateMissionUseCase.kt
new file mode 100644
index 00000000..44be1170
--- /dev/null
+++ b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/CreateMissionUseCase.kt
@@ -0,0 +1,19 @@
+package com.goalpanzi.mission_mate.core.domain.usecase
+
+import com.goalpanzi.mission_mate.core.domain.repository.OnboardingRepository
+import com.goalpanzi.core.model.base.NetworkResult
+import com.goalpanzi.core.model.request.CreateMissionRequest
+import com.goalpanzi.core.model.response.MissionDetailResponse
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import javax.inject.Inject
+
+class CreateMissionUseCase @Inject constructor(
+ private val onboardingRepository: OnboardingRepository
+) {
+ operator fun invoke(
+ createMissionRequest : CreateMissionRequest
+ ): Flow> = flow {
+ emit(onboardingRepository.createMission(createMissionRequest))
+ }
+}
\ No newline at end of file
diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/DeleteMissionUseCase.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/DeleteMissionUseCase.kt
new file mode 100644
index 00000000..13309a23
--- /dev/null
+++ b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/DeleteMissionUseCase.kt
@@ -0,0 +1,18 @@
+package com.goalpanzi.mission_mate.core.domain.usecase
+
+import com.goalpanzi.mission_mate.core.domain.repository.MissionRepository
+import com.goalpanzi.core.model.base.NetworkResult
+import com.goalpanzi.core.model.response.MissionDetailResponse
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import javax.inject.Inject
+
+class DeleteMissionUseCase @Inject constructor(
+ private val missionRepository: MissionRepository
+) {
+ operator fun invoke(
+ missionId: Long
+ ): Flow> = flow {
+ emit(missionRepository.deleteMission(missionId))
+ }
+}
\ No newline at end of file
diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/GetCachedMemberIdUseCase.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/GetCachedMemberIdUseCase.kt
new file mode 100644
index 00000000..3d8d4599
--- /dev/null
+++ b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/GetCachedMemberIdUseCase.kt
@@ -0,0 +1,11 @@
+package com.goalpanzi.mission_mate.core.domain.usecase
+
+import com.goalpanzi.mission_mate.core.datastore.datasource.DefaultDataSource
+import kotlinx.coroutines.flow.Flow
+import javax.inject.Inject
+
+class GetCachedMemberIdUseCase @Inject constructor(
+ private val defaultDataSource: DefaultDataSource
+) {
+ operator fun invoke(): Flow = defaultDataSource.getMemberId()
+}
\ No newline at end of file
diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/GetJoinedMissionsUseCase.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/GetJoinedMissionsUseCase.kt
new file mode 100644
index 00000000..281c875e
--- /dev/null
+++ b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/GetJoinedMissionsUseCase.kt
@@ -0,0 +1,16 @@
+package com.goalpanzi.mission_mate.core.domain.usecase
+
+import com.goalpanzi.mission_mate.core.domain.repository.OnboardingRepository
+import com.goalpanzi.core.model.base.NetworkResult
+import com.goalpanzi.core.model.response.MissionsResponse
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import javax.inject.Inject
+
+class GetJoinedMissionsUseCase @Inject constructor(
+ private val onboardingRepository: OnboardingRepository
+) {
+ operator fun invoke(): Flow> = flow {
+ emit(onboardingRepository.getJoinedMissions())
+ }
+}
\ No newline at end of file
diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/GetMissionBoardsUseCase.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/GetMissionBoardsUseCase.kt
new file mode 100644
index 00000000..bdb48b46
--- /dev/null
+++ b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/GetMissionBoardsUseCase.kt
@@ -0,0 +1,18 @@
+package com.goalpanzi.mission_mate.core.domain.usecase
+
+import com.goalpanzi.mission_mate.core.domain.repository.MissionRepository
+import com.goalpanzi.core.model.base.NetworkResult
+import com.goalpanzi.core.model.response.MissionBoardsResponse
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import javax.inject.Inject
+
+class GetMissionBoardsUseCase @Inject constructor(
+ private val missionRepository: MissionRepository
+) {
+ operator fun invoke(
+ missionId : Long
+ ): Flow> = flow {
+ emit(missionRepository.getMissionBoards(missionId))
+ }
+}
\ No newline at end of file
diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/GetMissionByInvitationCodeUseCase.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/GetMissionByInvitationCodeUseCase.kt
new file mode 100644
index 00000000..3f50e6c5
--- /dev/null
+++ b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/GetMissionByInvitationCodeUseCase.kt
@@ -0,0 +1,18 @@
+package com.goalpanzi.mission_mate.core.domain.usecase
+
+import com.goalpanzi.mission_mate.core.domain.repository.OnboardingRepository
+import com.goalpanzi.core.model.base.NetworkResult
+import com.goalpanzi.core.model.response.MissionDetailResponse
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import javax.inject.Inject
+
+class GetMissionByInvitationCodeUseCase @Inject constructor(
+ private val onboardingRepository: OnboardingRepository
+) {
+ operator fun invoke(
+ invitationCode: String
+ ): Flow> = flow {
+ emit(onboardingRepository.getMissionByInvitationCode(invitationCode))
+ }
+}
\ No newline at end of file
diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/GetMissionJoinedUseCase.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/GetMissionJoinedUseCase.kt
new file mode 100644
index 00000000..2bdb458a
--- /dev/null
+++ b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/GetMissionJoinedUseCase.kt
@@ -0,0 +1,11 @@
+package com.goalpanzi.mission_mate.core.domain.usecase
+
+import com.goalpanzi.mission_mate.core.datastore.datasource.MissionDataSource
+import kotlinx.coroutines.flow.Flow
+import javax.inject.Inject
+
+class GetMissionJoinedUseCase @Inject constructor(
+ private val missionDataSource: MissionDataSource
+) {
+ operator fun invoke(): Flow = missionDataSource.getIsMissionJoined()
+}
\ No newline at end of file
diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/GetMissionRankUseCase.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/GetMissionRankUseCase.kt
new file mode 100644
index 00000000..49a026b0
--- /dev/null
+++ b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/GetMissionRankUseCase.kt
@@ -0,0 +1,18 @@
+package com.goalpanzi.mission_mate.core.domain.usecase
+
+import com.goalpanzi.mission_mate.core.domain.repository.MissionRepository
+import com.goalpanzi.core.model.base.NetworkResult
+import com.goalpanzi.core.model.response.MissionRankResponse
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import javax.inject.Inject
+
+class GetMissionRankUseCase @Inject constructor(
+ private val missionRepository: MissionRepository
+) {
+ operator fun invoke(
+ missionId: Long
+ ): Flow> = flow {
+ emit(missionRepository.getMissionRank(missionId))
+ }
+}
\ No newline at end of file
diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/GetMissionUseCase.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/GetMissionUseCase.kt
new file mode 100644
index 00000000..ae38497c
--- /dev/null
+++ b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/GetMissionUseCase.kt
@@ -0,0 +1,18 @@
+package com.goalpanzi.mission_mate.core.domain.usecase
+
+import com.goalpanzi.mission_mate.core.domain.repository.MissionRepository
+import com.goalpanzi.core.model.base.NetworkResult
+import com.goalpanzi.core.model.response.MissionDetailResponse
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import javax.inject.Inject
+
+class GetMissionUseCase @Inject constructor(
+ private val missionRepository: MissionRepository
+) {
+ operator fun invoke(
+ missionId : Long
+ ): Flow> = flow {
+ emit(missionRepository.getMission(missionId))
+ }
+}
\ No newline at end of file
diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/GetMissionVerificationsUseCase.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/GetMissionVerificationsUseCase.kt
new file mode 100644
index 00000000..f7eb3292
--- /dev/null
+++ b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/GetMissionVerificationsUseCase.kt
@@ -0,0 +1,18 @@
+package com.goalpanzi.mission_mate.core.domain.usecase
+
+import com.goalpanzi.mission_mate.core.domain.repository.MissionRepository
+import com.goalpanzi.core.model.base.NetworkResult
+import com.goalpanzi.core.model.response.MissionVerificationsResponse
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import javax.inject.Inject
+
+class GetMissionVerificationsUseCase @Inject constructor(
+ private val missionRepository: MissionRepository
+) {
+ operator fun invoke(
+ missionId : Long
+ ): Flow> = flow {
+ emit(missionRepository.getMissionVerifications(missionId))
+ }
+}
\ No newline at end of file
diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/GetMyMissionVerificationUseCase.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/GetMyMissionVerificationUseCase.kt
new file mode 100644
index 00000000..e2e3e3cf
--- /dev/null
+++ b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/GetMyMissionVerificationUseCase.kt
@@ -0,0 +1,19 @@
+package com.goalpanzi.mission_mate.core.domain.usecase
+
+import com.goalpanzi.mission_mate.core.domain.repository.MissionRepository
+import com.goalpanzi.core.model.base.NetworkResult
+import com.goalpanzi.core.model.response.MissionVerificationResponse
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import javax.inject.Inject
+
+class GetMyMissionVerificationUseCase @Inject constructor(
+ private val missionRepository: MissionRepository
+) {
+ operator fun invoke(
+ missionId: Long,
+ number : Int
+ ): Flow> = flow {
+ emit(missionRepository.getMyMissionVerification(missionId,number))
+ }
+}
\ No newline at end of file
diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/GetViewedTooltipUseCase.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/GetViewedTooltipUseCase.kt
new file mode 100644
index 00000000..a19b3b59
--- /dev/null
+++ b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/GetViewedTooltipUseCase.kt
@@ -0,0 +1,11 @@
+package com.goalpanzi.mission_mate.core.domain.usecase
+
+import com.goalpanzi.mission_mate.core.datastore.datasource.DefaultDataSource
+import kotlinx.coroutines.flow.Flow
+import javax.inject.Inject
+
+class GetViewedTooltipUseCase @Inject constructor(
+ private val defaultDataSource: DefaultDataSource
+) {
+ operator fun invoke(): Flow = defaultDataSource.getViewedTooltip()
+}
\ No newline at end of file
diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/JoinMissionUseCase.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/JoinMissionUseCase.kt
new file mode 100644
index 00000000..edf48f2f
--- /dev/null
+++ b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/JoinMissionUseCase.kt
@@ -0,0 +1,17 @@
+package com.goalpanzi.mission_mate.core.domain.usecase
+
+import com.goalpanzi.mission_mate.core.domain.repository.OnboardingRepository
+import com.goalpanzi.core.model.base.NetworkResult
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import javax.inject.Inject
+
+class JoinMissionUseCase @Inject constructor(
+ private val onboardingRepository: OnboardingRepository
+) {
+ operator fun invoke(
+ invitationCode: String
+ ): Flow> = flow {
+ emit(onboardingRepository.joinMission(invitationCode))
+ }
+}
\ No newline at end of file
diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/LoginUseCase.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/LoginUseCase.kt
new file mode 100644
index 00000000..8c26acc6
--- /dev/null
+++ b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/LoginUseCase.kt
@@ -0,0 +1,47 @@
+package com.goalpanzi.mission_mate.core.domain.usecase
+
+import com.goalpanzi.mission_mate.core.datastore.datasource.AuthDataSource
+import com.goalpanzi.mission_mate.core.datastore.datasource.DefaultDataSource
+import com.goalpanzi.mission_mate.core.domain.repository.AuthRepository
+import com.goalpanzi.core.model.UserProfile
+import com.goalpanzi.core.model.base.NetworkResult
+import com.goalpanzi.core.model.response.GoogleLogin
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.runBlocking
+import javax.inject.Inject
+
+class LoginUseCase @Inject constructor(
+ private val authRepository: AuthRepository,
+ private val authDataSource: AuthDataSource,
+ private val defaultDataSource: DefaultDataSource
+) {
+ suspend fun requestGoogleLogin(email: String): GoogleLogin? {
+ return when (val response = authRepository.requestGoogleLogin(email)) {
+ is NetworkResult.Success -> {
+ response.data.also {
+ authDataSource.setAccessToken(it.accessToken).first()
+ authDataSource.setRefreshToken(it.refreshToken).first()
+ defaultDataSource.setMemberId(it.memberId).first()
+ (it.nickname to it.characterType).let { (nickname, character) ->
+ if (nickname != null && character != null) {
+ defaultDataSource.setUserProfile(
+ UserProfile(nickname, character)
+ ).first()
+ }
+ }
+ }
+ }
+
+ is NetworkResult.Error, is NetworkResult.Exception -> null
+ }
+ }
+
+ fun isNewUser(): Boolean = runBlocking(Dispatchers.IO) {
+ authDataSource.getAccessToken().first() == null
+ }
+
+ fun getCachedUserData(): UserProfile? = runBlocking(Dispatchers.IO) {
+ defaultDataSource.getUserProfile().first()
+ }
+}
\ No newline at end of file
diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/LogoutUseCase.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/LogoutUseCase.kt
new file mode 100644
index 00000000..98c88f13
--- /dev/null
+++ b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/LogoutUseCase.kt
@@ -0,0 +1,21 @@
+package com.goalpanzi.mission_mate.core.domain.usecase
+
+import com.goalpanzi.mission_mate.core.datastore.datasource.DefaultDataSource
+import com.goalpanzi.mission_mate.core.datastore.datasource.MissionDataSource
+import com.goalpanzi.mission_mate.core.domain.repository.AuthRepository
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flow
+import javax.inject.Inject
+
+class LogoutUseCase @Inject constructor(
+ private val authRepository: AuthRepository,
+ private val defaultDataSource: DefaultDataSource,
+ private val missionDataSource: MissionDataSource
+) {
+ operator fun invoke() = flow {
+ authRepository.requestLogout()
+ defaultDataSource.clearUserData().first()
+ missionDataSource.clearMissionData().first()
+ emit(Unit)
+ }
+}
\ No newline at end of file
diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/ProfileUseCase.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/ProfileUseCase.kt
new file mode 100644
index 00000000..989c6cac
--- /dev/null
+++ b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/ProfileUseCase.kt
@@ -0,0 +1,23 @@
+package com.goalpanzi.mission_mate.core.domain.usecase
+
+import com.goalpanzi.mission_mate.core.datastore.datasource.DefaultDataSource
+import com.goalpanzi.mission_mate.core.domain.repository.ProfileRepository
+import com.goalpanzi.core.model.CharacterType
+import com.goalpanzi.core.model.UserProfile
+import com.goalpanzi.core.model.base.NetworkResult
+import kotlinx.coroutines.flow.first
+import javax.inject.Inject
+
+class ProfileUseCase @Inject constructor(
+ private val profileRepository: ProfileRepository,
+ private val defaultDataSource: DefaultDataSource
+) {
+ suspend fun saveProfile(nickname: String, type: CharacterType, isEqualNickname: Boolean) =
+ profileRepository.saveProfile(nickname, type, isEqualNickname).also {
+ if (it is NetworkResult.Success) {
+ defaultDataSource.setUserProfile(UserProfile(nickname, type)).first()
+ }
+ }
+
+ suspend fun getProfile(): UserProfile? = defaultDataSource.getUserProfile().first()
+}
\ No newline at end of file
diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/SetMissionJoinedUseCase.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/SetMissionJoinedUseCase.kt
new file mode 100644
index 00000000..800a2c2a
--- /dev/null
+++ b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/SetMissionJoinedUseCase.kt
@@ -0,0 +1,13 @@
+package com.goalpanzi.mission_mate.core.domain.usecase
+
+import com.goalpanzi.mission_mate.core.datastore.datasource.MissionDataSource
+import kotlinx.coroutines.flow.Flow
+import javax.inject.Inject
+
+class SetMissionJoinedUseCase @Inject constructor(
+ private val missionDataSource: MissionDataSource
+) {
+ operator fun invoke(
+ isJoined : Boolean
+ ): Flow = missionDataSource.setIsMissionJoined(isJoined)
+}
\ No newline at end of file
diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/SetViewedTooltipUseCase.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/SetViewedTooltipUseCase.kt
new file mode 100644
index 00000000..c5a52a20
--- /dev/null
+++ b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/SetViewedTooltipUseCase.kt
@@ -0,0 +1,11 @@
+package com.goalpanzi.mission_mate.core.domain.usecase
+
+import com.goalpanzi.mission_mate.core.datastore.datasource.DefaultDataSource
+import kotlinx.coroutines.flow.Flow
+import javax.inject.Inject
+
+class SetViewedTooltipUseCase @Inject constructor(
+ private val defaultDataSource: DefaultDataSource
+) {
+ operator fun invoke(): Flow = defaultDataSource.setViewedTooltip()
+}
\ No newline at end of file
diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/VerifyMissionUseCase.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/VerifyMissionUseCase.kt
new file mode 100644
index 00000000..8d9d36e1
--- /dev/null
+++ b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/VerifyMissionUseCase.kt
@@ -0,0 +1,16 @@
+package com.goalpanzi.mission_mate.core.domain.usecase
+
+import com.goalpanzi.mission_mate.core.domain.repository.MissionRepository
+import com.goalpanzi.core.model.base.NetworkResult
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import java.io.File
+import javax.inject.Inject
+
+class VerifyMissionUseCase @Inject constructor(
+ private val missionRepository: MissionRepository
+) {
+ suspend operator fun invoke(missionId: Long, image: File) : Flow> = flow {
+ emit(missionRepository.verifyMission(missionId, image))
+ }
+}
\ No newline at end of file
diff --git a/core/model/.gitignore b/core/model/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/core/model/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/core/model/build.gradle.kts b/core/model/build.gradle.kts
new file mode 100644
index 00000000..055cf062
--- /dev/null
+++ b/core/model/build.gradle.kts
@@ -0,0 +1,41 @@
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.jetbrains.kotlin.android)
+ alias(libs.plugins.kotlin.plugin.serialization)
+ alias(libs.plugins.kotlin.parcelize)
+}
+
+android {
+ namespace = "com.luckyoct.core.model"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 26
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+}
+
+dependencies {
+
+ implementation(libs.kotlin.serialization.json)
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+}
\ No newline at end of file
diff --git a/core/model/proguard-rules.pro b/core/model/proguard-rules.pro
new file mode 100644
index 00000000..109525fd
--- /dev/null
+++ b/core/model/proguard-rules.pro
@@ -0,0 +1,29 @@
+# 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
+
+-keep class * { *; }
+-keep interface * { *; }
+
+# Keep Dependency Injection Framework related classes and methods
+-keep class dagger.hilt.** { *; }
+-keep class javax.inject.** { *; }
+-keep class javax.annotation.** { *; }
diff --git a/core/model/src/main/AndroidManifest.xml b/core/model/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..a5918e68
--- /dev/null
+++ b/core/model/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/core/model/src/main/java/com/goalpanzi/core/model/CharacterType.kt b/core/model/src/main/java/com/goalpanzi/core/model/CharacterType.kt
new file mode 100644
index 00000000..4da7487d
--- /dev/null
+++ b/core/model/src/main/java/com/goalpanzi/core/model/CharacterType.kt
@@ -0,0 +1,24 @@
+package com.goalpanzi.core.model
+
+import kotlinx.serialization.SerialName
+
+enum class CharacterType {
+
+ @SerialName("RABBIT")
+ RABBIT,
+
+ @SerialName("CAT")
+ CAT,
+
+ @SerialName("DOG")
+ DOG,
+
+ @SerialName("PANDA")
+ PANDA,
+
+ @SerialName("BEAR")
+ BEAR,
+
+ @SerialName("BIRD")
+ BIRD
+}
\ No newline at end of file
diff --git a/core/model/src/main/java/com/goalpanzi/core/model/MissionDetail.kt b/core/model/src/main/java/com/goalpanzi/core/model/MissionDetail.kt
new file mode 100644
index 00000000..9ceb0732
--- /dev/null
+++ b/core/model/src/main/java/com/goalpanzi/core/model/MissionDetail.kt
@@ -0,0 +1,77 @@
+package com.goalpanzi.mission_mate.feature.board.model
+
+import com.goalpanzi.core.model.response.MissionDetailResponse
+import java.time.DayOfWeek
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
+import java.time.format.TextStyle
+import java.util.Locale
+
+data class MissionDetail(
+ val missionId : Long,
+ val hostMemberId : Long,
+ val description : String,
+ val missionStartDate : String,
+ val missionEndDate : String,
+ val timeOfDay : String,
+ val missionDays : List,
+ val boardCount : Int,
+ val invitationCode : String
+){
+ val missionStartLocalDate: LocalDate by lazy {
+ val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")
+ LocalDate.parse(missionStartDate, formatter)
+ }
+
+ val missionEndLocalDateTime : LocalDateTime by lazy {
+ val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")
+ LocalDateTime.parse(missionEndDate, formatter)
+ }
+
+ fun isStartedMission() : Boolean {
+ val currentDate = LocalDate.now()
+ return currentDate.isEqual(missionStartLocalDate) || currentDate.isAfter(missionStartLocalDate)
+ }
+ val missionPeriod : String by lazy {
+ try {
+ val inputFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")
+ val outputFormatter = DateTimeFormatter.ofPattern("yyyy.MM.dd")
+
+ val startDate = LocalDateTime.parse(missionStartDate, inputFormatter)
+ val endDate = LocalDateTime.parse(missionEndDate, inputFormatter)
+
+ "${startDate.format(outputFormatter)} ~ ${endDate.format(outputFormatter)}"
+ }catch (e: Exception){
+ "$missionStartDate ~ $missionEndDate"
+ }
+ }
+
+ val missionDaysOfWeekTextLocale : List by lazy {
+ try {
+ missionDays.sortedBy {
+ it.ordinal
+ }.map {
+ it.getDisplayName(TextStyle.SHORT, Locale.getDefault())
+ }
+ }catch (e: Exception){
+ missionDays.map { it.name }
+ }
+ }
+}
+
+fun MissionDetailResponse.toModel() : MissionDetail {
+ return MissionDetail(
+ missionId = missionId,
+ hostMemberId = hostMemberId,
+ description = description,
+ missionStartDate = missionStartDate,
+ missionEndDate = missionEndDate,
+ boardCount = boardCount,
+ invitationCode = invitationCode,
+ missionDays = missionDays.map {
+ DayOfWeek.valueOf(it)
+ },
+ timeOfDay = timeOfDay
+ )
+}
\ No newline at end of file
diff --git a/core/model/src/main/java/com/goalpanzi/core/model/UserProfile.kt b/core/model/src/main/java/com/goalpanzi/core/model/UserProfile.kt
new file mode 100644
index 00000000..9167c6e1
--- /dev/null
+++ b/core/model/src/main/java/com/goalpanzi/core/model/UserProfile.kt
@@ -0,0 +1,6 @@
+package com.goalpanzi.core.model
+
+data class UserProfile(
+ val nickname: String,
+ val characterType: CharacterType
+)
\ No newline at end of file
diff --git a/core/model/src/main/java/com/goalpanzi/core/model/base/NetworkResult.kt b/core/model/src/main/java/com/goalpanzi/core/model/base/NetworkResult.kt
new file mode 100644
index 00000000..e1c4ff76
--- /dev/null
+++ b/core/model/src/main/java/com/goalpanzi/core/model/base/NetworkResult.kt
@@ -0,0 +1,7 @@
+package com.goalpanzi.core.model.base
+
+sealed interface NetworkResult {
+ data class Success(val data: T) : NetworkResult
+ data class Error(val code: Int? = null, val message: String? = null) : NetworkResult
+ data class Exception(val error: Throwable) : NetworkResult
+}
\ No newline at end of file
diff --git a/core/model/src/main/java/com/goalpanzi/core/model/request/CreateMissionRequest.kt b/core/model/src/main/java/com/goalpanzi/core/model/request/CreateMissionRequest.kt
new file mode 100644
index 00000000..f46fc6e8
--- /dev/null
+++ b/core/model/src/main/java/com/goalpanzi/core/model/request/CreateMissionRequest.kt
@@ -0,0 +1,13 @@
+package com.goalpanzi.core.model.request
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class CreateMissionRequest(
+ val description : String,
+ val missionStartDate : String,
+ val missionEndDate : String,
+ val timeOfDay : String,
+ val missionDays : List,
+ val boardCount : Int
+)
\ No newline at end of file
diff --git a/core/model/src/main/java/com/goalpanzi/core/model/request/GoogleLoginRequest.kt b/core/model/src/main/java/com/goalpanzi/core/model/request/GoogleLoginRequest.kt
new file mode 100644
index 00000000..0e514256
--- /dev/null
+++ b/core/model/src/main/java/com/goalpanzi/core/model/request/GoogleLoginRequest.kt
@@ -0,0 +1,8 @@
+package com.goalpanzi.core.model.request
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class GoogleLoginRequest(
+ val email: String
+)
diff --git a/core/model/src/main/java/com/goalpanzi/core/model/request/JoinMissionRequest.kt b/core/model/src/main/java/com/goalpanzi/core/model/request/JoinMissionRequest.kt
new file mode 100644
index 00000000..190549de
--- /dev/null
+++ b/core/model/src/main/java/com/goalpanzi/core/model/request/JoinMissionRequest.kt
@@ -0,0 +1,8 @@
+package com.goalpanzi.core.model.request
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class JoinMissionRequest(
+ val invitationCode : String
+)
\ No newline at end of file
diff --git a/core/model/src/main/java/com/goalpanzi/core/model/request/SaveProfileRequest.kt b/core/model/src/main/java/com/goalpanzi/core/model/request/SaveProfileRequest.kt
new file mode 100644
index 00000000..7d41141a
--- /dev/null
+++ b/core/model/src/main/java/com/goalpanzi/core/model/request/SaveProfileRequest.kt
@@ -0,0 +1,17 @@
+package com.goalpanzi.core.model.request
+
+import com.goalpanzi.core.model.CharacterType
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class SaveProfileRequest(
+ val nickname: String?,
+ val characterType: String,
+) {
+ companion object {
+ fun createRequest(nickname: String?, type: CharacterType) = SaveProfileRequest(
+ nickname = nickname,
+ characterType = type.name.uppercase()
+ )
+ }
+}
diff --git a/core/model/src/main/java/com/goalpanzi/core/model/request/TokenReissueRequest.kt b/core/model/src/main/java/com/goalpanzi/core/model/request/TokenReissueRequest.kt
new file mode 100644
index 00000000..8aaae340
--- /dev/null
+++ b/core/model/src/main/java/com/goalpanzi/core/model/request/TokenReissueRequest.kt
@@ -0,0 +1,8 @@
+package com.goalpanzi.core.model.request
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class TokenReissueRequest(
+ val refreshToken: String
+)
diff --git a/core/model/src/main/java/com/goalpanzi/core/model/request/VerifyMissionRequest.kt b/core/model/src/main/java/com/goalpanzi/core/model/request/VerifyMissionRequest.kt
new file mode 100644
index 00000000..6d4ce0e2
--- /dev/null
+++ b/core/model/src/main/java/com/goalpanzi/core/model/request/VerifyMissionRequest.kt
@@ -0,0 +1,8 @@
+package com.goalpanzi.core.model.request
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class VerifyMissionRequest(
+ val imageFile: String
+)
diff --git a/core/model/src/main/java/com/goalpanzi/core/model/response/GoogleLogin.kt b/core/model/src/main/java/com/goalpanzi/core/model/response/GoogleLogin.kt
new file mode 100644
index 00000000..4b1fd34c
--- /dev/null
+++ b/core/model/src/main/java/com/goalpanzi/core/model/response/GoogleLogin.kt
@@ -0,0 +1,14 @@
+package com.goalpanzi.core.model.response
+
+import com.goalpanzi.core.model.CharacterType
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class GoogleLogin(
+ val accessToken: String,
+ val refreshToken: String,
+ val nickname: String?,
+ val characterType: CharacterType?,
+ val isProfileSet: Boolean,
+ val memberId: Long
+)
diff --git a/core/model/src/main/java/com/goalpanzi/core/model/response/MissionBoardMembersResponse.kt b/core/model/src/main/java/com/goalpanzi/core/model/response/MissionBoardMembersResponse.kt
new file mode 100644
index 00000000..30f8fd7f
--- /dev/null
+++ b/core/model/src/main/java/com/goalpanzi/core/model/response/MissionBoardMembersResponse.kt
@@ -0,0 +1,11 @@
+package com.goalpanzi.core.model.response
+
+import com.goalpanzi.core.model.CharacterType
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class MissionBoardMembersResponse(
+ //val memberId : Long,
+ val nickname : String,
+ val characterType : CharacterType = CharacterType.RABBIT
+)
diff --git a/core/model/src/main/java/com/goalpanzi/core/model/response/MissionBoardResponse.kt b/core/model/src/main/java/com/goalpanzi/core/model/response/MissionBoardResponse.kt
new file mode 100644
index 00000000..e1ad9c3b
--- /dev/null
+++ b/core/model/src/main/java/com/goalpanzi/core/model/response/MissionBoardResponse.kt
@@ -0,0 +1,15 @@
+package com.goalpanzi.core.model.response
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class MissionBoardResponse(
+ val number : Int,
+ val reward : BoardReward = BoardReward.NONE,
+ val isMyPosition : Boolean = false,
+ val missionBoardMembers : List
+)
+
+enum class BoardReward {
+ ORANGE, CANOLA_FLOWER, DOLHARUBANG, HORSE_RIDING, HALLA_MOUNTAIN, WATERFALL, BLACK_PIG, SUNRISE, GREEN_TEA_FIELD, BEACH, NONE
+}
\ No newline at end of file
diff --git a/core/model/src/main/java/com/goalpanzi/core/model/response/MissionBoardsResponse.kt b/core/model/src/main/java/com/goalpanzi/core/model/response/MissionBoardsResponse.kt
new file mode 100644
index 00000000..3f065def
--- /dev/null
+++ b/core/model/src/main/java/com/goalpanzi/core/model/response/MissionBoardsResponse.kt
@@ -0,0 +1,10 @@
+package com.goalpanzi.core.model.response
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class MissionBoardsResponse(
+ val missionBoards : List,
+ val progressCount : Int,
+ val rank : Int
+)
\ No newline at end of file
diff --git a/core/model/src/main/java/com/goalpanzi/core/model/response/MissionDetailResponse.kt b/core/model/src/main/java/com/goalpanzi/core/model/response/MissionDetailResponse.kt
new file mode 100644
index 00000000..d8c2a061
--- /dev/null
+++ b/core/model/src/main/java/com/goalpanzi/core/model/response/MissionDetailResponse.kt
@@ -0,0 +1,16 @@
+package com.goalpanzi.core.model.response
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class MissionDetailResponse(
+ val missionId: Long,
+ val hostMemberId: Long,
+ val description: String,
+ val missionStartDate: String,
+ val missionEndDate: String,
+ val timeOfDay: String,
+ val missionDays: List,
+ val boardCount: Int,
+ val invitationCode: String
+)
\ No newline at end of file
diff --git a/core/model/src/main/java/com/goalpanzi/core/model/response/MissionRankResponse.kt b/core/model/src/main/java/com/goalpanzi/core/model/response/MissionRankResponse.kt
new file mode 100644
index 00000000..262b1a05
--- /dev/null
+++ b/core/model/src/main/java/com/goalpanzi/core/model/response/MissionRankResponse.kt
@@ -0,0 +1,8 @@
+package com.goalpanzi.core.model.response
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class MissionRankResponse(
+ val rank: Int
+)
\ No newline at end of file
diff --git a/core/model/src/main/java/com/goalpanzi/core/model/response/MissionResponse.kt b/core/model/src/main/java/com/goalpanzi/core/model/response/MissionResponse.kt
new file mode 100644
index 00000000..1537f26b
--- /dev/null
+++ b/core/model/src/main/java/com/goalpanzi/core/model/response/MissionResponse.kt
@@ -0,0 +1,9 @@
+package com.goalpanzi.core.model.response
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class MissionResponse(
+ val missionId : Long,
+ val description : String
+)
diff --git a/core/model/src/main/java/com/goalpanzi/core/model/response/MissionVerificationResponse.kt b/core/model/src/main/java/com/goalpanzi/core/model/response/MissionVerificationResponse.kt
new file mode 100644
index 00000000..b080b6b9
--- /dev/null
+++ b/core/model/src/main/java/com/goalpanzi/core/model/response/MissionVerificationResponse.kt
@@ -0,0 +1,12 @@
+package com.goalpanzi.core.model.response
+
+import com.goalpanzi.core.model.CharacterType
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class MissionVerificationResponse(
+ val nickname : String,
+ val characterType : CharacterType = CharacterType.RABBIT,
+ val imageUrl : String = "",
+ val verifiedAt : String = ""
+)
diff --git a/core/model/src/main/java/com/goalpanzi/core/model/response/MissionVerificationsResponse.kt b/core/model/src/main/java/com/goalpanzi/core/model/response/MissionVerificationsResponse.kt
new file mode 100644
index 00000000..8a32a3b7
--- /dev/null
+++ b/core/model/src/main/java/com/goalpanzi/core/model/response/MissionVerificationsResponse.kt
@@ -0,0 +1,8 @@
+package com.goalpanzi.core.model.response
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class MissionVerificationsResponse(
+ val missionVerifications : List
+)
\ No newline at end of file
diff --git a/core/model/src/main/java/com/goalpanzi/core/model/response/MissionsResponse.kt b/core/model/src/main/java/com/goalpanzi/core/model/response/MissionsResponse.kt
new file mode 100644
index 00000000..6bf229c1
--- /dev/null
+++ b/core/model/src/main/java/com/goalpanzi/core/model/response/MissionsResponse.kt
@@ -0,0 +1,9 @@
+package com.goalpanzi.core.model.response
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class MissionsResponse(
+ val profile : ProfileResponse,
+ val missions : List
+)
diff --git a/core/model/src/main/java/com/goalpanzi/core/model/response/ProfileResponse.kt b/core/model/src/main/java/com/goalpanzi/core/model/response/ProfileResponse.kt
new file mode 100644
index 00000000..0e4a9caf
--- /dev/null
+++ b/core/model/src/main/java/com/goalpanzi/core/model/response/ProfileResponse.kt
@@ -0,0 +1,9 @@
+package com.goalpanzi.core.model.response
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class ProfileResponse(
+ val nickname : String,
+ val characterType : String
+)
diff --git a/core/model/src/main/java/com/goalpanzi/core/model/response/TokenReissue.kt b/core/model/src/main/java/com/goalpanzi/core/model/response/TokenReissue.kt
new file mode 100644
index 00000000..87e90351
--- /dev/null
+++ b/core/model/src/main/java/com/goalpanzi/core/model/response/TokenReissue.kt
@@ -0,0 +1,9 @@
+package com.goalpanzi.core.model.response
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class TokenReissue(
+ val accessToken: String,
+ val refreshToken: String
+)
diff --git a/feature/board/src/test/java/com/goalpanzi/mission_mate/feature/board/ExampleUnitTest.kt b/core/model/src/test/java/com/goalpanzi/model/ExampleUnitTest.kt
similarity index 86%
rename from feature/board/src/test/java/com/goalpanzi/mission_mate/feature/board/ExampleUnitTest.kt
rename to core/model/src/test/java/com/goalpanzi/model/ExampleUnitTest.kt
index ffcafaa6..c2b23330 100644
--- a/feature/board/src/test/java/com/goalpanzi/mission_mate/feature/board/ExampleUnitTest.kt
+++ b/core/model/src/test/java/com/goalpanzi/model/ExampleUnitTest.kt
@@ -1,4 +1,4 @@
-package com.goalpanzi.mission_mate.feature.board
+package com.goalpanzi.model
import org.junit.Test
diff --git a/core/navigation/build.gradle.kts b/core/navigation/build.gradle.kts
index f32dd8ce..9a78be5f 100644
--- a/core/navigation/build.gradle.kts
+++ b/core/navigation/build.gradle.kts
@@ -14,12 +14,10 @@ android {
minSdk = 26
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
- isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
diff --git a/core/navigation/consumer-rules.pro b/core/navigation/consumer-rules.pro
deleted file mode 100644
index e69de29b..00000000
diff --git a/core/navigation/proguard-rules.pro b/core/navigation/proguard-rules.pro
index 481bb434..109525fd 100644
--- a/core/navigation/proguard-rules.pro
+++ b/core/navigation/proguard-rules.pro
@@ -18,4 +18,12 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
-#-renamesourcefileattribute SourceFile
\ No newline at end of file
+#-renamesourcefileattribute SourceFile
+
+-keep class * { *; }
+-keep interface * { *; }
+
+# Keep Dependency Injection Framework related classes and methods
+-keep class dagger.hilt.** { *; }
+-keep class javax.inject.** { *; }
+-keep class javax.annotation.** { *; }
diff --git a/core/navigation/src/androidTest/java/com/goalpanzi/mission_mate/core/navigation/ExampleInstrumentedTest.kt b/core/navigation/src/androidTest/java/com/goalpanzi/mission_mate/core/navigation/ExampleInstrumentedTest.kt
deleted file mode 100644
index df0b46c1..00000000
--- a/core/navigation/src/androidTest/java/com/goalpanzi/mission_mate/core/navigation/ExampleInstrumentedTest.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.goalpanzi.mission_mate.core.navigation
-
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.ext.junit.runners.AndroidJUnit4
-
-import org.junit.Test
-import org.junit.runner.RunWith
-
-import org.junit.Assert.*
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-@RunWith(AndroidJUnit4::class)
-class ExampleInstrumentedTest {
- @Test
- fun useAppContext() {
- // Context of the app under test.
- val appContext = InstrumentationRegistry.getInstrumentation().targetContext
- assertEquals("com.goalpanzi.mission_mate.core.navigation.test", appContext.packageName)
- }
-}
\ No newline at end of file
diff --git a/core/navigation/src/main/java/com/goalpanzi/mission_mate/core/navigation/Navigation.kt b/core/navigation/src/main/java/com/goalpanzi/mission_mate/core/navigation/Navigation.kt
deleted file mode 100644
index f95186bb..00000000
--- a/core/navigation/src/main/java/com/goalpanzi/mission_mate/core/navigation/Navigation.kt
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.goalpanzi.mission_mate.core.navigation
-
-class Navigation {
-}
\ No newline at end of file
diff --git a/core/navigation/src/main/java/com/goalpanzi/mission_mate/core/navigation/RouteModel.kt b/core/navigation/src/main/java/com/goalpanzi/mission_mate/core/navigation/RouteModel.kt
new file mode 100644
index 00000000..7c633133
--- /dev/null
+++ b/core/navigation/src/main/java/com/goalpanzi/mission_mate/core/navigation/RouteModel.kt
@@ -0,0 +1,44 @@
+package com.goalpanzi.mission_mate.core.navigation
+
+import kotlinx.serialization.Serializable
+
+sealed interface RouteModel {
+ @Serializable
+ data object Login : RouteModel
+
+ @Serializable
+ data object Onboarding : RouteModel
+
+ @Serializable
+ sealed interface Profile: RouteModel {
+ @Serializable
+ data object Create : Profile
+ @Serializable
+ data object Setting : Profile
+ }
+ @Serializable
+ data object Setting : RouteModel
+
+ @Serializable
+ data class Board(val missionId : Long) : RouteModel
+}
+
+sealed interface OnboardingRouteModel {
+ @Serializable
+ data object BoardSetup : OnboardingRouteModel
+
+ @Serializable
+ data object BoardSetupSuccess : OnboardingRouteModel
+
+ @Serializable
+ data object InvitationCode : OnboardingRouteModel
+}
+
+sealed interface SettingRouteModel {
+
+ @Serializable
+ data object ServicePolicy : SettingRouteModel
+
+ @Serializable
+ data object PrivacyPolicy : SettingRouteModel
+}
\ No newline at end of file
diff --git a/core/navigation/src/test/java/com/goalpanzi/mission_mate/core/navigation/ExampleUnitTest.kt b/core/navigation/src/test/java/com/goalpanzi/mission_mate/core/navigation/ExampleUnitTest.kt
index a626b5a9..d3de45c2 100644
--- a/core/navigation/src/test/java/com/goalpanzi/mission_mate/core/navigation/ExampleUnitTest.kt
+++ b/core/navigation/src/test/java/com/goalpanzi/mission_mate/core/navigation/ExampleUnitTest.kt
@@ -1,8 +1,6 @@
package com.goalpanzi.mission_mate.core.navigation
-import org.junit.Test
-import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
@@ -10,8 +8,5 @@ import org.junit.Assert.*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
- @Test
- fun addition_isCorrect() {
- assertEquals(4, 2 + 2)
- }
+
}
\ No newline at end of file
diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts
index 99e10005..ea1e4c2d 100644
--- a/core/network/build.gradle.kts
+++ b/core/network/build.gradle.kts
@@ -1,3 +1,4 @@
+import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
@@ -16,18 +17,23 @@ android {
minSdk = 26
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
+ debug {
+ buildConfigField("String", "BASE_URL", getMissionMateBaseUrl())
+ }
release {
- isMinifyEnabled = false
+ buildConfigField("String", "BASE_URL", getMissionMateBaseUrl())
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
+ buildFeatures {
+ buildConfig = true
+ }
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
@@ -48,4 +54,11 @@ dependencies {
ksp(libs.hilt.compiler)
implementation(libs.hilt.android)
-}
\ No newline at end of file
+
+ implementation(project(":core:model"))
+ implementation(project(":core:datastore"))
+}
+
+fun getMissionMateBaseUrl(): String {
+ return gradleLocalProperties(rootDir, providers).getProperty("MISSION_MATE_BASE_URL") ?: ""
+}
diff --git a/core/network/consumer-rules.pro b/core/network/consumer-rules.pro
deleted file mode 100644
index e69de29b..00000000
diff --git a/core/network/proguard-rules.pro b/core/network/proguard-rules.pro
index 481bb434..109525fd 100644
--- a/core/network/proguard-rules.pro
+++ b/core/network/proguard-rules.pro
@@ -18,4 +18,12 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
-#-renamesourcefileattribute SourceFile
\ No newline at end of file
+#-renamesourcefileattribute SourceFile
+
+-keep class * { *; }
+-keep interface * { *; }
+
+# Keep Dependency Injection Framework related classes and methods
+-keep class dagger.hilt.** { *; }
+-keep class javax.inject.** { *; }
+-keep class javax.annotation.** { *; }
diff --git a/core/network/src/androidTest/java/com/goalpanzi/mission_mate/core/network/ExampleInstrumentedTest.kt b/core/network/src/androidTest/java/com/goalpanzi/mission_mate/core/network/ExampleInstrumentedTest.kt
deleted file mode 100644
index 089f385d..00000000
--- a/core/network/src/androidTest/java/com/goalpanzi/mission_mate/core/network/ExampleInstrumentedTest.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.goalpanzi.mission_mate.core.network
-
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.ext.junit.runners.AndroidJUnit4
-
-import org.junit.Test
-import org.junit.runner.RunWith
-
-import org.junit.Assert.*
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-@RunWith(AndroidJUnit4::class)
-class ExampleInstrumentedTest {
- @Test
- fun useAppContext() {
- // Context of the app under test.
- val appContext = InstrumentationRegistry.getInstrumentation().targetContext
- assertEquals("com.goalpanzi.mission_mate.core.network.test", appContext.packageName)
- }
-}
\ No newline at end of file
diff --git a/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/Network.kt b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/Network.kt
deleted file mode 100644
index 4112733d..00000000
--- a/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/Network.kt
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.goalpanzi.mission_mate.core.network
-
-class Network {
-}
\ No newline at end of file
diff --git a/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/ResultHandler.kt b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/ResultHandler.kt
new file mode 100644
index 00000000..0c24d166
--- /dev/null
+++ b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/ResultHandler.kt
@@ -0,0 +1,28 @@
+package com.goalpanzi.mission_mate.core.network
+
+import com.goalpanzi.core.model.base.NetworkResult
+import retrofit2.HttpException
+import retrofit2.Response
+
+interface ResultHandler {
+
+ suspend fun handleResult(execute: suspend () -> Response): NetworkResult {
+ return try {
+ val response = execute()
+ if (response.isSuccessful) {
+ val body = response.body()
+ if (body != null) {
+ NetworkResult.Success(body)
+ } else {
+ NetworkResult.Error(response.code(), "Response body is null")
+ }
+ } else {
+ NetworkResult.Error(response.code(), response.errorBody()?.string())
+ }
+ } catch (e: HttpException) {
+ NetworkResult.Error(e.code(), e.message())
+ } catch (e: Throwable) {
+ NetworkResult.Exception(e)
+ }
+ }
+}
diff --git a/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/TokenInterceptor.kt b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/TokenInterceptor.kt
new file mode 100644
index 00000000..401b3cf7
--- /dev/null
+++ b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/TokenInterceptor.kt
@@ -0,0 +1,67 @@
+package com.goalpanzi.mission_mate.core.network
+
+import com.goalpanzi.mission_mate.core.datastore.datasource.AuthDataSource
+import com.goalpanzi.mission_mate.core.network.service.TokenService
+import com.goalpanzi.core.model.request.TokenReissueRequest
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import okhttp3.Interceptor
+import okhttp3.Response
+import java.net.HttpURLConnection
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class TokenInterceptor @Inject constructor(
+ private val authDataSource: AuthDataSource,
+ private val tokenService: TokenService
+) : Interceptor {
+
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val newRequest = chain.request().newBuilder().apply {
+ runBlocking {
+ val token = authDataSource.getAccessToken().first()
+ token?.let {
+ addHeader("Authorization", "Bearer $it")
+ }
+ }
+ }
+
+ val response = chain.proceed(newRequest.build())
+
+ when (response.code) {
+ HttpURLConnection.HTTP_OK -> {
+ val newAccessToken: String = response.header("Authorization", null) ?: return response
+ CoroutineScope(Dispatchers.IO).launch {
+ val existedAccessToken = authDataSource.getAccessToken().first()
+ if (existedAccessToken != newAccessToken) {
+ authDataSource.setAccessToken(newAccessToken)
+ }
+ }
+ }
+ HttpURLConnection.HTTP_UNAUTHORIZED -> {
+ val retryRequest = chain.request().newBuilder().apply {
+ runBlocking {
+ authDataSource.getRefreshToken().first()?.let {
+ val newToken = tokenService.requestTokenReissue(TokenReissueRequest(it))
+ if (newToken.isSuccessful) {
+ newToken.body()?.let { token ->
+ addHeader("Authorization", "Bearer ${token.accessToken}")
+ CoroutineScope(Dispatchers.IO).launch {
+ authDataSource.setAccessToken(token.accessToken)
+ authDataSource.setRefreshToken(token.refreshToken)
+ }
+ }
+ }
+ }
+ }
+ }
+ return chain.proceed(retryRequest.build())
+ }
+ }
+ return response
+ }
+}
\ No newline at end of file
diff --git a/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/di/NetworkModule.kt b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/di/NetworkModule.kt
new file mode 100644
index 00000000..63a96c09
--- /dev/null
+++ b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/di/NetworkModule.kt
@@ -0,0 +1,68 @@
+package com.goalpanzi.mission_mate.core.network.di
+
+import com.goalpanzi.mission_mate.core.network.BuildConfig
+import com.goalpanzi.mission_mate.core.network.TokenInterceptor
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import kotlinx.serialization.json.Json
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
+import retrofit2.Converter
+import retrofit2.Retrofit
+import retrofit2.converter.kotlinx.serialization.asConverterFactory
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+internal object NetworkModule {
+
+ @Provides
+ @Singleton
+ fun provideRequestHttpLoggingInterceptor(): HttpLoggingInterceptor {
+ return HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }
+ }
+
+ @Provides
+ @Singleton
+ fun provideOkhttpClient(
+ httpLoggingInterceptor: HttpLoggingInterceptor,
+ tokenInterceptor: TokenInterceptor
+ ): OkHttpClient {
+
+ return OkHttpClient.Builder()
+ .addInterceptor(tokenInterceptor)
+ .addInterceptor(httpLoggingInterceptor)
+ .build()
+ }
+
+ @Provides
+ @Singleton
+ fun provideJson(): Json = Json {
+ ignoreUnknownKeys = true
+ coerceInputValues = true
+ }
+
+ @Provides
+ @Singleton
+ fun provideConverterFactory(
+ json: Json,
+ ): Converter.Factory {
+ return json.asConverterFactory("application/json".toMediaType())
+ }
+
+ @Provides
+ @Singleton
+ fun provideRetrofit(
+ okHttpClient: OkHttpClient,
+ converterFactory: Converter.Factory
+ ) : Retrofit {
+ return Retrofit.Builder()
+ .client(okHttpClient)
+ .baseUrl(BuildConfig.BASE_URL)
+ .addConverterFactory(converterFactory)
+ .build()
+ }
+}
\ No newline at end of file
diff --git a/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/di/ServiceModule.kt b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/di/ServiceModule.kt
new file mode 100644
index 00000000..433fcaca
--- /dev/null
+++ b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/di/ServiceModule.kt
@@ -0,0 +1,83 @@
+package com.goalpanzi.mission_mate.core.network.di
+
+import android.util.Log
+import com.goalpanzi.mission_mate.core.datastore.datasource.AuthDataSource
+import com.goalpanzi.mission_mate.core.network.BuildConfig
+import com.goalpanzi.mission_mate.core.network.service.LoginService
+import com.goalpanzi.mission_mate.core.network.service.MissionService
+import com.goalpanzi.mission_mate.core.network.service.OnboardingService
+import com.goalpanzi.mission_mate.core.network.service.ProfileService
+import com.goalpanzi.mission_mate.core.network.service.TokenService
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.runBlocking
+import okhttp3.Interceptor
+import okhttp3.OkHttpClient
+import okhttp3.Response
+import okhttp3.logging.HttpLoggingInterceptor
+import retrofit2.Converter
+import retrofit2.Retrofit
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object ServiceModule {
+
+ @Provides
+ @Singleton
+ fun provideLoginService(retrofit: Retrofit): LoginService {
+ return retrofit.create(LoginService::class.java)
+ }
+
+ @Provides
+ @Singleton
+ fun provideProfileService(retrofit: Retrofit): ProfileService {
+ return retrofit.create(ProfileService::class.java)
+ }
+
+ @Provides
+ @Singleton
+ fun provideOnboardingService(retrofit: Retrofit): OnboardingService {
+ return retrofit.create(OnboardingService::class.java)
+ }
+
+ @Provides
+ @Singleton
+ fun provideMissionService(retrofit: Retrofit): MissionService {
+ return retrofit.create(MissionService::class.java)
+ }
+
+ @Provides
+ @Singleton
+ fun provideTokenService(
+ httpLoggingInterceptor: HttpLoggingInterceptor,
+ converterFactory: Converter.Factory,
+ authDataSource: AuthDataSource
+ ): TokenService {
+ val tokenReissueInterceptor = Interceptor { chain ->
+ val newRequest = chain.request().newBuilder().apply {
+ runBlocking {
+ val token = authDataSource.getAccessToken().first()
+ token?.let {
+ addHeader("Authorization", "Bearer $it")
+ }
+ }
+ }
+ chain.proceed(newRequest.build())
+ }
+ val retrofit = Retrofit.Builder()
+ .client(
+ OkHttpClient.Builder()
+ .addInterceptor(tokenReissueInterceptor)
+ .addInterceptor(httpLoggingInterceptor)
+ .build()
+ )
+ .baseUrl(BuildConfig.BASE_URL)
+ .addConverterFactory(converterFactory)
+ .build()
+ return retrofit.create(TokenService::class.java)
+ }
+}
\ No newline at end of file
diff --git a/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/service/LoginService.kt b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/service/LoginService.kt
new file mode 100644
index 00000000..790ca74a
--- /dev/null
+++ b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/service/LoginService.kt
@@ -0,0 +1,22 @@
+package com.goalpanzi.mission_mate.core.network.service
+
+import com.goalpanzi.core.model.response.GoogleLogin
+import com.goalpanzi.core.model.request.GoogleLoginRequest
+import retrofit2.Response
+import retrofit2.http.Body
+import retrofit2.http.DELETE
+import retrofit2.http.POST
+
+interface LoginService {
+
+ @POST("/api/auth/login/google")
+ suspend fun requestGoogleLogin(
+ @Body request: GoogleLoginRequest
+ ): Response
+
+ @POST("/api/auth/logout")
+ suspend fun requestLogout(): Response
+
+ @DELETE("/api/member")
+ suspend fun requestDeleteAccount(): Response
+}
\ No newline at end of file
diff --git a/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/service/MissionService.kt b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/service/MissionService.kt
new file mode 100644
index 00000000..7844f822
--- /dev/null
+++ b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/service/MissionService.kt
@@ -0,0 +1,57 @@
+package com.goalpanzi.mission_mate.core.network.service
+
+import com.goalpanzi.core.model.response.MissionBoardsResponse
+import com.goalpanzi.core.model.response.MissionDetailResponse
+import com.goalpanzi.core.model.response.MissionRankResponse
+import com.goalpanzi.core.model.response.MissionVerificationResponse
+import com.goalpanzi.core.model.response.MissionVerificationsResponse
+import okhttp3.MultipartBody
+import retrofit2.Response
+import retrofit2.http.DELETE
+import retrofit2.http.GET
+import retrofit2.http.Multipart
+import retrofit2.http.POST
+import retrofit2.http.Part
+import retrofit2.http.Path
+import retrofit2.http.Query
+
+interface MissionService {
+ @GET("/api/missions/{missionId}/board")
+ suspend fun getMissionBoards(
+ @Path("missionId") missionId: Long
+ ): Response
+
+
+ @GET("/api/missions/{missionId}")
+ suspend fun getMission(
+ @Path("missionId") missionId: Long
+ ): Response
+
+ @GET("/api/missions/{missionId}/verifications")
+ suspend fun getMissionVerifications(
+ @Path("missionId") missionId: Long
+ ) : Response
+
+ @DELETE("/api/missions/{missionId}")
+ suspend fun deleteMission(
+ @Path("missionId") missionId: Long
+ ) : Response
+
+ @GET("/api/mission-members/rank")
+ suspend fun getMissionRank(
+ @Query("missionId") missionId : Long
+ ) : Response
+
+ @Multipart
+ @POST("/api/missions/{missionId}/verifications/me")
+ suspend fun verifyMission(
+ @Path("missionId") missionId: Long,
+ @Part imageFile: MultipartBody.Part
+ ) : Response
+
+ @GET("/api/missions/{missionId}/verifications/me/{number}")
+ suspend fun getMyMissionVerification(
+ @Path("missionId") missionId: Long,
+ @Path("number") number: Int
+ ) : Response
+}
diff --git a/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/service/OnboardingService.kt b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/service/OnboardingService.kt
new file mode 100644
index 00000000..cd53f118
--- /dev/null
+++ b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/service/OnboardingService.kt
@@ -0,0 +1,33 @@
+package com.goalpanzi.mission_mate.core.network.service
+
+import com.goalpanzi.core.model.request.CreateMissionRequest
+import com.goalpanzi.core.model.request.JoinMissionRequest
+import com.goalpanzi.core.model.response.MissionDetailResponse
+import com.goalpanzi.core.model.response.MissionsResponse
+import retrofit2.Response
+import retrofit2.http.Body
+import retrofit2.http.GET
+import retrofit2.http.POST
+import retrofit2.http.Query
+
+interface OnboardingService {
+ @POST("/api/missions")
+ suspend fun createMission(
+ @Body request: CreateMissionRequest
+ ): Response
+
+ @GET("/api/mission:joinable")
+ suspend fun getMissionByInvitationCode(
+ @Query("invitationCode") invitationCode : String
+ ) : Response
+
+ @POST("/api/mission-members")
+ suspend fun joinMission(
+ @Body request : JoinMissionRequest
+ ) : Response
+
+ @GET("/api/mission-members/me")
+ suspend fun getJoinedMissions(
+ @Query("filter") filter : String = "PENDING,ONGOING"
+ ) : Response
+}
\ No newline at end of file
diff --git a/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/service/ProfileService.kt b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/service/ProfileService.kt
new file mode 100644
index 00000000..fbac243c
--- /dev/null
+++ b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/service/ProfileService.kt
@@ -0,0 +1,13 @@
+package com.goalpanzi.mission_mate.core.network.service
+
+import com.goalpanzi.core.model.request.SaveProfileRequest
+import retrofit2.Response
+import retrofit2.http.Body
+import retrofit2.http.PATCH
+
+interface ProfileService {
+ @PATCH("/api/member/profile")
+ suspend fun saveProfile(
+ @Body request: SaveProfileRequest
+ ): Response
+}
\ No newline at end of file
diff --git a/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/service/TokenService.kt b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/service/TokenService.kt
new file mode 100644
index 00000000..53cad9eb
--- /dev/null
+++ b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/service/TokenService.kt
@@ -0,0 +1,14 @@
+package com.goalpanzi.mission_mate.core.network.service
+
+import com.goalpanzi.core.model.request.TokenReissueRequest
+import com.goalpanzi.core.model.response.TokenReissue
+import retrofit2.Response
+import retrofit2.http.Body
+import retrofit2.http.POST
+
+interface TokenService {
+ @POST("/api/auth/token:reissue")
+ suspend fun requestTokenReissue(
+ @Body request: TokenReissueRequest
+ ): Response
+}
\ No newline at end of file
diff --git a/fastlane/Appfile b/fastlane/Appfile
new file mode 100644
index 00000000..755cf3ce
--- /dev/null
+++ b/fastlane/Appfile
@@ -0,0 +1,2 @@
+json_key_file("") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one
+package_name("com.goalpanzi.mission_mate") # e.g. com.krausefx.app
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
new file mode 100644
index 00000000..2b1d9504
--- /dev/null
+++ b/fastlane/Fastfile
@@ -0,0 +1,45 @@
+# This file contains the fastlane.tools configuration
+# You can find the documentation at https://docs.fastlane.tools
+#
+# For a list of all available actions, check out
+#
+# https://docs.fastlane.tools/actions
+#
+# For a list of all available plugins, check out
+#
+# https://docs.fastlane.tools/plugins/available-plugins
+#
+
+# Uncomment the line if you want fastlane to automatically update itself
+# update_fastlane
+
+default_platform(:android)
+
+platform :android do
+ desc "Runs all the tests"
+ lane :test do
+ gradle(task: "test")
+ end
+
+ desc "assembleRelease"
+ lane :beta do
+ gradle(task: "clean assembleRelease")
+ # crashlytics
+
+ # sh "your_script.sh"
+ # You can also use other beta testing services here
+ end
+
+ desc "Submit a new Release Build to Firebase App Distribution"
+ lane :publishDevDebug do
+ beta
+
+ firebase_app_distribution(
+ service_credentials_file: "firebase_credentials.json",
+ app: ENV["APP_ID"],
+ groups: "QA",
+ release_notes: "Test version of devDebug build."
+ )
+ end
+
+end
diff --git a/feature/board/build.gradle.kts b/feature/board/build.gradle.kts
index 12d15d30..6fdbb031 100644
--- a/feature/board/build.gradle.kts
+++ b/feature/board/build.gradle.kts
@@ -16,12 +16,10 @@ android {
minSdk = 26
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
- isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
@@ -63,5 +61,13 @@ dependencies {
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
+ implementation(libs.balloon)
+
+ implementation(libs.coil.compose)
+
implementation(project(":core:designsystem"))
+ implementation(project(":core:navigation"))
+ implementation(project(":core:domain"))
+ implementation(project(":core:model"))
+ implementation(project(":feature:onboarding"))
}
\ No newline at end of file
diff --git a/feature/board/consumer-rules.pro b/feature/board/consumer-rules.pro
deleted file mode 100644
index e69de29b..00000000
diff --git a/feature/board/proguard-rules.pro b/feature/board/proguard-rules.pro
index 481bb434..109525fd 100644
--- a/feature/board/proguard-rules.pro
+++ b/feature/board/proguard-rules.pro
@@ -18,4 +18,12 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
-#-renamesourcefileattribute SourceFile
\ No newline at end of file
+#-renamesourcefileattribute SourceFile
+
+-keep class * { *; }
+-keep interface * { *; }
+
+# Keep Dependency Injection Framework related classes and methods
+-keep class dagger.hilt.** { *; }
+-keep class javax.inject.** { *; }
+-keep class javax.annotation.** { *; }
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/Board.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/Board.kt
deleted file mode 100644
index 9e9cbf52..00000000
--- a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/Board.kt
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.goalpanzi.mission_mate.feature.board
-
-class Board {
-}
\ No newline at end of file
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/BoardNavigation.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/BoardNavigation.kt
new file mode 100644
index 00000000..04407b45
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/BoardNavigation.kt
@@ -0,0 +1,187 @@
+package com.goalpanzi.mission_mate.feature.board
+
+import android.net.Uri
+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.goalpanzi.mission_mate.feature.board.model.Character
+import com.goalpanzi.mission_mate.feature.board.model.UserStory
+import com.goalpanzi.mission_mate.feature.board.screen.BoardFinishRoute
+import com.goalpanzi.mission_mate.feature.board.screen.BoardMissionDetailRoute
+import com.goalpanzi.mission_mate.feature.board.screen.BoardRoute
+import com.goalpanzi.mission_mate.feature.board.screen.UserStoryScreen
+import com.goalpanzi.mission_mate.feature.board.screen.VerificationPreviewRoute
+import java.net.URLEncoder
+import java.nio.charset.StandardCharsets
+
+internal const val missionIdArg = "missionId"
+internal const val userCharacterTypeArg = "userCharacterType"
+internal const val nicknameArg = "nickname"
+internal const val dateArg = "date"
+internal const val imageUrlArg = "imageUrl"
+internal const val isUploadSuccessArg = "isUploadSuccess"
+
+fun NavController.navigateToBoard(
+ missionId: Long,
+ navOptions: NavOptions? = androidx.navigation.navOptions {
+ popUpTo(this@navigateToBoard.graph.id) {
+ inclusive = true
+ }
+ }
+) {
+ this.navigate("RouteModel.Board" + "/${missionId}", navOptions = navOptions)
+}
+
+fun NavGraphBuilder.boardNavGraph(
+ onNavigateOnboarding: () -> Unit,
+ onNavigateDetail : (Long) -> Unit,
+ onNavigateFinish : (Long) -> Unit,
+ onNavigateStory: (UserStory) -> Unit,
+ onClickSetting: () -> Unit,
+ onNavigateToPreview: (Long, Uri) -> Unit
+) {
+ composable(
+ "RouteModel.Board/{$missionIdArg}",
+ arguments = listOf(navArgument(missionIdArg) { type = NavType.LongType })
+ ) { navBackStackEntry ->
+ val missionId = navBackStackEntry.arguments?.getLong(missionIdArg)
+ val isUploadSuccess = navBackStackEntry.savedStateHandle.get(isUploadSuccessArg)
+ BoardRoute(
+ onNavigateOnboarding = onNavigateOnboarding,
+ onNavigateDetail = {
+ missionId?.let {
+ onNavigateDetail(missionId)
+ }
+ },
+ onNavigateFinish = onNavigateFinish,
+ onClickSetting = onClickSetting,
+ onClickStory = onNavigateStory,
+ onPreviewImage = onNavigateToPreview,
+ isUploadSuccess = isUploadSuccess ?: false
+ )
+ }
+}
+
+fun NavController.navigateToBoardDetail(
+ missionId: Long
+) {
+ this.navigate("RouteModel.BoardDetail" + "/${missionId}")
+}
+
+fun NavGraphBuilder.boardDetailNavGraph(
+ onNavigateOnboarding: () -> Unit,
+ onBackClick: () -> Unit
+) {
+ composable(
+ "RouteModel.BoardDetail/{$missionIdArg}",
+ arguments = listOf(navArgument(missionIdArg) { type = NavType.LongType })
+ ) {
+ BoardMissionDetailRoute(
+ onNavigateOnboarding = onNavigateOnboarding,
+ onBackClick = onBackClick
+ )
+ }
+}
+
+fun NavController.navigateToBoardFinish(
+ missionId: Long
+) {
+ this.navigate("RouteModel.BoardFinish" + "/${missionId}")
+}
+
+fun NavGraphBuilder.boardFinishNavGraph(
+ onClickSetting: () -> Unit,
+ onClickOk: () -> Unit,
+) {
+ composable(
+ "RouteModel.BoardFinish/{$missionIdArg}",
+ arguments = listOf(navArgument(missionIdArg) { type = NavType.LongType })
+ ) {
+ BoardFinishRoute(
+ onClickSetting = onClickSetting,
+ onClickOk = onClickOk
+ )
+ }
+}
+
+fun NavController.navigateToUserStory(
+ userStory: UserStory
+) = with(userStory) {
+ val encodedUrl = URLEncoder.encode(imageUrl, StandardCharsets.UTF_8.toString())
+ this@navigateToUserStory
+ .navigate(
+ route = "RouteModel.UserStory" + "/${characterType.name.uppercase()}" + "/${nickname}" + "/${verifiedAt}" + "/${encodedUrl}"
+ )
+}
+
+fun NavGraphBuilder.userStoryNavGraph(
+ onClickClose: () -> Unit
+) {
+ composable(
+ route = "RouteModel.UserStory/{$userCharacterTypeArg}/{$nicknameArg}/{$dateArg}/{$imageUrlArg}",
+ arguments = listOf(
+ navArgument(userCharacterTypeArg) {
+ defaultValue = Character.RABBIT.name.uppercase()
+ type = NavType.StringType
+ },
+ navArgument(nicknameArg) {
+ type = NavType.StringType
+ },
+ navArgument(dateArg) {
+ type = NavType.StringType
+ },
+ navArgument(imageUrlArg) {
+ type = NavType.StringType
+ }
+ )
+ ) { backStackEntry ->
+ backStackEntry.arguments?.run {
+ val character = getString(userCharacterTypeArg)?.let { Character.valueOf(it) }
+ ?: Character.RABBIT
+ val nickname = getString(nicknameArg) ?: ""
+ val verifiedAt = getString(dateArg) ?: ""
+ val imageUrl = getString(imageUrlArg) ?: ""
+
+ UserStoryScreen(
+ character = character,
+ nickname = nickname,
+ verifiedAt = verifiedAt,
+ imageUrl = imageUrl,
+ onClickClose = onClickClose
+ )
+ }
+ }
+}
+
+fun NavController.navigateToVerificationPreview(
+ missionId: Long,
+ imageUrl: Uri
+) {
+ val encodedUrl = URLEncoder.encode(imageUrl.toString(), StandardCharsets.UTF_8.toString())
+ this.navigate("RouteModel.VerificationPreview" + "/${missionId}" +"/${encodedUrl}")
+}
+
+fun NavGraphBuilder.verificationPreviewNavGraph(
+ onClickClose: () -> Unit,
+ onUploadSuccess: (key: String) -> Unit
+) {
+ composable(
+ route = "RouteModel.VerificationPreview/{$missionIdArg}/{$imageUrlArg}",
+ arguments = listOf(
+ navArgument(missionIdArg) {
+ type = NavType.LongType
+ },
+ navArgument(imageUrlArg) {
+ type = NavType.StringType
+ }
+ )
+ ) {
+ VerificationPreviewRoute(
+ onClickClose = onClickClose,
+ onUploadSuccess = { onUploadSuccess(isUploadSuccessArg) }
+ )
+ }
+}
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/component/Block.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/component/Block.kt
new file mode 100644
index 00000000..e4069e50
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/component/Block.kt
@@ -0,0 +1,142 @@
+package com.goalpanzi.mission_mate.feature.board.component
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+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.alpha
+import androidx.compose.ui.layout.ContentScale
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF
+import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography
+import com.goalpanzi.mission_mate.feature.board.R
+import com.goalpanzi.mission_mate.feature.board.model.BlockEventType
+import com.goalpanzi.mission_mate.feature.board.model.BlockType
+import com.goalpanzi.mission_mate.feature.onboarding.component.StableImage
+
+@Composable
+fun Block(
+ index: Int,
+ type: BlockType,
+ eventType: BlockEventType?,
+ numberOfColumns: Int,
+ onClickPassedBlock: (Int) -> Unit,
+ modifier: Modifier = Modifier,
+ isPassed: Boolean = false,
+ isStartedMission: Boolean = false
+) {
+ val isBright = (index % (numberOfColumns * 2)) % 2 != 0
+ Box(
+ modifier = modifier
+ ) {
+ if (type == BlockType.EMPTY) {
+ Spacer(modifier = modifier)
+ } else {
+ BlockImage(
+ index = index,
+ type = type,
+ eventType = eventType,
+ isBright = isBright,
+ onClickPassedBlock = onClickPassedBlock,
+ modifier = modifier,
+ isPassed = isPassed,
+ isStartedMission = isStartedMission
+ )
+ }
+ if (type == BlockType.START) {
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ text = BlockType.START.name,
+ color = ColorWhite_FFFFFFFF,
+ style = MissionMateTypography.title_lg_bold
+ )
+ } else if (
+ (eventType is BlockEventType.GoalWithEvent || eventType is BlockEventType.Goal)
+ && !isPassed
+ ) {
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ text = "GOAL",
+ color = ColorGray1_FF404249,
+ style = MissionMateTypography.title_lg_bold
+ )
+ } else if (eventType is BlockEventType.Item && type != BlockType.EMPTY) {
+ if (!isPassed) {
+ StableImage(
+ modifier = Modifier
+ .fillMaxWidth(50f / 114f)
+ .fillMaxHeight(48f / 114f)
+ .align(Alignment.Center)
+ .alpha(
+ if (isStartedMission) 1f else 0.5f
+ ),
+ drawableResId = R.drawable.img_present
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun BlockImage(
+ index : Int,
+ type: BlockType,
+ eventType: BlockEventType?,
+ isBright : Boolean,
+ onClickPassedBlock: (Int) -> Unit,
+ modifier: Modifier = Modifier,
+ isPassed: Boolean = false,
+ isStartedMission: Boolean = false
+) {
+ val drawableRes = if (type == BlockType.START) R.drawable.img_board_start
+ else if (isStartedMission && isPassed) {
+ if (eventType is BlockEventType.Item) {
+ eventType.boardEventItem.eventType?.imageId
+ } else if (eventType is BlockEventType.GoalWithEvent) {
+ eventType.boardEventItem.eventType?.imageId
+ } else {
+ when (type) {
+ BlockType.CENTER -> R.drawable.img_board_center_jeju
+ BlockType.TOP_LEFT_CORNER -> R.drawable.img_board_left_top_jeju
+ BlockType.BOTTOM_LEFT_CORNER -> R.drawable.img_board_left_bottom_jeju
+ BlockType.TOP_RIGHT_CORNER -> R.drawable.img_board_right_top_jeju
+ BlockType.BOTTOM_RIGHT_CORNER -> R.drawable.img_board_right_bottom_jeju
+ else -> null
+ }
+ }
+
+ } else {
+ when (type) {
+ BlockType.CENTER -> if (isBright) R.drawable.img_board_center_light else R.drawable.img_board_center_dark
+ BlockType.TOP_LEFT_CORNER -> if (isBright) R.drawable.img_board_left_top_light else R.drawable.img_board_left_top_dark
+ BlockType.BOTTOM_LEFT_CORNER -> if (isBright) R.drawable.img_board_left_bottom_light else R.drawable.img_board_left_bottom_dark
+ BlockType.TOP_RIGHT_CORNER -> if (isBright) R.drawable.img_board_right_top_light else R.drawable.img_board_right_top_dark
+ BlockType.BOTTOM_RIGHT_CORNER -> if (isBright) R.drawable.img_board_right_bottom_light else R.drawable.img_board_right_bottom_dark
+ else -> null
+ }
+ }
+
+ if(drawableRes == null){
+ Spacer(modifier = modifier)
+ }else {
+ StableImage(
+ modifier = modifier.then(
+ if (isStartedMission && isPassed) {
+ Modifier.clickable {
+ onClickPassedBlock(index)
+ }
+ } else {
+ Modifier
+ }
+ ),
+ drawableResId = drawableRes,
+ contentScale = ContentScale.FillWidth
+ )
+ }
+
+}
\ No newline at end of file
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/component/Board.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/component/Board.kt
new file mode 100644
index 00000000..7e08754a
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/component/Board.kt
@@ -0,0 +1,285 @@
+package com.goalpanzi.mission_mate.feature.board.component
+
+import android.annotation.SuppressLint
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.navigationBars
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBars
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.verticalScroll
+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.key
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.blur
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.graphics.drawscope.clipRect
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray2_FF4F505C
+import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography
+import com.goalpanzi.mission_mate.feature.board.R
+import com.goalpanzi.mission_mate.feature.board.model.BoardEventItem
+import com.goalpanzi.mission_mate.feature.board.model.BoardPiece
+import com.goalpanzi.mission_mate.feature.board.model.MissionBoards
+import com.goalpanzi.mission_mate.feature.board.model.MissionDetail
+import com.goalpanzi.mission_mate.feature.board.model.MissionState
+import com.goalpanzi.mission_mate.feature.board.model.toEventType
+import com.goalpanzi.mission_mate.feature.board.util.BoardManager
+import com.goalpanzi.mission_mate.feature.board.util.BoardManager.getPositionScrollToMyIndex
+import com.goalpanzi.core.model.response.MissionVerificationResponse
+import kotlin.math.absoluteValue
+
+
+@Composable
+fun Board(
+ scrollState: ScrollState,
+ missionBoards: MissionBoards,
+ missionDetail: MissionDetail,
+ numberOfColumns: Int,
+ boardPieces: List,
+ profile: MissionVerificationResponse,
+ missionState: MissionState,
+ onClickPassedBlock : (Int) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val statusBar = WindowInsets.statusBars
+ val navigationBar = WindowInsets.navigationBars
+ val localDensity = LocalDensity.current
+ val configuration = LocalConfiguration.current
+ val statusBarHeight =
+ remember { (statusBar.getTop(localDensity) - statusBar.getBottom(localDensity)).absoluteValue }
+ val navigationBarHeight =
+ remember { (navigationBar.getTop(localDensity) - navigationBar.getBottom(localDensity)).absoluteValue }
+ val isVisiblePieces by remember(missionState) { derivedStateOf { missionState.isVisiblePiece() } }
+ val myIndex by remember(missionBoards) {
+ derivedStateOf {
+ missionBoards.missionBoardList.find {
+ it.isMyPosition
+ }?.number ?: 0
+ }
+ }
+
+ LaunchedEffect(myIndex) {
+ scrollState.animateScrollTo(
+ getPositionScrollToMyIndex(
+ myIndex = myIndex,
+ numberOfColumns = numberOfColumns,
+ blockSize = (configuration.screenWidthDp - 48) / numberOfColumns,
+ localDensity = localDensity
+ )
+ )
+ }
+
+ Box(
+ modifier = modifier
+ .fillMaxSize()
+ ) {
+ Column(
+ modifier = modifier.modifierWithClipRect(
+ scrollState = scrollState,
+ isVisiblePieces = isVisiblePieces,
+ innerModifier = Modifier
+ .drawWithContent {
+ clipRect(bottom = statusBarHeight + 178.dp.toPx()) {
+ this@drawWithContent.drawContent()
+ }
+ }
+ .blur(10.dp, 10.dp),
+ )
+ ) {
+ BoardContent(
+ missionBoards,
+ missionDetail,
+ numberOfColumns,
+ boardPieces,
+ profile,
+ missionState,
+ isVisiblePieces = isVisiblePieces,
+ onClickPassedBlock = onClickPassedBlock,
+ modifier
+ )
+ }
+ Column(
+ modifier = modifier.modifierWithClipRect(
+ scrollState = scrollState,
+ isVisiblePieces = isVisiblePieces,
+ innerModifier = Modifier
+ .drawWithContent {
+ clipRect(
+ top = statusBarHeight + 178.dp.toPx() - 1,
+ bottom = if (!isVisiblePieces) {
+ size.height
+ } else {
+ size.height + navigationBarHeight - 188.dp.toPx()
+ }
+ ) {
+ this@drawWithContent.drawContent()
+ }
+ }
+ )
+ ) {
+ BoardContent(
+ missionBoards,
+ missionDetail,
+ numberOfColumns,
+ boardPieces,
+ profile,
+ missionState,
+ isVisiblePieces = isVisiblePieces,
+ onClickPassedBlock = onClickPassedBlock,
+ modifier
+ )
+ }
+ if (isVisiblePieces) {
+ Column(
+ modifier = modifier.modifierWithClipRect(
+ scrollState = scrollState,
+ isVisiblePieces = isVisiblePieces,
+ innerModifier = Modifier
+ .drawWithContent {
+ clipRect(top = (size.height + navigationBarHeight - 188.dp.toPx())) {
+ this@drawWithContent.drawContent()
+ }
+ }
+ .blur(10.dp, 10.dp)
+ )
+ ) {
+ BoardContent(
+ missionBoards,
+ missionDetail,
+ numberOfColumns,
+ boardPieces,
+ profile,
+ missionState,
+ isVisiblePieces = isVisiblePieces,
+ onClickPassedBlock = onClickPassedBlock,
+ modifier
+ )
+ }
+ }
+ }
+
+}
+
+@Composable
+fun ColumnScope.BoardContent(
+ missionBoards: MissionBoards,
+ missionDetail: MissionDetail,
+ numberOfColumns: Int,
+ boardPieces: List,
+ profile: MissionVerificationResponse,
+ missionState: MissionState,
+ isVisiblePieces: Boolean,
+ onClickPassedBlock : (Int) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val boardCount = missionBoards.missionBoardList.size
+ val passedCount = missionBoards.passedCountByMe
+ val startDateText = stringResource(
+ id = R.string.board_before_start_title,
+ missionDetail.missionStartLocalDate.monthValue,
+ missionDetail.missionStartLocalDate.dayOfMonth
+ )
+ Text(
+ modifier = Modifier.padding(top = 28.dp),
+ text = if (missionState.isRankBoardTitle()) stringResource(
+ id = R.string.board_rank_title,
+ missionBoards.progressCount, 1
+ )
+ else if (missionState.isEncourageBoardTitle()) stringResource(id = R.string.board_encourage_title)
+ else startDateText,
+ style = MissionMateTypography.heading_md_bold,
+ color = ColorGray1_FF404249
+ )
+ Text(
+ modifier = Modifier.padding(top = 2.dp, bottom = 20.dp),
+ text = if (missionState.isRankBoardTitle() || missionState.isEncourageBoardTitle()) stringResource(
+ id = R.string.board_after_start_description,
+ missionBoards.rank
+ ) else stringResource(id = R.string.board_before_start_description),
+ style = MissionMateTypography.body_lg_bold,
+ color = ColorGray2_FF4F505C
+ )
+ BoxWithConstraints {
+ val width = maxWidth
+ Column {
+ BoardManager.getBlockListByBoardCount(
+ boardCount,
+ numberOfColumns,
+ passedCount,
+ missionBoards.boardRewardList.map {
+ BoardEventItem(
+ index = it.number,
+ eventType = it.boardReward.toEventType()
+ )
+ }
+ ).chunked(numberOfColumns).forEach {
+ Row() {
+ it.forEach {
+ Block(
+ index = it.index,
+ eventType = it.blockEventType,
+ type = it.blockType, modifier = Modifier
+ .weight(1f)
+ .aspectRatio(1f),
+ numberOfColumns = numberOfColumns,
+ onClickPassedBlock = onClickPassedBlock,
+ isPassed = it.isPassed,
+ isStartedMission = isVisiblePieces
+ )
+ }
+ }
+ }
+ }
+ if (isVisiblePieces) {
+ boardPieces.forEach { piece ->
+ key(piece.nickname) {
+ Piece(
+ boardPiece = piece,
+ sizePerBlock = width / numberOfColumns,
+ numberOfColumn = numberOfColumns,
+ )
+ }
+
+ }
+ }
+ }
+}
+
+@SuppressLint("ModifierFactoryUnreferencedReceiver")
+fun Modifier.modifierWithClipRect(
+ scrollState: ScrollState,
+ isVisiblePieces: Boolean,
+ innerModifier: Modifier,
+ modifier: Modifier = Modifier,
+): Modifier {
+ return modifier
+ .fillMaxSize()
+ .navigationBarsPadding()
+ .then(innerModifier)
+ .verticalScroll(scrollState)
+ .statusBarsPadding()
+ .padding(
+ top = 180.dp,
+ start = 24.dp,
+ end = 24.dp,
+ bottom = if (isVisiblePieces) 188.dp else 46.dp
+ )
+}
\ No newline at end of file
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/component/BoardBottomView.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/component/BoardBottomView.kt
new file mode 100644
index 00000000..b68c8ecf
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/component/BoardBottomView.kt
@@ -0,0 +1,96 @@
+package com.goalpanzi.mission_mate.feature.board.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentWidth
+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.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateButtonType
+import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateTextButton
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray2_FF4F505C
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF
+import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography
+import com.goalpanzi.mission_mate.feature.board.R
+import com.goalpanzi.mission_mate.feature.board.model.MissionDetail
+import com.goalpanzi.mission_mate.feature.board.model.MissionState
+import com.goalpanzi.mission_mate.feature.onboarding.component.StableImage
+import com.goalpanzi.mission_mate.feature.onboarding.model.VerificationTimeType
+
+@Composable
+fun BoardBottomView(
+ onClickButton: () -> Unit,
+ missionDetail: MissionDetail,
+ missionState: MissionState,
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier = modifier
+ .navigationBarsPadding()
+ .clip(RoundedCornerShape(topEnd = 20.dp, topStart = 20.dp))
+ .background(ColorWhite_FFFFFFFF.copy(alpha = 0.7f))
+ .padding(top = 16.dp, bottom = 36.dp, start = 24.dp, end = 24.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ Row(
+ modifier = Modifier.wrapContentWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ StableImage(drawableResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.ic_time)
+ Text(
+ text = missionDetail.missionDaysOfWeekTextLocale.joinToString(" ") + " | " + when(VerificationTimeType.valueOf(missionDetail.timeOfDay)){
+ VerificationTimeType.MORNING -> stringResource(id = R.string.board_verification_am_time_limit)
+ VerificationTimeType.AFTERNOON -> stringResource(id = R.string.board_verification_pm_time_limit)
+ VerificationTimeType.EVERYDAY -> stringResource(id = R.string.board_verification_all_day_time_limit)
+ },
+ style = MissionMateTypography.body_lg_bold,
+ color = ColorGray2_FF4F505C
+ )
+ }
+ MissionMateTextButton(
+ modifier = Modifier.fillMaxWidth(),
+ textId = when(missionState){
+ MissionState.IN_PROGRESS_NON_MISSION_DAY -> R.string.board_verification_not_day
+ MissionState.IN_PROGRESS_MISSION_DAY_AFTER_CONFIRM -> R.string.board_verification_done
+ MissionState.IN_PROGRESS_MISSION_DAY_CLOSED -> R.string.board_verification_closed
+ MissionState.IN_PROGRESS_MISSION_DAY_NON_MISSION_TIME -> R.string.board_verification_not_time
+ else -> R.string.board_verification
+ },
+ buttonType = if (missionState.enabledVerification()) MissionMateButtonType.ACTIVE else MissionMateButtonType.DISABLED,
+ onClick = onClickButton
+ )
+ }
+}
+
+@Preview(showBackground = true, backgroundColor = 0xFF000000)
+@Composable
+fun PreviewBoardBottomView() {
+ BoardBottomView(
+ missionState = MissionState.POST_END,
+ missionDetail = MissionDetail(
+ missionId = 1,
+ hostMemberId = 2,
+ description = "convallis",
+ missionStartDate = "mnesarchum",
+ missionEndDate = "congue",
+ timeOfDay = "MORNING",
+ missionDays = listOf(),
+ boardCount = 12,
+ invitationCode = "ABDC"
+ ),
+ onClickButton = {}
+ )
+}
\ No newline at end of file
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/component/BoardTopStory.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/component/BoardTopStory.kt
new file mode 100644
index 00000000..bb8983db
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/component/BoardTopStory.kt
@@ -0,0 +1,153 @@
+package com.goalpanzi.mission_mate.feature.board.component
+
+import android.annotation.SuppressLint
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+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.layout.widthIn
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.CircleShape
+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.alpha
+import androidx.compose.ui.draw.paint
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray5_FFF5F6F9
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorOrange_FFFF5732
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF
+import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography
+import com.goalpanzi.mission_mate.core.designsystem.theme.OrangeGradient_FFFF5F3C_FFFFAE50
+import com.goalpanzi.mission_mate.feature.board.model.MissionState
+import com.goalpanzi.mission_mate.feature.board.model.UserStory
+
+@Composable
+fun BoardTopStory(
+ userList: List,
+ missionState : MissionState,
+ modifier: Modifier = Modifier,
+ onClickStory: (UserStory) -> Unit,
+) {
+ LazyRow(
+ modifier = modifier,
+ contentPadding = PaddingValues(top = 10.dp, bottom = 14.dp, start = 24.dp, end = 24.dp),
+ horizontalArrangement = Arrangement.spacedBy(14.dp)
+ ) {
+ if (userList.isNotEmpty()) {
+ items(userList) { userStory ->
+ UserStoryItem(
+ userStory = userStory,
+ missionState = missionState,
+ onClickStory = onClickStory
+ )
+ }
+ } else {
+ item {
+ Spacer(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(98.dp)
+ )
+ }
+ }
+ }
+}
+
+@SuppressLint("UnrememberedMutableInteractionSource")
+@Composable
+fun UserStoryItem(
+ userStory: UserStory,
+ missionState : MissionState,
+ modifier: Modifier = Modifier,
+ onClickStory: (UserStory) -> Unit
+) {
+ Box(
+ modifier = modifier
+ .height(98.dp)
+ .widthIn(min = 70.dp),
+ contentAlignment = Alignment.TopCenter
+ ) {
+ Image(
+ painter = painterResource(id = userStory.characterType.imageId),
+ contentDescription = null,
+ modifier = Modifier
+ .padding(top = 8.dp)
+ .height(64.dp)
+ .width(64.dp)
+ .then(
+ if (userStory.isVerified) {
+ Modifier
+ .border(3.dp, OrangeGradient_FFFF5F3C_FFFFAE50, CircleShape)
+ .clickable(
+ interactionSource = MutableInteractionSource(),
+ indication = null,
+ onClick = { onClickStory(userStory) }
+ )
+ } else {
+ Modifier
+ .border(3.dp, ColorWhite_FFFFFFFF, CircleShape)
+ .alpha(if(userStory.isMe) 1f else 0.5f)
+ }
+ )
+ .paint(
+ painter = painterResource(userStory.characterType.backgroundId),
+ contentScale = ContentScale.FillWidth
+ )
+ .padding(5.dp),
+ )
+ if (userStory.isMe) {
+ Text(
+ modifier = Modifier
+ .wrapContentSize()
+ .background(color = ColorOrange_FFFF5732, shape = RoundedCornerShape(16.dp))
+ .padding(vertical = 1.dp, horizontal = 14.dp)
+ .align(Alignment.TopCenter),
+ text = "나",
+ color = ColorWhite_FFFFFFFF,
+ style = MissionMateTypography.body_md_bold
+
+ )
+ }
+ Text(
+ modifier = Modifier
+ .wrapContentHeight()
+ .widthIn(min = 70.dp)
+ .background(
+ color = ColorGray5_FFF5F6F9.copy(alpha = 0.5f),
+ shape = RoundedCornerShape(20.dp)
+ )
+ .border(
+ 1.dp,
+ color = ColorWhite_FFFFFFFF.copy(alpha = 0.75f),
+ shape = RoundedCornerShape(20.dp)
+ )
+ .padding(vertical = 1.dp, horizontal = 5.dp)
+ .align(Alignment.BottomCenter),
+ text = userStory.nickname,
+ textAlign = TextAlign.Center,
+ color = ColorGray1_FF404249,
+ style = MissionMateTypography.body_sm_regular
+
+ )
+ }
+}
+
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/component/BoardTopView.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/component/BoardTopView.kt
new file mode 100644
index 00000000..038b3c73
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/component/BoardTopView.kt
@@ -0,0 +1,155 @@
+package com.goalpanzi.mission_mate.feature.board.component
+
+import android.annotation.SuppressLint
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+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.unit.dp
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF
+import com.goalpanzi.mission_mate.core.designsystem.theme.component.MissionMateTopAppBar
+import com.goalpanzi.mission_mate.core.designsystem.theme.component.NavigationType
+import com.goalpanzi.mission_mate.feature.board.R
+import com.goalpanzi.mission_mate.feature.board.model.MissionState
+import com.goalpanzi.mission_mate.feature.board.model.UserStory
+import com.goalpanzi.mission_mate.feature.onboarding.component.StableImage
+
+
+@SuppressLint("UnrememberedMutableInteractionSource")
+@Composable
+fun BoardTopView(
+ title: String,
+ viewedTooltip: Boolean,
+ isAddingUserEnabled: Boolean,
+ userList: List,
+ missionState : MissionState,
+ onClickFlag: () -> Unit,
+ onClickAddUser: () -> Unit,
+ onClickSetting: () -> Unit,
+ onClickTooltip : () -> Unit,
+ onClickStory: (UserStory) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Box(
+ modifier = modifier
+ .background(ColorWhite_FFFFFFFF.copy(alpha = 0.5f))
+ .statusBarsPadding()
+ ) {
+ MissionMateTopAppBar(
+ modifier = modifier,
+ navigationType = NavigationType.NONE,
+ title = title,
+ leftActionButtons = {
+ IconButton(
+ onClick = onClickFlag,
+ modifier = Modifier.wrapContentSize()
+ ) {
+ Icon(
+ imageVector = ImageVector.vectorResource(id = com.goalpanzi.mission_mate.core.designsystem.R.drawable.ic_flag),
+ contentDescription = "",
+ tint = ColorGray1_FF404249
+ )
+ }
+ },
+ rightActionButtons = {
+ BoardTopViewRightActionButtons(
+ isAddingUserEnabled = isAddingUserEnabled,
+ onClickAddUser = onClickAddUser,
+ onClickSetting = onClickSetting
+ )
+ },
+ containerColor = Color.Transparent
+ )
+ BoardTopStory(
+ modifier = Modifier.padding(top = 56.dp),
+ userList = userList,
+ missionState = missionState,
+ onClickStory = onClickStory
+ )
+ if(!viewedTooltip){
+ if (isAddingUserEnabled) {
+ // datastore 조건 추가
+ StableImage(
+ modifier = Modifier
+ .align(Alignment.TopEnd)
+ .clickable(
+ interactionSource = MutableInteractionSource(),
+ indication = null,
+ onClick = onClickTooltip
+ )
+ .padding(end = 43.dp,top = 56.dp)
+ .width(161.dp),
+ drawableResId = R.drawable.img_tooltip_mission_invitation_code,
+ contentScale = ContentScale.Crop
+ )
+ } else {
+ StableImage(
+ modifier = Modifier
+ .align(Alignment.TopStart)
+ .clickable(
+ interactionSource = MutableInteractionSource(),
+ indication = null,
+ onClick = onClickTooltip
+ )
+ .padding(start = 8.dp, top = 56.dp)
+ .width(161.dp),
+ drawableResId = R.drawable.img_tooltip_mission_detail,
+ contentScale = ContentScale.Crop
+ )
+ }
+ }
+
+
+ }
+}
+
+@Composable
+fun BoardTopViewRightActionButtons(
+ isAddingUserEnabled: Boolean,
+ onClickAddUser: () -> Unit,
+ onClickSetting: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Row(
+ modifier = modifier,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ if (isAddingUserEnabled) {
+ IconButton(
+ onClick = onClickAddUser,
+ modifier = Modifier.wrapContentSize()
+ ) {
+ Icon(
+ imageVector = ImageVector.vectorResource(id = com.goalpanzi.mission_mate.core.designsystem.R.drawable.ic_add_user),
+ contentDescription = "",
+ tint = ColorGray1_FF404249
+ )
+ }
+ }
+ IconButton(
+ onClick = onClickSetting,
+ modifier = Modifier.wrapContentSize()
+ ) {
+ Icon(
+ imageVector = ImageVector.vectorResource(id = com.goalpanzi.mission_mate.core.designsystem.R.drawable.ic_setting),
+ contentDescription = "",
+ tint = ColorGray1_FF404249
+ )
+ }
+ }
+}
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/component/InvitationCodeDialog.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/component/InvitationCodeDialog.kt
new file mode 100644
index 00000000..9382140c
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/component/InvitationCodeDialog.kt
@@ -0,0 +1,123 @@
+package com.goalpanzi.mission_mate.feature.board.component
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+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.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+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.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateDialog
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray2_FF4F505C
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorOrange_FFFF5732
+import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography
+import com.goalpanzi.mission_mate.feature.board.R
+import com.goalpanzi.mission_mate.feature.onboarding.component.InvitationCodeTextField
+import com.goalpanzi.mission_mate.feature.onboarding.util.styledTextWithHighlights
+
+@Composable
+fun InvitationCodeDialog(
+ code : String,
+ onDismissRequest: () -> Unit,
+ modifier: Modifier = Modifier,
+ minMemberSize : Int = 2,
+ maxMemberSize : Int = 10,
+ titleStyle: TextStyle = MissionMateTypography.title_xl_bold,
+ descriptionStyle: TextStyle = MissionMateTypography.body_lg_regular,
+ okTextStyle: TextStyle = MissionMateTypography.body_lg_bold,
+ cancelTextStyle: TextStyle = MissionMateTypography.body_lg_bold
+) {
+ if(code.length != 4) return
+ val context = LocalContext.current
+ val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+
+ MissionMateDialog(
+ modifier = modifier,
+ onDismissRequest = onDismissRequest,
+ onClickOk = {
+ clipboardManager.setPrimaryClip(
+ ClipData.newPlainText("MissionMate 초대 코드", code)
+ )
+ },
+ okTextId = R.string.board_invitation_dialog_copy,
+ cancelTextId = R.string.close,
+ okTextStyle = okTextStyle,
+ cancelTextStyle = cancelTextStyle
+ ){
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Text(
+ text = styledTextWithHighlights(
+ text = stringResource(id = R.string.board_invitation_dialog_title),
+ colorTargetTexts = listOf(stringResource(id = R.string.board_invitation_dialog_title_color_target)),
+ targetTextColor = ColorOrange_FFFF5732,
+ textColor = ColorGray1_FF404249
+ ),
+ style = titleStyle,
+ textAlign = TextAlign.Center,
+ color = ColorGray1_FF404249
+ )
+ Text(
+ modifier = Modifier.padding(top = 4.dp, bottom = 24.dp),
+ text = stringResource(id = R.string.board_invitation_dialog_description,minMemberSize,maxMemberSize),
+ style = descriptionStyle,
+ textAlign = TextAlign.Center,
+ color = ColorGray2_FF4F505C
+ )
+
+ Row(
+ modifier = Modifier.padding(bottom = 32.dp).wrapContentHeight(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ InvitationCodeTextField(
+ modifier = Modifier.weight(1f).aspectRatio(1f),
+ text = "${code[0]}",
+ onValueChange = {},
+ readOnly = true
+ )
+ InvitationCodeTextField(
+ modifier = Modifier.weight(1f).aspectRatio(1f),
+ text = "${code[1]}",
+ onValueChange = {},
+ readOnly = true
+ )
+ InvitationCodeTextField(
+ modifier = Modifier.weight(1f).aspectRatio(1f),
+ text = "${code[2]}",
+ onValueChange = {},
+ readOnly = true
+ )
+ InvitationCodeTextField(
+ modifier = Modifier.weight(1f).aspectRatio(1f),
+ text = "${code[3]}",
+ onValueChange = {},
+ readOnly = true
+ )
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun PreviewInvitationCodeDialog() {
+ InvitationCodeDialog(
+ code = "ABCD",
+ onDismissRequest = {}
+ )
+}
\ No newline at end of file
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/component/Piece.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/component/Piece.kt
new file mode 100644
index 00000000..48265ea8
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/component/Piece.kt
@@ -0,0 +1,174 @@
+package com.goalpanzi.mission_mate.feature.board.component
+
+import androidx.annotation.DrawableRes
+import androidx.compose.animation.core.LinearOutSlowInEasing
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.absoluteOffset
+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.widthIn
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.shape.RoundedCornerShape
+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.draw.clip
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorOrange_FFFF5732
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF
+import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography
+import com.goalpanzi.mission_mate.feature.board.model.BoardPiece
+import com.goalpanzi.mission_mate.feature.board.model.BoardPieceType
+import com.goalpanzi.mission_mate.feature.board.util.PieceGenerator
+import com.goalpanzi.mission_mate.feature.onboarding.component.OutlinedBox
+import com.goalpanzi.mission_mate.feature.onboarding.component.StableImage
+
+@Composable
+fun BoxScope.Piece(
+ boardPiece: BoardPiece,
+ numberOfColumn: Int,
+ sizePerBlock: Dp,
+ modifier: Modifier = Modifier
+) {
+ val isAnimated by remember(boardPiece) {
+ derivedStateOf { boardPiece.boardPieceType == BoardPieceType.MOVED }
+ }
+ val x = animateDpAsState(
+ targetValue = if (isAnimated) PieceGenerator.getXOffset(
+ boardPiece.index + 1,
+ numberOfColumn,
+ sizePerBlock
+ ) else PieceGenerator.getXOffset(boardPiece.index , numberOfColumn, sizePerBlock),
+ animationSpec = tween(
+ durationMillis = 500,
+ easing = LinearOutSlowInEasing
+ )
+ )
+ val y = animateDpAsState(
+ targetValue = if (isAnimated) PieceGenerator.getYOffset(
+ boardPiece.index + 1,
+ numberOfColumn,
+ sizePerBlock
+ ) else PieceGenerator.getYOffset(boardPiece.index , numberOfColumn, sizePerBlock),
+ animationSpec = tween(
+ durationMillis = 500,
+ easing = LinearOutSlowInEasing
+ )
+ )
+
+
+ Box(
+ modifier = modifier
+ .absoluteOffset(
+ x = x.value,
+ y = y.value
+ )
+ .size(
+ sizePerBlock
+ )
+ ) {
+ if(boardPiece.boardPieceType != BoardPieceType.HIDDEN){
+ StableImage(
+ modifier = Modifier
+ .padding(top = 4.dp)
+ .fillMaxWidth(88f / 114f)
+ .aspectRatio(1f)
+ .align(Alignment.TopCenter),
+ drawableResId = boardPiece.drawableRes,
+ )
+ if(boardPiece.count > 1){
+ PieceCountChip(
+ modifier = Modifier.align(Alignment.TopEnd),
+ count = boardPiece.count,
+ imageId = boardPiece.drawableRes
+ )
+ }
+ PieceNameChip(
+ modifier = Modifier
+ .align(
+ Alignment.BottomCenter
+ )
+ .padding(bottom = 7.dp),
+ name = boardPiece.nickname,
+ isMe = boardPiece.isMe
+ )
+ }
+
+ }
+
+
+}
+
+@Composable
+fun PieceNameChip(
+ name : String,
+ modifier: Modifier = Modifier,
+ isMe : Boolean = false,
+ textStyle : TextStyle = MissionMateTypography.body_md_bold
+){
+ Text(
+ modifier = modifier
+ .padding(horizontal = 12.dp)
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .clip(RoundedCornerShape(20.dp))
+ .background(
+ if (isMe) ColorOrange_FFFF5732 else ColorWhite_FFFFFFFF
+ )
+ .padding(horizontal = 8.5.dp, 0.85.dp)
+ ,
+ text = name,
+ style = textStyle,
+ color = if(isMe) ColorWhite_FFFFFFFF else ColorGray1_FF404249,
+ textAlign = TextAlign.Center
+ )
+}
+
+@Composable
+fun PieceCountChip(
+ @DrawableRes imageId: Int,
+ count : Int,
+ modifier: Modifier = Modifier,
+ textStyle : TextStyle = MissionMateTypography.body_md_bold
+){
+ OutlinedBox(
+ modifier = modifier
+ ){
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(5.dp)
+ ){
+ StableImage(
+ modifier = Modifier.size(22.dp),
+ drawableResId = imageId,
+ )
+ Text(
+ modifier = Modifier.padding(end = 4.dp),
+ text = "$count",
+ style = textStyle,
+ color = ColorWhite_FFFFFFFF
+ )
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/component/RequestDeleteMissionDialog.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/component/RequestDeleteMissionDialog.kt
new file mode 100644
index 00000000..8219bd97
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/component/RequestDeleteMissionDialog.kt
@@ -0,0 +1,35 @@
+package com.goalpanzi.mission_mate.feature.board.component
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.window.DialogProperties
+import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateDialog
+import com.goalpanzi.mission_mate.feature.board.R
+
+@Composable
+fun RequestDeleteMissionDialog(
+ onDismissRequest: () -> Unit,
+ onClickOk: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ MissionMateDialog(
+ modifier = modifier,
+ titleId = R.string.board_request_delete_title,
+ descriptionId = R.string.board_request_delete_description,
+ onDismissRequest = onDismissRequest,
+ onClickOk = onClickOk,
+ okTextId = R.string.ok,
+ cancelTextId = R.string.cancel
+ )
+}
+
+@Preview
+@Composable
+private fun PreviewRequestDeleteMissionDialog() {
+ RequestDeleteMissionDialog(
+ onClickOk = {},
+ onDismissRequest = {}
+ )
+
+}
\ No newline at end of file
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/component/dialog/BoardEventDialog.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/component/dialog/BoardEventDialog.kt
new file mode 100644
index 00000000..083d0a08
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/component/dialog/BoardEventDialog.kt
@@ -0,0 +1,156 @@
+package com.goalpanzi.mission_mate.feature.board.component.dialog
+
+import android.annotation.SuppressLint
+import androidx.annotation.StringRes
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+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.fillMaxWidth
+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.Shape
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.DialogProperties
+import com.goalpanzi.mission_mate.core.designsystem.component.LottieImage
+import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateTextButton
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray2_FF4F505C
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray3_FF727484
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF
+import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography
+import com.goalpanzi.mission_mate.feature.board.R
+import com.goalpanzi.mission_mate.feature.board.model.toEventType
+import com.goalpanzi.mission_mate.feature.onboarding.component.StableImage
+import com.goalpanzi.core.model.response.BoardReward
+
+@SuppressLint("UnrememberedMutableInteractionSource")
+@Composable
+fun BoardEventDialog(
+ reward: BoardReward,
+ onDismissRequest: () -> Unit,
+ onClickOk: () -> Unit,
+ modifier: Modifier = Modifier,
+ titleStyle: TextStyle = MissionMateTypography.title_xl_bold,
+ descriptionStyle: TextStyle = MissionMateTypography.body_lg_regular,
+ @StringRes okTextId: Int? = R.string.ok,
+ @StringRes cancelTextId: Int? = null,
+ okTextStyle: TextStyle = MissionMateTypography.body_lg_bold,
+ cancelTextStyle: TextStyle = MissionMateTypography.body_lg_bold,
+ shape: Shape = RoundedCornerShape(20.dp),
+ dialogInnerPadding: PaddingValues = PaddingValues(
+ top = 40.dp,
+ bottom = 34.dp,
+ start = 24.dp,
+ end = 24.dp
+ ),
+ dialogProperties: DialogProperties = DialogProperties(
+ usePlatformDefaultWidth = false
+ )
+) {
+ val event = reward.toEventType()
+ Dialog(
+ properties = dialogProperties,
+ onDismissRequest = onDismissRequest,
+ ) {
+ Box(
+ modifier = Modifier.fillMaxSize().clickable(
+ interactionSource = MutableInteractionSource(),
+ indication = null,
+ onClick = onDismissRequest
+ ),
+ contentAlignment = Alignment.Center
+ ){
+ Column(
+ modifier = modifier
+ .padding(horizontal = 20.dp)
+ .clip(shape)
+ .background(ColorWhite_FFFFFFFF)
+ .padding(dialogInnerPadding)
+ .clickable(
+ interactionSource = MutableInteractionSource(),
+ indication = null,
+ onClick = {}
+ ),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = if (event != null) {
+ stringResource(
+ id = R.string.board_mission_verification_success_dialog_reward_title,
+ stringResource(id = event.stringRes)
+ )
+ } else {
+ stringResource(id = R.string.board_mission_verification_success_dialog_title)
+ },
+ style = titleStyle,
+ textAlign = TextAlign.Center,
+ color = ColorGray1_FF404249
+ )
+ Text(
+ modifier = Modifier.padding(top = 12.dp, bottom = 32.dp),
+ text = if (event != null) stringResource(id = R.string.board_mission_verification_success_dialog_reward_description)
+ else stringResource(id = R.string.board_mission_verification_success_dialog_description),
+ style = descriptionStyle,
+ textAlign = TextAlign.Center,
+ color = ColorGray2_FF4F505C
+ )
+ StableImage(
+ drawableResId = event?.fullImageId ?: R.drawable.img_default_move,
+ modifier = Modifier
+ .padding(bottom = 32.dp)
+ .size(180.dp)
+ )
+
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ if (okTextId != null) {
+ MissionMateTextButton(
+ modifier = Modifier
+ .fillMaxWidth(),
+ textId = okTextId,
+ textStyle = okTextStyle,
+ onClick = onClickOk
+ )
+ }
+
+ if (cancelTextId != null) {
+ Text(
+ modifier = Modifier
+ .padding(top = 20.dp)
+ .clickable(
+ interactionSource = MutableInteractionSource(),
+ indication = null,
+ onClick = onDismissRequest
+ ),
+ text = stringResource(id = cancelTextId),
+ style = cancelTextStyle,
+ textAlign = TextAlign.Center,
+ color = ColorGray3_FF727484
+ )
+ }
+ }
+
+ }
+ LottieImage(
+ modifier = Modifier.align(Alignment.Center),
+ lottieRes = com.goalpanzi.mission_mate.core.designsystem.R.raw.animation_celebration
+ )
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/component/dialog/DeleteMissionDialog.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/component/dialog/DeleteMissionDialog.kt
new file mode 100644
index 00000000..fdcef368
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/component/dialog/DeleteMissionDialog.kt
@@ -0,0 +1,27 @@
+package com.goalpanzi.mission_mate.feature.board.component.dialog
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.window.DialogProperties
+import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateDialog
+import com.goalpanzi.mission_mate.feature.board.R
+
+@Composable
+fun DeleteMissionDialog(
+ onDismissRequest: () -> Unit,
+ onClickOk: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ MissionMateDialog(
+ titleId = R.string.board_delete_title,
+ descriptionId = R.string.board_delete_description,
+ onDismissRequest = onDismissRequest,
+ onClickOk = onClickOk,
+ okTextId = R.string.ok,
+ dialogProperties = DialogProperties(
+ usePlatformDefaultWidth = false,
+ dismissOnBackPress = false,
+ dismissOnClickOutside = false
+ )
+ )
+}
\ No newline at end of file
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/BlockType.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/BlockType.kt
new file mode 100644
index 00000000..ee09658b
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/BlockType.kt
@@ -0,0 +1,18 @@
+package com.goalpanzi.mission_mate.feature.board.model
+
+enum class BlockType {
+ TOP_RIGHT_CORNER,
+ TOP_LEFT_CORNER,
+ BOTTOM_RIGHT_CORNER,
+ BOTTOM_LEFT_CORNER,
+ CENTER,
+ START,
+ EMPTY
+}
+
+sealed class BlockEventType {
+ data class GoalWithEvent(val boardEventItem: BoardEventItem) : BlockEventType()
+ data object Goal : BlockEventType()
+ data class Item(val boardEventItem: BoardEventItem) : BlockEventType()
+ data object None : BlockEventType()
+}
\ No newline at end of file
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/BoardEventItem.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/BoardEventItem.kt
new file mode 100644
index 00000000..11357de5
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/BoardEventItem.kt
@@ -0,0 +1,73 @@
+package com.goalpanzi.mission_mate.feature.board.model
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import com.goalpanzi.mission_mate.feature.board.R
+import com.goalpanzi.core.model.response.BoardReward
+
+data class BoardEventItem(
+ val index: Int,
+ val eventType: EventType?
+)
+
+enum class EventType(
+ @DrawableRes val imageId: Int,
+ @StringRes val stringRes: Int,
+ @DrawableRes val fullImageId: Int,
+) {
+ ORANGE(
+ imageId = R.drawable.img_orange,
+ stringRes = R.string.board_mission_event_orange,
+ fullImageId = R.drawable.img_orange_full
+ ),
+ CANOLA_FLOWER(
+ imageId = R.drawable.img_flower,
+ stringRes = R.string.board_mission_event_canola_flower,
+ fullImageId = R.drawable.img_canola_flower_full
+ ),
+ DOLHARUBANG(
+ imageId = R.drawable.img_stone,
+ stringRes = R.string.board_mission_event_dolharubang,
+ fullImageId = R.drawable.img_dolharubang_full
+ ),
+ HORSE_RIDING(
+ imageId = R.drawable.img_horse,
+ stringRes = R.string.board_mission_event_horse_riding,
+ fullImageId = R.drawable.img_horse_riding_full
+ ),
+ HALLA_MOUNTAIN(
+ imageId = R.drawable.img_mountain,
+ stringRes = R.string.board_mission_event_halla_mountain,
+ fullImageId = R.drawable.img_halla_mountain_full
+ ),
+ WATERFALL(
+ imageId = R.drawable.img_waterfall,
+ stringRes = R.string.board_mission_event_waterfall,
+ fullImageId = R.drawable.img_waterfall_full
+ ),
+ BLACK_PIG(
+ imageId = R.drawable.img_pig,
+ stringRes = R.string.board_mission_event_black_pig,
+ fullImageId = R.drawable.img_black_pig_full
+ ),
+ SUNRISE(
+ imageId = R.drawable.img_bong,
+ stringRes = R.string.board_mission_event_sunrise,
+ fullImageId = R.drawable.img_sunrise_full
+ ),
+ GREEN_TEA_FIELD(
+ imageId = R.drawable.img_green_tea,
+ stringRes = R.string.board_mission_event_green_tea_field,
+ fullImageId = R.drawable.img_green_tea_field_full
+ ),
+ BEACH(
+ imageId = R.drawable.img_sea,
+ stringRes = R.string.board_mission_event_beach,
+ fullImageId = R.drawable.img_beach_full
+ )
+}
+
+
+fun BoardReward.toEventType(): EventType? {
+ return EventType.entries.find { it.name == this.name }
+}
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/BoardPiece.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/BoardPiece.kt
new file mode 100644
index 00000000..2c5baddf
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/BoardPiece.kt
@@ -0,0 +1,17 @@
+package com.goalpanzi.mission_mate.feature.board.model
+
+data class BoardPiece(
+ val index : Int,
+ val count : Int,
+ val nickname : String,
+ val isMe : Boolean,
+ val drawableRes: Int,
+ val boardPieceType: BoardPieceType = BoardPieceType.INITIAL
+)
+
+enum class BoardPieceType {
+ INITIAL,
+ MOVED,
+ NOT_CHANGED,
+ HIDDEN
+}
\ No newline at end of file
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/Character.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/Character.kt
new file mode 100644
index 00000000..deaee4c9
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/Character.kt
@@ -0,0 +1,39 @@
+package com.goalpanzi.mission_mate.feature.board.model
+
+import androidx.annotation.DrawableRes
+import com.goalpanzi.core.model.CharacterType
+
+enum class Character(
+ @DrawableRes val imageId: Int,
+ @DrawableRes val backgroundId: Int,
+) {
+ CAT(
+ imageId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_cat_selected,
+ backgroundId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_cat
+ ),
+ DOG(
+ imageId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_dog_selected,
+ backgroundId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_dog
+ ),
+ RABBIT(
+ imageId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_rabbit_selected,
+ backgroundId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_rabbit
+ ),
+ BEAR(
+ imageId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_bear_selected,
+ backgroundId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_bear
+ ),
+ PANDA(
+ imageId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_panda_selected,
+ backgroundId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_panda
+ ),
+ BIRD(
+ imageId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_bird_selected,
+ backgroundId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_bird
+ )
+}
+
+fun CharacterType.toCharacter() : Character {
+ return Character.entries
+ .find { it.name == this.name } ?: Character.RABBIT
+}
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/MissionBoard.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/MissionBoard.kt
new file mode 100644
index 00000000..cc27d5f3
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/MissionBoard.kt
@@ -0,0 +1,20 @@
+package com.goalpanzi.mission_mate.feature.board.model
+
+import com.goalpanzi.core.model.response.BoardReward
+import com.goalpanzi.core.model.response.MissionBoardResponse
+
+data class MissionBoard(
+ val number : Int,
+ val boardReward: BoardReward,
+ val isMyPosition : Boolean,
+ val missionBoardMembers : List
+)
+
+fun MissionBoardResponse.toModel() : MissionBoard {
+ return MissionBoard(
+ number = number,
+ isMyPosition = isMyPosition,
+ boardReward = reward,
+ missionBoardMembers = missionBoardMembers.map { it.toModel() }
+ )
+}
\ No newline at end of file
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/MissionBoardMember.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/MissionBoardMember.kt
new file mode 100644
index 00000000..8ddd5b5f
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/MissionBoardMember.kt
@@ -0,0 +1,17 @@
+package com.goalpanzi.mission_mate.feature.board.model
+
+import com.goalpanzi.core.model.response.MissionBoardMembersResponse
+
+data class MissionBoardMember(
+ val nickname : String,
+ val character : Character,
+ // val memberId : Long
+)
+
+fun MissionBoardMembersResponse.toModel() : MissionBoardMember {
+ return MissionBoardMember(
+ nickname = nickname,
+ character = characterType.toCharacter(),
+ // memberId = memberId
+ )
+}
\ No newline at end of file
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/MissionBoards.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/MissionBoards.kt
new file mode 100644
index 00000000..c7f5212d
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/MissionBoards.kt
@@ -0,0 +1,48 @@
+package com.goalpanzi.mission_mate.feature.board.model
+
+import com.goalpanzi.core.model.UserProfile
+import com.goalpanzi.core.model.response.BoardReward
+import com.goalpanzi.core.model.response.MissionBoardsResponse
+
+data class MissionBoards(
+ val missionBoardList : List,
+ val progressCount : Int,
+ val rank : Int,
+ val passedCountByMe : Int
+){
+ val boardRewardList : List by lazy {
+ missionBoardList.filter { it.boardReward != BoardReward.NONE }
+ }
+
+// val passedCountByMe : Int
+// get() {
+// return missionBoardList.find { it.isMyPosition }?.number ?: 0
+// }
+}
+
+fun MissionBoardsResponse.toModel() : MissionBoards {
+ return MissionBoards(
+ progressCount = progressCount,
+ rank = rank,
+ missionBoardList = missionBoards.map { it.toModel() },
+ passedCountByMe = missionBoards.find { it.isMyPosition }?.number ?: 0
+ )
+}
+
+fun MissionBoards.toBoardPieces(
+ profile: UserProfile?
+) : List {
+ return missionBoardList.filter { block ->
+ block.missionBoardMembers.isNotEmpty()
+ }.map { block ->
+ BoardPiece(
+ index = block.number,
+ count = block.missionBoardMembers.size,
+ nickname = if(block.isMyPosition && profile != null) profile.nickname
+ else block.missionBoardMembers.first().nickname,
+ isMe = block.isMyPosition,
+ drawableRes = if(block.isMyPosition && profile != null) profile.characterType.toCharacter().imageId
+ else block.missionBoardMembers.first().character.imageId
+ )
+ }
+}
\ No newline at end of file
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/MissionError.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/MissionError.kt
new file mode 100644
index 00000000..2d8e7bde
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/MissionError.kt
@@ -0,0 +1,5 @@
+package com.goalpanzi.mission_mate.feature.board.model
+
+enum class MissionError {
+ NOT_EXIST,
+}
\ No newline at end of file
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/MissionState.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/MissionState.kt
new file mode 100644
index 00000000..19e66701
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/MissionState.kt
@@ -0,0 +1,180 @@
+package com.goalpanzi.mission_mate.feature.board.model
+
+import com.goalpanzi.mission_mate.feature.board.model.uimodel.MissionBoardUiModel
+import com.goalpanzi.mission_mate.feature.board.model.uimodel.MissionUiModel
+import com.goalpanzi.mission_mate.feature.board.model.uimodel.MissionVerificationUiModel
+import com.goalpanzi.mission_mate.feature.onboarding.model.VerificationTimeType
+import com.goalpanzi.core.model.response.MissionVerificationResponse
+import java.time.DayOfWeek
+import java.time.LocalDate
+import java.time.LocalDateTime
+
+enum class MissionState {
+ LOADING,
+
+ DELETABLE, // 삭제_가능
+
+ PRE_START_SOLO, // 시작_전_혼자
+ PRE_START_MULTI, // 시작_전_2명_이상
+
+ IN_PROGRESS_MISSION_DAY_BEFORE_CONFIRM, // 진행_중_미션일_인증_전
+ IN_PROGRESS_MISSION_DAY_AFTER_CONFIRM, // 진행_중_미션일_인증_후
+ IN_PROGRESS_MISSION_DAY_CLOSED, // 진행_중_미션일_인증_마감
+ IN_PROGRESS_NON_MISSION_DAY, // 진행_중_미션일_아님
+ IN_PROGRESS_MISSION_DAY_NON_MISSION_TIME, // 진행_중_미션일O_미션시간X
+
+ POST_END; // 종료_후
+
+ fun isEnabledToInvite() : Boolean {
+ return this in setOf(
+ DELETABLE,
+ PRE_START_SOLO,
+ PRE_START_MULTI
+ )
+ }
+
+ fun isVisiblePiece() : Boolean {
+ return this in setOf(
+ IN_PROGRESS_MISSION_DAY_BEFORE_CONFIRM,
+ IN_PROGRESS_MISSION_DAY_AFTER_CONFIRM,
+ IN_PROGRESS_MISSION_DAY_CLOSED,
+ IN_PROGRESS_NON_MISSION_DAY,
+ IN_PROGRESS_MISSION_DAY_NON_MISSION_TIME,
+ POST_END
+ )
+ }
+
+ fun enabledVerification() : Boolean {
+ return this == IN_PROGRESS_MISSION_DAY_BEFORE_CONFIRM
+ }
+
+ fun isRankBoardTitle() : Boolean {
+ return this in setOf(
+ IN_PROGRESS_MISSION_DAY_BEFORE_CONFIRM,
+ IN_PROGRESS_MISSION_DAY_AFTER_CONFIRM
+ )
+ }
+
+ fun isEncourageBoardTitle() : Boolean {
+ return this in setOf(
+ IN_PROGRESS_MISSION_DAY_CLOSED,
+ IN_PROGRESS_NON_MISSION_DAY,
+ IN_PROGRESS_MISSION_DAY_NON_MISSION_TIME
+ )
+ }
+
+ companion object {
+ fun getMissionState(
+ missionBoardUiModel: MissionBoardUiModel,
+ missionUiModel: MissionUiModel,
+ missionVerificationUiModel: MissionVerificationUiModel
+ ): MissionState {
+ if (missionBoardUiModel !is MissionBoardUiModel.Success) return LOADING
+ if (missionUiModel !is MissionUiModel.Success) return LOADING
+ if (missionVerificationUiModel !is MissionVerificationUiModel.Success) return LOADING
+
+ val todayLocalDateTime = LocalDateTime.now()
+
+ return getMissionState(
+ todayLocalDateTime = todayLocalDateTime,
+ startDate = missionUiModel.missionDetail.missionStartLocalDate,
+ endDateTime = missionUiModel.missionDetail.missionEndLocalDateTime,
+ memberList = missionVerificationUiModel.missionVerificationsResponse.missionVerifications,
+ verificationTimeType = VerificationTimeType.valueOf(missionUiModel.missionDetail.timeOfDay),
+ daysOfWeek = missionUiModel.missionDetail.missionDays
+ )
+ }
+
+ internal fun getMissionState(
+ todayLocalDateTime : LocalDateTime,
+ startDate : LocalDate,
+ endDateTime: LocalDateTime,
+ memberList: List,
+ verificationTimeType: VerificationTimeType,
+ daysOfWeek : List
+ ) : MissionState {
+ val todayLocalDate = todayLocalDateTime.toLocalDate()
+
+ return if (!startDate.isAfter(todayLocalDate)) {
+ if (memberList.size <= 1)
+ DELETABLE
+ else {
+ if (isPassedEndTime(todayLocalDateTime, endDateTime, verificationTimeType)) {
+ POST_END
+ } else {
+ getMissionStateAsInProgress(
+ todayLocalDate,
+ todayLocalDateTime,
+ daysOfWeek,
+ verificationTimeType,
+ memberList
+ )
+ }
+ }
+ } else {
+ getMissionStateAsPreStart(memberList.size > 1)
+ }
+ }
+
+ private fun getMissionStateAsPreStart(
+ isMultipleMembers: Boolean
+ ): MissionState {
+ return if (isMultipleMembers) PRE_START_MULTI else PRE_START_SOLO
+ }
+
+ internal fun isPassedEndTime(
+ todayLocalDateTime: LocalDateTime,
+ endDateTime: LocalDateTime,
+ verificationTimeType: VerificationTimeType
+ ): Boolean {
+ val targetTime = verificationTimeType.getVerificationEndTime(endDateTime)
+ return todayLocalDateTime.isAfter(targetTime)
+ }
+
+ internal fun getMissionStateAsInProgress(
+ todayLocalDate: LocalDate,
+ todayLocalDateTime: LocalDateTime,
+ daysOfWeek: List,
+ verificationTimeType: VerificationTimeType,
+ memberList : List
+ ): MissionState {
+ val endTime = verificationTimeType.getVerificationEndTime(todayLocalDateTime)
+ if (isTodayMissionDay(todayLocalDate, daysOfWeek)) {
+ when (verificationTimeType) {
+ VerificationTimeType.AFTERNOON -> {
+ val startTime = todayLocalDateTime.withHour(12).withMinute(0).withSecond(0).withNano(0)
+ val target = VerificationTimeType.MORNING.getVerificationEndTime(todayLocalDateTime)
+ if(target.isBefore(startTime)){
+ return IN_PROGRESS_MISSION_DAY_NON_MISSION_TIME
+ }
+ }
+ else -> {
+ if(isVerifiedInMissionTime(memberList))
+ return IN_PROGRESS_MISSION_DAY_AFTER_CONFIRM
+ if(todayLocalDateTime.isAfter(endTime)){
+ return IN_PROGRESS_MISSION_DAY_CLOSED
+ }
+ }
+ }
+ return if(isVerifiedInMissionTime(memberList)) IN_PROGRESS_MISSION_DAY_AFTER_CONFIRM
+ else IN_PROGRESS_MISSION_DAY_BEFORE_CONFIRM
+ } else {
+ return IN_PROGRESS_NON_MISSION_DAY
+ }
+ }
+
+ internal fun isTodayMissionDay(
+ todayLocalDate: LocalDate,
+ missionDaysOfWeek: List
+ ): Boolean {
+ return missionDaysOfWeek.contains(todayLocalDate.dayOfWeek)
+ }
+
+ internal fun isVerifiedInMissionTime(
+ memberList : List
+ ) : Boolean {
+ if(memberList.isEmpty()) return false
+ return memberList.first().imageUrl.isNotEmpty()
+ }
+ }
+}
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/UserStory.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/UserStory.kt
new file mode 100644
index 00000000..eda10609
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/UserStory.kt
@@ -0,0 +1,22 @@
+package com.goalpanzi.mission_mate.feature.board.model
+
+import com.goalpanzi.core.model.response.MissionVerificationResponse
+
+data class UserStory(
+ val nickname : String,
+ val characterType : Character,
+ val imageUrl : String,
+ val isVerified : Boolean,
+ val isMe : Boolean = false,
+ val verifiedAt : String
+)
+
+fun MissionVerificationResponse.toUserStory(isMe: Boolean = false) : UserStory =
+ UserStory(
+ nickname = nickname,
+ characterType = characterType.toCharacter(),
+ imageUrl = imageUrl,
+ isVerified = imageUrl.isNotEmpty(),
+ isMe = isMe,
+ verifiedAt = verifiedAt
+ )
\ No newline at end of file
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/uimodel/BlockUiModel.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/uimodel/BlockUiModel.kt
new file mode 100644
index 00000000..abff85f7
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/uimodel/BlockUiModel.kt
@@ -0,0 +1,12 @@
+package com.goalpanzi.mission_mate.feature.board.model.uimodel
+
+import com.goalpanzi.mission_mate.feature.board.model.BlockEventType
+import com.goalpanzi.mission_mate.feature.board.model.BlockType
+
+data class BlockUiModel(
+ val index : Int,
+ val blockType : BlockType,
+ val blockEventType : BlockEventType = BlockEventType.None,
+ val isEvenGroup : Boolean,
+ val isPassed : Boolean = false
+)
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/uimodel/MissionBoardUiModel.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/uimodel/MissionBoardUiModel.kt
new file mode 100644
index 00000000..fe04a7b0
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/uimodel/MissionBoardUiModel.kt
@@ -0,0 +1,9 @@
+package com.goalpanzi.mission_mate.feature.board.model.uimodel
+
+import com.goalpanzi.mission_mate.feature.board.model.MissionBoards
+
+sealed class MissionBoardUiModel {
+ data object Loading : MissionBoardUiModel()
+ data object Error : MissionBoardUiModel()
+ data class Success(val missionBoards : MissionBoards) : MissionBoardUiModel()
+}
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/uimodel/MissionUiModel.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/uimodel/MissionUiModel.kt
new file mode 100644
index 00000000..ac74d363
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/uimodel/MissionUiModel.kt
@@ -0,0 +1,11 @@
+package com.goalpanzi.mission_mate.feature.board.model.uimodel
+
+import com.goalpanzi.mission_mate.feature.board.model.MissionDetail
+
+
+sealed class MissionUiModel {
+ data object Loading : MissionUiModel()
+ data object Error : MissionUiModel()
+ data class Success(val missionDetail: MissionDetail) : MissionUiModel()
+}
+
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/uimodel/MissionVerificationUiModel.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/uimodel/MissionVerificationUiModel.kt
new file mode 100644
index 00000000..647b324b
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/uimodel/MissionVerificationUiModel.kt
@@ -0,0 +1,9 @@
+package com.goalpanzi.mission_mate.feature.board.model.uimodel
+
+import com.goalpanzi.core.model.response.MissionVerificationsResponse
+
+sealed class MissionVerificationUiModel {
+ data object Loading : MissionVerificationUiModel()
+ data object Error : MissionVerificationUiModel()
+ data class Success(val missionVerificationsResponse: MissionVerificationsResponse) : MissionVerificationUiModel()
+}
\ No newline at end of file
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/BoardDetailViewModel.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/BoardDetailViewModel.kt
new file mode 100644
index 00000000..f2489c67
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/BoardDetailViewModel.kt
@@ -0,0 +1,96 @@
+package com.goalpanzi.mission_mate.feature.board.screen
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.goalpanzi.core.model.base.NetworkResult
+import com.goalpanzi.mission_mate.core.domain.usecase.DeleteMissionUseCase
+import com.goalpanzi.mission_mate.core.domain.usecase.GetCachedMemberIdUseCase
+import com.goalpanzi.mission_mate.core.domain.usecase.GetMissionUseCase
+import com.goalpanzi.mission_mate.feature.board.model.MissionError
+import com.goalpanzi.mission_mate.feature.board.model.toModel
+import com.goalpanzi.mission_mate.feature.board.model.uimodel.MissionUiModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class BoardDetailViewModel @Inject constructor(
+ private val getMissionUseCase: GetMissionUseCase,
+ private val deleteMissionUseCase: DeleteMissionUseCase,
+ private val getCachedMemberIdUseCase: GetCachedMemberIdUseCase,
+ savedStateHandle: SavedStateHandle
+) : ViewModel() {
+
+ private val missionId: Long = savedStateHandle.get("missionId")!!
+
+ private val memberId : StateFlow = getCachedMemberIdUseCase().stateIn(
+ viewModelScope,
+ started = SharingStarted.WhileSubscribed(500),
+ initialValue = null
+ )
+
+ private val _missionError = MutableSharedFlow()
+ val missionError : SharedFlow =_missionError.asSharedFlow()
+
+ private val _deleteMissionResultEvent = MutableSharedFlow()
+ val deleteMissionResultEvent: SharedFlow = _deleteMissionResultEvent.asSharedFlow()
+
+ private val _missionUiModel =
+ MutableStateFlow(MissionUiModel.Loading)
+ val missionUiModel: StateFlow = _missionUiModel.asStateFlow()
+
+ val isHost : StateFlow =
+ combine(
+ memberId,
+ missionUiModel.filter { it is MissionUiModel.Success }
+ ){ id, mission ->
+ if(mission !is MissionUiModel.Success) return@combine false
+ id == mission.missionDetail.hostMemberId
+ }.stateIn(
+ viewModelScope,
+ started = SharingStarted.WhileSubscribed(500),
+ initialValue = false
+ )
+
+ fun getMission() {
+ viewModelScope.launch {
+ getMissionUseCase(missionId).catch {
+ _missionUiModel.emit(MissionUiModel.Error)
+ }.collect {
+ when (it) {
+ is NetworkResult.Success -> {
+ _missionUiModel.emit(MissionUiModel.Success(it.data.toModel()))
+ }
+
+ else -> {
+ _missionUiModel.emit(MissionUiModel.Error)
+ _missionError.emit(MissionError.NOT_EXIST)
+ }
+ }
+ }
+ }
+ }
+
+ fun deleteMission() {
+ viewModelScope.launch {
+ deleteMissionUseCase(missionId)
+ .catch {
+ _deleteMissionResultEvent.emit(false)
+ }.collect {
+ _deleteMissionResultEvent.emit(true)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/BoardFinishScreen.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/BoardFinishScreen.kt
new file mode 100644
index 00000000..7acd1808
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/BoardFinishScreen.kt
@@ -0,0 +1,202 @@
+package com.goalpanzi.mission_mate.feature.board.screen
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.goalpanzi.mission_mate.core.designsystem.component.LottieImage
+import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateButtonType
+import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateTextButton
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF
+import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography
+import com.goalpanzi.mission_mate.core.designsystem.theme.component.MissionMateTopAppBar
+import com.goalpanzi.mission_mate.core.designsystem.theme.component.NavigationType
+import com.goalpanzi.mission_mate.feature.board.R
+import com.goalpanzi.mission_mate.feature.board.model.Character
+import com.goalpanzi.mission_mate.feature.board.model.toCharacter
+import com.goalpanzi.mission_mate.feature.onboarding.component.StableImage
+
+@Composable
+fun BoardFinishRoute(
+ onClickSetting: () -> Unit,
+ onClickOk : () -> Unit,
+ modifier: Modifier = Modifier,
+ viewModel : BoardFinishViewModel = hiltViewModel()
+) {
+ val rank by viewModel.rank.collectAsStateWithLifecycle()
+ val userProfile by viewModel.profile.collectAsStateWithLifecycle()
+
+ LaunchedEffect(key1 = Unit) {
+ viewModel.getRankByMissionId()
+ viewModel.getUserProfile()
+ }
+
+ LaunchedEffect(key1 = Unit) {
+ viewModel.setMissionFinished()
+ }
+
+
+ if(rank != null && userProfile != null){
+ BoardFinishScreen(
+ modifier = modifier,
+ rank = rank!!,
+ character = userProfile!!.characterType.toCharacter(),
+ onClickOk = onClickOk,
+ onClickSetting = onClickSetting
+ )
+ }else {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ){
+ CircularProgressIndicator()
+ }
+ }
+
+}
+
+@Composable
+fun BoardFinishScreen(
+ character : Character,
+ rank : Int,
+ onClickSetting: () -> Unit,
+ onClickOk : () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Box(
+ modifier = modifier
+ .fillMaxSize()
+ .background(color = ColorWhite_FFFFFFFF)
+ ) {
+ StableImage(
+ modifier = Modifier.fillMaxWidth(),
+ drawableResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_jeju,
+ contentScale = ContentScale.Crop
+ )
+ Column(
+ modifier = modifier
+ .statusBarsPadding()
+ .navigationBarsPadding(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ MissionMateTopAppBar(
+ modifier = modifier,
+ navigationType = NavigationType.NONE,
+ rightActionButtons = {
+ IconButton(
+ onClick = onClickSetting,
+ modifier = Modifier.wrapContentSize()
+ ) {
+ Icon(
+ imageVector = ImageVector.vectorResource(id = com.goalpanzi.mission_mate.core.designsystem.R.drawable.ic_setting),
+ contentDescription = "",
+ tint = ColorGray1_FF404249
+ )
+ }
+ },
+ containerColor = Color.Transparent
+ )
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f),
+ contentAlignment = Alignment.BottomCenter
+ ){
+ Box(
+ modifier = Modifier.fillMaxWidth(),
+ contentAlignment = Alignment.Center
+ ){
+ StableImage(
+ modifier = Modifier.fillMaxWidth(),
+ drawableResId = R.drawable.img_mission_finish,
+ contentScale = ContentScale.Crop
+ )
+ StableImage(
+ modifier = Modifier
+ .fillMaxWidth(212f / 390f)
+ .aspectRatio(1f),
+ drawableResId = character.imageId
+ )
+ }
+ LottieImage(
+ modifier = Modifier.wrapContentSize().align(Alignment.Center),
+ lottieRes = com.goalpanzi.mission_mate.core.designsystem.R.raw.animation_celebration
+ )
+ }
+ Column(
+ modifier = Modifier.background(color = ColorWhite_FFFFFFFF),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ modifier = Modifier.padding(top = 8.dp),
+ text = stringResource(id = R.string.board_finish_rank, rank),
+ color = ColorGray1_FF404249,
+ style = MissionMateTypography.heading_xl_bold
+ )
+ Text(
+ modifier = Modifier.padding(top = 20.dp, bottom = 4.dp),
+ text = stringResource(id = R.string.board_finish_description),
+ color = ColorGray1_FF404249,
+ style = MissionMateTypography.title_xl_bold
+ )
+ Text(
+ text = stringResource(id = R.string.board_finish_sub_description),
+ color = ColorGray1_FF404249,
+ style = MissionMateTypography.body_xl_regular,
+ textAlign = TextAlign.Center
+ )
+ MissionMateTextButton(
+ modifier = Modifier
+ .padding(bottom = 36.dp, start = 24.dp, end = 24.dp, top = 68.dp)
+ .fillMaxWidth()
+ .wrapContentHeight(),
+ buttonType = MissionMateButtonType.ACTIVE,
+ textId = com.goalpanzi.mission_mate.feature.onboarding.R.string.start,
+ onClick = onClickOk
+ )
+ }
+
+ }
+ }
+
+}
+
+@Preview
+@Composable
+private fun PreviewBoardFinishScreen() {
+ BoardFinishScreen(
+ character = Character.RABBIT,
+ rank = 10,
+ onClickSetting = {},
+ onClickOk = {}
+ )
+
+}
\ No newline at end of file
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/BoardFinishViewModel.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/BoardFinishViewModel.kt
new file mode 100644
index 00000000..48dbb347
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/BoardFinishViewModel.kt
@@ -0,0 +1,66 @@
+package com.goalpanzi.mission_mate.feature.board.screen
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.goalpanzi.mission_mate.core.domain.usecase.GetMissionRankUseCase
+import com.goalpanzi.mission_mate.core.domain.usecase.ProfileUseCase
+import com.goalpanzi.mission_mate.core.domain.usecase.SetMissionJoinedUseCase
+import com.goalpanzi.core.model.UserProfile
+import com.goalpanzi.core.model.base.NetworkResult
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class BoardFinishViewModel @Inject constructor(
+ private val getMissionRankUseCase : GetMissionRankUseCase,
+ private val profileUseCase : ProfileUseCase,
+ private val setMissionJoinedUseCase: SetMissionJoinedUseCase,
+ savedStateHandle: SavedStateHandle
+) : ViewModel() {
+
+ private val missionId: Long = savedStateHandle.get("missionId")!!
+
+ private val _rank : MutableStateFlow = MutableStateFlow(null)
+ val rank : StateFlow = _rank.asStateFlow()
+
+ private val _profile : MutableStateFlow = MutableStateFlow(null)
+ val profile : StateFlow = _profile.asStateFlow()
+
+ fun setMissionFinished() {
+ viewModelScope.launch {
+ setMissionJoinedUseCase(false).collect()
+ }
+ }
+
+ fun getRankByMissionId() {
+ viewModelScope.launch {
+ getMissionRankUseCase(missionId)
+ .catch {
+ _rank.emit(null)
+ }.collect {
+ when(it){
+ is NetworkResult.Success -> {
+ _rank.emit(it.data.rank)
+ }
+ else -> {
+ _rank.emit(null)
+ }
+ }
+ }
+ }
+ }
+
+ fun getUserProfile() {
+ viewModelScope.launch {
+ _profile.emit(profileUseCase.getProfile())
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/BoardMissionDetailScreen.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/BoardMissionDetailScreen.kt
new file mode 100644
index 00000000..01c69fc4
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/BoardMissionDetailScreen.kt
@@ -0,0 +1,283 @@
+package com.goalpanzi.mission_mate.feature.board.screen
+
+import android.annotation.SuppressLint
+import android.widget.Toast
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.HorizontalDivider
+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.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray2_FF4F505C
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray3_FF727484
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray4_FFE5E5E5
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorOrange_FFFF5732
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorRed_FFFF5858
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF
+import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography
+import com.goalpanzi.mission_mate.core.designsystem.theme.component.MissionMateTopAppBar
+import com.goalpanzi.mission_mate.core.designsystem.theme.component.NavigationType
+import com.goalpanzi.mission_mate.feature.board.R
+import com.goalpanzi.mission_mate.feature.board.component.RequestDeleteMissionDialog
+import com.goalpanzi.mission_mate.feature.board.model.uimodel.MissionUiModel
+import com.goalpanzi.mission_mate.feature.onboarding.model.VerificationTimeType
+import com.goalpanzi.mission_mate.feature.onboarding.util.styledTextWithHighlights
+import kotlinx.coroutines.launch
+
+@Composable
+fun BoardMissionDetailRoute(
+ onNavigateOnboarding : () -> Unit,
+ onBackClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ viewModel: BoardDetailViewModel = hiltViewModel()
+) {
+ val context = LocalContext.current
+ val missionUiModel by viewModel.missionUiModel.collectAsStateWithLifecycle()
+ val isHost by viewModel.isHost.collectAsStateWithLifecycle()
+ var isShownRequestDeleteMissionDialog by remember { mutableStateOf(false) }
+
+ LaunchedEffect(key1 = Unit) {
+ viewModel.getMission()
+
+ launch {
+ viewModel.deleteMissionResultEvent.collect {
+ if (it) {
+ isShownRequestDeleteMissionDialog = false
+ onNavigateOnboarding()
+ }
+ }
+ }
+
+ launch {
+ viewModel.missionError.collect {
+ Toast.makeText(context,context.getString(R.string.board_mission_not_exist),Toast.LENGTH_SHORT).show()
+ onNavigateOnboarding()
+ }
+ }
+
+ }
+
+ if(isShownRequestDeleteMissionDialog){
+ RequestDeleteMissionDialog(
+ onDismissRequest = {
+ isShownRequestDeleteMissionDialog = false
+ },
+ onClickOk = {
+ viewModel.deleteMission()
+ }
+ )
+ }
+ if(missionUiModel is MissionUiModel.Success){
+ val missionDetail = (missionUiModel as MissionUiModel.Success).missionDetail
+ BoardMissionDetailScreen(
+ modifier = modifier,
+ boardCount = missionDetail.boardCount ,
+ missionTitle = missionDetail.description,
+ missionPeriod = missionDetail.missionPeriod,
+ missionDays = missionDetail.missionDaysOfWeekTextLocale ,
+ missionTime = VerificationTimeType.valueOf(missionDetail.timeOfDay),
+ isHost = isHost,
+ onClickDelete = {
+ isShownRequestDeleteMissionDialog = !isShownRequestDeleteMissionDialog
+ },
+ onBackClick = onBackClick
+ )
+ }else {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ){
+ CircularProgressIndicator()
+ }
+ }
+
+}
+
+@SuppressLint("UnrememberedMutableInteractionSource")
+@Composable
+fun BoardMissionDetailScreen(
+ boardCount: Int,
+ missionTitle: String,
+ missionPeriod: String,
+ missionDays: List,
+ missionTime: VerificationTimeType,
+ isHost : Boolean,
+ onClickDelete : () -> Unit,
+ onBackClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .background(ColorWhite_FFFFFFFF)
+ .statusBarsPadding()
+ .navigationBarsPadding(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ MissionMateTopAppBar(
+ navigationType = NavigationType.BACK,
+ onNavigationClick = onBackClick,
+ containerColor = ColorWhite_FFFFFFFF
+ )
+ Text(
+ modifier = Modifier.padding(bottom = 16.dp),
+ text = stringResource(id = R.string.board_mission_detail_title),
+ style = MissionMateTypography.heading_sm_bold,
+ color = ColorGray1_FF404249
+ )
+ Text(
+ modifier = Modifier.padding(
+ bottom = 20.dp
+ ),
+ text = styledTextWithHighlights(
+ text = stringResource(id = R.string.board_mission_detail_description, boardCount),
+ colorTargetTexts = listOf(
+ stringResource(
+ id = R.string.board_mission_detail_description_color_target,
+ boardCount
+ )
+ ),
+ targetTextColor = ColorOrange_FFFF5732,
+ textColor = ColorGray2_FF4F505C
+ ),
+ style = MissionMateTypography.body_xl_regular,
+ textAlign = TextAlign.Center
+ )
+ Column(
+ modifier = Modifier
+ .padding(horizontal = 24.dp, vertical = 20.dp)
+ .verticalScroll(scrollState),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Text(
+ modifier = Modifier.align(Alignment.Start),
+ text = stringResource(id = com.goalpanzi.mission_mate.feature.onboarding.R.string.onboarding_board_setup_mission_input_title),
+ color = ColorGray3_FF727484,
+ style = MissionMateTypography.body_md_regular
+ )
+ Row(
+ modifier = Modifier.align(Alignment.Start),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ modifier = Modifier
+ .wrapContentHeight()
+ .weight(1f),
+ text = missionTitle,
+ color = ColorGray1_FF404249,
+ style = MissionMateTypography.body_lg_bold
+ )
+ if(isHost){
+ Text(
+ modifier = Modifier
+ .padding(start = 8.dp)
+ .clickable(
+ interactionSource = MutableInteractionSource(),
+ indication = null,
+ onClick = onClickDelete
+ ),
+ text = stringResource(id = R.string.board_mission_detail_delete),
+ color = ColorRed_FFFF5858,
+ style = MissionMateTypography.body_lg_regular
+ )
+ }
+ }
+
+ HorizontalDivider(
+ modifier = Modifier.padding(vertical = 8.dp),
+ color = ColorGray4_FFE5E5E5
+ )
+
+ Text(
+ modifier = Modifier.align(Alignment.Start),
+ text = stringResource(id = com.goalpanzi.mission_mate.feature.onboarding.R.string.onboarding_board_setup_schedule_period_input_title),
+ color = ColorGray3_FF727484,
+ style = MissionMateTypography.body_md_regular
+ )
+ Text(
+ modifier = Modifier.align(Alignment.Start),
+ text = missionPeriod,
+ color = ColorGray1_FF404249,
+ style = MissionMateTypography.body_lg_bold
+ )
+ HorizontalDivider(
+ modifier = Modifier.padding(vertical = 8.dp),
+ color = ColorGray4_FFE5E5E5
+ )
+
+ Text(
+ modifier = Modifier.align(Alignment.Start),
+ text = stringResource(id = com.goalpanzi.mission_mate.feature.onboarding.R.string.onboarding_invitation_dialog_schedule_day_title),
+ color = ColorGray3_FF727484,
+ style = MissionMateTypography.body_md_regular
+ )
+ Text(
+ modifier = Modifier.align(Alignment.Start),
+ text = missionDays.joinToString("/"),
+ color = ColorGray1_FF404249,
+ style = MissionMateTypography.body_lg_bold
+ )
+ HorizontalDivider(
+ modifier = Modifier.padding(vertical = 8.dp),
+ color = ColorGray4_FFE5E5E5
+ )
+
+ Text(
+ modifier = Modifier.align(Alignment.Start),
+ text = stringResource(id = com.goalpanzi.mission_mate.feature.onboarding.R.string.onboarding_board_setup_verification_time_input_title),
+ color = ColorGray3_FF727484,
+ style = MissionMateTypography.body_md_regular
+ )
+ Text(
+ modifier = Modifier.align(Alignment.Start),
+ text = stringResource(id = missionTime.titleId).replace("\n", " "),
+ color = ColorGray1_FF404249,
+ style = MissionMateTypography.body_lg_bold
+ )
+ HorizontalDivider(
+ modifier = Modifier.padding(vertical = 8.dp),
+ color = ColorGray4_FFE5E5E5
+ )
+
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun PreviewBoardMissionDetailRoute() {
+ BoardMissionDetailRoute(
+ onBackClick = {},
+ onNavigateOnboarding = {}
+ )
+
+}
\ No newline at end of file
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/BoardScreen.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/BoardScreen.kt
new file mode 100644
index 00000000..2fe8773b
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/BoardScreen.kt
@@ -0,0 +1,323 @@
+package com.goalpanzi.mission_mate.feature.board.screen
+
+import android.content.Intent
+import android.net.Uri
+import android.widget.Toast
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.PickVisualMediaRequest
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.material3.CircularProgressIndicator
+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.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF
+import com.goalpanzi.mission_mate.feature.board.R
+import com.goalpanzi.mission_mate.feature.board.component.Board
+import com.goalpanzi.mission_mate.feature.board.component.BoardBottomView
+import com.goalpanzi.mission_mate.feature.board.component.BoardTopView
+import com.goalpanzi.mission_mate.feature.board.component.InvitationCodeDialog
+import com.goalpanzi.mission_mate.feature.board.component.dialog.BoardEventDialog
+import com.goalpanzi.mission_mate.feature.board.component.dialog.DeleteMissionDialog
+import com.goalpanzi.mission_mate.feature.board.model.BoardPiece
+import com.goalpanzi.mission_mate.feature.board.model.MissionState
+import com.goalpanzi.mission_mate.feature.board.model.UserStory
+import com.goalpanzi.mission_mate.feature.board.model.toCharacter
+import com.goalpanzi.mission_mate.feature.board.model.toUserStory
+import com.goalpanzi.mission_mate.feature.board.model.uimodel.MissionBoardUiModel
+import com.goalpanzi.mission_mate.feature.board.model.uimodel.MissionUiModel
+import com.goalpanzi.mission_mate.feature.board.model.uimodel.MissionVerificationUiModel
+import com.goalpanzi.mission_mate.feature.onboarding.component.StableImage
+import com.goalpanzi.core.model.response.BoardReward
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+@Composable
+fun BoardRoute(
+ onNavigateOnboarding: () -> Unit,
+ onNavigateDetail: () -> Unit,
+ onNavigateFinish : (Long) -> Unit,
+ onClickSetting: () -> Unit,
+ onClickStory: (UserStory) -> Unit,
+ onPreviewImage: (Long, Uri) -> Unit,
+ isUploadSuccess: Boolean,
+ modifier: Modifier = Modifier,
+ viewModel: BoardViewModel = hiltViewModel()
+) {
+ val context = LocalContext.current
+ val missionBoardUiModel by viewModel.missionBoardUiModel.collectAsStateWithLifecycle()
+ val missionUiModel by viewModel.missionUiModel.collectAsStateWithLifecycle()
+ val missionVerificationUiModel by viewModel.missionVerificationUiModel.collectAsStateWithLifecycle()
+ val missionState by viewModel.missionState.collectAsStateWithLifecycle()
+ val viewedTooltip by viewModel.viewedToolTip.collectAsStateWithLifecycle()
+ val isHost by viewModel.isHost.collectAsStateWithLifecycle()
+ val boardPieces by viewModel.boardPieces.collectAsStateWithLifecycle()
+
+ val scrollState = rememberScrollState()
+ var isShownDeleteMissionDialog by remember { mutableStateOf(false) }
+ var isShownBoardRewardDialog by remember { mutableStateOf(null) }
+ var isShownInvitationCodeDialog by remember { mutableStateOf(false) }
+
+ val imagePicker = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.PickVisualMedia(),
+ onResult = { imageUri ->
+ imageUri?.let { original ->
+ context.contentResolver.takePersistableUriPermission(
+ original,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION
+ )
+ onPreviewImage(viewModel.missionId, original)
+ }
+ }
+ )
+
+ LaunchedEffect(key1 = Unit) {
+ viewModel.getMissionBoards()
+ viewModel.getMission()
+ viewModel.getMissionVerification()
+
+ launch {
+ viewModel.deleteMissionResultEvent.collect {
+ if (it) {
+ isShownDeleteMissionDialog = false
+ onNavigateOnboarding()
+ }
+ }
+ }
+
+ launch {
+ viewModel.boardRewardEvent.collect {
+ delay(500L)
+ isShownBoardRewardDialog = it
+ }
+ }
+ launch {
+ viewModel.myMissionVerification.collect {
+ onClickStory(it)
+ }
+ }
+ launch {
+ viewModel.missionError.collect {
+ if(it != null){
+ Toast.makeText(context,context.getString(R.string.board_mission_not_exist), Toast.LENGTH_SHORT).show()
+ onNavigateOnboarding()
+ return@collect
+ }
+ }
+ }
+ }
+
+ LaunchedEffect(missionState) {
+ if (missionState == MissionState.DELETABLE) {
+ isShownDeleteMissionDialog = true
+ }else if(missionState == MissionState.POST_END){
+ onNavigateFinish(viewModel.missionId)
+ }
+ }
+
+ LaunchedEffect(key1 = isUploadSuccess) {
+ if (isUploadSuccess) {
+ viewModel.onVerifySuccess()
+ }
+ }
+
+ if (isShownDeleteMissionDialog) {
+ DeleteMissionDialog(
+ onDismissRequest = {
+ isShownDeleteMissionDialog = false
+ },
+ onClickOk = {
+ viewModel.deleteMission()
+ }
+ )
+ }
+ if (isShownBoardRewardDialog != null) {
+ BoardEventDialog(
+ reward = isShownBoardRewardDialog!!,
+ onDismissRequest = {
+ isShownBoardRewardDialog = null
+ },
+ onClickOk = {
+ isShownBoardRewardDialog = null
+ }
+ )
+ }
+ if (isShownInvitationCodeDialog) {
+ if (missionUiModel !is MissionUiModel.Success) return
+ InvitationCodeDialog(
+ code = (missionUiModel as MissionUiModel.Success).missionDetail.invitationCode,
+ onDismissRequest = {
+ isShownInvitationCodeDialog = !isShownInvitationCodeDialog
+ }
+ )
+ }
+
+ BoardScreen(
+ modifier = modifier,
+ viewedTooltip = viewedTooltip,
+ scrollState = scrollState,
+ missionBoardUiModel = missionBoardUiModel,
+ missionUiModel = missionUiModel,
+ missionVerificationUiModel = missionVerificationUiModel,
+ missionState = missionState,
+ boardPieces = boardPieces,
+ isHost = isHost,
+ onClickSetting = onClickSetting,
+ onClickFlag = {
+ viewModel.setViewedTooltip()
+ onNavigateDetail()
+ },
+ onClickAddUser = {
+ viewModel.setViewedTooltip()
+ isShownInvitationCodeDialog = !isShownInvitationCodeDialog
+ },
+ onClickVerification = {
+ imagePicker.launch(
+ PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
+ )
+ },
+ onClickTooltip = {
+ viewModel.setViewedTooltip()
+ },
+ onClickStory = onClickStory,
+ onClickMyVerificationBoardBlock = {
+ viewModel.getMyMissionVerification(it)
+ }
+ )
+
+}
+
+@Composable
+fun BoardScreen(
+ scrollState: ScrollState,
+ viewedTooltip: Boolean,
+ missionBoardUiModel: MissionBoardUiModel,
+ missionUiModel: MissionUiModel,
+ missionVerificationUiModel: MissionVerificationUiModel,
+ missionState: MissionState,
+ boardPieces: List,
+ isHost: Boolean,
+ onClickSetting: () -> Unit,
+ onClickVerification: () -> Unit,
+ onClickFlag: () -> Unit,
+ onClickAddUser: () -> Unit,
+ onClickTooltip: () -> Unit,
+ onClickStory : (UserStory) -> Unit,
+ onClickMyVerificationBoardBlock : (Int) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Box(
+ modifier = modifier
+ .fillMaxSize()
+ .background(ColorWhite_FFFFFFFF)
+ ) {
+ if (missionBoardUiModel is MissionBoardUiModel.Success
+ && missionUiModel is MissionUiModel.Success
+ && missionVerificationUiModel is MissionVerificationUiModel.Success
+ ) {
+ Image(
+ modifier = Modifier.fillMaxSize(),
+ painter = painterResource(id = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_jeju_full),
+ contentDescription = null,
+ contentScale = ContentScale.Crop
+ )
+ Board(
+ missionBoards = missionBoardUiModel.missionBoards,
+ missionDetail = missionUiModel.missionDetail,
+ numberOfColumns = 3,
+ boardPieces = boardPieces,
+ scrollState = scrollState,
+ profile = missionVerificationUiModel.missionVerificationsResponse.missionVerifications.first(),
+ missionState = missionState,
+ onClickPassedBlock = onClickMyVerificationBoardBlock
+ )
+ BoardTopView(
+ title = missionUiModel.missionDetail.description,
+ isAddingUserEnabled = missionState.isEnabledToInvite(),//&& isHost ,
+ viewedTooltip = viewedTooltip,
+ userList = missionVerificationUiModel.missionVerificationsResponse.missionVerifications.mapIndexed { i, item ->
+ item.toUserStory(
+ isMe = i == 0
+ )
+ },
+ missionState = missionState,
+ onClickFlag = onClickFlag,
+ onClickAddUser = onClickAddUser,
+ onClickSetting = onClickSetting,
+ onClickTooltip = onClickTooltip,
+ onClickStory = onClickStory
+ )
+
+ if (!missionState.isVisiblePiece()) {
+ Box(
+ modifier = Modifier
+ .wrapContentSize()
+ .padding(
+ top = 195.dp,
+ bottom = if (missionState.isVisiblePiece()) 188.dp else 46.dp
+ )
+ .align(Alignment.Center),
+ contentAlignment = Alignment.TopCenter,
+ ) {
+ if (missionState == MissionState.PRE_START_SOLO) {
+ StableImage(
+ drawableResId = R.drawable.img_tooltip_mission_delete_warning,
+ modifier = Modifier
+ .fillMaxWidth(276f / 390f)
+ .wrapContentHeight(),
+ contentScale = ContentScale.FillWidth
+ )
+ } else if (missionState == MissionState.PRE_START_MULTI) {
+ StableImage(
+ drawableResId = R.drawable.img_tooltip_mission_welcome,
+ modifier = Modifier
+ .fillMaxWidth(276f / 390f)
+ .wrapContentHeight(),
+ contentScale = ContentScale.FillWidth
+ )
+ }
+ StableImage(
+ missionVerificationUiModel.missionVerificationsResponse.missionVerifications.first().characterType.toCharacter().imageId,
+ modifier = Modifier
+ .padding(top = 75.dp)
+ .fillMaxWidth(240f / 390f)
+ .aspectRatio(1f)
+ )
+ }
+
+ } else {
+ BoardBottomView(
+ modifier = Modifier.align(Alignment.BottomCenter),
+ missionState = missionState,
+ missionDetail = missionUiModel.missionDetail,
+ onClickButton = onClickVerification
+ )
+ }
+ } else {
+ CircularProgressIndicator(
+ modifier = Modifier.align(Alignment.Center)
+ )
+ }
+ }
+}
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/BoardViewModel.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/BoardViewModel.kt
new file mode 100644
index 00000000..9e940e80
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/BoardViewModel.kt
@@ -0,0 +1,312 @@
+package com.goalpanzi.mission_mate.feature.board.screen
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.goalpanzi.core.model.base.NetworkResult
+import com.goalpanzi.core.model.response.BoardReward
+import com.goalpanzi.mission_mate.core.domain.usecase.DeleteMissionUseCase
+import com.goalpanzi.mission_mate.core.domain.usecase.GetCachedMemberIdUseCase
+import com.goalpanzi.mission_mate.core.domain.usecase.GetMissionBoardsUseCase
+import com.goalpanzi.mission_mate.core.domain.usecase.GetMissionUseCase
+import com.goalpanzi.mission_mate.core.domain.usecase.GetMissionVerificationsUseCase
+import com.goalpanzi.mission_mate.core.domain.usecase.GetMyMissionVerificationUseCase
+import com.goalpanzi.mission_mate.core.domain.usecase.GetViewedTooltipUseCase
+import com.goalpanzi.mission_mate.core.domain.usecase.ProfileUseCase
+import com.goalpanzi.mission_mate.core.domain.usecase.SetViewedTooltipUseCase
+import com.goalpanzi.mission_mate.core.domain.usecase.VerifyMissionUseCase
+import com.goalpanzi.mission_mate.feature.board.model.BoardPiece
+import com.goalpanzi.mission_mate.feature.board.model.BoardPieceType
+import com.goalpanzi.mission_mate.feature.board.model.MissionError
+import com.goalpanzi.mission_mate.feature.board.model.MissionState
+import com.goalpanzi.mission_mate.feature.board.model.MissionState.Companion.getMissionState
+import com.goalpanzi.mission_mate.feature.board.model.UserStory
+import com.goalpanzi.mission_mate.feature.board.model.toBoardPieces
+import com.goalpanzi.mission_mate.feature.board.model.toCharacter
+import com.goalpanzi.mission_mate.feature.board.model.toModel
+import com.goalpanzi.mission_mate.feature.board.model.uimodel.MissionBoardUiModel
+import com.goalpanzi.mission_mate.feature.board.model.uimodel.MissionUiModel
+import com.goalpanzi.mission_mate.feature.board.model.uimodel.MissionVerificationUiModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class BoardViewModel @Inject constructor(
+ savedStateHandle: SavedStateHandle,
+ getCachedMemberIdUseCase: GetCachedMemberIdUseCase,
+ getViewedTooltipUseCase: GetViewedTooltipUseCase,
+ private val getMissionUseCase: GetMissionUseCase,
+ private val getMissionBoardsUseCase: GetMissionBoardsUseCase,
+ private val getMissionVerificationsUseCase: GetMissionVerificationsUseCase,
+ private val deleteMissionUseCase: DeleteMissionUseCase,
+ private val profileUseCase: ProfileUseCase,
+ private val setViewedTooltipUseCase: SetViewedTooltipUseCase,
+ private val verifyMissionUseCase: VerifyMissionUseCase,
+ private val getMyMissionVerificationUseCase : GetMyMissionVerificationUseCase,
+) : ViewModel() {
+
+ val missionId: Long = savedStateHandle.get("missionId")!!
+
+ val viewedToolTip: StateFlow = getViewedTooltipUseCase().stateIn(
+ viewModelScope,
+ started = SharingStarted.WhileSubscribed(500),
+ initialValue = true
+ )
+
+ private val _missionError = MutableStateFlow(null)
+ val missionError : StateFlow =_missionError.asStateFlow()
+
+ private val _myMissionVerification = MutableSharedFlow()
+ val myMissionVerification : SharedFlow = _myMissionVerification.asSharedFlow()
+
+ private val _deleteMissionResultEvent = MutableSharedFlow()
+ val deleteMissionResultEvent: SharedFlow = _deleteMissionResultEvent.asSharedFlow()
+
+ private val _missionBoardUiModel =
+ MutableStateFlow(MissionBoardUiModel.Loading)
+ val missionBoardUiModel: StateFlow = _missionBoardUiModel.asStateFlow()
+
+ private val _missionUiModel =
+ MutableStateFlow(MissionUiModel.Loading)
+ val missionUiModel: StateFlow = _missionUiModel.asStateFlow()
+
+ private val _missionVerificationUiModel =
+ MutableStateFlow(MissionVerificationUiModel.Loading)
+ val missionVerificationUiModel: StateFlow =
+ _missionVerificationUiModel.asStateFlow()
+
+ val isHost: StateFlow =
+ combine(
+ getCachedMemberIdUseCase(),
+ missionUiModel.filter { it is MissionUiModel.Success }
+ ) { memberId, mission ->
+ if (mission !is MissionUiModel.Success) return@combine false
+ memberId == mission.missionDetail.hostMemberId
+ }.stateIn(
+ viewModelScope,
+ started = SharingStarted.WhileSubscribed(500),
+ initialValue = false
+ )
+
+ val missionState: StateFlow =
+ combine(
+ missionBoardUiModel.filter { it is MissionBoardUiModel.Success },
+ missionUiModel.filter { it is MissionUiModel.Success },
+ missionVerificationUiModel.filter { it is MissionVerificationUiModel.Success }
+ ) { missionBoard, mission, missionVerification ->
+ getMissionState(missionBoard, mission, missionVerification)
+ }.stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(500),
+ initialValue = MissionState.LOADING
+ )
+ private val _boardRewardEvent = MutableSharedFlow()
+ val boardRewardEvent: SharedFlow = _boardRewardEvent.asSharedFlow()
+
+ private val _boardPieces = MutableStateFlow>(emptyList())
+ val boardPieces: StateFlow> = _boardPieces.asStateFlow()
+
+ fun getMissionBoards() {
+ viewModelScope.launch {
+ getMissionBoardsUseCase(missionId)
+ .catch {
+ _missionBoardUiModel.emit(MissionBoardUiModel.Error)
+ }.collect {
+ when (it) {
+ is NetworkResult.Success -> {
+ _missionBoardUiModel.emit(
+ MissionBoardUiModel.Success(
+ it.data.toModel()
+ )
+ )
+ _boardPieces.emit(
+ it.data.toModel().toBoardPieces(
+ profileUseCase.getProfile()
+ )
+ )
+
+ }
+
+ else -> {
+ _missionBoardUiModel.emit(MissionBoardUiModel.Error)
+ _missionError.emit(MissionError.NOT_EXIST)
+ }
+ }
+ }
+ }
+ }
+
+ fun getMission() {
+ viewModelScope.launch {
+ getMissionUseCase(missionId).catch {
+ _missionUiModel.emit(MissionUiModel.Error)
+ }.collect {
+ when (it) {
+ is NetworkResult.Success -> {
+ _missionUiModel.emit(MissionUiModel.Success(it.data.toModel()))
+ }
+
+ else -> {
+ _missionUiModel.emit(MissionUiModel.Error)
+ _missionError.emit(MissionError.NOT_EXIST)
+ }
+ }
+ }
+ }
+ }
+
+ fun getMissionVerification() {
+ viewModelScope.launch {
+ getMissionVerificationsUseCase(missionId).catch {
+ _missionVerificationUiModel.emit(MissionVerificationUiModel.Error)
+ }.collect {
+ when (it) {
+ is NetworkResult.Success -> {
+ _missionVerificationUiModel.emit(MissionVerificationUiModel.Success(it.data))
+ }
+
+ else -> {
+ _missionVerificationUiModel.emit(MissionVerificationUiModel.Error)
+ _missionError.emit(MissionError.NOT_EXIST)
+ }
+ }
+ }
+ }
+ }
+
+ fun deleteMission() {
+ viewModelScope.launch {
+ deleteMissionUseCase(missionId)
+ .catch {
+ _deleteMissionResultEvent.emit(false)
+ }.collect {
+ _deleteMissionResultEvent.emit(true)
+ }
+ }
+ }
+
+ fun setViewedTooltip() {
+ viewModelScope.launch {
+ setViewedTooltipUseCase().collect()
+ }
+ }
+
+ fun onVerifySuccess() {
+ if(missionState.value != MissionState.IN_PROGRESS_MISSION_DAY_BEFORE_CONFIRM) return
+ viewModelScope.launch {
+ // 내 캐릭터
+ val myBoardPiece = boardPieces.value.find { it.isMe }
+ val missionBoard = missionBoardUiModel.value
+ if (myBoardPiece != null && missionBoard is MissionBoardUiModel.Success) {
+ val missionBoardList = missionBoard.missionBoards.missionBoardList
+ // 내 캐릭터보다 한칸 앞션 캐릭터
+ val nextBoardPiece = missionBoardList.filter { block ->
+ block.missionBoardMembers.isNotEmpty()
+ }.find {
+ it.number == myBoardPiece.index + 1
+ }
+
+ // 내 캐릭터와 같이 있던 캐릭터
+ val prevBoardPiece = missionBoardList.filter { block ->
+ block.missionBoardMembers.size >= 2
+ }.find {
+ it.number == myBoardPiece.index
+ }
+ _boardPieces.emit(
+ buildList {
+ addAll(
+ boardPieces.value.map {
+ if (it.isMe) it.copy(
+ boardPieceType = BoardPieceType.MOVED,
+ count = if (nextBoardPiece != null) nextBoardPiece.missionBoardMembers.size + 1 else 1
+ ) else if (nextBoardPiece != null && it.index == nextBoardPiece.number) {
+ it.copy(boardPieceType = BoardPieceType.HIDDEN)
+ } else it
+ }
+ )
+ if (prevBoardPiece != null) {
+ val target = prevBoardPiece.missionBoardMembers.first {
+ it.nickname != myBoardPiece.nickname
+ }
+ add(
+ BoardPiece(
+ count = prevBoardPiece.missionBoardMembers.size - 1,
+ nickname = target.nickname,
+ drawableRes = target.character.imageId,
+ index = prevBoardPiece.number,
+ isMe = false
+ )
+ )
+ }
+ }
+ )
+
+ _missionBoardUiModel.emit(
+ missionBoard.copy(
+ missionBoards = missionBoard.missionBoards.copy(
+ passedCountByMe = missionBoard.missionBoards.passedCountByMe + 1
+ )
+ )
+ )
+ delay(400)
+ _boardRewardEvent.emit(
+ missionBoardList.find {
+ myBoardPiece.index + 1 == it.number
+ }?.boardReward
+ )
+ }
+ getMissionBoards()
+ getMission()
+ getMissionVerification()
+ }
+ }
+
+ fun getMyMissionVerification(
+ number : Int
+ ){
+ viewModelScope.launch {
+ getMyMissionVerificationUseCase(
+ missionId,
+ number
+ ).catch {
+
+ }.collect {
+ when(it){
+ is NetworkResult.Success -> {
+ _myMissionVerification.emit(
+ UserStory(
+ characterType = it.data.characterType.toCharacter(),
+ imageUrl = it.data.imageUrl,
+ isVerified = true,
+ nickname = it.data.nickname,
+ verifiedAt = it.data.verifiedAt
+ )
+ )
+ }
+ else -> {
+
+ }
+ }
+
+
+
+ }
+
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/UserStroyScreen.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/UserStroyScreen.kt
new file mode 100644
index 00000000..36a9aa02
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/UserStroyScreen.kt
@@ -0,0 +1,147 @@
+package com.goalpanzi.mission_mate.feature.board.screen
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.paint
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.FilterQuality
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import coil.request.ImageRequest
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorBlack_FF000000
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF
+import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography
+import com.goalpanzi.mission_mate.feature.board.model.Character
+import java.net.URLDecoder
+import java.nio.charset.StandardCharsets
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
+
+@Composable
+fun UserStoryScreen(
+ character: Character = Character.RABBIT,
+ nickname: String = "",
+ verifiedAt: String = "",
+ imageUrl: String = "",
+ onClickClose: () -> Unit
+) {
+ val dateTime = LocalDateTime.parse(verifiedAt)
+ val formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd")
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(ColorWhite_FFFFFFFF)
+ .navigationBarsPadding()
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .statusBarsPadding()
+ ) {
+ AsyncImage(
+ model = ImageRequest.Builder(LocalContext.current)
+ .data(URLDecoder.decode(imageUrl, StandardCharsets.UTF_8.toString()))
+ .build(),
+ contentDescription = null,
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.FillBounds,
+ filterQuality = FilterQuality.None
+ )
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(
+ brush = Brush.verticalGradient(
+ colors = listOf(
+ ColorBlack_FF000000.copy(alpha = 0.4f),
+ Color.Transparent
+ )
+ )
+ )
+ .height(93.dp)
+ .padding(horizontal = 24.dp, vertical = 14.dp)
+ ) {
+ Image(
+ modifier = Modifier
+ .padding(top = 6.dp)
+ .size(28.dp)
+ .border(1.dp, ColorWhite_FFFFFFFF, CircleShape)
+ .paint(
+ painter = painterResource(character.backgroundId),
+ contentScale = ContentScale.FillWidth
+ )
+ .padding(5.dp),
+ painter = painterResource(character.imageId),
+ contentDescription = ""
+ )
+ Text(
+ text = nickname,
+ style = MissionMateTypography.body_xl_bold,
+ color = ColorWhite_FFFFFFFF,
+ modifier = Modifier
+ .padding(start = 8.dp)
+ .wrapContentWidth()
+ .padding(top = 6.dp)
+ )
+
+ Text(
+ text = dateTime.format(formatter),
+ style = MissionMateTypography.body_xl_regular,
+ color = ColorWhite_FFFFFFFF,
+ modifier = Modifier
+ .padding(start = 8.dp)
+ .weight(1f)
+ .padding(top = 6.dp)
+ )
+
+ IconButton(
+ onClick = onClickClose,
+ modifier = Modifier.wrapContentSize()
+ ) {
+ Icon(
+ imageVector = ImageVector.vectorResource(id = com.goalpanzi.mission_mate.core.designsystem.R.drawable.ic_close),
+ contentDescription = "",
+ tint = ColorWhite_FFFFFFFF
+ )
+ }
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+fun UserStoryScreenPreview() {
+ UserStoryScreen(
+ nickname = "토끼는깡총깡",
+ verifiedAt = "2024.08.08",
+ onClickClose = {}
+ )
+}
\ No newline at end of file
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/VerificationPreviewScreen.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/VerificationPreviewScreen.kt
new file mode 100644
index 00000000..26308515
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/VerificationPreviewScreen.kt
@@ -0,0 +1,256 @@
+package com.goalpanzi.mission_mate.feature.board.screen
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+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.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.paint
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.FilterQuality
+import androidx.compose.ui.graphics.vector.ImageVector
+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.res.vectorResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.core.net.toUri
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import coil.compose.AsyncImage
+import coil.request.ImageRequest
+import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateButton
+import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateButtonType
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorBlack_FF000000
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF
+import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography
+import com.goalpanzi.mission_mate.feature.board.R
+import com.goalpanzi.mission_mate.feature.board.model.Character
+import com.goalpanzi.mission_mate.feature.board.util.ImageCompressor
+import kotlinx.coroutines.flow.collectLatest
+import java.io.File
+import java.net.URLDecoder
+import java.nio.charset.StandardCharsets
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
+
+@Composable
+fun VerificationPreviewRoute(
+ onClickClose: () -> Unit,
+ onUploadSuccess: () -> Unit,
+ viewModel: VerificationPreviewViewModel = hiltViewModel()
+) {
+
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+ var showProgress by rememberSaveable { mutableStateOf(false) }
+
+ LaunchedEffect(Unit) {
+ viewModel.eventFlow.collectLatest {
+ when (it) {
+ UploadEvent.Loading -> {
+ showProgress = true
+ }
+ UploadEvent.Success -> {
+ showProgress = false
+ onUploadSuccess()
+ }
+ UploadEvent.Error -> {
+ showProgress = false
+ }
+ }
+ }
+ }
+
+ VerificationPreviewScreen(
+ onClickClose = onClickClose,
+ uiState = uiState,
+ onClickUpload = viewModel::uploadImage
+ )
+
+ if (showProgress) {
+ ProgressBar()
+ }
+}
+
+@Composable
+fun VerificationPreviewScreen(
+ onClickClose: () -> Unit,
+ uiState: VerificationPreviewUiState,
+ onClickUpload: (File) -> Unit
+) {
+ val context = LocalContext.current
+ val dateTime = LocalDateTime.now()
+ val formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd")
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(ColorWhite_FFFFFFFF)
+ .navigationBarsPadding()
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .statusBarsPadding()
+ ) {
+ when (uiState) {
+ VerificationPreviewUiState.Loading -> VerificationPreviewLoading()
+ is VerificationPreviewUiState.Success -> {
+ AsyncImage(
+ model = ImageRequest.Builder(context)
+ .data(uiState.imageUrl)
+ .build(),
+ contentDescription = null,
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.FillBounds,
+ filterQuality = FilterQuality.None
+ )
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(
+ brush = Brush.verticalGradient(
+ colors = listOf(
+ ColorBlack_FF000000.copy(alpha = 0.4f),
+ Color.Transparent
+ )
+ )
+ )
+ .height(93.dp)
+ .padding(horizontal = 24.dp, vertical = 14.dp)
+ ) {
+ Image(
+ modifier = Modifier
+ .padding(top = 6.dp)
+ .size(28.dp)
+ .border(1.dp, ColorWhite_FFFFFFFF, CircleShape)
+ .paint(
+ painter = painterResource(uiState.character.backgroundId),
+ contentScale = ContentScale.FillWidth
+ )
+ .padding(5.dp),
+ painter = painterResource(uiState.character.imageId),
+ contentDescription = ""
+ )
+ Text(
+ text = uiState.nickname,
+ style = MissionMateTypography.body_xl_bold,
+ color = ColorWhite_FFFFFFFF,
+ modifier = Modifier
+ .padding(start = 8.dp)
+ .wrapContentWidth()
+ .padding(top = 6.dp)
+ )
+
+ Text(
+ text = dateTime.format(formatter),
+ style = MissionMateTypography.body_xl_regular,
+ color = ColorWhite_FFFFFFFF,
+ modifier = Modifier
+ .padding(start = 8.dp)
+ .weight(1f)
+ .padding(top = 6.dp)
+ )
+
+ IconButton(
+ onClick = onClickClose,
+ modifier = Modifier.wrapContentSize()
+ ) {
+ Icon(
+ imageVector = ImageVector.vectorResource(id = com.goalpanzi.mission_mate.core.designsystem.R.drawable.ic_close),
+ contentDescription = "",
+ tint = ColorWhite_FFFFFFFF
+ )
+ }
+ }
+
+ MissionMateButton(
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .padding(horizontal = 24.dp, vertical = 36.dp)
+ .fillMaxWidth()
+ .navigationBarsPadding(),
+ buttonType = MissionMateButtonType.ACTIVE,
+ onClick = {
+ val file = ImageCompressor.getCompressedImage(context, uiState.imageUrl.toUri())
+ onClickUpload(file)
+ }
+ ) {
+ Text(
+ text = stringResource(id = R.string.upload),
+ style = MissionMateTypography.body_xl_bold,
+ color = ColorWhite_FFFFFFFF
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun VerificationPreviewLoading() {
+ Box(
+ modifier = Modifier
+ .background(ColorWhite_FFFFFFFF)
+ .statusBarsPadding()
+ .navigationBarsPadding()
+ ) {
+ CircularProgressIndicator(
+ modifier = Modifier.align(Alignment.Center)
+ )
+ }
+}
+
+@Composable
+fun ProgressBar() {
+ Box(
+ modifier = Modifier
+ .statusBarsPadding()
+ .navigationBarsPadding()
+ ) {
+ CircularProgressIndicator(
+ modifier = Modifier.align(Alignment.Center)
+ )
+ }
+}
+
+@Preview
+@Composable
+fun VerificationPreviewScreenPreview() {
+ VerificationPreviewScreen(
+ onClickClose = {},
+ uiState = VerificationPreviewUiState.Success(
+ character = Character.RABBIT,
+ nickname = "닉네임",
+ imageUrl = ""
+ ),
+ onClickUpload = {}
+ )
+}
\ No newline at end of file
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/VerificationPreviewViewModel.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/VerificationPreviewViewModel.kt
new file mode 100644
index 00000000..ca9f415b
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/screen/VerificationPreviewViewModel.kt
@@ -0,0 +1,89 @@
+package com.goalpanzi.mission_mate.feature.board.screen
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.goalpanzi.mission_mate.core.domain.usecase.ProfileUseCase
+import com.goalpanzi.mission_mate.core.domain.usecase.VerifyMissionUseCase
+import com.goalpanzi.mission_mate.feature.board.imageUrlArg
+import com.goalpanzi.mission_mate.feature.board.missionIdArg
+import com.goalpanzi.mission_mate.feature.board.model.Character
+import com.goalpanzi.core.model.UserProfile
+import com.goalpanzi.core.model.base.NetworkResult
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+import java.io.File
+import javax.inject.Inject
+
+@HiltViewModel
+class VerificationPreviewViewModel @Inject constructor(
+ private val verifyMissionUseCase: VerifyMissionUseCase,
+ private val profileUseCase: ProfileUseCase,
+ savedStateHandle: SavedStateHandle
+) : ViewModel() {
+
+ private val missionId = savedStateHandle.get(missionIdArg)
+
+ private val _eventFlow = MutableSharedFlow()
+ val eventFlow = _eventFlow.asSharedFlow()
+
+ val uiState: StateFlow = flow {
+ val profile = profileUseCase.getProfile() as UserProfile
+ emit(profile)
+ }.catch {
+ _eventFlow.emit(UploadEvent.Error)
+ }.map {
+ VerificationPreviewUiState.Success(
+ nickname = it.nickname,
+ character = Character.valueOf(it.characterType.name),
+ imageUrl = savedStateHandle.get(imageUrlArg) ?: ""
+ )
+ }.stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(5_000),
+ initialValue = VerificationPreviewUiState.Loading
+ )
+
+ fun uploadImage(image: File) {
+ if (missionId == null) return
+ viewModelScope.launch {
+ _eventFlow.emit(UploadEvent.Loading)
+ verifyMissionUseCase(missionId, image).collectLatest {
+ when (it) {
+ is NetworkResult.Success -> {
+ _eventFlow.emit(UploadEvent.Success)
+ }
+
+ else -> {
+ _eventFlow.emit(UploadEvent.Error)
+ }
+ }
+ }
+ }
+ }
+
+}
+
+sealed interface VerificationPreviewUiState {
+ data object Loading : VerificationPreviewUiState
+ data class Success(
+ val nickname: String,
+ val character: Character,
+ val imageUrl: String
+ ) : VerificationPreviewUiState
+}
+
+sealed interface UploadEvent {
+ data object Loading : UploadEvent
+ data object Success : UploadEvent
+ data object Error : UploadEvent
+}
\ No newline at end of file
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/util/BoardManager.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/util/BoardManager.kt
new file mode 100644
index 00000000..47c2077f
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/util/BoardManager.kt
@@ -0,0 +1,137 @@
+package com.goalpanzi.mission_mate.feature.board.util
+
+import androidx.compose.ui.unit.Density
+import com.goalpanzi.mission_mate.feature.board.model.BlockEventType
+import com.goalpanzi.mission_mate.feature.board.model.BlockType
+import com.goalpanzi.mission_mate.feature.board.model.uimodel.BlockUiModel
+import com.goalpanzi.mission_mate.feature.board.model.BoardEventItem
+
+object BoardManager {
+
+
+ fun getBlockListByBoardCount(
+ boardCount: Int,
+ numberOfColumns: Int,
+ passedCount: Int,
+ eventList: List
+ ): List {
+
+ val quotient = boardCount / (numberOfColumns * 2)
+ val remainder = boardCount % (numberOfColumns * 2)
+
+ val blockList = mutableListOf()
+
+ repeat(quotient) { innerQuotient ->
+ getTopRow(
+ blockList,
+ innerQuotient,
+ numberOfColumns,
+ numberOfColumns,
+ boardCount,
+ passedCount,
+ eventList
+ )
+ getBottomRow(
+ blockList,
+ innerQuotient, numberOfColumns, numberOfColumns * 2, boardCount, passedCount,
+ eventList
+ )
+ }
+ if (remainder != 0) {
+ if (remainder in 1..numberOfColumns) {
+ getTopRow(
+ blockList, quotient, numberOfColumns, remainder, boardCount, passedCount,
+ eventList
+ )
+ } else {
+ getTopRow(
+ blockList, quotient, numberOfColumns, remainder, boardCount, passedCount,
+ eventList
+ )
+ getBottomRow(
+ blockList, quotient, numberOfColumns, remainder, boardCount, passedCount,
+ eventList
+ )
+ }
+ }
+ return blockList
+ }
+
+ private fun getTopRow(
+ blockList: MutableList,
+ quotient: Int,
+ numberOfColumns: Int,
+ remainder: Int,
+ boardCount: Int,
+ passedCount: Int,
+ indicesForPresent: List
+ ) {
+ for (i in 1..numberOfColumns) {
+ val index = quotient * numberOfColumns * 2 + i - 1
+ val itemEvent = indicesForPresent.find { it.index == index }
+
+ blockList.add(
+ BlockUiModel(
+ index = index,
+ blockType =
+ if (quotient * numberOfColumns * 2 + i - 1 == 0) BlockType.START
+ else if (index == boardCount - 1) BlockType.CENTER
+ else if (i > remainder) BlockType.EMPTY
+ else if (i == 1) BlockType.BOTTOM_LEFT_CORNER
+ else if (i == numberOfColumns) BlockType.TOP_RIGHT_CORNER
+ else BlockType.CENTER,
+ blockEventType =
+ if (itemEvent != null && index == boardCount - 1) BlockEventType.GoalWithEvent(itemEvent)
+ else if(index == boardCount - 1) BlockEventType.Goal
+ else if (itemEvent != null) BlockEventType.Item(itemEvent)
+ else BlockEventType.None,
+ isEvenGroup = quotient % 2 == 0,
+ isPassed = index <= passedCount
+ )
+ )
+ }
+ }
+
+ private fun getBottomRow(
+ blockList: MutableList,
+ quotient: Int,
+ numberOfColumns: Int,
+ remainder: Int,
+ boardCount: Int,
+ passedCount: Int,
+ indicesForPresent: List
+ ) {
+ for (i in numberOfColumns * 2 downTo numberOfColumns + 1) {
+ val index = quotient * (numberOfColumns * 2) + i - 1
+ val itemEvent = indicesForPresent.find { it.index == index }
+ blockList.add(
+ BlockUiModel(
+ index = index,
+ blockType =
+ if (i > remainder && remainder != 0) BlockType.EMPTY
+ else if (index == boardCount - 1) BlockType.CENTER
+ else if (i == numberOfColumns + 1) BlockType.BOTTOM_RIGHT_CORNER
+ else if (i == numberOfColumns * 2) BlockType.TOP_LEFT_CORNER
+ else BlockType.CENTER,
+ isEvenGroup = quotient % 2 == 0,
+ blockEventType =
+ if (itemEvent != null && index == boardCount) BlockEventType.GoalWithEvent(itemEvent)
+ else if(index == boardCount - 1) BlockEventType.Goal
+ else if (itemEvent != null) BlockEventType.Item(itemEvent)
+ else BlockEventType.None,
+ isPassed = index <= passedCount
+ )
+ )
+ }
+ }
+
+ fun getPositionScrollToMyIndex(
+ myIndex : Int,
+ numberOfColumns: Int,
+ blockSize : Int,
+ localDensity: Density
+ ) : Int {
+ return ((myIndex / numberOfColumns) * (blockSize) * localDensity.density).toInt()
+ }
+
+}
\ No newline at end of file
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/util/ImageCompressor.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/util/ImageCompressor.kt
new file mode 100644
index 00000000..7a198ae9
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/util/ImageCompressor.kt
@@ -0,0 +1,43 @@
+package com.goalpanzi.mission_mate.feature.board.util
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.ImageDecoder
+import android.net.Uri
+import android.os.Build
+import android.provider.MediaStore
+import java.io.ByteArrayOutputStream
+import java.io.File
+import java.io.FileOutputStream
+
+object ImageCompressor {
+
+ fun getCompressedImage(context: Context, filePath: Uri) : File {
+
+ val bitmap = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
+ MediaStore.Images.Media.getBitmap(context.contentResolver, filePath)
+ } else {
+ val source = ImageDecoder
+ .createSource(context.contentResolver, filePath)
+ ImageDecoder.decodeBitmap(source)
+ }
+
+ val file = File(context.cacheDir, "${filePath.toString().split("/").last()}.jpg")
+
+ try {
+ val outputStream = ByteArrayOutputStream()
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
+ val byteArray = outputStream.toByteArray()
+ val compressedBitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size)
+
+ val fileOutputStream = FileOutputStream(file)
+ compressedBitmap.compress(Bitmap.CompressFormat.JPEG, 100, fileOutputStream)
+ outputStream.flush()
+ outputStream.close()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ return file
+ }
+}
\ No newline at end of file
diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/util/PieceGenerator.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/util/PieceGenerator.kt
new file mode 100644
index 00000000..db05cf52
--- /dev/null
+++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/util/PieceGenerator.kt
@@ -0,0 +1,31 @@
+package com.goalpanzi.mission_mate.feature.board.util
+
+import androidx.compose.ui.unit.Dp
+
+object PieceGenerator {
+
+ fun getXOffset(
+ index : Int,
+ numberOfColumn : Int,
+ sizePerBlock : Dp,
+ ) : Dp {
+ val quotient = index / numberOfColumn
+ val remainder = index % numberOfColumn
+
+ val offsetUnit = sizePerBlock.value / 2f
+ val xIndex = if(quotient % 2 == 0) remainder else (numberOfColumn - remainder - 1)
+
+ return sizePerBlock * xIndex// + offsetUnit.dp
+ }
+
+ fun getYOffset(
+ index : Int,
+ numberOfColumn : Int,
+ sizePerBlock : Dp,
+ ) : Dp {
+ val quotient = index / numberOfColumn
+ val offsetUnit = sizePerBlock.value / 2f
+
+ return sizePerBlock * quotient// + offsetUnit.dp
+ }
+}
\ No newline at end of file
diff --git a/feature/board/src/main/res/drawable/img_beach_full.png b/feature/board/src/main/res/drawable/img_beach_full.png
new file mode 100644
index 00000000..ecbcb227
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_beach_full.png differ
diff --git a/feature/board/src/main/res/drawable/img_black_pig_full.png b/feature/board/src/main/res/drawable/img_black_pig_full.png
new file mode 100644
index 00000000..18d894a5
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_black_pig_full.png differ
diff --git a/feature/board/src/main/res/drawable/img_board_center_dark.png b/feature/board/src/main/res/drawable/img_board_center_dark.png
new file mode 100644
index 00000000..0476f607
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_board_center_dark.png differ
diff --git a/feature/board/src/main/res/drawable/img_board_center_jeju.png b/feature/board/src/main/res/drawable/img_board_center_jeju.png
new file mode 100644
index 00000000..9474c9c5
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_board_center_jeju.png differ
diff --git a/feature/board/src/main/res/drawable/img_board_center_light.png b/feature/board/src/main/res/drawable/img_board_center_light.png
new file mode 100644
index 00000000..0a72b2db
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_board_center_light.png differ
diff --git a/feature/board/src/main/res/drawable/img_board_left_bottom_dark.png b/feature/board/src/main/res/drawable/img_board_left_bottom_dark.png
new file mode 100644
index 00000000..1bb67032
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_board_left_bottom_dark.png differ
diff --git a/feature/board/src/main/res/drawable/img_board_left_bottom_jeju.png b/feature/board/src/main/res/drawable/img_board_left_bottom_jeju.png
new file mode 100644
index 00000000..2f36f883
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_board_left_bottom_jeju.png differ
diff --git a/feature/board/src/main/res/drawable/img_board_left_bottom_light.png b/feature/board/src/main/res/drawable/img_board_left_bottom_light.png
new file mode 100644
index 00000000..599196ef
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_board_left_bottom_light.png differ
diff --git a/feature/board/src/main/res/drawable/img_board_left_top_dark.png b/feature/board/src/main/res/drawable/img_board_left_top_dark.png
new file mode 100644
index 00000000..13000509
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_board_left_top_dark.png differ
diff --git a/feature/board/src/main/res/drawable/img_board_left_top_jeju.png b/feature/board/src/main/res/drawable/img_board_left_top_jeju.png
new file mode 100644
index 00000000..3aa34a61
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_board_left_top_jeju.png differ
diff --git a/feature/board/src/main/res/drawable/img_board_left_top_light.png b/feature/board/src/main/res/drawable/img_board_left_top_light.png
new file mode 100644
index 00000000..529c115b
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_board_left_top_light.png differ
diff --git a/feature/board/src/main/res/drawable/img_board_right_bottom_dark.png b/feature/board/src/main/res/drawable/img_board_right_bottom_dark.png
new file mode 100644
index 00000000..632167b0
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_board_right_bottom_dark.png differ
diff --git a/feature/board/src/main/res/drawable/img_board_right_bottom_jeju.png b/feature/board/src/main/res/drawable/img_board_right_bottom_jeju.png
new file mode 100644
index 00000000..d84e7761
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_board_right_bottom_jeju.png differ
diff --git a/feature/board/src/main/res/drawable/img_board_right_bottom_light.png b/feature/board/src/main/res/drawable/img_board_right_bottom_light.png
new file mode 100644
index 00000000..3438d15a
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_board_right_bottom_light.png differ
diff --git a/feature/board/src/main/res/drawable/img_board_right_top_dark.png b/feature/board/src/main/res/drawable/img_board_right_top_dark.png
new file mode 100644
index 00000000..6e3daf18
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_board_right_top_dark.png differ
diff --git a/feature/board/src/main/res/drawable/img_board_right_top_jeju.png b/feature/board/src/main/res/drawable/img_board_right_top_jeju.png
new file mode 100644
index 00000000..027483b3
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_board_right_top_jeju.png differ
diff --git a/feature/board/src/main/res/drawable/img_board_right_top_light.png b/feature/board/src/main/res/drawable/img_board_right_top_light.png
new file mode 100644
index 00000000..65881e05
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_board_right_top_light.png differ
diff --git a/feature/board/src/main/res/drawable/img_board_start.png b/feature/board/src/main/res/drawable/img_board_start.png
new file mode 100644
index 00000000..987c4a53
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_board_start.png differ
diff --git a/feature/board/src/main/res/drawable/img_bong.png b/feature/board/src/main/res/drawable/img_bong.png
new file mode 100644
index 00000000..4518df9b
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_bong.png differ
diff --git a/feature/board/src/main/res/drawable/img_canola_flower_full.png b/feature/board/src/main/res/drawable/img_canola_flower_full.png
new file mode 100644
index 00000000..bc6030de
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_canola_flower_full.png differ
diff --git a/feature/board/src/main/res/drawable/img_default_move.png b/feature/board/src/main/res/drawable/img_default_move.png
new file mode 100644
index 00000000..d62f7560
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_default_move.png differ
diff --git a/feature/board/src/main/res/drawable/img_dolharubang_full.png b/feature/board/src/main/res/drawable/img_dolharubang_full.png
new file mode 100644
index 00000000..d5054a7b
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_dolharubang_full.png differ
diff --git a/feature/board/src/main/res/drawable/img_flower.png b/feature/board/src/main/res/drawable/img_flower.png
new file mode 100644
index 00000000..3fb5c40b
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_flower.png differ
diff --git a/feature/board/src/main/res/drawable/img_green_tea.png b/feature/board/src/main/res/drawable/img_green_tea.png
new file mode 100644
index 00000000..45144a61
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_green_tea.png differ
diff --git a/feature/board/src/main/res/drawable/img_green_tea_field_full.png b/feature/board/src/main/res/drawable/img_green_tea_field_full.png
new file mode 100644
index 00000000..0b59f097
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_green_tea_field_full.png differ
diff --git a/feature/board/src/main/res/drawable/img_halla_mountain_full.png b/feature/board/src/main/res/drawable/img_halla_mountain_full.png
new file mode 100644
index 00000000..8e68b4ed
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_halla_mountain_full.png differ
diff --git a/feature/board/src/main/res/drawable/img_horse.png b/feature/board/src/main/res/drawable/img_horse.png
new file mode 100644
index 00000000..bd2d81c8
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_horse.png differ
diff --git a/feature/board/src/main/res/drawable/img_horse_riding_full.png b/feature/board/src/main/res/drawable/img_horse_riding_full.png
new file mode 100644
index 00000000..8c1fe944
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_horse_riding_full.png differ
diff --git a/feature/board/src/main/res/drawable/img_mission_finish.png b/feature/board/src/main/res/drawable/img_mission_finish.png
new file mode 100644
index 00000000..57125040
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_mission_finish.png differ
diff --git a/feature/board/src/main/res/drawable/img_mountain.png b/feature/board/src/main/res/drawable/img_mountain.png
new file mode 100644
index 00000000..f1f4bcb8
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_mountain.png differ
diff --git a/feature/board/src/main/res/drawable/img_orange.png b/feature/board/src/main/res/drawable/img_orange.png
new file mode 100644
index 00000000..97784354
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_orange.png differ
diff --git a/feature/board/src/main/res/drawable/img_orange_full.png b/feature/board/src/main/res/drawable/img_orange_full.png
new file mode 100644
index 00000000..6bc0660e
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_orange_full.png differ
diff --git a/feature/board/src/main/res/drawable/img_pig.png b/feature/board/src/main/res/drawable/img_pig.png
new file mode 100644
index 00000000..3a1344c0
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_pig.png differ
diff --git a/feature/board/src/main/res/drawable/img_present.png b/feature/board/src/main/res/drawable/img_present.png
new file mode 100644
index 00000000..4e6519bd
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_present.png differ
diff --git a/feature/board/src/main/res/drawable/img_sea.png b/feature/board/src/main/res/drawable/img_sea.png
new file mode 100644
index 00000000..997f8154
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_sea.png differ
diff --git a/feature/board/src/main/res/drawable/img_stone.png b/feature/board/src/main/res/drawable/img_stone.png
new file mode 100644
index 00000000..be6ba272
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_stone.png differ
diff --git a/feature/board/src/main/res/drawable/img_sunrise_full.png b/feature/board/src/main/res/drawable/img_sunrise_full.png
new file mode 100644
index 00000000..d3390148
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_sunrise_full.png differ
diff --git a/feature/board/src/main/res/drawable/img_tooltip_mission_delete_warning.png b/feature/board/src/main/res/drawable/img_tooltip_mission_delete_warning.png
new file mode 100644
index 00000000..a1a615b5
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_tooltip_mission_delete_warning.png differ
diff --git a/feature/board/src/main/res/drawable/img_tooltip_mission_detail.png b/feature/board/src/main/res/drawable/img_tooltip_mission_detail.png
new file mode 100644
index 00000000..a767b661
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_tooltip_mission_detail.png differ
diff --git a/feature/board/src/main/res/drawable/img_tooltip_mission_invitation_code.png b/feature/board/src/main/res/drawable/img_tooltip_mission_invitation_code.png
new file mode 100644
index 00000000..bf07c855
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_tooltip_mission_invitation_code.png differ
diff --git a/feature/board/src/main/res/drawable/img_tooltip_mission_welcome.png b/feature/board/src/main/res/drawable/img_tooltip_mission_welcome.png
new file mode 100644
index 00000000..619a5be0
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_tooltip_mission_welcome.png differ
diff --git a/feature/board/src/main/res/drawable/img_waterfall.png b/feature/board/src/main/res/drawable/img_waterfall.png
new file mode 100644
index 00000000..08d047cd
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_waterfall.png differ
diff --git a/feature/board/src/main/res/drawable/img_waterfall_full.png b/feature/board/src/main/res/drawable/img_waterfall_full.png
new file mode 100644
index 00000000..20010e5a
Binary files /dev/null and b/feature/board/src/main/res/drawable/img_waterfall_full.png differ
diff --git a/feature/board/src/main/res/values/strings.xml b/feature/board/src/main/res/values/strings.xml
new file mode 100644
index 00000000..4ce61848
--- /dev/null
+++ b/feature/board/src/main/res/values/strings.xml
@@ -0,0 +1,62 @@
+
+
+ 경쟁시작 %d월 %d일
+ 오늘 %d명이 %d칸 이동
+ 꾸준하게 완수해봐요!
+ 해당일에 자동으로 경쟁이 시작돼요.
+ 나의 꾸준함 순위는? %d등
+
+ 오전 00~12시
+ 오후 12~24시
+ 종일 00~24시
+ 미션 요일 : %s
+
+ 오늘 미션 인증하기
+ 오늘 미션 인증 완료!
+ 오늘 미션 인증 시간 마감
+ 오늘은 미션일이 아니에요
+ 지금은 미션 인증시간이 아니에요.
+
+ 시작일까지 아무도 오지않아\n미션보드를 삭제할게요.
+ 미션보드는 삭제되고\n초기화면으로 이동해요.
+
+ 인증완료!!\n한 칸 이동했어요.
+ 대단해요. 오늘도 해냈어요.
+ 인증완료!!\n\‘%s\’를 획득했어요!
+ 꾸준히 하면 재밌는 이벤트가 또 나타날걸요?
+
+ 감귤 먹기
+ 유채꽃 보기
+ 돌하르방 만나기
+ 승마 체험하기
+ 한라산 등반하기
+ 폭포 감상하기
+ 흑돼지 먹기
+ 성산일출봉 보기
+ 녹차밭
+ 바다 보기
+
+ %d등
+ 경쟁이 종료되었어요.
+ 보드판은 삭제되며\n초기화면으로 돌아가요!
+
+ 시작일까지 초대된 인원과\n경쟁이 자동으로 시작돼요!
+ 경쟁이 자동으로
+ 경쟁인원은 최소 %d명부터 최대 %d명 이에요!
+ 친구 초대 코드 복사
+
+ 진행중인 미션을\n삭제하시겠습니까?
+ 미션을 삭제하면\n미션 보드판이 초기화돼요.
+
+ 경쟁 내용
+ *기간 대비 인증 요일을 계산해\n인증횟수(보드판 수)는 총 %d개 가\n생성되었어요.
+ 인증횟수(보드판 수)는 총 %d개
+ 삭제하기
+
+ 미션을 불러올 수 없습니다.
+
+ 확인
+ 취소
+ 닫기
+ 업로드
+
\ No newline at end of file
diff --git a/feature/board/src/test/java/com/goalpanzi/mission_mate/feature/board/util/boardmanager/MissionStateAsInProgressTest.kt b/feature/board/src/test/java/com/goalpanzi/mission_mate/feature/board/util/boardmanager/MissionStateAsInProgressTest.kt
new file mode 100644
index 00000000..e2d9d9df
--- /dev/null
+++ b/feature/board/src/test/java/com/goalpanzi/mission_mate/feature/board/util/boardmanager/MissionStateAsInProgressTest.kt
@@ -0,0 +1,94 @@
+package com.goalpanzi.mission_mate.feature.board.util.boardmanager
+
+import com.goalpanzi.mission_mate.feature.board.model.MissionState
+import com.goalpanzi.mission_mate.feature.board.model.MissionState.Companion.getMissionStateAsInProgress
+import com.goalpanzi.mission_mate.feature.onboarding.model.VerificationTimeType
+import com.goalpanzi.core.model.response.MissionVerificationResponse
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import java.time.DayOfWeek
+import java.time.LocalDate
+import java.time.LocalDateTime
+
+class MissionStateAsInProgressTest {
+
+ @Test
+ fun 오늘이_인증_요일이고_현재_시간이_인증_시간대_이전일_때_IN_PROGRESS_MISSION_DAY_NON_MISSION_TIME를_반환한다() {
+ val todayLocalDate = LocalDate.of(2024, 8, 14) // 수요일
+ val todayLocalDateTime = LocalDateTime.of(2024, 8, 14, 11, 59, 59)
+ val daysOfWeek = listOf(DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY)
+ val verificationTimeType = VerificationTimeType.AFTERNOON
+ val memberList = listOf(
+ MissionVerificationResponse(
+ nickname = "",
+ imageUrl = "image"
+ )
+ )
+
+ val result = getMissionStateAsInProgress(todayLocalDate, todayLocalDateTime, daysOfWeek, verificationTimeType, memberList)
+
+ assertEquals(MissionState.IN_PROGRESS_MISSION_DAY_NON_MISSION_TIME, result)
+ }
+
+ @Test
+ fun 오늘이_인증_요일이고_현재_시간이_인증_시간대_이후일_때_IN_PROGRESS_MISSION_DAY_CLOSED를_반환한다() {
+ val todayLocalDate = LocalDate.of(2024, 8, 14) // 수요일
+ val todayLocalDateTime = LocalDateTime.of(2024, 8, 14, 12, 0, 0)
+ val daysOfWeek = listOf(DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY)
+ val verificationTimeType = VerificationTimeType.MORNING
+ val memberList = listOf(
+ MissionVerificationResponse(
+ nickname = "",
+ imageUrl = ""
+ )
+ )
+
+ val result = getMissionStateAsInProgress(todayLocalDate, todayLocalDateTime, daysOfWeek, verificationTimeType, memberList)
+
+ assertEquals(MissionState.IN_PROGRESS_MISSION_DAY_CLOSED, result)
+ }
+
+ @Test
+ fun 오늘이_인증_요일이고_현재_시간이_인증_시간대일_때_인증한_이미지_데이터가_없으면_IN_PROGRESS_MISSION_DAY_BEFORE_CONFIRM를_반환한다() {
+ val todayLocalDate = LocalDate.of(2024, 8, 14) // 수요일
+ val todayLocalDateTime = LocalDateTime.of(2024, 8, 14, 10, 0, 0)
+ val daysOfWeek = listOf(DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY)
+ val verificationTimeType = VerificationTimeType.MORNING
+ val memberList = listOf(
+ MissionVerificationResponse(
+ nickname = "",
+ imageUrl = ""
+ )
+ )
+
+ val result = getMissionStateAsInProgress(todayLocalDate, todayLocalDateTime, daysOfWeek, verificationTimeType, memberList)
+
+ assertEquals(MissionState.IN_PROGRESS_MISSION_DAY_BEFORE_CONFIRM, result)
+ }
+
+ @Test
+ fun 오늘이_인증_요일이고_현재_시간이_인증_시간대일_때_인증한_이미지_데이터가_있으면_IN_PROGRESS_MISSION_DAY_AFTER_CONFIRM를_반환한다() {
+ val todayLocalDate = LocalDate.of(2024, 8, 14) // 수요일
+ val todayLocalDateTime = LocalDateTime.of(2024, 8, 14, 10, 0, 0)
+ val daysOfWeek = listOf(DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY)
+ val verificationTimeType = VerificationTimeType.MORNING
+ val memberList = listOf(MissionVerificationResponse(nickname = "user", imageUrl = "image_url"))
+
+ val result = getMissionStateAsInProgress(todayLocalDate, todayLocalDateTime, daysOfWeek, verificationTimeType, memberList)
+
+ assertEquals(MissionState.IN_PROGRESS_MISSION_DAY_AFTER_CONFIRM, result)
+ }
+
+ @Test
+ fun 오늘이_인증_요일이_아니면_IN_PROGRESS_NON_MISSION_DAY를_반환한다() {
+ val todayLocalDate = LocalDate.of(2024, 8, 13) // 화요일
+ val todayLocalDateTime = LocalDateTime.of(2024, 8, 13, 10, 0, 0)
+ val daysOfWeek = listOf(DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY)
+ val verificationTimeType = VerificationTimeType.MORNING
+ val memberList = emptyList()
+
+ val result = getMissionStateAsInProgress(todayLocalDate, todayLocalDateTime, daysOfWeek, verificationTimeType, memberList)
+
+ assertEquals(MissionState.IN_PROGRESS_NON_MISSION_DAY, result)
+ }
+}
\ No newline at end of file
diff --git a/feature/board/src/test/java/com/goalpanzi/mission_mate/feature/board/util/boardmanager/MissionStateTest.kt b/feature/board/src/test/java/com/goalpanzi/mission_mate/feature/board/util/boardmanager/MissionStateTest.kt
new file mode 100644
index 00000000..4ebcad37
--- /dev/null
+++ b/feature/board/src/test/java/com/goalpanzi/mission_mate/feature/board/util/boardmanager/MissionStateTest.kt
@@ -0,0 +1,132 @@
+package com.goalpanzi.mission_mate.feature.board.util.boardmanager
+
+import com.goalpanzi.mission_mate.feature.board.model.MissionState
+import com.goalpanzi.mission_mate.feature.board.model.MissionState.Companion.getMissionState
+import com.goalpanzi.mission_mate.feature.onboarding.model.VerificationTimeType
+import com.goalpanzi.core.model.response.MissionVerificationResponse
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import java.time.DayOfWeek
+import java.time.LocalDate
+import java.time.LocalDateTime
+import kotlin.test.assertNotEquals
+
+class MissionStateTest {
+
+ @Test
+ fun 현재_날짜가_인증_시작일이고_멤버_수가_1이하_일때_DELETABLE를_반환한다() {
+ val todayLocalDateTime = LocalDateTime.of(2024, 8, 14, 10, 0, 0) // 오전 10시
+ val startDate = LocalDate.of(2024, 8, 14) // 시작 날짜가 오늘
+ val endDateTime = LocalDateTime.of(2024, 8, 15, 23, 59, 59)
+ val memberList = emptyList()
+ val verificationTimeType = VerificationTimeType.MORNING
+ val daysOfWeek = listOf(DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY)
+
+ val result = getMissionState(
+ todayLocalDateTime,
+ startDate,
+ endDateTime,
+ memberList,
+ verificationTimeType,
+ daysOfWeek
+ )
+ assertEquals(MissionState.DELETABLE, result)
+ }
+
+ @Test
+ fun 현재_날짜가_인증_시작일_이후이고_멤버_수가_1이하_일때_DELETABLE를_반환한다() {
+ val todayLocalDateTime = LocalDateTime.of(2024, 8, 16, 10, 0, 0) // 오전 10시
+ val startDate = LocalDate.of(2024, 8, 14)
+ val endDateTime = LocalDateTime.of(2024, 8, 15, 23, 59, 59)
+ val memberList = emptyList()
+ val verificationTimeType = VerificationTimeType.MORNING
+ val daysOfWeek = listOf(DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY)
+
+ val result = getMissionState(
+ todayLocalDateTime,
+ startDate,
+ endDateTime,
+ memberList,
+ verificationTimeType,
+ daysOfWeek
+ )
+ assertEquals(MissionState.DELETABLE, result)
+ }
+
+ @Test
+ fun 현재_날짜가_인증_시작일_이전이고_멤버_수가_1이하_일때_PRE_START_SOLO를_반환한다() {
+ val todayLocalDateTime = LocalDateTime.of(2024, 8, 14, 10, 0, 0)
+ val startDate = LocalDate.of(2024, 8, 15)
+ val endDateTime = LocalDateTime.of(2024, 8, 20, 23, 59, 59)
+ val memberList = listOf(MissionVerificationResponse(nickname = "user", imageUrl = "image_url"))
+ val verificationTimeType = VerificationTimeType.MORNING
+ val daysOfWeek = listOf(DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY)
+
+ val result = getMissionState(todayLocalDateTime, startDate, endDateTime, memberList, verificationTimeType, daysOfWeek)
+ assertEquals(MissionState.PRE_START_SOLO, result)
+ }
+
+ @Test
+ fun 현재_날짜가_인증_시작일_이전이고_멤버_수가_2이상_일때_PRE_START_MULTI를_반환한다() {
+ val todayLocalDateTime = LocalDateTime.of(2024, 8, 14, 10, 0, 0)
+ val startDate = LocalDate.of(2024, 8, 15)
+ val endDateTime = LocalDateTime.of(2024, 8, 20, 23, 59, 59)
+ val memberList = listOf(
+ MissionVerificationResponse(nickname = "user1", imageUrl = "image_url"),
+ MissionVerificationResponse(nickname = "user2", imageUrl = "image_url")
+ )
+ val verificationTimeType = VerificationTimeType.MORNING
+ val daysOfWeek = listOf(DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY)
+
+ val result = getMissionState(todayLocalDateTime, startDate, endDateTime, memberList, verificationTimeType, daysOfWeek)
+ assertEquals(MissionState.PRE_START_MULTI, result)
+ }
+
+ @Test
+ fun 현재_날짜가_인증_마감일_이후일_때_POST_END를_반환한다() {
+ val todayLocalDateTime = LocalDateTime.of(2024, 8, 16, 0, 0, 0)
+ val startDate = LocalDate.of(2024, 8, 13)
+ val endDateTime = LocalDateTime.of(2024, 8, 15, 23, 59, 59)
+ val memberList = listOf(
+ MissionVerificationResponse(nickname = "user", imageUrl = "image_url"),
+ MissionVerificationResponse(nickname = "user1", imageUrl = "image_url")
+ )
+ val verificationTimeType = VerificationTimeType.MORNING
+ val daysOfWeek = listOf(DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY)
+
+ val result = getMissionState(todayLocalDateTime, startDate, endDateTime, memberList, verificationTimeType, daysOfWeek)
+ assertEquals(MissionState.POST_END, result)
+ }
+
+ @Test
+ fun 인증_시간대가_오전이고_현재_날짜가_인증_마감일이고_현재_시간이_인증_마감_시간_이후일_때_POST_END를_반환한다() {
+ val todayLocalDateTime = LocalDateTime.of(2024, 8, 15, 12, 0, 0)
+ val startDate = LocalDate.of(2024, 8, 13)
+ val endDateTime = LocalDateTime.of(2024, 8, 15, 0, 0, 0)
+ val memberList = listOf(
+ MissionVerificationResponse(nickname = "user", imageUrl = "image_url"),
+ MissionVerificationResponse(nickname = "user1", imageUrl = "image_url")
+ )
+ val verificationTimeType = VerificationTimeType.MORNING
+ val daysOfWeek = listOf(DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY)
+
+ val result = getMissionState(todayLocalDateTime, startDate, endDateTime, memberList, verificationTimeType, daysOfWeek)
+ assertEquals(MissionState.POST_END, result)
+ }
+
+ @Test
+ fun 인증_시간대가_오전이_아니고_현재_날짜가_인증_마감일일_때_POST_END를_반환하지_않는다() {
+ val todayLocalDateTime = LocalDateTime.of(2024, 8, 15, 12, 0, 0)
+ val startDate = LocalDate.of(2024, 8, 13)
+ val endDateTime = LocalDateTime.of(2024, 8, 15, 0, 0, 0)
+ val memberList = listOf(
+ MissionVerificationResponse(nickname = "user", imageUrl = "image_url"),
+ MissionVerificationResponse(nickname = "user1", imageUrl = "image_url")
+ )
+ val verificationTimeType = VerificationTimeType.AFTERNOON
+ val daysOfWeek = listOf(DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY)
+
+ val result = getMissionState(todayLocalDateTime, startDate, endDateTime, memberList, verificationTimeType, daysOfWeek)
+ assertNotEquals(MissionState.POST_END, result)
+ }
+}
\ No newline at end of file
diff --git a/feature/board/src/test/java/com/goalpanzi/mission_mate/feature/board/util/boardmanager/MissionStateUtilityMethodTests.kt b/feature/board/src/test/java/com/goalpanzi/mission_mate/feature/board/util/boardmanager/MissionStateUtilityMethodTests.kt
new file mode 100644
index 00000000..7ba4911b
--- /dev/null
+++ b/feature/board/src/test/java/com/goalpanzi/mission_mate/feature/board/util/boardmanager/MissionStateUtilityMethodTests.kt
@@ -0,0 +1,151 @@
+package com.goalpanzi.mission_mate.feature.board.util.boardmanager
+
+import com.goalpanzi.mission_mate.feature.board.model.MissionState.Companion.isPassedEndTime
+import com.goalpanzi.mission_mate.feature.board.model.MissionState.Companion.isTodayMissionDay
+import com.goalpanzi.mission_mate.feature.board.model.MissionState.Companion.isVerifiedInMissionTime
+import com.goalpanzi.mission_mate.feature.onboarding.model.VerificationTimeType
+import com.goalpanzi.core.model.response.MissionVerificationResponse
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import java.time.DayOfWeek
+import java.time.LocalDate
+import java.time.LocalDateTime
+
+class MissionStateUtilityMethodTests {
+
+ // 인증 시간대가 오전이고 현재 날짜가 인증 종료일이고 현재 시간이 인증 시간대 이내라면 false를 반환한다
+ @Test
+ fun isPassedEndTime_MorningBeforeEnd_ReturnsFalse() {
+ val todayLocalDateTime = LocalDateTime.of(2024, 8, 11, 10, 0, 0)
+ val endDateTime = LocalDateTime.of(2024, 8, 11, 11, 59, 59)
+ val verificationTimeType = VerificationTimeType.MORNING
+
+ val result = isPassedEndTime(todayLocalDateTime, endDateTime, verificationTimeType)
+ assertFalse(result)
+ }
+
+ // 인증 시간대가 오전이고 현재 날짜가 인증 종료일이고 현재 시간이 인증 시간대 이내가 아니라면 true를 반환한다
+ @Test
+ fun isPassedEndTime_MorningAfterEnd_ReturnsTrue() {
+ val todayLocalDateTime = LocalDateTime.of(2024, 8, 11, 12, 0, 0)
+ val endDateTime = LocalDateTime.of(2024, 8, 11, 11, 59, 59)
+ val verificationTimeType = VerificationTimeType.MORNING
+
+ val result = isPassedEndTime(todayLocalDateTime, endDateTime, verificationTimeType)
+ assertTrue(result)
+ }
+
+ // 인증 시간대가 오후이고 현재 날짜가 인증 종료일이고 현재 시간이 인증 시간대 이내라면 false를 반환한다
+ @Test
+ fun isPassedEndTime_AfternoonBeforeEnd_ReturnsFalse() {
+ val todayLocalDateTime = LocalDateTime.of(2024, 8, 11, 22, 0, 0)
+ val endDateTime = LocalDateTime.of(2024, 8, 11, 23, 59, 59)
+ val verificationTimeType = VerificationTimeType.AFTERNOON
+
+ val result = isPassedEndTime(todayLocalDateTime, endDateTime, verificationTimeType)
+ assertFalse(result)
+ }
+
+ // 인증 시간대가 오후이고 현재 날짜가 인증 종료일이고 현재 시간이 인증 시간대 이내가 아니라면 true를 반환한다
+ @Test
+ fun isPassedEndTime_AfternoonAfterEnd_ReturnsTrue() {
+ val todayLocalDateTime = LocalDateTime.of(2024, 8, 12, 0, 0, 0)
+ val endDateTime = LocalDateTime.of(2024, 8, 11, 23, 59, 59)
+ val verificationTimeType = VerificationTimeType.AFTERNOON
+
+ val result = isPassedEndTime(todayLocalDateTime, endDateTime, verificationTimeType)
+ assertTrue(result)
+ }
+
+ // 인증 시간대가 종일이고 현재 날짜가 인증 종료일이고 현재 시간이 인증 시간대 이내라면 false를 반환한다
+ @Test
+ fun isPassedEndTime_EverydayBeforeEnd_ReturnsFalse() {
+ val todayLocalDateTime = LocalDateTime.of(2024, 8, 11, 22, 0, 0)
+ val endDateTime = LocalDateTime.of(2024, 8, 11, 23, 59, 59)
+ val verificationTimeType = VerificationTimeType.EVERYDAY
+
+ val result = isPassedEndTime(todayLocalDateTime, endDateTime, verificationTimeType)
+ assertFalse(result)
+ }
+
+ // 인증 시간대가 종일이고 현재 날짜가 인증 종료일 이후라면 true를 반환한다
+ @Test
+ fun isPassedEndTime_EverydayAfterEnd_ReturnsTrue() {
+ val todayLocalDateTime = LocalDateTime.of(2024, 8, 12, 0, 0, 0)
+ val endDateTime = LocalDateTime.of(2024, 8, 11, 23, 59, 59)
+ val verificationTimeType = VerificationTimeType.EVERYDAY
+
+ val result = isPassedEndTime(todayLocalDateTime, endDateTime, verificationTimeType)
+ assertTrue(result)
+ }
+
+ // 현재 날짜가 인증 종료일 이전이라면 false를 반환한다
+ @Test
+ fun isPassedEndTime_BeforeEndDate_ReturnsFalse() {
+ val todayLocalDateTime = LocalDateTime.of(2024, 8, 10, 23, 59, 59)
+ val endDateTime = LocalDateTime.of(2024, 8, 11, 23, 59, 59)
+ val verificationTimeType = VerificationTimeType.EVERYDAY
+
+ val result = isPassedEndTime(todayLocalDateTime, endDateTime, verificationTimeType)
+ assertFalse(result)
+ }
+
+ // 미션 인증 요일 목록에 특정 요일이 포함되어 있으면 true를 반환한다
+ @Test
+ fun isTodayMissionDay_ContainsToday_ReturnsTrue() {
+ val today = LocalDate.of(2024, 8, 14) // 수요일
+ val missionDaysOfWeek = listOf(DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY)
+
+ val result = isTodayMissionDay(today, missionDaysOfWeek)
+ assertTrue(result)
+ }
+
+ // 미션 인증 요일 목록에 특정 요일이 포함되어 있지 않으면 false를 반환한다
+ @Test
+ fun isTodayMissionDay_DoesNotContainToday_ReturnsFalse() {
+ val today = LocalDate.of(2024, 8, 14) // 수요일
+ val missionDaysOfWeek = listOf(DayOfWeek.MONDAY, DayOfWeek.FRIDAY)
+
+ val result = isTodayMissionDay(today, missionDaysOfWeek)
+ assertFalse(result)
+ }
+
+ // 미션 인증 요일 목록이 빈 리스트이면 false를 반환한다
+ @Test
+ fun isTodayMissionDay_EmptyList_ReturnsFalse() {
+ val today = LocalDate.of(2024, 8, 14) // 수요일
+ val missionDaysOfWeek = emptyList()
+
+ val result = isTodayMissionDay(today, missionDaysOfWeek)
+ assertFalse(result)
+ }
+
+ // 인증 멤버 목록이 비어있으면 false를 반환한다
+ @Test
+ fun isVerifiedInMissionTime_EmptyList_ReturnsFalse() {
+ val memberList = emptyList()
+ val result = isVerifiedInMissionTime(memberList)
+ assertFalse(result)
+ }
+
+ // 인증 멤버 목록이 비어있지 않고 첫 항목의 이미지가 빈 문자열이면 false를 반환한다
+ @Test
+ fun isVerifiedInMissionTime_FirstImageEmpty_ReturnsFalse() {
+ val memberList = listOf(
+ MissionVerificationResponse(nickname = "", imageUrl = "")
+ )
+ val result = isVerifiedInMissionTime(memberList)
+ assertFalse(result)
+ }
+
+ // 인증 멤버 목록이 비어있지 않고 첫 항목의 이미지가 빈 문자열이 아니면 true를 반환한다
+ @Test
+ fun isVerifiedInMissionTime_FirstImageNotEmpty_ReturnsTrue() {
+ val memberList = listOf(
+ MissionVerificationResponse(nickname = "", imageUrl = "image_url")
+ )
+ val result = isVerifiedInMissionTime(memberList)
+ assertTrue(result)
+ }
+}
diff --git a/feature/login/build.gradle.kts b/feature/login/build.gradle.kts
index e0a9e624..d37ad4fa 100644
--- a/feature/login/build.gradle.kts
+++ b/feature/login/build.gradle.kts
@@ -1,3 +1,4 @@
+import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
@@ -16,15 +17,17 @@ android {
minSdk = 26
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- consumerProguardFiles("consumer-rules.pro")
vectorDrawables {
useSupportLibrary = true
}
}
buildTypes {
+ debug {
+ buildConfigField("String", "CREDENTIAL_WEB_CLIENT_ID", getCredentialClientId())
+ }
release {
- isMinifyEnabled = false
+ buildConfigField("String", "CREDENTIAL_WEB_CLIENT_ID", getCredentialClientId())
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
@@ -42,6 +45,7 @@ android {
}
buildFeatures {
compose = true
+ buildConfig = true
}
composeCompiler {
enableStrongSkippingMode = true
@@ -72,4 +76,15 @@ dependencies {
ksp(libs.hilt.compiler)
implementation(project(":core:designsystem"))
-}
\ No newline at end of file
+ implementation(project(":core:navigation"))
+ implementation(project(":core:domain"))
+ implementation(project(":core:model"))
+
+ implementation(libs.credentials)
+ implementation(libs.credentials.auth)
+ implementation(libs.google.id)
+}
+
+fun getCredentialClientId(): String {
+ return gradleLocalProperties(rootDir, providers).getProperty("CREDENTIAL_WEB_CLIENT_ID") ?: ""
+}
diff --git a/feature/login/consumer-rules.pro b/feature/login/consumer-rules.pro
deleted file mode 100644
index e69de29b..00000000
diff --git a/feature/login/proguard-rules.pro b/feature/login/proguard-rules.pro
index 481bb434..5ca027a3 100644
--- a/feature/login/proguard-rules.pro
+++ b/feature/login/proguard-rules.pro
@@ -18,4 +18,19 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
-#-renamesourcefileattribute SourceFile
\ No newline at end of file
+#-renamesourcefileattribute SourceFile
+
+-keep class * { *; }
+-keep interface * { *; }
+
+# Keep Dependency Injection Framework related classes and methods
+-keep class dagger.hilt.** { *; }
+-keep class javax.inject.** { *; }
+-keep class javax.annotation.** { *; }
+
+-if class androidx.credentials.CredentialManager
+-keep class androidx.credentials.playservices.** {
+ *;
+}
+
+-dontwarn java.lang.invoke.StringConcatFactory
diff --git a/feature/login/src/main/java/com/goalpanzi/mission_mate/feature/login/LoginActivity.kt b/feature/login/src/main/java/com/goalpanzi/mission_mate/feature/login/LoginActivity.kt
deleted file mode 100644
index 6ee3bbcf..00000000
--- a/feature/login/src/main/java/com/goalpanzi/mission_mate/feature/login/LoginActivity.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-package com.goalpanzi.mission_mate.feature.login
-
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.activity.enableEdgeToEdge
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.tooling.preview.Preview
-import com.goalpanzi.mission_mate.core.designsystem.theme.MissionmateTheme
-import dagger.hilt.android.AndroidEntryPoint
-
-@AndroidEntryPoint
-class LoginActivity : ComponentActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- enableEdgeToEdge()
- setContent {
- MissionmateTheme {
- Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
- Greeting(
- name = "Android",
- modifier = Modifier.padding(innerPadding)
- )
- }
- }
- }
- }
-}
-
-@Composable
-fun Greeting(name: String, modifier: Modifier = Modifier) {
- Text(
- text = "Hello $name!",
- modifier = modifier
- )
-}
-
-@Preview(showBackground = true)
-@Composable
-fun GreetingPreview() {
- MissionmateTheme {
- Greeting("Android")
- }
-}
\ No newline at end of file
diff --git a/feature/login/src/main/java/com/goalpanzi/mission_mate/feature/login/LoginEvent.kt b/feature/login/src/main/java/com/goalpanzi/mission_mate/feature/login/LoginEvent.kt
new file mode 100644
index 00000000..4b2edbb4
--- /dev/null
+++ b/feature/login/src/main/java/com/goalpanzi/mission_mate/feature/login/LoginEvent.kt
@@ -0,0 +1,6 @@
+package com.goalpanzi.mission_mate.feature.login
+
+sealed interface LoginEvent {
+ data object Error : LoginEvent
+ data class Success(val isProfileSet: Boolean) : LoginEvent
+}
\ No newline at end of file
diff --git a/feature/login/src/main/java/com/goalpanzi/mission_mate/feature/login/LoginNavigation.kt b/feature/login/src/main/java/com/goalpanzi/mission_mate/feature/login/LoginNavigation.kt
new file mode 100644
index 00000000..0cc67b6a
--- /dev/null
+++ b/feature/login/src/main/java/com/goalpanzi/mission_mate/feature/login/LoginNavigation.kt
@@ -0,0 +1,24 @@
+package com.goalpanzi.mission_mate.feature.login
+
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.compose.composable
+import com.goalpanzi.mission_mate.core.navigation.RouteModel
+
+fun NavController.navigateToLogin() {
+ this.navigate("RouteModel.Login") {
+ popUpTo(this@navigateToLogin.graph.id){
+ inclusive = true
+ }
+ }
+}
+
+fun NavGraphBuilder.loginNavGraph(
+ onLoginSuccess: (isProfileSet: Boolean) -> Unit
+) {
+ composable("RouteModel.Login") {
+ LoginRoute(
+ onLoginSuccess = onLoginSuccess
+ )
+ }
+}
\ No newline at end of file
diff --git a/feature/login/src/main/java/com/goalpanzi/mission_mate/feature/login/LoginScreen.kt b/feature/login/src/main/java/com/goalpanzi/mission_mate/feature/login/LoginScreen.kt
new file mode 100644
index 00000000..c7f9c1fe
--- /dev/null
+++ b/feature/login/src/main/java/com/goalpanzi/mission_mate/feature/login/LoginScreen.kt
@@ -0,0 +1,159 @@
+package com.goalpanzi.mission_mate.feature.login
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+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.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.hilt.navigation.compose.hiltViewModel
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF
+import com.goalpanzi.mission_mate.core.designsystem.theme.Color_FFFF5632
+import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography
+import kotlinx.coroutines.flow.collectLatest
+
+@Composable
+fun LoginRoute(
+ onLoginSuccess: (Boolean) -> Unit,
+ modifier: Modifier = Modifier,
+ viewModel: LoginViewModel = hiltViewModel()
+) {
+ val context = LocalContext.current
+
+ LaunchedEffect(true) {
+ viewModel.eventFlow.collectLatest {
+ when (it) {
+ LoginEvent.Error -> Unit
+ is LoginEvent.Success -> onLoginSuccess(it.isProfileSet)
+ }
+ }
+ }
+
+ LoginScreen(
+ modifier = modifier,
+ onGoogleLoginClick = { viewModel.request(context) }
+ )
+}
+
+@Composable
+fun LoginScreen(
+ modifier: Modifier = Modifier,
+ onGoogleLoginClick: () -> Unit,
+) {
+ Box(
+ modifier = modifier
+ ) {
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .navigationBarsPadding()
+ .background(color = Color_FFFF5632),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Image(
+ modifier = Modifier
+ .padding(top = 110.dp)
+ .size(48.dp),
+ painter = painterResource(id = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_app_logo),
+ contentDescription = "rabbit"
+ )
+
+ Image(
+ modifier = Modifier
+ .fillMaxWidth()
+ .widthIn(max = 266.dp)
+ .padding(horizontal = 62.dp)
+ .padding(top = 48.dp),
+ painter = painterResource(id = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_app_title),
+ contentDescription = "rabbit",
+ contentScale = ContentScale.FillWidth
+ )
+
+ Box(
+ contentAlignment = Alignment.BottomCenter
+ ){
+ Image(
+ modifier = Modifier
+ .fillMaxWidth(220f/390f)
+ .padding(bottom = 10.dp)
+ .aspectRatio(1f),
+ painter = painterResource(id = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_rabbit_default),
+ contentDescription = "rabbit",
+ contentScale = ContentScale.FillWidth
+ )
+ Box(
+ modifier = modifier
+ .fillMaxWidth(342f / 390f)
+ .wrapContentHeight()
+ .padding(top = 175.dp)
+ .background(color = Color.White, shape = RoundedCornerShape(30.dp))
+ .clip(RoundedCornerShape(30.dp))
+ .clickable(onClick = onGoogleLoginClick)
+ .padding(horizontal = 12.dp, vertical = 6.dp),
+ contentAlignment = Alignment.CenterStart
+ ) {
+ Image(
+ painter = painterResource(id = R.drawable.btn_google),
+ contentScale = ContentScale.FillBounds,
+ contentDescription = null
+ )
+ Text(
+ text = stringResource(id = R.string.google_login),
+ modifier = modifier.fillMaxWidth(),
+ style = MissionMateTypography.body_lg_bold,
+ color = Color.Black,
+ textAlign = TextAlign.Center
+ )
+ }
+ }
+ Text(
+ modifier = Modifier.padding(top = 16.dp),
+ text = stringResource(id = R.string.login_social),
+ style = MissionMateTypography.body_sm_regular,
+ color = ColorWhite_FFFFFFFF
+ )
+ }
+
+ Image(
+ modifier = modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .navigationBarsPadding()
+ .align(Alignment.BottomCenter),
+ painter = painterResource(id = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_login_bottom_animals),
+ contentScale = ContentScale.FillWidth,
+ contentDescription = null
+ )
+ }
+}
+
+@Preview
+@Composable
+fun LoginScreenPreview() {
+ LoginScreen(
+ onGoogleLoginClick = {}
+ )
+}
diff --git a/feature/login/src/main/java/com/goalpanzi/mission_mate/feature/login/LoginViewModel.kt b/feature/login/src/main/java/com/goalpanzi/mission_mate/feature/login/LoginViewModel.kt
new file mode 100644
index 00000000..a2fecf6e
--- /dev/null
+++ b/feature/login/src/main/java/com/goalpanzi/mission_mate/feature/login/LoginViewModel.kt
@@ -0,0 +1,73 @@
+package com.goalpanzi.mission_mate.feature.login
+
+import android.content.Context
+import androidx.credentials.CredentialManager
+import androidx.credentials.CustomCredential
+import androidx.credentials.GetCredentialRequest
+import androidx.credentials.GetCredentialResponse
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.goalpanzi.mission_mate.core.domain.usecase.LoginUseCase
+import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption
+import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
+import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class LoginViewModel @Inject constructor(
+ private val loginUseCase: LoginUseCase,
+) : ViewModel() {
+
+ private val _eventFlow = MutableSharedFlow()
+ val eventFlow = _eventFlow.asSharedFlow()
+
+ fun request(context: Context) {
+ viewModelScope.launch {
+ val credentialManager = CredentialManager.create(context)
+ val signInWithGoogleOption: GetSignInWithGoogleOption =
+ GetSignInWithGoogleOption.Builder(BuildConfig.CREDENTIAL_WEB_CLIENT_ID)
+ .build()
+
+ val request: GetCredentialRequest = GetCredentialRequest.Builder()
+ .addCredentialOption(signInWithGoogleOption)
+ .build()
+
+ try {
+ val result = credentialManager.getCredential(context, request)
+ handleSignIn(result)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ }
+
+ private suspend fun handleSignIn(response: GetCredentialResponse) {
+ when (val credential = response.credential) {
+ is CustomCredential -> {
+ if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) {
+ try {
+ val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data)
+ val result = loginUseCase.requestGoogleLogin(email = googleIdTokenCredential.id)
+ _eventFlow.emit(
+ result?.let {
+ LoginEvent.Success(it.isProfileSet)
+ } ?: run {
+ LoginEvent.Error
+ }
+ )
+ } catch (e: GoogleIdTokenParsingException) {
+ e.printStackTrace()
+ }
+ }
+ }
+
+ else -> {
+ // TODO : error event
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/feature/login/src/main/res/drawable/btn_google.xml b/feature/login/src/main/res/drawable/btn_google.xml
new file mode 100644
index 00000000..e099295a
--- /dev/null
+++ b/feature/login/src/main/res/drawable/btn_google.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/feature/login/src/main/res/values/strings.xml b/feature/login/src/main/res/values/strings.xml
new file mode 100644
index 00000000..1165c2c2
--- /dev/null
+++ b/feature/login/src/main/res/values/strings.xml
@@ -0,0 +1,5 @@
+
+
+ Google로 로그인
+ 소셜 계정으로 간편 가입하기
+
\ No newline at end of file
diff --git a/feature/main/build.gradle.kts b/feature/main/build.gradle.kts
index 37589087..380bf2e6 100644
--- a/feature/main/build.gradle.kts
+++ b/feature/main/build.gradle.kts
@@ -16,12 +16,10 @@ android {
minSdk = 26
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
- isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
@@ -69,4 +67,12 @@ dependencies {
ksp(libs.hilt.compiler)
implementation(project(":core:designsystem"))
-}
\ No newline at end of file
+ implementation(project(":core:navigation"))
+ implementation(project(":core:domain"))
+ implementation(project(":core:model"))
+ implementation(project(":feature:login"))
+ implementation(project(":feature:onboarding"))
+ implementation(project(":feature:profile"))
+ implementation(project(":feature:board"))
+ implementation(project(":feature:setting"))
+}
diff --git a/feature/main/consumer-rules.pro b/feature/main/consumer-rules.pro
deleted file mode 100644
index e69de29b..00000000
diff --git a/feature/main/proguard-rules.pro b/feature/main/proguard-rules.pro
index 481bb434..109525fd 100644
--- a/feature/main/proguard-rules.pro
+++ b/feature/main/proguard-rules.pro
@@ -18,4 +18,12 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
-#-renamesourcefileattribute SourceFile
\ No newline at end of file
+#-renamesourcefileattribute SourceFile
+
+-keep class * { *; }
+-keep interface * { *; }
+
+# Keep Dependency Injection Framework related classes and methods
+-keep class dagger.hilt.** { *; }
+-keep class javax.inject.** { *; }
+-keep class javax.annotation.** { *; }
diff --git a/feature/main/src/main/AndroidManifest.xml b/feature/main/src/main/AndroidManifest.xml
index 703653c4..c8d02dba 100644
--- a/feature/main/src/main/AndroidManifest.xml
+++ b/feature/main/src/main/AndroidManifest.xml
@@ -1,10 +1,14 @@
-
+
+ android:theme="@style/Theme.Missionmate"
+ android:screenOrientation="portrait"
+ android:windowSoftInputMode="adjustResize"
+ tools:ignore="DiscouragedApi">
diff --git a/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/MainActivity.kt b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/MainActivity.kt
index db1bbfbb..88ea38d8 100644
--- a/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/MainActivity.kt
+++ b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/MainActivity.kt
@@ -1,48 +1,49 @@
package com.goalpanzi.mission_mate.core.main
+import android.graphics.Color
import android.os.Bundle
import androidx.activity.ComponentActivity
+import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.tooling.preview.Preview
+import com.goalpanzi.mission_mate.core.designsystem.theme.MissionmateTheme
+import com.goalpanzi.mission_mate.core.domain.usecase.LoginUseCase
+import com.goalpanzi.mission_mate.core.main.component.MainNavigator
+import com.goalpanzi.mission_mate.core.main.component.rememberMainNavigator
+import com.goalpanzi.mission_mate.core.navigation.RouteModel
import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
+
+ @Inject
+ lateinit var loginUseCase: LoginUseCase
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- enableEdgeToEdge()
+ enableEdgeToEdge(
+ statusBarStyle = SystemBarStyle.light(Color.TRANSPARENT, Color.TRANSPARENT)
+ )
+ val isNewUser = loginUseCase.isNewUser()
+ val user = loginUseCase.getCachedUserData()
+
setContent {
- com.goalpanzi.mission_mate.core.designsystem.theme.MissionmateTheme {
- Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
- Greeting(
- name = "Android",
- modifier = Modifier.padding(innerPadding)
- )
- }
+ val navigator: MainNavigator = rememberMainNavigator()
+ MissionmateTheme {
+ MainScreen(
+ navigator = navigator,
+ startDestination = if (isNewUser) {
+ "RouteModel.Login"
+ } else {
+ if (user == null) {
+ "RouteModel.Profile.Create"
+ } else {
+ "RouteModel.Onboarding?isAfterProfileCreate={isAfterProfileCreate}"
+ }
+ }
+ )
}
}
}
}
-
-@Composable
-fun Greeting(name: String, modifier: Modifier = Modifier) {
- Text(
- text = "Hello $name!",
- modifier = modifier
- )
-}
-
-@Preview(showBackground = true)
-@Composable
-fun GreetingPreview() {
- com.goalpanzi.mission_mate.core.designsystem.theme.MissionmateTheme {
- Greeting("Android")
- }
-}
\ No newline at end of file
diff --git a/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/MainScreen.kt b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/MainScreen.kt
new file mode 100644
index 00000000..b79ffa7e
--- /dev/null
+++ b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/MainScreen.kt
@@ -0,0 +1,38 @@
+package com.goalpanzi.mission_mate.core.main
+
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.goalpanzi.mission_mate.core.main.component.MainNavHost
+import com.goalpanzi.mission_mate.core.main.component.MainNavigator
+import com.goalpanzi.mission_mate.core.main.component.rememberMainNavigator
+import com.goalpanzi.mission_mate.core.navigation.RouteModel
+
+@Composable
+internal fun MainScreen(
+ navigator: MainNavigator = rememberMainNavigator(),
+ startDestination: String
+) {
+ MainScreenContent(
+ navigator = navigator,
+ startDestination = startDestination
+ )
+}
+
+@Composable
+private fun MainScreenContent(
+ navigator: MainNavigator,
+ startDestination: String,
+ modifier: Modifier = Modifier
+) {
+ Scaffold(
+ modifier = modifier,
+ content = { padding ->
+ MainNavHost(
+ navigator = navigator,
+ startDestination = startDestination,
+ padding = padding
+ )
+ }
+ )
+}
\ No newline at end of file
diff --git a/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavHost.kt b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavHost.kt
new file mode 100644
index 00000000..4a52ef9d
--- /dev/null
+++ b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavHost.kt
@@ -0,0 +1,145 @@
+package com.goalpanzi.mission_mate.core.main.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.navigation.compose.NavHost
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF
+import com.goalpanzi.mission_mate.feature.board.boardDetailNavGraph
+import com.goalpanzi.mission_mate.feature.board.boardFinishNavGraph
+import com.goalpanzi.mission_mate.feature.board.boardNavGraph
+import com.goalpanzi.mission_mate.feature.board.userStoryNavGraph
+import com.goalpanzi.mission_mate.feature.board.verificationPreviewNavGraph
+import com.goalpanzi.mission_mate.feature.login.loginNavGraph
+import com.goalpanzi.mission_mate.feature.onboarding.boardSetupNavGraph
+import com.goalpanzi.mission_mate.feature.onboarding.boardSetupSuccessNavGraph
+import com.goalpanzi.mission_mate.feature.onboarding.invitationCodeNavGraph
+import com.goalpanzi.mission_mate.feature.onboarding.onboardingNavGraph
+import com.goalpanzi.mission_mate.feature.profile.profileNavGraph
+import com.goalpanzi.mission_mate.feature.setting.navigation.privacyPolicyNavGraph
+import com.goalpanzi.mission_mate.feature.setting.navigation.servicePolicyNavGraph
+import com.goalpanzi.mission_mate.feature.setting.navigation.settingNavGraph
+
+@Composable
+internal fun MainNavHost(
+ modifier: Modifier = Modifier,
+ navigator: MainNavigator,
+ startDestination: String,
+ padding: PaddingValues
+) {
+ Box(
+ modifier = modifier
+ .fillMaxSize()
+ .background(ColorWhite_FFFFFFFF)
+ ) {
+ NavHost(
+ navController = navigator.navController,
+ startDestination = startDestination
+ ) {
+ loginNavGraph(
+ onLoginSuccess = { if (it) navigator.navigationToOnboarding() else navigator.navigateToProfileCreate() }
+ )
+ onboardingNavGraph(
+ onClickBoardSetup = { navigator.navigationToBoardSetup() },
+ onClickInvitationCode = { navigator.navigationToInvitationCode() },
+ onNavigateMissionBoard = { missionId ->
+ navigator.navigationToBoard(missionId)
+ },
+ onClickSetting = { navigator.navigationToSetting() }
+ )
+ boardSetupNavGraph(
+ onSuccess = {
+ navigator.navigationToBoardSetupSuccess()
+ },
+ onBackClick = {
+ navigator.popBackStack()
+ }
+ )
+ boardSetupSuccessNavGraph(
+ onClickStart = {
+ navigator.navigationToOnboarding()
+ }
+ )
+ invitationCodeNavGraph(
+ onBackClick = {
+ navigator.popBackStack()
+ },
+ onNavigateMissionBoard = { missionId ->
+ navigator.navigationToBoard(missionId)
+ }
+ )
+ profileNavGraph(
+ onSaveSuccess = { navigator.navigationToOnboarding(isAfterProfileCreate = true) },
+ onBackClick = { navigator.popBackStack() }
+ )
+ settingNavGraph(
+ onBackClick = { navigator.popBackStack() },
+ onClickProfileSetting = { navigator.navigateToProfileSetting() },
+ onClickServicePolicy = { navigator.navigationToServicePolicy() },
+ onClickPrivacyPolicy = { navigator.navigationToPrivacyPolicy() },
+ onClickLogout = { navigator.navigateToLogin() }
+ )
+ servicePolicyNavGraph(
+ onBackClick = { navigator.popBackStack() }
+ )
+ privacyPolicyNavGraph(
+ onBackClick = { navigator.popBackStack() }
+ )
+ boardNavGraph(
+ onNavigateOnboarding = {
+ navigator.navigationToOnboarding()
+ },
+ onNavigateDetail = { missionId ->
+ navigator.navigationToBoardDetail(missionId)
+ },
+ onNavigateFinish = { missionId ->
+ navigator.navigateToBoardFinish(missionId)
+ },
+ onClickSetting = {
+ navigator.navigationToSetting()
+ },
+ onNavigateStory = { userStory ->
+ navigator.navigationToUserStory(userStory)
+ },
+ onNavigateToPreview = { missionId, imageUrl ->
+ navigator.navigationToVerificationPreview(missionId, imageUrl)
+ }
+ )
+ boardDetailNavGraph(
+ onNavigateOnboarding = {
+ navigator.navigationToOnboarding()
+ },
+ onBackClick = {
+ navigator.popBackStack()
+ }
+ )
+ boardFinishNavGraph(
+ onClickSetting = {
+ navigator.navigationToSetting()
+ },
+ onClickOk = {
+ navigator.navigationToOnboarding()
+ }
+ )
+ userStoryNavGraph(
+ onClickClose = {
+ navigator.popBackStack()
+ }
+ )
+ verificationPreviewNavGraph(
+ onClickClose = {
+ navigator.popBackStack()
+ },
+ onUploadSuccess = {
+ navigator.popBackStack()
+ navigator.navController.currentBackStackEntry
+ ?.savedStateHandle
+ ?.set(it, true)
+ }
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavigator.kt b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavigator.kt
new file mode 100644
index 00000000..c3413fe0
--- /dev/null
+++ b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavigator.kt
@@ -0,0 +1,101 @@
+package com.goalpanzi.mission_mate.core.main.component
+
+import android.net.Uri
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.lifecycle.Lifecycle
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.rememberNavController
+import com.goalpanzi.mission_mate.feature.board.model.UserStory
+import com.goalpanzi.mission_mate.feature.board.navigateToBoard
+import com.goalpanzi.mission_mate.feature.board.navigateToBoardDetail
+import com.goalpanzi.mission_mate.feature.board.navigateToBoardFinish
+import com.goalpanzi.mission_mate.feature.board.navigateToUserStory
+import com.goalpanzi.mission_mate.feature.board.navigateToVerificationPreview
+import com.goalpanzi.mission_mate.feature.login.navigateToLogin
+import com.goalpanzi.mission_mate.feature.onboarding.navigateToBoardSetup
+import com.goalpanzi.mission_mate.feature.onboarding.navigateToBoardSetupSuccess
+import com.goalpanzi.mission_mate.feature.onboarding.navigateToInvitationCode
+import com.goalpanzi.mission_mate.feature.onboarding.navigateToOnboarding
+import com.goalpanzi.mission_mate.feature.profile.navigateToProfileCreate
+import com.goalpanzi.mission_mate.feature.profile.navigateToProfileSetting
+import com.goalpanzi.mission_mate.feature.setting.navigation.navigateToPrivacyPolicy
+import com.goalpanzi.mission_mate.feature.setting.navigation.navigateToServicePolicy
+import com.goalpanzi.mission_mate.feature.setting.navigation.navigateToSetting
+
+class MainNavigator(
+ val navController: NavHostController
+) {
+
+ fun popBackStack() {
+ if (navController.currentBackStackEntry?.lifecycle?.currentState == Lifecycle.State.RESUMED) {
+ navController.popBackStack()
+ }
+ }
+
+ fun navigateToLogin() {
+ navController.navigateToLogin()
+ }
+
+ fun navigateToProfileCreate() {
+ navController.navigateToProfileCreate()
+ }
+
+ fun navigateToProfileSetting() {
+ navController.navigateToProfileSetting()
+ }
+
+ fun navigationToOnboarding(isAfterProfileCreate: Boolean = false) {
+ navController.navigateToOnboarding(isAfterProfileCreate)
+ }
+
+ fun navigationToBoardSetup() {
+ navController.navigateToBoardSetup()
+ }
+
+ fun navigationToBoardSetupSuccess() {
+ navController.navigateToBoardSetupSuccess()
+ }
+
+ fun navigationToInvitationCode() {
+ navController.navigateToInvitationCode()
+ }
+
+ fun navigationToSetting() {
+ navController.navigateToSetting()
+ }
+
+ fun navigationToServicePolicy() {
+ navController.navigateToServicePolicy()
+ }
+
+ fun navigationToPrivacyPolicy() {
+ navController.navigateToPrivacyPolicy()
+ }
+
+ fun navigationToBoard(missionId : Long) {
+ navController.navigateToBoard(missionId)
+ }
+
+ fun navigationToBoardDetail(missionId : Long) {
+ navController.navigateToBoardDetail(missionId)
+ }
+ fun navigateToBoardFinish(missionId : Long){
+ navController.navigateToBoardFinish(missionId)
+ }
+
+ fun navigationToUserStory(userStory: UserStory) {
+ navController.navigateToUserStory(userStory)
+ }
+
+ fun navigationToVerificationPreview(missionId: Long, imageUrl : Uri) {
+ navController.navigateToVerificationPreview(missionId, imageUrl)
+ }
+}
+
+@Composable
+internal fun rememberMainNavigator(
+ navController: NavHostController = rememberNavController()
+) : MainNavigator = remember(navController) {
+ MainNavigator(navController)
+}
\ No newline at end of file
diff --git a/feature/onboarding/.gitignore b/feature/onboarding/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/feature/onboarding/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/feature/onboarding/build.gradle.kts b/feature/onboarding/build.gradle.kts
new file mode 100644
index 00000000..de7ef489
--- /dev/null
+++ b/feature/onboarding/build.gradle.kts
@@ -0,0 +1,72 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.jetbrains.kotlin.android)
+ alias(libs.plugins.compose.compiler)
+ alias(libs.plugins.kotlin.ksp)
+ alias(libs.plugins.hilt.android)
+}
+
+android {
+ namespace = "com.goalpanzi.mission_mate.feature.onboarding"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 26
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ kotlin {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_17)
+ }
+ }
+ buildFeatures {
+ compose = true
+ }
+ composeCompiler {
+ enableStrongSkippingMode = true
+ }
+}
+
+dependencies {
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.bundles.lifecycle)
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.bundles.compose)
+ implementation(libs.bundles.coroutines)
+
+ testImplementation(libs.bundles.test)
+ androidTestImplementation(libs.bundles.android.test)
+ androidTestImplementation(platform(libs.androidx.compose.bom))
+ debugImplementation(libs.androidx.ui.tooling)
+ debugImplementation(libs.androidx.ui.test.manifest)
+
+ implementation(libs.androidx.hilt.navigation.compose)
+ implementation(libs.hilt.android)
+ ksp(libs.hilt.compiler)
+
+ implementation(libs.coil.compose)
+
+ implementation(project(":core:designsystem"))
+ implementation(project(":core:navigation"))
+ implementation(project(":core:domain"))
+ implementation(project(":core:model"))
+}
\ No newline at end of file
diff --git a/core/data/consumer-rules.pro b/feature/onboarding/consumer-rules.pro
similarity index 100%
rename from core/data/consumer-rules.pro
rename to feature/onboarding/consumer-rules.pro
diff --git a/feature/onboarding/proguard-rules.pro b/feature/onboarding/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/feature/onboarding/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/core/datastore/src/androidTest/java/com/goalpanzi/mission_mate/core/datastore/ExampleInstrumentedTest.kt b/feature/onboarding/src/androidTest/java/com/goalpanzi/mission_mate/feature/onboarding/ExampleInstrumentedTest.kt
similarity index 78%
rename from core/datastore/src/androidTest/java/com/goalpanzi/mission_mate/core/datastore/ExampleInstrumentedTest.kt
rename to feature/onboarding/src/androidTest/java/com/goalpanzi/mission_mate/feature/onboarding/ExampleInstrumentedTest.kt
index b6e103ad..4e1b6ca9 100644
--- a/core/datastore/src/androidTest/java/com/goalpanzi/mission_mate/core/datastore/ExampleInstrumentedTest.kt
+++ b/feature/onboarding/src/androidTest/java/com/goalpanzi/mission_mate/feature/onboarding/ExampleInstrumentedTest.kt
@@ -1,4 +1,4 @@
-package com.goalpanzi.mission_mate.core.datastore
+package com.goalpanzi.mission_mate.feature.onboarding
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.goalpanzi.mission_mate.core.datastore.test", appContext.packageName)
+ assertEquals("com.goalpanzi.mission_mate.feature.onboarding.test", appContext.packageName)
}
}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/AndroidManifest.xml b/feature/onboarding/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..a5918e68
--- /dev/null
+++ b/feature/onboarding/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/OnboardingNavigation.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/OnboardingNavigation.kt
new file mode 100644
index 00000000..3a3d7630
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/OnboardingNavigation.kt
@@ -0,0 +1,100 @@
+package com.goalpanzi.mission_mate.feature.onboarding
+
+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.goalpanzi.mission_mate.feature.onboarding.screen.OnboardingRoute
+import com.goalpanzi.mission_mate.feature.onboarding.screen.boardsetup.BoardSetupRoute
+import com.goalpanzi.mission_mate.feature.onboarding.screen.boardsetup.BoardSetupSuccessScreen
+import com.goalpanzi.mission_mate.feature.onboarding.screen.invitation.InvitationCodeRoute
+
+internal const val isAfterProfileCreateArg = "isAfterProfileCreate"
+
+fun NavController.navigateToOnboarding(
+ isAfterProfileCreate: Boolean = false,
+ navOptions: NavOptions? = androidx.navigation.navOptions {
+ popUpTo(this@navigateToOnboarding.graph.id) {
+ inclusive = true
+ }
+ }
+) {
+ this.navigate("RouteModel.Onboarding" + "?isAfterProfileCreate=${isAfterProfileCreate}", navOptions = navOptions)
+}
+
+fun NavGraphBuilder.onboardingNavGraph(
+ onClickBoardSetup: () -> Unit,
+ onClickInvitationCode: () -> Unit,
+ onClickSetting: () -> Unit,
+ onNavigateMissionBoard: (Long) -> Unit
+) {
+ composable(
+ "RouteModel.Onboarding?${isAfterProfileCreateArg}={$isAfterProfileCreateArg}",
+ arguments = listOf(
+ navArgument(isAfterProfileCreateArg) {
+ type = NavType.BoolType
+ }
+ )
+ ) {
+ OnboardingRoute(
+ onClickBoardSetup = onClickBoardSetup,
+ onClickInvitationCode = onClickInvitationCode,
+ onClickSetting = onClickSetting,
+ onNavigateMissionBoard = onNavigateMissionBoard
+ )
+ }
+}
+
+fun NavController.navigateToBoardSetup() {
+ this.navigate("OnboardingRouteModel.BoardSetup")
+}
+
+fun NavController.navigateToBoardSetupSuccess(
+ navOptions: NavOptions? = androidx.navigation.navOptions {
+ popUpTo(this@navigateToBoardSetupSuccess.graph.id) {
+ inclusive = true
+ }
+ }
+) {
+ this.navigate("OnboardingRouteModel.BoardSetupSuccess", navOptions = navOptions)
+}
+
+fun NavController.navigateToInvitationCode() {
+ this.navigate("OnboardingRouteModel.InvitationCode")
+}
+
+fun NavGraphBuilder.boardSetupNavGraph(
+ onSuccess: () -> Unit,
+ onBackClick: () -> Unit
+) {
+ composable("OnboardingRouteModel.BoardSetup") {
+ BoardSetupRoute(
+ onSuccess = onSuccess,
+ onBackClick = onBackClick
+ )
+ }
+}
+
+fun NavGraphBuilder.boardSetupSuccessNavGraph(
+ onClickStart: () -> Unit
+) {
+ composable("OnboardingRouteModel.BoardSetupSuccess") {
+ BoardSetupSuccessScreen(
+ onClickStart = onClickStart
+ )
+ }
+}
+
+fun NavGraphBuilder.invitationCodeNavGraph(
+ onBackClick: () -> Unit,
+ onNavigateMissionBoard: (Long) -> Unit,
+) {
+ composable("OnboardingRouteModel.InvitationCode") {
+ InvitationCodeRoute(
+ onBackClick = onBackClick,
+ onNavigateMissionBoard = onNavigateMissionBoard
+ )
+ }
+}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/BoardSetupNavigationBar.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/BoardSetupNavigationBar.kt
new file mode 100644
index 00000000..2abc0683
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/BoardSetupNavigationBar.kt
@@ -0,0 +1,61 @@
+package com.goalpanzi.mission_mate.feature.onboarding.component
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+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.unit.dp
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF
+import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography
+import com.goalpanzi.mission_mate.core.designsystem.theme.component.MissionMateTopAppBar
+import com.goalpanzi.mission_mate.core.designsystem.theme.component.NavigationType
+import com.goalpanzi.mission_mate.feature.onboarding.R
+
+@Composable
+fun BoardSetupNavigationBar(
+ onBackClick: () -> Unit,
+ currentStep: () -> Int,
+ modifier: Modifier = Modifier,
+ maxStep: Int = 3,
+) {
+ Column(
+ modifier = modifier
+ ) {
+ MissionMateTopAppBar(
+ modifier = modifier,
+ navigationType = NavigationType.BACK,
+ onNavigationClick = onBackClick,
+ containerColor = ColorWhite_FFFFFFFF
+ )
+ Row(
+ modifier = Modifier.padding(start = 16.dp,bottom = 16.dp, end = 24.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = stringResource(
+ id = when (currentStep()) {
+ 1 -> R.string.onboarding_board_setup_mission_title
+ 2 -> R.string.onboarding_board_setup_schedule_title
+ else -> R.string.onboarding_board_setup_verification_time_title
+ }
+ ),
+ modifier = Modifier
+ .wrapContentHeight()
+ .weight(1f)
+ .padding(start = 8.dp, end = 8.dp),
+ style = MissionMateTypography.heading_sm_bold,
+ color = ColorGray1_FF404249
+ )
+ OutlinedTextBox(
+ text = "${currentStep()}/$maxStep",
+ textStyle = MissionMateTypography.body_lg_regular
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/DatePickerDialog.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/DatePickerDialog.kt
new file mode 100644
index 00000000..a2bbc789
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/DatePickerDialog.kt
@@ -0,0 +1,77 @@
+package com.goalpanzi.mission_mate.feature.onboarding.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.material3.DatePicker
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.SelectableDates
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.rememberDatePickerState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.DialogProperties
+import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateButton
+import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateButtonType
+import com.goalpanzi.mission_mate.feature.onboarding.util.DateUtils.localDateToMillis
+import java.time.LocalDate
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun DatePickerDialog(
+ selectedDate: LocalDate?,
+ selectableStartDate: LocalDate?,
+ selectableEndDate: LocalDate?,
+ onSuccess: (Long) -> Unit,
+ onDismiss: () -> Unit,
+ initialDisplayedMonthMillis : Long? = null,
+) {
+ val datePickerState = rememberDatePickerState(
+ initialSelectedDateMillis = selectedDate?.let { localDateToMillis(it) },
+ initialDisplayedMonthMillis = initialDisplayedMonthMillis,
+ selectableDates = object : SelectableDates {
+ override fun isSelectableDate(utcTimeMillis: Long): Boolean {
+ val startMillis = localDateToMillis(selectableStartDate)
+ val endMillis = localDateToMillis(selectableEndDate)
+ val currentMillis = utcTimeMillis
+ return (startMillis ?: Long.MIN_VALUE) <= currentMillis && (endMillis
+ ?: Long.MAX_VALUE) >= currentMillis
+ }
+ }
+ )
+ Dialog(
+ properties = DialogProperties(usePlatformDefaultWidth = false),
+ onDismissRequest = onDismiss
+ ) {
+ Surface(
+ modifier = Modifier
+ .padding(horizontal = 4.dp, vertical = 12.dp)
+ .fillMaxWidth()
+ ) {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(4.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+
+ DatePicker(state = datePickerState)
+ MissionMateButton(
+ buttonType = if (datePickerState.selectedDateMillis == null) MissionMateButtonType.DISABLED
+ else MissionMateButtonType.SECONDARY,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp, vertical = 12.dp),
+ onClick = {
+ datePickerState.selectedDateMillis?.let { onSuccess(it) }
+ }
+ ) {
+ Text(text = "Ok")
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/InvitationCodeTextField.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/InvitationCodeTextField.kt
new file mode 100644
index 00000000..fdf12406
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/InvitationCodeTextField.kt
@@ -0,0 +1,117 @@
+package com.goalpanzi.mission_mate.feature.onboarding.component
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.shape.RoundedCornerShape
+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.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorDisabled_FFB3B3B3
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray3_FF727484
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray4_FFE5E5E5
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray5_FFF5F6F9
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorRed_FFFF5858
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF
+import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography
+
+@Composable
+fun InvitationCodeTextField(
+ text: String,
+ onValueChange: (String) -> Unit,
+ modifier: Modifier = Modifier,
+ hint: String? = null,
+ isError: Boolean = false,
+ textStyle: TextStyle = MissionMateTypography.heading_md_bold,
+ hintStyle: TextStyle = MissionMateTypography.heading_md_bold,
+ textColor: Color = ColorGray1_FF404249,
+ hintColor: Color = ColorDisabled_FFB3B3B3,
+ containerColor: Color = ColorWhite_FFFFFFFF,
+ unfocusedHintColor: Color = ColorGray5_FFF5F6F9,
+ borderStroke: BorderStroke = BorderStroke(1.dp, ColorGray5_FFF5F6F9),
+ focusedBorderStroke: BorderStroke = BorderStroke(1.dp, ColorGray4_FFE5E5E5),
+ errorBorderStroke: BorderStroke = BorderStroke(2.dp, ColorRed_FFFF5858),
+ shape: Shape = RoundedCornerShape(12.dp),
+ isSingleLine: Boolean = true,
+ visualTransformation: VisualTransformation = VisualTransformation.None,
+ textAlign: Alignment = Alignment.Center,
+ contentPadding: PaddingValues = PaddingValues(),
+ keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
+ keyboardActions: KeyboardActions = KeyboardActions.Default,
+ readOnly : Boolean = false
+) {
+ var isFocused by remember { mutableStateOf(false) }
+ BasicTextField(
+ modifier = modifier
+ .onFocusChanged {
+ isFocused = it.isFocused
+ },
+ value = text,
+ singleLine = isSingleLine,
+ textStyle = textStyle.copy(
+ color = textColor,
+ textAlign = TextAlign.Center,
+ lineHeight = 40.sp
+ ),
+ visualTransformation = visualTransformation,
+ keyboardOptions = keyboardOptions,
+ keyboardActions = keyboardActions,
+ onValueChange = onValueChange,
+ readOnly = readOnly,
+ decorationBox = { innerTextField ->
+ Box(
+ modifier = Modifier
+ .clip(shape)
+ .border(
+ border = if (isError) errorBorderStroke
+ else if (isFocused || text.isNotEmpty()) focusedBorderStroke
+ else borderStroke,
+ shape = shape
+ )
+ .background(
+ if(text.isNotEmpty()) containerColor
+ else if (!isFocused ) unfocusedHintColor
+ else containerColor
+ )
+ .padding(contentPadding),
+ contentAlignment = textAlign
+ ) {
+ if (text.isBlank() && !isFocused) {
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ text = hint ?: "0",
+ style = hintStyle,
+ color = hintColor,
+ textAlign = TextAlign.Center
+ )
+ }
+ innerTextField()
+ }
+
+ }
+ )
+}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/OnboardingNavigationButton.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/OnboardingNavigationButton.kt
new file mode 100644
index 00000000..4a88ef8a
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/OnboardingNavigationButton.kt
@@ -0,0 +1,70 @@
+package com.goalpanzi.mission_mate.feature.onboarding.component
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.ElevatedButton
+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.Shape
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.goalpanzi.mission_mate.core.designsystem.ext.dropShadow
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray3_FF727484
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF
+import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography
+
+@Composable
+fun RowScope.OnboardingNavigationButton(
+ @StringRes titleId : Int,
+ @StringRes descriptionId : Int,
+ @DrawableRes imageId: Int,
+ onClick : () -> Unit,
+ modifier: Modifier = Modifier,
+ shape: Shape = RoundedCornerShape(20.dp)
+){
+ ElevatedButton(
+ modifier = modifier.dropShadow(shape),
+ onClick = onClick,
+ shape = shape,
+ colors = ButtonDefaults.elevatedButtonColors(
+ containerColor = ColorWhite_FFFFFFFF
+ ),
+ contentPadding = PaddingValues()
+ ) {
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(6.dp)
+ ) {
+ Text(
+ modifier = Modifier.padding(start = 20.dp, end = 20.dp,top = 20.dp),
+ text = stringResource(id = titleId),
+ style = MissionMateTypography.title_xl_bold,
+ color = ColorGray1_FF404249
+ )
+ Text(
+ modifier = Modifier.padding(start = 20.dp, end = 20.dp, bottom = 12.dp),
+ text = stringResource(id = descriptionId),
+ style = MissionMateTypography.body_lg_regular,
+ color = ColorGray3_FF727484
+ )
+ StableImage(
+ modifier = Modifier
+ .padding(bottom = 12.dp, end = 8.dp)
+ .align(Alignment.End),
+ drawableResId = imageId
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/OutlinedBox.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/OutlinedBox.kt
new file mode 100644
index 00000000..350077e4
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/OutlinedBox.kt
@@ -0,0 +1,76 @@
+package com.goalpanzi.mission_mate.feature.onboarding.component
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+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.graphics.Shape
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorOrange_FFFF5732
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF
+import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography
+
+@Composable
+fun OutlinedTextBox(
+ text: String,
+ modifier: Modifier = Modifier,
+ borderStroke: BorderStroke = BorderStroke(1.dp, ColorOrange_FFFF5732),
+ shape: Shape = RoundedCornerShape(50),
+ contentPadding: PaddingValues = PaddingValues(vertical = 1.dp, horizontal = 14.dp),
+ textStyle: TextStyle = MissionMateTypography.body_xl_regular,
+ textColor: Color = ColorOrange_FFFF5732
+) {
+ Box(
+ modifier = modifier
+ .border(borderStroke, shape)
+ .padding(contentPadding),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = text,
+ style = textStyle,
+ color = textColor
+ )
+ }
+}
+
+@Composable
+fun OutlinedBox(
+ modifier: Modifier = Modifier,
+ backgroundColor: Color = ColorGray1_FF404249,
+ borderStroke: BorderStroke = BorderStroke((0.5f).dp, ColorWhite_FFFFFFFF),
+ shape: Shape = RoundedCornerShape(16.dp),
+ contentPadding: PaddingValues = PaddingValues(vertical = 1.dp, horizontal = 4.dp),
+ content: @Composable () -> Unit
+) {
+ Box(
+ modifier = modifier
+ .background(color = backgroundColor, shape = shape)
+ .border(borderStroke, shape)
+ .padding(contentPadding),
+ contentAlignment = Alignment.Center
+ ) {
+ content()
+ }
+}
+
+@Preview
+@Composable
+private fun PreviewOutlinedBox() {
+ OutlinedBox {
+ Text("test")
+ }
+}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/StableImage.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/StableImage.kt
new file mode 100644
index 00000000..7f657642
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/StableImage.kt
@@ -0,0 +1,24 @@
+package com.goalpanzi.mission_mate.feature.onboarding.component
+
+import androidx.annotation.DrawableRes
+import androidx.compose.foundation.Image
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+
+@Composable
+fun StableImage (
+ @DrawableRes drawableResId: Int,
+ modifier: Modifier = Modifier,
+ contentScale: ContentScale = ContentScale.Fit,
+ description : String? = null,
+) {
+ val painter = painterResource(id = drawableResId)
+ Image(
+ modifier = modifier,
+ painter = painter,
+ contentScale = contentScale,
+ contentDescription = description
+ )
+}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/model/BoardSetupResult.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/model/BoardSetupResult.kt
new file mode 100644
index 00000000..1b5bb58e
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/model/BoardSetupResult.kt
@@ -0,0 +1,8 @@
+package com.goalpanzi.mission_mate.feature.onboarding.model
+
+import com.goalpanzi.core.model.response.MissionDetailResponse
+
+sealed class BoardSetupResult {
+ data class Success(val data : MissionDetailResponse) : BoardSetupResult()
+ data class Error(val message : String) : BoardSetupResult()
+}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/model/CodeResultEvent.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/model/CodeResultEvent.kt
new file mode 100644
index 00000000..5f9cdd96
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/model/CodeResultEvent.kt
@@ -0,0 +1,6 @@
+package com.goalpanzi.mission_mate.feature.onboarding.model
+
+sealed class CodeResultEvent {
+ data class Success(val mission : MissionUiModel) : CodeResultEvent()
+ data object Error : CodeResultEvent()
+}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/model/JoinResultEvent.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/model/JoinResultEvent.kt
new file mode 100644
index 00000000..ce450e3b
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/model/JoinResultEvent.kt
@@ -0,0 +1,6 @@
+package com.goalpanzi.mission_mate.feature.onboarding.model
+
+sealed class JoinResultEvent {
+ data class Success(val missionId : Long) : JoinResultEvent()
+ data object Error : JoinResultEvent()
+}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/model/MissionUiModel.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/model/MissionUiModel.kt
new file mode 100644
index 00000000..6bb7751c
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/model/MissionUiModel.kt
@@ -0,0 +1,22 @@
+package com.goalpanzi.mission_mate.feature.onboarding.model
+
+import com.goalpanzi.mission_mate.feature.board.model.MissionDetail
+
+data class MissionUiModel(
+ val missionId : Long,
+ val missionTitle : String,
+ val missionPeriod : String,
+ val missionDays : List,
+ val missionTime : VerificationTimeType,
+ val missionBoardCount : Int
+)
+
+fun MissionDetail.toMissionUiModel() =
+ MissionUiModel(
+ missionId = missionId,
+ missionTitle = description,
+ missionPeriod = missionPeriod,
+ missionDays = missionDaysOfWeekTextLocale,
+ missionTime = VerificationTimeType.valueOf(timeOfDay),
+ missionBoardCount = boardCount
+ )
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/model/OnboardingResultEvent.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/model/OnboardingResultEvent.kt
new file mode 100644
index 00000000..87c7937b
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/model/OnboardingResultEvent.kt
@@ -0,0 +1,8 @@
+package com.goalpanzi.mission_mate.feature.onboarding.model
+
+import com.goalpanzi.core.model.response.MissionResponse
+
+sealed class OnboardingResultEvent {
+ data class SuccessWithJoinedMissions(val mission : MissionResponse) : OnboardingResultEvent()
+ data object Error : OnboardingResultEvent()
+}
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/model/ProfileUiModel.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/model/ProfileUiModel.kt
new file mode 100644
index 00000000..1524f8d0
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/model/ProfileUiModel.kt
@@ -0,0 +1,8 @@
+package com.goalpanzi.mission_mate.feature.onboarding.model
+
+import com.goalpanzi.core.model.response.ProfileResponse
+
+sealed class OnboardingUiModel {
+ data object Loading : OnboardingUiModel()
+ data class Success(val profileResponse: ProfileResponse) : OnboardingUiModel()
+}
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/model/VerificationTimeType.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/model/VerificationTimeType.kt
new file mode 100644
index 00000000..8a41cf21
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/model/VerificationTimeType.kt
@@ -0,0 +1,22 @@
+package com.goalpanzi.mission_mate.feature.onboarding.model
+
+import androidx.annotation.StringRes
+import com.goalpanzi.mission_mate.feature.onboarding.R
+import java.time.LocalDateTime
+
+enum class VerificationTimeType(
+ @StringRes val titleId : Int
+) {
+ MORNING(R.string.onboarding_board_setup_verification_time_input_content_am),
+ AFTERNOON(R.string.onboarding_board_setup_verification_time_input_content_pm),
+ EVERYDAY(R.string.onboarding_board_setup_verification_time_input_content_all);
+
+ fun getVerificationEndTime(
+ targetTime : LocalDateTime
+ ) : LocalDateTime {
+ return when (this) {
+ MORNING -> targetTime.withHour(11)
+ else -> targetTime.withHour(23)
+ }.withMinute(59).withSecond(59).withNano(999_999_999)
+ }
+}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/OnboardingScreen.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/OnboardingScreen.kt
new file mode 100644
index 00000000..1284af19
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/OnboardingScreen.kt
@@ -0,0 +1,343 @@
+package com.goalpanzi.mission_mate.feature.onboarding.screen
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+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.paint
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.goalpanzi.core.model.CharacterType
+import com.goalpanzi.core.model.UserProfile
+import com.goalpanzi.core.model.response.ProfileResponse
+import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateDialog
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray2_FF4F505C
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF
+import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography
+import com.goalpanzi.mission_mate.core.designsystem.theme.component.MissionMateTopAppBar
+import com.goalpanzi.mission_mate.core.designsystem.theme.component.NavigationType
+import com.goalpanzi.mission_mate.feature.onboarding.R
+import com.goalpanzi.mission_mate.feature.onboarding.component.OnboardingNavigationButton
+import com.goalpanzi.mission_mate.feature.onboarding.component.OutlinedTextBox
+import com.goalpanzi.mission_mate.feature.onboarding.component.StableImage
+import com.goalpanzi.mission_mate.feature.onboarding.model.OnboardingResultEvent
+import com.goalpanzi.mission_mate.feature.onboarding.model.OnboardingUiModel
+import kotlinx.coroutines.flow.collectLatest
+import com.goalpanzi.mission_mate.core.designsystem.R as designSystemResource
+
+@Composable
+fun OnboardingRoute(
+ modifier: Modifier = Modifier,
+ onClickBoardSetup: () -> Unit,
+ onClickInvitationCode: () -> Unit,
+ onClickSetting: () -> Unit,
+ onNavigateMissionBoard: (Long) -> Unit,
+ viewModel: OnboardingViewModel = hiltViewModel()
+) {
+ val onboardingUiModel by viewModel.onboardingUiModel.collectAsStateWithLifecycle()
+ var profileCreateSuccessData by remember { mutableStateOf(null) }
+
+ LaunchedEffect(key1 = Unit) {
+ viewModel.getJoinedMissions()
+
+ viewModel.onboardingResultEvent.collect { result ->
+ when (result) {
+ is OnboardingResultEvent.SuccessWithJoinedMissions -> {
+ onNavigateMissionBoard(result.mission.missionId)
+ }
+
+ is OnboardingResultEvent.Error -> {
+ // 에러
+ }
+ }
+ }
+ }
+
+ LaunchedEffect(key1 = Unit) {
+ viewModel.profileCreateSuccessEvent.collectLatest {
+ it?.let { profileCreateSuccessData = it }
+ }
+ }
+
+ OnboardingScreen(
+ onboardingUiModel = onboardingUiModel,
+ modifier = modifier.fillMaxSize(),
+ onClickBoardSetup = onClickBoardSetup,
+ onClickInvitationCode = onClickInvitationCode,
+ onClickSetting = onClickSetting
+ )
+
+ profileCreateSuccessData?.let {
+ ProfileCreateSuccessDialog(
+ nickname = it.nickname,
+ character = it.characterType,
+ onClickOk = {
+ profileCreateSuccessData = null
+ }
+ )
+ }
+}
+
+@Composable
+fun OnboardingScreen(
+ onboardingUiModel: OnboardingUiModel,
+ onClickBoardSetup: () -> Unit,
+ onClickInvitationCode: () -> Unit,
+ onClickSetting: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Box(
+ modifier = modifier.background(ColorWhite_FFFFFFFF)
+ ) {
+ Image(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight(),
+ painter = painterResource(id = designSystemResource.drawable.background_jeju),
+ contentDescription = null,
+ contentScale = ContentScale.FillWidth
+ )
+ when (onboardingUiModel) {
+ is OnboardingUiModel.Success -> {
+ Column(
+ modifier = modifier
+ .statusBarsPadding()
+ .navigationBarsPadding(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ MissionMateTopAppBar(
+ modifier = modifier,
+ navigationType = NavigationType.NONE,
+ containerColor = Color.Transparent,
+ rightActionButtons = {
+ TopBarSetting(
+ onClick = { onClickSetting() }
+ )
+ }
+ )
+ Text(
+ modifier = Modifier.padding(bottom = 52.dp),
+ text = stringResource(id = R.string.onboarding_ready_title),
+ textAlign = TextAlign.Center,
+ style = MissionMateTypography.heading_sm_bold,
+ color = ColorGray1_FF404249
+ )
+ OutlinedTextBox(
+ text = stringResource(id = R.string.onboarding_level_1),
+ modifier = Modifier.padding(bottom = 23.dp)
+ )
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 7.dp)
+ .wrapContentHeight(),
+ contentAlignment = Alignment.BottomCenter
+ ) {
+ StableImage(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight(),
+ drawableResId = designSystemResource.drawable.img_jeju_theme,
+ contentScale = ContentScale.FillWidth
+ )
+ StableImage(
+ modifier = Modifier
+ .fillMaxWidth(0.564f)
+ .wrapContentHeight(),
+ drawableResId = when (onboardingUiModel.profileResponse.characterType) {
+ "CAT" -> designSystemResource.drawable.img_cat_selected
+ "DOG" -> designSystemResource.drawable.img_dog_selected
+ "RABBIT" -> designSystemResource.drawable.img_rabbit_selected
+ "BEAR" -> designSystemResource.drawable.img_bear_selected
+ "PANDA" -> designSystemResource.drawable.img_panda_selected
+ "BIRD" -> designSystemResource.drawable.img_bird_selected
+ else -> designSystemResource.drawable.img_rabbit_selected
+ },
+ contentScale = ContentScale.FillWidth
+ )
+ }
+ Row(
+ modifier = Modifier
+ .fillMaxSize(348f / 390f),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ OnboardingNavigationButton(
+ modifier = Modifier.weight(162f / 324f),
+ titleId = R.string.onboarding_crating_board_title,
+ descriptionId = R.string.onboarding_crating_board_desription,
+ imageId = designSystemResource.drawable.ic_creating_board,
+ onClick = onClickBoardSetup
+ )
+ Spacer(
+ modifier = Modifier
+ .height(1.dp)
+ .weight(24f / 324f)
+ )
+ OnboardingNavigationButton(
+ modifier = Modifier.weight(162f / 324f),
+ titleId = R.string.onboarding_code_title,
+ descriptionId = R.string.onboarding_code_desription,
+ imageId = designSystemResource.drawable.ic_invitation_friend,
+ onClick = onClickInvitationCode
+ )
+ }
+ }
+ }
+
+ is OnboardingUiModel.Loading -> {
+ CircularProgressIndicator(
+ modifier = Modifier.align(Alignment.Center)
+ )
+ }
+ }
+
+ }
+}
+
+@Composable
+fun TopBarSetting(
+ onClick: () -> Unit
+) {
+ IconButton(
+ onClick = onClick,
+ modifier = Modifier.wrapContentSize()
+ ) {
+ Icon(
+ imageVector = ImageVector.vectorResource(id = designSystemResource.drawable.ic_setting),
+ contentDescription = "",
+ tint = ColorGray1_FF404249
+ )
+ }
+}
+
+@Composable
+fun ProfileCreateSuccessDialog(
+ nickname: String,
+ character: CharacterType,
+ onClickOk: () -> Unit
+) {
+ MissionMateDialog(
+ onDismissRequest = {},
+ onClickOk = onClickOk,
+ okTextId = R.string.confirm
+ ) {
+ Column(
+ modifier = Modifier
+ .wrapContentHeight(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = stringResource(
+ id = R.string.onboarding_profile_create_dialog_title,
+ nickname
+ ),
+ style = MissionMateTypography.title_xl_bold,
+ textAlign = TextAlign.Center,
+ color = ColorGray1_FF404249
+ )
+ Text(
+ modifier = Modifier.padding(top = 12.dp),
+ text = stringResource(id = R.string.onboarding_profile_create_dialog_description),
+ style = MissionMateTypography.body_lg_regular,
+ textAlign = TextAlign.Center,
+ color = ColorGray2_FF4F505C
+ )
+ Box(
+ modifier = Modifier
+ .padding(vertical = 32.dp)
+ .size(180.dp)
+ .paint(
+ painter = painterResource(
+ id = when (character) {
+ CharacterType.RABBIT -> designSystemResource.drawable.background_rabbit
+ CharacterType.CAT -> designSystemResource.drawable.background_cat
+ CharacterType.DOG -> designSystemResource.drawable.background_dog
+ CharacterType.PANDA -> designSystemResource.drawable.background_panda
+ CharacterType.BEAR -> designSystemResource.drawable.background_bear
+ CharacterType.BIRD -> designSystemResource.drawable.background_bird
+ }
+ ),
+ contentScale = ContentScale.FillWidth,
+ )
+ ) {
+ Image(
+ painter = painterResource(
+ id = when (character) {
+ CharacterType.RABBIT -> designSystemResource.drawable.img_rabbit_default
+ CharacterType.CAT -> designSystemResource.drawable.img_cat_default
+ CharacterType.DOG -> designSystemResource.drawable.img_dog_default
+ CharacterType.PANDA -> designSystemResource.drawable.img_panda_default
+ CharacterType.BEAR -> designSystemResource.drawable.img_bear_default
+ CharacterType.BIRD -> designSystemResource.drawable.img_bird_default
+ }
+ ),
+ contentDescription = null,
+ contentScale = ContentScale.FillWidth,
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(18.dp)
+ .align(Alignment.Center)
+ )
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+fun OnboardingScreenPreview() {
+ OnboardingScreen(
+ onboardingUiModel = OnboardingUiModel.Success(
+ ProfileResponse(
+ nickname = "Test",
+ characterType = "CAT"
+ )
+ ),
+ onClickBoardSetup = {},
+ onClickInvitationCode = {},
+ onClickSetting = {}
+ )
+}
+
+@Preview
+@Composable
+fun ProfileCreateSuccessDialogPreview() {
+ ProfileCreateSuccessDialog(
+ nickname = "Test",
+ character = CharacterType.RABBIT,
+ onClickOk = {}
+ )
+}
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/OnboardingViewModel.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/OnboardingViewModel.kt
new file mode 100644
index 00000000..84123548
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/OnboardingViewModel.kt
@@ -0,0 +1,89 @@
+package com.goalpanzi.mission_mate.feature.onboarding.screen
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.goalpanzi.core.model.UserProfile
+import com.goalpanzi.core.model.base.NetworkResult
+import com.goalpanzi.mission_mate.core.domain.usecase.GetJoinedMissionsUseCase
+import com.goalpanzi.mission_mate.core.domain.usecase.GetMissionJoinedUseCase
+import com.goalpanzi.mission_mate.core.domain.usecase.ProfileUseCase
+import com.goalpanzi.mission_mate.feature.onboarding.isAfterProfileCreateArg
+import com.goalpanzi.mission_mate.feature.onboarding.model.OnboardingResultEvent
+import com.goalpanzi.mission_mate.feature.onboarding.model.OnboardingUiModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@HiltViewModel
+class OnboardingViewModel @Inject constructor(
+ private val getJoinedMissionsUseCase: GetJoinedMissionsUseCase,
+ private val getMissionJoinedUseCase: GetMissionJoinedUseCase,
+ private val profileUseCase: ProfileUseCase,
+ savedStateHandle: SavedStateHandle
+) : ViewModel() {
+
+ private val _onboardingUiModel = MutableStateFlow(OnboardingUiModel.Loading)
+ val onboardingUiModel: StateFlow = _onboardingUiModel.asStateFlow()
+
+ private val _onboardingResultEvent = MutableSharedFlow()
+ val onboardingResultEvent: SharedFlow =
+ _onboardingResultEvent.asSharedFlow()
+
+ val profileCreateSuccessEvent = MutableSharedFlow()
+
+ init {
+ viewModelScope.launch {
+ savedStateHandle.get(isAfterProfileCreateArg)?.let {
+ if (it) {
+ profileCreateSuccessEvent.emit(profileUseCase.getProfile())
+ }
+ }
+ }
+ }
+
+ fun getJoinedMissions() {
+ viewModelScope.launch {
+ _onboardingUiModel.emit(OnboardingUiModel.Loading)
+
+ val isJoined = getMissionJoinedUseCase().first()
+
+ getJoinedMissionsUseCase()
+ .catch {
+ _onboardingResultEvent.emit(OnboardingResultEvent.Error)
+ }.collect { result ->
+ when (result) {
+ is NetworkResult.Success -> {
+ result.data.missions.let { missions ->
+ if (missions.isNotEmpty() && isJoined != false) {
+ _onboardingResultEvent.emit(
+ OnboardingResultEvent.SuccessWithJoinedMissions(missions.first())
+ )
+ } else {
+ _onboardingUiModel.emit(
+ OnboardingUiModel.Success(result.data.profile)
+ )
+
+ }
+ }
+ }
+
+ else -> {
+ _onboardingResultEvent.emit(OnboardingResultEvent.Error)
+ }
+ }
+ }
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupMission.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupMission.kt
new file mode 100644
index 00000000..14ff89c9
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupMission.kt
@@ -0,0 +1,49 @@
+package com.goalpanzi.mission_mate.feature.onboarding.screen.boardsetup
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateTextFieldGroup
+import com.goalpanzi.mission_mate.feature.onboarding.R
+
+@Composable
+fun BoardSetupMission(
+ missionTitle: String,
+ isNotTitleValid : Boolean,
+ onTitleChange: (String) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .verticalScroll(scrollState)
+ .padding(horizontal = 24.dp)
+ ) {
+ BoardSetupDescription(
+ text = stringResource(id = R.string.onboarding_board_setup_mission_description),
+ colorTargetTexts = listOf(
+ stringResource(R.string.onboarding_board_setup_mission_description_color_target1),
+ stringResource(R.string.onboarding_board_setup_mission_description_color_target2)
+ )
+ )
+ MissionMateTextFieldGroup(
+ modifier = modifier.fillMaxWidth(),
+ text = missionTitle,
+ onValueChange = onTitleChange,
+ isError = isNotTitleValid,
+ useMaxLength = true,
+ maxLength = 12,
+ titleId = R.string.onboarding_board_setup_mission_input_title,
+ hintId = R.string.onboarding_board_setup_mission_input_hint,
+ guidanceId = R.string.onboarding_board_setup_mission_input_guide,
+ )
+ }
+}
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupSchedule.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupSchedule.kt
new file mode 100644
index 00000000..d45369ae
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupSchedule.kt
@@ -0,0 +1,228 @@
+package com.goalpanzi.mission_mate.feature.onboarding.screen.boardsetup
+
+import androidx.compose.foundation.BorderStroke
+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.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.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+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.draw.alpha
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorBlack_FF000000
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray2_FF4F505C
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray3_FF727484
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray4_FFE5E5E5
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray5_FFF5F6F9
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF
+import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography
+import com.goalpanzi.mission_mate.feature.onboarding.R
+import com.goalpanzi.mission_mate.feature.onboarding.util.getStringId
+import java.time.DayOfWeek
+
+@Composable
+fun BoardSetupSchedule(
+ startDate: String,
+ endDate: String,
+ selectedDays: List,
+ enabledDaysOfWeek: Set,
+ count: String,
+ onClickStartDate: () -> Unit,
+ onClickEndDate: () -> Unit,
+ onSelectDay: (DayOfWeek) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .padding(horizontal = 24.dp)
+ ) {
+ BoardSetupDescription(
+ text = stringResource(id = R.string.onboarding_board_setup_schedule_description, count),
+ colorTargetTexts = listOf(
+ stringResource(R.string.onboarding_board_setup_schedule_description_color_target1),
+ stringResource(R.string.onboarding_board_setup_schedule_description_color_target2)
+ ),
+ count = count + stringResource(id = R.string.onboarding_board_setup_schedule_description_style_target)
+ )
+ Period(
+ startDate = startDate,
+ endDate = endDate,
+ modifier = Modifier.fillMaxWidth(),
+ onClickStartDate = onClickStartDate,
+ onClickEndDate = onClickEndDate
+ )
+ Frequency(
+ modifier = Modifier
+ .padding(top = 40.dp)
+ .fillMaxWidth(),
+ enabledDaysOfWeek = enabledDaysOfWeek,
+ selectedDays = selectedDays,
+ onClickDay = onSelectDay
+ )
+ }
+}
+
+@Composable
+fun Period(
+ startDate: String,
+ endDate: String,
+ modifier: Modifier = Modifier,
+ onClickStartDate: () -> Unit,
+ onClickEndDate: () -> Unit
+) {
+ Column(
+ modifier = modifier,
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Text(
+ text = stringResource(id = R.string.onboarding_board_setup_schedule_period_input_title),
+ style = MissionMateTypography.body_md_bold,
+ color = ColorGray3_FF727484
+ )
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Button(
+ modifier = Modifier
+ .height(60.dp)
+ .weight(1f),
+ border = if (startDate.isBlank()) null else BorderStroke(1.dp, ColorGray4_FFE5E5E5),
+ shape = RoundedCornerShape(12.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = if (startDate.isBlank()) ColorGray5_FFF5F6F9 else ColorWhite_FFFFFFFF
+ ),
+ contentPadding = PaddingValues(horizontal = 16.dp),
+ onClick = onClickStartDate
+ ) {
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ text = startDate.ifBlank { stringResource(id = R.string.onboarding_board_setup_schedule_period_start_hint) },
+ color = if (startDate.isBlank()) ColorGray3_FF727484 else ColorGray1_FF404249,
+ style = MissionMateTypography.body_lg_regular
+ )
+ }
+ Text(
+ modifier = Modifier.padding(horizontal = 7.dp),
+ text = "~",
+ color = ColorBlack_FF000000,
+ style = MissionMateTypography.body_lg_regular
+ )
+ Button(
+ modifier = Modifier
+ .height(60.dp)
+ .weight(1f),
+ border = if (endDate.isBlank()) null else BorderStroke(1.dp, ColorGray4_FFE5E5E5),
+ shape = RoundedCornerShape(12.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = if (endDate.isBlank()) ColorGray5_FFF5F6F9 else ColorWhite_FFFFFFFF
+ ),
+ contentPadding = PaddingValues(horizontal = 16.dp),
+ onClick = onClickEndDate
+ ) {
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ text = endDate.ifBlank { stringResource(id = R.string.onboarding_board_setup_schedule_period_end_hint) },
+ color = if (endDate.isBlank()) ColorGray3_FF727484 else ColorGray1_FF404249,
+ style = MissionMateTypography.body_lg_regular
+ )
+ }
+ }
+ Text(
+ text = stringResource(id = R.string.onboarding_board_setup_schedule_period_input_guide),
+ style = MissionMateTypography.body_md_regular,
+ color = ColorGray2_FF4F505C
+ )
+ }
+}
+
+@Composable
+fun Frequency(
+ selectedDays: List,
+ enabledDaysOfWeek: Set,
+ onClickDay: (DayOfWeek) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier = modifier,
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Text(
+ modifier = Modifier
+ .padding(bottom = 4.dp),
+ text = stringResource(id = R.string.onboarding_board_setup_schedule_day_input_title),
+ style = MissionMateTypography.body_md_bold,
+ color = ColorGray3_FF727484.copy(alpha = if (enabledDaysOfWeek.isNotEmpty()) 1f else 0.3f)
+ )
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(4.5.dp)
+ ) {
+ DayOfWeek.entries.forEach {
+ DayItem(
+ modifier = Modifier
+ .weight(1f)
+ .aspectRatio(1f),
+ dayOfWeek = it,
+ enabled = enabledDaysOfWeek.contains(it),
+ selected = it in selectedDays,
+ onClick = {
+ onClickDay(it)
+ }
+ )
+ }
+ }
+ Text(
+ text = stringResource(id = R.string.onboarding_board_setup_schedule_day_input_guide),
+ style = MissionMateTypography.body_md_regular,
+ color = ColorGray2_FF4F505C.copy(alpha = if (enabledDaysOfWeek.isNotEmpty()) 1f else 0.3f)
+ )
+ }
+}
+
+
+@Composable
+fun DayItem(
+ dayOfWeek: DayOfWeek,
+ enabled: Boolean,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ shape: Shape = CircleShape,
+ selected: Boolean = false
+) {
+ TextButton(
+ modifier = modifier,
+ contentPadding = PaddingValues(horizontal = 16.dp),
+ shape = shape,
+ colors = ButtonDefaults.textButtonColors(
+ containerColor = if (selected) ColorGray1_FF404249 else ColorGray5_FFF5F6F9,
+ disabledContainerColor = ColorGray5_FFF5F6F9.copy(0.3f)
+ ),
+ enabled = enabled,
+ onClick = onClick,
+ ) {
+ Text(
+ text = stringResource(id = dayOfWeek.getStringId()),
+ color = if (selected && enabled) ColorWhite_FFFFFFFF
+ else if(enabled) ColorGray1_FF404249 else ColorGray1_FF404249.copy(0.3f),
+ style = MissionMateTypography.body_lg_regular,
+ textAlign = TextAlign.Center
+ )
+ }
+}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupScreen.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupScreen.kt
new file mode 100644
index 00000000..bfd4649a
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupScreen.kt
@@ -0,0 +1,303 @@
+package com.goalpanzi.mission_mate.feature.onboarding.screen.boardsetup
+
+import android.annotation.SuppressLint
+import android.widget.Toast
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.PagerState
+import androidx.compose.foundation.pager.rememberPagerState
+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.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateButtonType
+import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateTextButton
+import com.goalpanzi.mission_mate.core.designsystem.ext.clickableWithoutRipple
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray2_FF4F505C
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorOrange_FFFF5732
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF
+import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography
+import com.goalpanzi.mission_mate.feature.onboarding.R
+import com.goalpanzi.mission_mate.feature.onboarding.component.BoardSetupNavigationBar
+import com.goalpanzi.mission_mate.feature.onboarding.component.DatePickerDialog
+import com.goalpanzi.mission_mate.feature.onboarding.model.BoardSetupResult
+import com.goalpanzi.mission_mate.feature.onboarding.model.VerificationTimeType
+import com.goalpanzi.mission_mate.feature.onboarding.screen.boardsetup.BoardSetupViewModel.Companion.BoardSetupStep
+import com.goalpanzi.mission_mate.feature.onboarding.util.DateUtils.dateToString
+import com.goalpanzi.mission_mate.feature.onboarding.util.DateUtils.filterDatesByDayOfWeek
+import com.goalpanzi.mission_mate.feature.onboarding.util.DateUtils.localDateToMillis
+import com.goalpanzi.mission_mate.feature.onboarding.util.styledTextWithHighlights
+import java.time.DayOfWeek
+import java.time.LocalDate
+
+@SuppressLint("UnrememberedMutableInteractionSource")
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun BoardSetupRoute(
+ onSuccess : () -> Unit,
+ onBackClick: () -> Unit,
+ viewModel: BoardSetupViewModel = hiltViewModel()
+) {
+ val context = LocalContext.current
+ val keyboardController = LocalSoftwareKeyboardController.current
+ val localFocusManager = LocalFocusManager.current
+ val pagerState = rememberPagerState { BoardSetupStep.entries.size }
+
+ val isNotTitleValid by viewModel.isNotTitleValid.collectAsStateWithLifecycle()
+
+ val startDate by viewModel.startDate.collectAsStateWithLifecycle()
+ val endDate by viewModel.endDate.collectAsStateWithLifecycle()
+ val selectedDays by viewModel.selectedDays.collectAsStateWithLifecycle()
+ val selectedVerificationTimeType by viewModel.selectedVerificationTimeType.collectAsStateWithLifecycle()
+
+ val enabledDaysOfWeek by viewModel.enabledDaysOfWeek.collectAsStateWithLifecycle()
+ val enabledButton by viewModel.enabledButton.collectAsStateWithLifecycle()
+ val currentStep by viewModel.currentStep.collectAsStateWithLifecycle()
+
+ var isShownStartDateDialog by remember { mutableStateOf(false) }
+ var isShownEndDateDialog by remember { mutableStateOf(false) }
+
+ if (isShownStartDateDialog) {
+ DatePickerDialog(
+ selectedDate = startDate,
+ onSuccess = {
+ viewModel.updateStartDate(it)
+ isShownStartDateDialog = !isShownStartDateDialog
+ },
+ selectableStartDate = LocalDate.now().plusDays(1),
+ selectableEndDate = null,
+ initialDisplayedMonthMillis = localDateToMillis(startDate),
+ onDismiss = { isShownStartDateDialog = !isShownStartDateDialog }
+ )
+ }
+
+ if (isShownEndDateDialog) {
+ DatePickerDialog(
+ selectedDate = endDate,
+ onSuccess = {
+ viewModel.updateEndDate(it)
+ isShownEndDateDialog = !isShownEndDateDialog
+ },
+ selectableStartDate = startDate,
+ selectableEndDate = startDate?.plusDays(30),
+ initialDisplayedMonthMillis = localDateToMillis(startDate),
+ onDismiss = { isShownEndDateDialog = !isShownEndDateDialog }
+ )
+ }
+
+ LaunchedEffect(currentStep) {
+ if(currentStep != BoardSetupStep.MISSION){
+ localFocusManager.clearFocus()
+ keyboardController?.hide()
+ }
+ if(currentStep.ordinal in 0 until pagerState.pageCount)
+ pagerState.animateScrollToPage(currentStep.ordinal)
+ }
+
+ LaunchedEffect(key1 = Unit) {
+ viewModel.setupEvent.collect { event ->
+ when(event){
+ is BoardSetupResult.Success -> {
+ onSuccess()
+ }
+ is BoardSetupResult.Error -> {
+ Toast.makeText(context, event.message,Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ }
+ }
+
+ BoardSetupScreen(
+ modifier = Modifier.clickableWithoutRipple {
+ keyboardController?.hide()
+ localFocusManager.clearFocus()
+ },
+ currentStep = currentStep,
+ missionTitle = viewModel.missionTitle,
+ startDate = startDate?.let {
+ dateToString(it)
+ } ?: "",
+ endDate = endDate?.let {
+ dateToString(it)
+ } ?: "",
+ selectedDays = selectedDays,
+ count = filterDatesByDayOfWeek(startDate, endDate, selectedDays),
+ selectedVerificationTimeType = selectedVerificationTimeType,
+ enabledDaysOfWeek = enabledDaysOfWeek,
+ isNotTitleValid = isNotTitleValid,
+ enabledButton = enabledButton,
+ pagerState = pagerState,
+ onClickNextStep = viewModel::updateCurrentStepToNext,
+ onMissionTitleChange = viewModel::updateMissionTitle,
+ onClickStartDate = {
+ isShownStartDateDialog = true
+ },
+ onClickEndDate = {
+ isShownEndDateDialog = true
+ },
+ onClickDayOfWeek = viewModel::updateSelectedDays,
+ onClickVerificationTimeType = viewModel::updateSelectedVerificationTimeType,
+ onBackClick = {
+ when(currentStep){
+ BoardSetupStep.MISSION -> {
+ onBackClick()
+ }
+ else -> {
+ viewModel.updateCurrentStepToBack()
+ }
+ }
+ }
+ )
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun BoardSetupScreen(
+ currentStep: BoardSetupStep,
+ missionTitle: String,
+ startDate: String,
+ endDate: String,
+ selectedDays : List,
+ count : Int,
+ selectedVerificationTimeType: VerificationTimeType?,
+ enabledDaysOfWeek : Set,
+ isNotTitleValid : Boolean,
+ enabledButton: Boolean,
+ pagerState : PagerState,
+ onClickNextStep: () -> Unit,
+ onMissionTitleChange: (String) -> Unit,
+ onClickStartDate: () -> Unit,
+ onClickEndDate: () -> Unit,
+ onClickDayOfWeek : (DayOfWeek) -> Unit,
+ onClickVerificationTimeType : (VerificationTimeType) -> Unit,
+ onBackClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .background(ColorWhite_FFFFFFFF)
+ .statusBarsPadding()
+ .navigationBarsPadding()
+ .imePadding()
+ ) {
+ BoardSetupNavigationBar(
+ onBackClick = onBackClick,
+ currentStep = {
+ currentStep.ordinal + 1
+ }
+ )
+
+ HorizontalPager(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f),
+ state = pagerState,
+ userScrollEnabled = false
+ ) {
+ when (it) {
+ 0 -> {
+ BoardSetupMission(
+ missionTitle = missionTitle,
+ isNotTitleValid = isNotTitleValid,
+ onTitleChange = onMissionTitleChange,
+ )
+ }
+
+ 1 -> {
+ BoardSetupSchedule(
+ startDate = startDate,
+ endDate = endDate,
+ count = "$count".padStart(2,'0'),
+ enabledDaysOfWeek = enabledDaysOfWeek,
+ onClickStartDate = onClickStartDate,
+ onClickEndDate = onClickEndDate,
+ selectedDays = selectedDays,
+ onSelectDay = onClickDayOfWeek
+ )
+ }
+
+ 2 -> {
+ BoardSetupVerificationTime(
+ selectedTimeType = selectedVerificationTimeType,
+ onClickTime = onClickVerificationTimeType
+ )
+ }
+ }
+ }
+ MissionMateTextButton(
+ modifier = Modifier
+ .padding(vertical = 36.dp, horizontal = 24.dp)
+ .fillMaxWidth()
+ .wrapContentHeight(),
+ buttonType = if (enabledButton) MissionMateButtonType.ACTIVE else MissionMateButtonType.DISABLED,
+ textId = if (currentStep.ordinal == 2) R.string.done else R.string.next,
+ onClick = {
+ onClickNextStep()
+ }
+ )
+ }
+}
+
+@Composable
+fun ColumnScope.BoardSetupDescription(
+ text: String,
+ colorTargetTexts: List
+) {
+ Text(
+ modifier = Modifier.padding(top = 6.dp, bottom = 60.dp),
+ text = styledTextWithHighlights(
+ text = text,
+ colorTargetTexts = colorTargetTexts,
+ textColor = ColorGray2_FF4F505C,
+ targetTextColor = ColorOrange_FFFF5732,
+ targetFontWeight = FontWeight.Bold,
+ ),
+ style = MissionMateTypography.title_xl_regular
+ )
+}
+
+@Composable
+fun ColumnScope.BoardSetupDescription(
+ text: String,
+ count : String,
+ colorTargetTexts: List
+) {
+ Text(
+ modifier = Modifier.padding(top = 6.dp, bottom = 60.dp),
+ text = styledTextWithHighlights(
+ text = text,
+ colorTargetTexts = colorTargetTexts,
+ weightTargetTexts = listOf(count),
+ underlineTargetTexts = listOf(count),
+ textColor = ColorGray2_FF4F505C,
+ targetTextColor = ColorOrange_FFFF5732,
+ targetFontWeight = FontWeight.Bold,
+ ),
+ style = MissionMateTypography.title_xl_regular
+ )
+}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupSuccessScreen.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupSuccessScreen.kt
new file mode 100644
index 00000000..046fd61d
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupSuccessScreen.kt
@@ -0,0 +1,115 @@
+package com.goalpanzi.mission_mate.feature.onboarding.screen.boardsetup
+
+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.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.layout.wrapContentHeight
+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.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 com.goalpanzi.mission_mate.core.designsystem.component.MissionMateButtonType
+import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateTextButton
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray2_FF4F505C
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray3_FF727484
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF
+import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography
+import com.goalpanzi.mission_mate.feature.onboarding.R
+import com.goalpanzi.mission_mate.feature.onboarding.component.OutlinedTextBox
+import com.goalpanzi.mission_mate.feature.onboarding.component.StableImage
+
+@Composable
+fun BoardSetupSuccessScreen(
+ onClickStart: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Box(
+ modifier = modifier.background(ColorWhite_FFFFFFFF)
+ ) {
+ Image(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight(),
+ painter = painterResource(id = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_jeju),
+ contentDescription = null,
+ contentScale = ContentScale.FillWidth
+ )
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .navigationBarsPadding()
+ .padding(horizontal = 14.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Text(
+ modifier = Modifier
+ .statusBarsPadding()
+ .padding(top = 56.dp, bottom = 52.dp),
+ text = stringResource(id = R.string.onboarding_board_setup_success_title),
+ style = MissionMateTypography.heading_sm_bold,
+ color = ColorGray1_FF404249,
+ textAlign = TextAlign.Center
+ )
+ OutlinedTextBox(
+ text = stringResource(id = R.string.onboarding_level_1),
+ modifier = Modifier.padding(bottom = 12.dp)
+ )
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 7.dp)
+ .weight(1f),
+ contentAlignment = Alignment.Center
+ ) {
+ StableImage(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight(),
+ drawableResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.image_jeju_success,
+ contentScale = ContentScale.FillWidth
+ )
+ }
+
+ Text(
+ text = stringResource(id = R.string.onboarding_board_setup_success_description),
+ style = MissionMateTypography.body_xl_regular,
+ color = ColorGray1_FF404249,
+ textAlign = TextAlign.Center
+ )
+ MissionMateTextButton(
+ modifier = Modifier
+ .padding(bottom = 36.dp, start = 10.dp, end = 10.dp, top = 71.dp)
+ .fillMaxWidth()
+ .wrapContentHeight(),
+ buttonType = MissionMateButtonType.ACTIVE,
+ textId = R.string.start,
+ onClick = onClickStart
+ )
+ }
+ }
+
+}
+
+@Preview
+@Composable
+fun PreviewBoardSetupSuccessScreen(){
+ BoardSetupSuccessScreen(
+ onClickStart = {},
+ modifier = Modifier.fillMaxSize()
+ )
+}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupVerificationTime.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupVerificationTime.kt
new file mode 100644
index 00000000..179eb9bf
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupVerificationTime.kt
@@ -0,0 +1,108 @@
+package com.goalpanzi.mission_mate.feature.onboarding.screen.boardsetup
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+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.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray3_FF727484
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray5_FFF5F6F9
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF
+import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography
+import com.goalpanzi.mission_mate.feature.onboarding.R
+import com.goalpanzi.mission_mate.feature.onboarding.model.VerificationTimeType
+
+@Composable
+fun BoardSetupVerificationTime(
+ selectedTimeType : VerificationTimeType?,
+ onClickTime : (VerificationTimeType) -> Unit,
+ modifier : Modifier = Modifier
+){
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .padding(horizontal = 24.dp)
+ ) {
+ BoardSetupDescription(
+ text = stringResource(id = R.string.onboarding_board_setup_verification_time_description),
+ colorTargetTexts = listOf(
+ stringResource(R.string.onboarding_board_setup_verification_time_color_target),
+ )
+ )
+ VerificationTime(
+ modifier = Modifier.fillMaxWidth(),
+ selectedTime = selectedTimeType,
+ onClick = onClickTime
+ )
+ }
+}
+
+
+
+@Composable
+fun VerificationTime(
+ modifier: Modifier = Modifier,
+ selectedTime: VerificationTimeType?,
+ onClick: (VerificationTimeType) -> Unit
+) {
+ Column(
+ modifier = modifier,
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Text(
+ text = stringResource(id = R.string.onboarding_board_setup_verification_time_input_title),
+ style = MissionMateTypography.body_md_bold,
+ color = ColorGray3_FF727484
+ )
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ VerificationTimeType.entries.forEach {
+ Button(
+ modifier = Modifier
+ .height(84.dp)
+ .weight(1f),
+ shape = RoundedCornerShape(12.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = if (selectedTime == it) ColorGray1_FF404249
+ else ColorGray5_FFF5F6F9
+ ),
+ onClick = {
+ onClick(it)
+ },
+ contentPadding = PaddingValues()
+ ) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = stringResource(id = it.titleId),
+ color = if (selectedTime != it) ColorGray3_FF727484
+ else ColorWhite_FFFFFFFF,
+ textAlign = TextAlign.Center,
+ style = MissionMateTypography.body_lg_regular
+ )
+
+ }
+ }
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupViewModel.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupViewModel.kt
new file mode 100644
index 00000000..9c6e4023
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupViewModel.kt
@@ -0,0 +1,224 @@
+package com.goalpanzi.mission_mate.feature.onboarding.screen.boardsetup
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.goalpanzi.mission_mate.core.domain.usecase.CreateMissionUseCase
+import com.goalpanzi.mission_mate.core.domain.usecase.SetMissionJoinedUseCase
+import com.goalpanzi.mission_mate.feature.onboarding.model.BoardSetupResult
+import com.goalpanzi.mission_mate.feature.onboarding.model.VerificationTimeType
+import com.goalpanzi.mission_mate.feature.onboarding.util.DateUtils.filterDatesByDayOfWeek
+import com.goalpanzi.mission_mate.feature.onboarding.util.DateUtils.formatLocalDateToString
+import com.goalpanzi.mission_mate.feature.onboarding.util.DateUtils.isDifferenceTargetDaysOrMore
+import com.goalpanzi.mission_mate.feature.onboarding.util.DateUtils.longToLocalDate
+import com.goalpanzi.core.model.base.NetworkResult
+import com.goalpanzi.core.model.request.CreateMissionRequest
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import java.time.DayOfWeek
+import java.time.LocalDate
+import javax.inject.Inject
+
+@HiltViewModel
+class BoardSetupViewModel @Inject constructor(
+ private val createMissionUseCase : CreateMissionUseCase,
+ private val setMissionJoinedUseCase: SetMissionJoinedUseCase
+) : ViewModel() {
+
+ private val _setupEvent = MutableSharedFlow()
+ val setupEvent: SharedFlow = _setupEvent.asSharedFlow()
+
+ private val _isNotTitleValid = MutableStateFlow(false)
+ val isNotTitleValid: StateFlow = _isNotTitleValid.asStateFlow()
+
+ private val _currentStep = MutableStateFlow(BoardSetupStep.MISSION)
+ val currentStep: StateFlow = _currentStep.asStateFlow()
+
+ var missionTitle by mutableStateOf("")
+ private set
+
+ private val _startDate = MutableStateFlow(null)
+ val startDate: StateFlow = _startDate.asStateFlow()
+
+ private val _endDate = MutableStateFlow(null)
+ val endDate: StateFlow = _endDate.asStateFlow()
+
+ private val _selectedDays = MutableStateFlow>(emptyList())
+ val selectedDays: StateFlow> = _selectedDays.asStateFlow()
+
+ private val _selectedVerificationTimeType = MutableStateFlow(null)
+ val selectedVerificationTimeType: StateFlow =
+ _selectedVerificationTimeType.asStateFlow()
+
+ val enabledDaysOfWeek: StateFlow> =
+ combine(
+ startDate,
+ endDate
+ ) { startDate, endDate ->
+ if (startDate == null || endDate == null) {
+ emptySet()
+ } else if (isDifferenceTargetDaysOrMore(startDate,endDate)) {
+ DayOfWeek.entries.toSet()
+ } else {
+ getUniqueDaysOfWeekInRange(startDate,endDate)
+ }
+ }.stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(500),
+ initialValue = emptySet()
+ )
+
+ val enabledButton: StateFlow =
+ combine(
+ currentStep,
+ snapshotFlow { missionTitle },
+ enabledDaysOfWeek,
+ selectedDays,
+ selectedVerificationTimeType
+ ) { step, title, enabledDaysOfWeek, selectedDays, selectedVerificationTimeType ->
+ when (step) {
+ BoardSetupStep.MISSION -> {
+ title.length in MISSION_TITLE_MIN_LENGTH..MISSION_TITLE_MAX_LENGTH
+ }
+
+ BoardSetupStep.SCHEDULE -> {
+ enabledDaysOfWeek.isNotEmpty() && selectedDays.isNotEmpty()
+ }
+
+ BoardSetupStep.VERIFICATION_TIME -> {
+ selectedVerificationTimeType != null
+ }
+ }
+ }.stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(500),
+ initialValue = false
+ )
+
+ fun updateCurrentStepToNext() {
+ viewModelScope.launch {
+ if (currentStep.value.ordinal in 0 until BoardSetupStep.entries.lastIndex) {
+ _currentStep.emit(
+ BoardSetupStep.entries[currentStep.value.ordinal + 1]
+ )
+ } else if (currentStep.value == BoardSetupStep.VERIFICATION_TIME) {
+ createMission()
+ }
+ }
+ }
+
+ fun updateCurrentStepToBack() {
+ viewModelScope.launch {
+ if (currentStep.value.ordinal in 1 .. BoardSetupStep.entries.lastIndex) {
+ _currentStep.emit(
+ BoardSetupStep.entries[currentStep.value.ordinal - 1]
+ )
+ }
+ }
+ }
+
+ fun updateMissionTitle(title: String) {
+ missionTitle = title
+ _isNotTitleValid.value = title.length in 1.. 3 || title.length > 12
+
+ }
+
+ fun updateStartDate(date: Long) {
+ viewModelScope.launch {
+ _startDate.emit(longToLocalDate(date))
+// endDate.value?.let { endDate ->
+// if(longToLocalDate(date).isAfter(endDate)) _endDate.emit(null)
+// }
+ _endDate.emit(null)
+ _selectedDays.emit(emptyList())
+ }
+ }
+
+ fun updateEndDate(date: Long) {
+ viewModelScope.launch {
+ _endDate.emit(longToLocalDate(date))
+ }
+ }
+
+ fun updateSelectedDays(targetDay: DayOfWeek) {
+ viewModelScope.launch {
+ _selectedDays.update { days ->
+ if (days.contains(targetDay)) {
+ days.filter { it != targetDay }
+ } else days + targetDay
+ }
+ }
+ }
+
+ fun updateSelectedVerificationTimeType(timeType: VerificationTimeType) {
+ viewModelScope.launch {
+ _selectedVerificationTimeType.emit(timeType)
+ }
+ }
+
+ private suspend fun createMission(){
+ val timeOfDay = selectedVerificationTimeType.value?.name
+ val startDate = startDate.value
+ val endDate = endDate.value
+ if(timeOfDay == null || startDate == null || endDate == null){
+ _setupEvent.emit(BoardSetupResult.Error("Board Setup is Failed"))
+ return
+ }
+
+ createMissionUseCase(
+ CreateMissionRequest(
+ description = missionTitle,
+ missionStartDate = formatLocalDateToString(startDate),
+ missionEndDate = formatLocalDateToString(endDate),
+ missionDays = selectedDays.value.sortedBy { it.ordinal }.map { it.name },
+ timeOfDay = timeOfDay,
+ boardCount = filterDatesByDayOfWeek(startDate, endDate, selectedDays.value)
+ )
+ ).collect { result ->
+ when(result){
+ is NetworkResult.Success -> {
+ setMissionJoinedUseCase(true).collect()
+ _setupEvent.emit(BoardSetupResult.Success(result.data))
+ }
+ else -> {
+ _setupEvent.emit(BoardSetupResult.Error("Board Setup is Failed"))
+ }
+ }
+ }
+ }
+
+ private fun getUniqueDaysOfWeekInRange(
+ startDate : LocalDate,
+ endDate: LocalDate
+ ) : Set {
+ return generateSequence(startDate) { date ->
+ if (date.isBefore(endDate)) date.plusDays(1) else null
+ }.map { it.dayOfWeek }
+ .toSet()
+ }
+
+ companion object {
+
+ const val MISSION_TITLE_MIN_LENGTH = 4
+ const val MISSION_TITLE_MAX_LENGTH = 12
+
+
+ enum class BoardSetupStep {
+ MISSION, SCHEDULE, VERIFICATION_TIME
+ }
+ }
+}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/invitation/InvitationCodeScreen.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/invitation/InvitationCodeScreen.kt
new file mode 100644
index 00000000..2e93acfd
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/invitation/InvitationCodeScreen.kt
@@ -0,0 +1,317 @@
+package com.goalpanzi.mission_mate.feature.onboarding.screen.invitation
+
+import android.widget.Toast
+import androidx.compose.foundation.ScrollState
+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.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
+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.focus.FocusDirection
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardCapitalization
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateButtonType
+import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateTextButton
+import com.goalpanzi.mission_mate.core.designsystem.ext.clickableWithoutRipple
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray2_FF4F505C
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorOrange_FFFF5732
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorRed_FFFF5858
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF
+import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography
+import com.goalpanzi.mission_mate.core.designsystem.theme.component.MissionMateTopAppBar
+import com.goalpanzi.mission_mate.core.designsystem.theme.component.NavigationType
+import com.goalpanzi.mission_mate.feature.onboarding.R
+import com.goalpanzi.mission_mate.feature.onboarding.component.InvitationCodeTextField
+import com.goalpanzi.mission_mate.feature.onboarding.model.CodeResultEvent
+import com.goalpanzi.mission_mate.feature.onboarding.model.JoinResultEvent
+import com.goalpanzi.mission_mate.feature.onboarding.model.MissionUiModel
+import com.goalpanzi.mission_mate.feature.onboarding.screen.invitation.InvitationCodeViewModel.Companion.CodeActionEvent
+import com.goalpanzi.mission_mate.feature.onboarding.util.styledTextWithHighlights
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+@Composable
+fun InvitationCodeRoute(
+ onBackClick: () -> Unit,
+ onNavigateMissionBoard: (Long) -> Unit,
+ viewModel: InvitationCodeViewModel = hiltViewModel()
+) {
+ val keyboardController = LocalSoftwareKeyboardController.current
+ val localFocusManager = LocalFocusManager.current
+ val context = LocalContext.current
+
+ val isNotCodeValid by viewModel.isNotCodeValid.collectAsStateWithLifecycle()
+ val enabledButton by viewModel.enabledButton.collectAsStateWithLifecycle()
+
+ var hasInvitationDialogData by remember { mutableStateOf(null) }
+
+ LaunchedEffect(key1 = Unit) {
+ launch {
+ viewModel.codeInputActionEvent.collect {
+ when (it) {
+ CodeActionEvent.FIRST_DONE,
+ CodeActionEvent.SECOND_DONE,
+ CodeActionEvent.THIRD_DONE -> {
+ delay(80)
+ localFocusManager.moveFocus(FocusDirection.Next)
+ }
+
+ CodeActionEvent.SECOND_CLEAR,
+ CodeActionEvent.THIRD_CLEAR,
+ CodeActionEvent.FOURTH_CLEAR -> {
+ delay(80)
+ localFocusManager.moveFocus(FocusDirection.Previous)
+ }
+
+ else -> {
+
+ }
+ }
+ }
+ }
+
+ launch {
+ viewModel.codeResultEvent.collect { result ->
+ when (result) {
+ is CodeResultEvent.Success -> {
+ hasInvitationDialogData = result.mission
+ }
+
+ is CodeResultEvent.Error -> {
+
+ }
+ }
+ }
+ }
+
+ launch {
+ viewModel.joinResultEvent.collect { result ->
+ hasInvitationDialogData = null
+
+ when (result) {
+ is JoinResultEvent.Success -> {
+ onNavigateMissionBoard(result.missionId)
+ }
+
+ is JoinResultEvent.Error -> {
+
+ }
+ }
+ }
+ }
+ launch {
+ viewModel.isErrorToastEvent.collect {
+ val message = when (it) {
+ "CAN_NOT_JOIN_MISSION" -> context.getString(R.string.onboarding_invitation_error_already_start)
+ "EXCEED_MAX_PERSONNEL" -> context.getString(R.string.onboarding_invitation_error_exceed_capacity)
+ else -> context.getString(R.string.onboarding_invitation_error)
+ }
+ Toast.makeText(
+ context,
+ message,
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ }
+ hasInvitationDialogData?.let { mission ->
+ InvitationDialog(
+ count = mission.missionBoardCount,
+ missionTitle = mission.missionTitle,
+ missionPeriod = mission.missionPeriod,
+ missionDays = mission.missionDays,
+ missionTime = mission.missionTime,
+ onDismissRequest = {
+ hasInvitationDialogData = null
+ },
+ onClickOk = {
+ viewModel.joinMission(mission.missionId)
+ }
+ )
+ }
+ InvitationCodeScreen(
+ codeFirst = viewModel.codeFirst,
+ codeSecond = viewModel.codeSecond,
+ codeThird = viewModel.codeThird,
+ codeFourth = viewModel.codeFourth,
+ onCodeFirstChange = viewModel::updateCodeFirst,
+ onCodeSecondChange = viewModel::updateCodeSecond,
+ onCodeThirdChange = viewModel::updateCodeThird,
+ onCodeFourthChange = viewModel::updateCodeFourth,
+ onClickButton = {
+ keyboardController?.hide()
+ localFocusManager.clearFocus()
+ viewModel.checkCode()
+ },
+ onBackClick = onBackClick,
+ isNotCodeValid = isNotCodeValid,
+ enabledButton = enabledButton,
+ modifier = Modifier.clickableWithoutRipple {
+ keyboardController?.hide()
+ localFocusManager.clearFocus()
+ }
+ )
+}
+
+@Composable
+fun InvitationCodeScreen(
+ codeFirst: String,
+ codeSecond: String,
+ codeThird: String,
+ codeFourth: String,
+ onCodeFirstChange: (String) -> Unit,
+ onCodeSecondChange: (String) -> Unit,
+ onCodeThirdChange: (String) -> Unit,
+ onCodeFourthChange: (String) -> Unit,
+ onClickButton: () -> Unit,
+ onBackClick: () -> Unit,
+ isNotCodeValid: Boolean,
+ enabledButton: Boolean,
+ modifier: Modifier = Modifier,
+ scrollState: ScrollState = rememberScrollState()
+) {
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .background(ColorWhite_FFFFFFFF)
+ .verticalScroll(scrollState)
+ .imePadding()
+ .statusBarsPadding()
+ .navigationBarsPadding()
+ ) {
+ MissionMateTopAppBar(
+ modifier = modifier,
+ navigationType = NavigationType.BACK,
+ onNavigationClick = onBackClick,
+ containerColor = ColorWhite_FFFFFFFF,
+ )
+ Text(
+ text = stringResource(id = R.string.onboarding_invitation_title),
+ modifier = Modifier.padding(start = 24.dp, end = 24.dp, bottom = 22.dp),
+ style = MissionMateTypography.heading_sm_bold,
+ color = ColorGray1_FF404249
+ )
+
+ Text(
+ text = styledTextWithHighlights(
+ text = stringResource(id = R.string.onboarding_invitation_description),
+ colorTargetTexts = listOf(stringResource(id = R.string.onboarding_invitation_description_color_target)),
+ textColor = ColorGray2_FF4F505C,
+ targetTextColor = ColorOrange_FFFF5732,
+ targetFontWeight = FontWeight.Bold,
+ ),
+ modifier = Modifier.padding(start = 24.dp, end = 24.dp, bottom = 54.dp),
+ style = MissionMateTypography.title_xl_regular,
+ )
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 24.dp)
+ .wrapContentHeight(),
+ verticalAlignment = Alignment.Top,
+ horizontalArrangement = Arrangement.spacedBy(18.dp)
+ ) {
+ InvitationCodeTextField(
+ modifier = Modifier
+ .weight(1f)
+ .aspectRatio(1f),
+ text = codeFirst,
+ keyboardOptions = KeyboardOptions(
+ imeAction = ImeAction.Next,
+ capitalization = KeyboardCapitalization.Characters
+ ),
+ isError = isNotCodeValid,
+ onValueChange = onCodeFirstChange
+ )
+ InvitationCodeTextField(
+ modifier = Modifier
+ .weight(1f)
+ .aspectRatio(1f),
+ text = codeSecond,
+ keyboardOptions = KeyboardOptions(
+ imeAction = ImeAction.Next,
+ capitalization = KeyboardCapitalization.Characters
+ ),
+ isError = isNotCodeValid,
+ onValueChange = onCodeSecondChange
+ )
+ InvitationCodeTextField(
+ modifier = Modifier
+ .weight(1f)
+ .aspectRatio(1f),
+ text = codeThird,
+ keyboardOptions = KeyboardOptions(
+ imeAction = ImeAction.Next,
+ capitalization = KeyboardCapitalization.Characters
+ ),
+ isError = isNotCodeValid,
+ onValueChange = onCodeThirdChange
+ )
+ InvitationCodeTextField(
+ modifier = Modifier
+ .weight(1f)
+ .aspectRatio(1f),
+ text = codeFourth,
+ keyboardOptions = KeyboardOptions(
+ capitalization = KeyboardCapitalization.Characters
+ ),
+ isError = isNotCodeValid,
+ onValueChange = onCodeFourthChange
+ )
+ }
+ if (isNotCodeValid) {
+ val set = mutableSetOf()
+ set.forEach {
+ return@forEach
+ }
+ Text(
+ modifier = Modifier.padding(top = 12.dp, start = 24.dp, end = 24.dp),
+ text = stringResource(id = R.string.onboarding_invitation_error),
+ style = MissionMateTypography.body_md_regular,
+ color = ColorRed_FFFF5858
+ )
+ }
+
+ Spacer(modifier = Modifier.weight(1f))
+ MissionMateTextButton(
+ modifier = Modifier
+ .padding(vertical = 36.dp, horizontal = 24.dp)
+ .fillMaxWidth()
+ .wrapContentHeight(),
+ buttonType = if (enabledButton) MissionMateButtonType.ACTIVE else MissionMateButtonType.DISABLED,
+ textId = R.string.confirm,
+ onClick = onClickButton
+ )
+ }
+
+}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/invitation/InvitationCodeViewModel.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/invitation/InvitationCodeViewModel.kt
new file mode 100644
index 00000000..86ffa99e
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/invitation/InvitationCodeViewModel.kt
@@ -0,0 +1,203 @@
+package com.goalpanzi.mission_mate.feature.onboarding.screen.invitation
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.goalpanzi.mission_mate.core.domain.usecase.GetMissionByInvitationCodeUseCase
+import com.goalpanzi.mission_mate.core.domain.usecase.JoinMissionUseCase
+import com.goalpanzi.mission_mate.core.domain.usecase.SetMissionJoinedUseCase
+import com.goalpanzi.mission_mate.feature.onboarding.model.CodeResultEvent
+import com.goalpanzi.mission_mate.feature.onboarding.model.JoinResultEvent
+import com.goalpanzi.mission_mate.feature.onboarding.model.toMissionUiModel
+import com.goalpanzi.core.model.base.NetworkResult
+import com.goalpanzi.mission_mate.feature.board.model.toModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filterNot
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class InvitationCodeViewModel @Inject constructor(
+ private val getMissionByInvitationCodeUseCase : GetMissionByInvitationCodeUseCase,
+ private val joinMissionUseCase : JoinMissionUseCase,
+ private val setMissionJoinedUseCase: SetMissionJoinedUseCase
+) : ViewModel() {
+
+ var codeFirst by mutableStateOf("")
+ private set
+
+ private val codeFirstFlow =
+ snapshotFlow { codeFirst }
+
+ var codeSecond by mutableStateOf("")
+ private set
+
+ private val codeSecondFlow =
+ snapshotFlow { codeSecond }
+
+ var codeThird by mutableStateOf("")
+ private set
+
+ private val codeThirdFlow =
+ snapshotFlow { codeThird }
+
+ var codeFourth by mutableStateOf("")
+ private set
+
+ private val codeFourthFlow =
+ snapshotFlow { codeFourth }
+
+ private val _isNotCodeValid = MutableStateFlow(false)
+ val isNotCodeValid: StateFlow = _isNotCodeValid.asStateFlow()
+
+ private val _isErrorToastEvent = MutableSharedFlow()
+ val isErrorToastEvent: SharedFlow = _isErrorToastEvent.asSharedFlow()
+
+ val enabledButton: StateFlow =
+ combine(
+ codeFirstFlow.map { it.isNotEmpty() },
+ codeSecondFlow.map { it.isNotEmpty() },
+ codeThirdFlow.map { it.isNotEmpty() },
+ codeFourthFlow.map { it.isNotEmpty() },
+ isNotCodeValid
+ ) { isNotFirstEmpty, isNotSecondEmpty, isNotThirdEmpty, isNotFourthEmpty, isNotValid ->
+ isNotFirstEmpty && isNotSecondEmpty && isNotThirdEmpty && isNotFourthEmpty && !isNotValid
+ }.stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(500),
+ initialValue = false
+ )
+
+ private val _codeInputActionEvent = MutableSharedFlow()
+ val codeInputActionEvent: SharedFlow = _codeInputActionEvent.asSharedFlow()
+
+ private val _codeResultEvent = MutableSharedFlow()
+ val codeResultEvent: SharedFlow = _codeResultEvent.asSharedFlow()
+
+ private val _joinResultEvent = MutableSharedFlow()
+ val joinResultEvent: SharedFlow = _joinResultEvent.asSharedFlow()
+
+ fun updateCodeFirst(code: String) {
+ if(code == " ") return
+ if (isNotCodeValid.value) resetCodeValidState()
+ if (code.length <= 1) codeFirst = code
+ viewModelScope.launch {
+ _codeInputActionEvent.emit(if(code.isNotEmpty()) CodeActionEvent.FIRST_DONE else CodeActionEvent.FIRST_CLEAR)
+ }
+ }
+
+ fun updateCodeSecond(code: String) {
+ if(code == " ") return
+ if (isNotCodeValid.value) resetCodeValidState()
+ if (code.length <= 1) codeSecond = code
+ viewModelScope.launch {
+ _codeInputActionEvent.emit(if(code.isNotEmpty()) CodeActionEvent.SECOND_DONE else CodeActionEvent.SECOND_CLEAR)
+ }
+ }
+
+ fun updateCodeThird(code: String) {
+ if(code == " ") return
+ if (isNotCodeValid.value) resetCodeValidState()
+ if (code.length <= 1) codeThird = code
+ viewModelScope.launch {
+ _codeInputActionEvent.emit(if(code.isNotEmpty()) CodeActionEvent.THIRD_DONE else CodeActionEvent.THIRD_CLEAR)
+ }
+ }
+
+ fun updateCodeFourth(code: String) {
+ if(code == " ") return
+ if (isNotCodeValid.value) resetCodeValidState()
+ if (code.length <= 1) codeFourth = code
+ viewModelScope.launch {
+ _codeInputActionEvent.emit(if(code.isNotEmpty()) CodeActionEvent.FOURTH_DONE else CodeActionEvent.FOURTH_CLEAR)
+ }
+ }
+
+ fun checkCode() {
+ viewModelScope.launch {
+ getMissionByInvitationCodeUseCase(
+ codeFirst + codeSecond + codeThird + codeFourth
+ ).catch {
+ _codeResultEvent.emit(CodeResultEvent.Error)
+ }.collect { result ->
+ when(result){
+ is NetworkResult.Success -> {
+ _codeResultEvent.emit(
+ CodeResultEvent.Success(result.data.toModel().toMissionUiModel())
+ )
+ }
+ is NetworkResult.Error -> {
+ result.message?.let {
+ if(it.contains("CAN_NOT_JOIN_MISSION")){
+ _isErrorToastEvent.emit("CAN_NOT_JOIN_MISSION")
+ }else if(it.contains("EXCEED_MAX_PERSONNEL")){
+ _isErrorToastEvent.emit("EXCEED_MAX_PERSONNEL")
+ }else {
+ _isNotCodeValid.emit(true)
+ }
+ }
+ }
+ else -> {
+ _isNotCodeValid.emit(true)
+ }
+ }
+ }
+ }
+ }
+
+ private fun resetCodeValidState() {
+ viewModelScope.launch {
+ _isNotCodeValid.emit(false)
+ }
+ }
+
+ fun joinMission(
+ missionId : Long
+ ){
+ viewModelScope.launch {
+ joinMissionUseCase(
+ codeFirst + codeSecond + codeThird + codeFourth
+ ).catch {
+
+ }.collect {
+ //
+ setMissionJoinedUseCase(true).collect()
+
+ _joinResultEvent.emit(
+ JoinResultEvent.Success(missionId)
+ )
+ }
+ }
+ }
+
+ companion object {
+ enum class CodeActionEvent {
+ FIRST_DONE,
+ FIRST_CLEAR,
+ SECOND_DONE,
+ SECOND_CLEAR,
+ THIRD_DONE,
+ THIRD_CLEAR,
+ FOURTH_DONE,
+ FOURTH_CLEAR
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/invitation/InvitationDialog.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/invitation/InvitationDialog.kt
new file mode 100644
index 00000000..797fed69
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/invitation/InvitationDialog.kt
@@ -0,0 +1,169 @@
+package com.goalpanzi.mission_mate.feature.onboarding.screen.invitation
+
+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.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+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.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateDialog
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray2_FF4F505C
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray3_FF727484
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray4_FFE5E5E5
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorOrange_FFFF5732
+import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography
+import com.goalpanzi.mission_mate.feature.onboarding.R
+import com.goalpanzi.mission_mate.feature.onboarding.model.VerificationTimeType
+import com.goalpanzi.mission_mate.feature.onboarding.util.styledTextWithHighlights
+
+@Composable
+fun InvitationDialog(
+ count : Int,
+ missionTitle : String,
+ missionPeriod : String,
+ missionDays : List,
+ missionTime : VerificationTimeType,
+ onDismissRequest: () -> Unit,
+ onClickOk: () -> Unit,
+ modifier: Modifier = Modifier,
+ titleStyle: TextStyle = MissionMateTypography.title_xl_bold,
+ descriptionStyle: TextStyle = MissionMateTypography.body_lg_regular,
+ okTextStyle: TextStyle = MissionMateTypography.body_lg_bold,
+ cancelTextStyle: TextStyle = MissionMateTypography.body_lg_bold
+){
+ val scrollState = rememberScrollState()
+
+ MissionMateDialog(
+ modifier = modifier,
+ onDismissRequest = onDismissRequest,
+ onClickOk = onClickOk,
+ okTextId = R.string.check_ok,
+ cancelTextId = R.string.check_no,
+ okTextStyle = okTextStyle,
+ cancelTextStyle = cancelTextStyle
+ ){
+ Column(
+ modifier = Modifier
+ .weight(1f, false)
+ .padding(bottom = 29.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Text(
+ text = stringResource(id = R.string.onboarding_invitation_dialog_title),
+ style = titleStyle,
+ textAlign = TextAlign.Center,
+ color = ColorGray1_FF404249
+ )
+ Text(
+ modifier = Modifier.padding(top = 4.dp, bottom = 20.dp),
+ text = styledTextWithHighlights(
+ text = stringResource(id = R.string.onboarding_invitation_dialog_description,count),
+ colorTargetTexts = listOf(stringResource(id = R.string.onboarding_invitation_dialog_description_color_target,count)),
+ targetTextColor = ColorOrange_FFFF5732,
+ textColor = ColorGray2_FF4F505C
+ ),
+ style = descriptionStyle,
+ textAlign = TextAlign.Center,
+ color = ColorGray2_FF4F505C
+ )
+ Column(
+ modifier = Modifier.verticalScroll(scrollState),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Text (
+ modifier = Modifier.align(Alignment.Start),
+ text = stringResource(id = R.string.onboarding_board_setup_mission_input_title),
+ color = ColorGray3_FF727484,
+ style = MissionMateTypography.body_md_regular
+ )
+ Text(
+ modifier = Modifier.align(Alignment.Start),
+ text = missionTitle,
+ color = ColorGray1_FF404249,
+ style = MissionMateTypography.body_lg_bold
+ )
+ HorizontalDivider(
+ modifier = Modifier.padding(vertical = 8.dp),
+ color = ColorGray4_FFE5E5E5
+ )
+
+ Text (
+ modifier = Modifier.align(Alignment.Start),
+ text = stringResource(id = R.string.onboarding_board_setup_schedule_period_input_title),
+ color = ColorGray3_FF727484,
+ style = MissionMateTypography.body_md_regular
+ )
+ Text(
+ modifier = Modifier.align(Alignment.Start),
+ text = missionPeriod,
+ color = ColorGray1_FF404249,
+ style = MissionMateTypography.body_lg_bold
+ )
+ HorizontalDivider(
+ modifier = Modifier.padding(vertical = 8.dp),
+ color = ColorGray4_FFE5E5E5
+ )
+
+ Text (
+ modifier = Modifier.align(Alignment.Start),
+ text = stringResource(id = R.string.onboarding_invitation_dialog_schedule_day_title),
+ color = ColorGray3_FF727484,
+ style = MissionMateTypography.body_md_regular
+ )
+ Text(
+ modifier = Modifier.align(Alignment.Start),
+ text = missionDays.joinToString("/"),
+ color = ColorGray1_FF404249,
+ style = MissionMateTypography.body_lg_bold
+ )
+ HorizontalDivider(
+ modifier = Modifier.padding(vertical = 8.dp),
+ color = ColorGray4_FFE5E5E5
+ )
+
+ Text (
+ modifier = Modifier.align(Alignment.Start),
+ text = stringResource(id = R.string.onboarding_board_setup_verification_time_input_title),
+ color = ColorGray3_FF727484,
+ style = MissionMateTypography.body_md_regular
+ )
+ Text(
+ modifier = Modifier.align(Alignment.Start),
+ text = stringResource(id = missionTime.titleId).replace("\n"," "),
+ color = ColorGray1_FF404249,
+ style = MissionMateTypography.body_lg_bold
+ )
+ }
+
+ }
+ }
+}
+
+@Preview
+@Composable
+fun PreviewInvitationDialog(){
+ InvitationDialog(
+ count = 12,
+ modifier = Modifier.fillMaxWidth(),
+ missionTitle = "매일 유산소 1시간",
+ missionPeriod = "2024.07.24~2024.08.14",
+ missionDays = listOf("월","수"),
+ missionTime = VerificationTimeType.MORNING,
+ onDismissRequest = {},
+ onClickOk = {}
+
+ )
+}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/util/DateUtils.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/util/DateUtils.kt
new file mode 100644
index 00000000..76dfe863
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/util/DateUtils.kt
@@ -0,0 +1,65 @@
+package com.goalpanzi.mission_mate.feature.onboarding.util
+
+import java.time.DayOfWeek
+import java.time.Instant
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.LocalTime
+import java.time.ZoneId
+import java.time.ZoneOffset
+import java.time.format.DateTimeFormatter
+import java.time.temporal.ChronoUnit
+import java.util.Locale
+import kotlin.math.absoluteValue
+
+object DateUtils {
+ private fun convertMillisToLocalDateWithFormatter(date: LocalDate, dateTimeFormatter: DateTimeFormatter) : LocalDate {
+ val dateInMillis = LocalDate.parse(date.format(dateTimeFormatter), dateTimeFormatter)
+ .atStartOfDay(ZoneId.systemDefault())
+ .toInstant()
+ .toEpochMilli()
+
+ return Instant
+ .ofEpochMilli(dateInMillis)
+ .atZone(ZoneId.systemDefault())
+ .toLocalDate()
+ }
+ fun dateToString(date: LocalDate): String {
+ val dateFormatter = DateTimeFormatter.ofPattern("yyyy.MM.dd", Locale.getDefault())
+ val dateInMillis = convertMillisToLocalDateWithFormatter(date, dateFormatter)
+ return dateFormatter.format(dateInMillis)
+ }
+
+ fun longToLocalDate(milliseconds: Long): LocalDate {
+ val instant = Instant.ofEpochMilli(milliseconds)
+
+ return instant.atZone(ZoneId.systemDefault()).toLocalDate()
+ }
+
+ fun localDateToMillis(localDate: LocalDate?): Long? {
+ val instant = localDate?.atStartOfDay(ZoneId.of("UTC"))?.toInstant() ?: return null
+
+ return instant.toEpochMilli()
+ }
+
+ fun filterDatesByDayOfWeek(startDate: LocalDate?, endDate: LocalDate?, days: List): Int {
+ if(startDate == null || endDate == null) return 0
+ return generateSequence(startDate) { date ->
+ if (date.isBefore(endDate)) date.plusDays(1) else null
+ }.filter { it.dayOfWeek in days }.toList().size
+ }
+
+ fun isDifferenceTargetDaysOrMore(
+ startDate: LocalDate,
+ endDate: LocalDate,
+ targetDifferenceDays : Int = 7
+ ) = ChronoUnit.DAYS.between(startDate, endDate).absoluteValue >= targetDifferenceDays
+
+
+ fun formatLocalDateToString(date: LocalDate): String {
+ val dateTime = LocalDateTime.of(date, LocalTime.MIDNIGHT)
+
+ val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
+ return dateTime.atOffset(ZoneOffset.UTC).format(formatter)
+ }
+}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/util/DayOfWeekExt.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/util/DayOfWeekExt.kt
new file mode 100644
index 00000000..046f5211
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/util/DayOfWeekExt.kt
@@ -0,0 +1,16 @@
+package com.goalpanzi.mission_mate.feature.onboarding.util
+
+import com.goalpanzi.mission_mate.core.designsystem.R
+import java.time.DayOfWeek
+
+fun DayOfWeek.getStringId() : Int {
+ return when(this){
+ DayOfWeek.MONDAY -> R.string.monday_short
+ DayOfWeek.TUESDAY -> R.string.tuesday_short
+ DayOfWeek.WEDNESDAY -> R.string.wednesday_short
+ DayOfWeek.THURSDAY -> R.string.thursday_short
+ DayOfWeek.FRIDAY -> R.string.friday_short
+ DayOfWeek.SATURDAY -> R.string.saturday_short
+ DayOfWeek.SUNDAY -> R.string.sunday_short
+ }
+}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/util/StringUtils.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/util/StringUtils.kt
new file mode 100644
index 00000000..9edcb56b
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/util/StringUtils.kt
@@ -0,0 +1,75 @@
+package com.goalpanzi.mission_mate.feature.onboarding.util
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextDecoration
+
+fun List.getStringRanges(text: String): List {
+ return this.mapNotNull { target ->
+ val index = text.indexOf(target, 0)
+ if (index != -1) {
+ (index until index + target.length)
+ } else {
+ null
+ }
+ }
+}
+
+fun styledTextWithHighlights(
+ text: String,
+ colorTargetTexts: List,
+ textColor: Color,
+ targetTextColor: Color,
+ weightTargetTexts: List = emptyList(),
+ underlineTargetTexts: List = emptyList(),
+ targetFontWeight: FontWeight = FontWeight.Normal
+): AnnotatedString {
+ return styledTextWithHighlightsWithIndices(
+ text = text,
+ colorTargetTextIndices = colorTargetTexts.getStringRanges(text),
+ weightTargetTextIndices = weightTargetTexts.getStringRanges(text),
+ underlineTargetTextIndices = underlineTargetTexts.getStringRanges(text),
+ textColor = textColor,
+ targetTextColor = targetTextColor,
+ targetFontWeight = targetFontWeight
+ )
+}
+
+fun styledTextWithHighlightsWithIndices(
+ text: String,
+ colorTargetTextIndices: List,
+ weightTargetTextIndices: List,
+ underlineTargetTextIndices: List,
+ textColor: Color,
+ targetTextColor: Color,
+ targetFontWeight: FontWeight = FontWeight.Normal,
+): AnnotatedString {
+ return buildAnnotatedString {
+ addStyle(style = SpanStyle(color = textColor), start = 0, end = text.length)
+ append(text)
+ colorTargetTextIndices.forEach { range ->
+ addStyle(
+ style = SpanStyle(color = targetTextColor),
+ start = range.first,
+ end = range.last + 1
+ )
+ }
+ weightTargetTextIndices.forEach { range ->
+ addStyle(
+ style = SpanStyle(fontWeight = targetFontWeight),
+ start = range.first,
+ end = range.last + 1
+ )
+ }
+ underlineTargetTextIndices.forEach { range ->
+ addStyle(
+ style = SpanStyle(textDecoration = TextDecoration.Underline),
+ start = range.first,
+ end = range.last + 1
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/res/values/strings.xml b/feature/onboarding/src/main/res/values/strings.xml
new file mode 100644
index 00000000..0c9fb806
--- /dev/null
+++ b/feature/onboarding/src/main/res/values/strings.xml
@@ -0,0 +1,63 @@
+
+
+ %s님,프로필 완성!
+ 캐릭터와 닉네임은 프로필 수정에서\n언제든지 바꿀 수 있어요!
+
+ 미션 완수를 위해\n경쟁할 준비가 되었나요?
+ 미션보드\n생성하기
+ 내 목표는 내가~
+ 초대코드\n입력하기
+ 초대받고 왔지~
+
+ LV1. 제주도
+
+ 미션 설정
+ 경쟁인원은\n최소 2명에서 최대 10명이에요!
+ 최소 2명
+ 최대 10명
+ 미션
+ ex) 주3회 러닝하기 / 매일 책3장씩 읽기
+ 4~12자 이내로 입력하세요.
+
+ 기간 및 요일 설정
+ 경쟁 기간내 인증 요일로 계산한\n총 인증 횟수는 %s번 이에요!
+ 경쟁 기간
+ 인증 요일
+ 번
+ 미션 기간
+ 시작일
+ 마감일
+ 내일부터 시작일로 지정할 수 있어요.
+ 인증 요일 (다중선택)
+ 선택한 요일에만 미션 인증할 수 있어요. (ex.월,수,금)
+
+ 인증 시간 설정
+ 해당 시간에만 인증 가능해요!\n신중히 선택해주세요.
+ 해당 시간에만 인증 가능
+ 인증 시간
+ 오전\n00~12시
+ 오후\n12~00시
+ 종일\n00~24시
+
+ 미션설정 완료!\n이제 시작해볼까요?
+ 친구와 함께 꾸준히 미션을 완수해\n세계 곳곳을 경험해봐요!
+
+ 초대코드 입력
+ 친구에게 전송받은\n초대코드 4자리를 입력해주세요!
+ 초대코드 4자리
+ 알맞지 않은 초대코드 입니다! 다시 확인해주세요.
+ 미션 최대 인원을 초과했습니다.
+ 미션 참여가능 날짜가 아닙니다.
+ 초대받은 경쟁이 맞나요?
+ *기간 대비 인증 요일을 계산해\n인증횟수(보드판 수)는 총 %d개 가\n생성되었어요.
+ 인증횟수(보드판 수)는 총 %d개
+ 인증 요일
+
+ 다음
+ 완성
+ 시작하기
+ 확인
+
+ 맞아요
+ 아니에요
+
\ No newline at end of file
diff --git a/feature/onboarding/src/test/java/com/goalpanzi/mission_mate/feature/onboarding/ExampleUnitTest.kt b/feature/onboarding/src/test/java/com/goalpanzi/mission_mate/feature/onboarding/ExampleUnitTest.kt
new file mode 100644
index 00000000..9bab1c85
--- /dev/null
+++ b/feature/onboarding/src/test/java/com/goalpanzi/mission_mate/feature/onboarding/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.goalpanzi.mission_mate.feature.onboarding
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/feature/profile/.gitignore b/feature/profile/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/feature/profile/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/feature/profile/build.gradle.kts b/feature/profile/build.gradle.kts
new file mode 100644
index 00000000..e0abf1ad
--- /dev/null
+++ b/feature/profile/build.gradle.kts
@@ -0,0 +1,74 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.jetbrains.kotlin.android)
+ alias(libs.plugins.compose.compiler)
+ alias(libs.plugins.kotlin.ksp)
+ alias(libs.plugins.hilt.android)
+}
+
+android {
+ namespace = "com.luckyoct.feature.profile"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 26
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ kotlin {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_17)
+ }
+ }
+ buildFeatures {
+ compose = true
+ }
+ composeCompiler {
+ enableStrongSkippingMode = true
+ }
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+}
+
+dependencies {
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.bundles.lifecycle)
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.bundles.compose)
+ implementation(libs.androidx.activity.compose)
+ implementation(libs.bundles.coroutines)
+
+ testImplementation(libs.bundles.test)
+ androidTestImplementation(libs.bundles.android.test)
+ androidTestImplementation(platform(libs.androidx.compose.bom))
+ debugImplementation(libs.androidx.ui.tooling)
+ debugImplementation(libs.androidx.ui.test.manifest)
+
+ implementation(libs.androidx.hilt.navigation.compose)
+ implementation(libs.hilt.android)
+ ksp(libs.hilt.compiler)
+
+ implementation(project(":core:designsystem"))
+ implementation(project(":core:navigation"))
+ implementation(project(":core:domain"))
+ implementation(project(":core:model"))
+}
\ No newline at end of file
diff --git a/feature/profile/proguard-rules.pro b/feature/profile/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/feature/profile/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/core/data/src/androidTest/java/com/goalpanzi/mission_mate/core/data/ExampleInstrumentedTest.kt b/feature/profile/src/androidTest/java/com/goalpanzi/feature/profile/ExampleInstrumentedTest.kt
similarity index 80%
rename from core/data/src/androidTest/java/com/goalpanzi/mission_mate/core/data/ExampleInstrumentedTest.kt
rename to feature/profile/src/androidTest/java/com/goalpanzi/feature/profile/ExampleInstrumentedTest.kt
index 893f4c99..ad7af8ef 100644
--- a/core/data/src/androidTest/java/com/goalpanzi/mission_mate/core/data/ExampleInstrumentedTest.kt
+++ b/feature/profile/src/androidTest/java/com/goalpanzi/feature/profile/ExampleInstrumentedTest.kt
@@ -1,4 +1,4 @@
-package com.goalpanzi.mission_mate.core.data
+package com.goalpanzi.feature.profile
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.goalpanzi.mission_mate.core.data.test", appContext.packageName)
+ assertEquals("com.luckyoct.feature.profile.test", appContext.packageName)
}
}
\ No newline at end of file
diff --git a/feature/profile/src/main/AndroidManifest.xml b/feature/profile/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..a5918e68
--- /dev/null
+++ b/feature/profile/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/feature/profile/src/main/java/com/goalpanzi/mission_mate/feature/profile/ProfileNavigation.kt b/feature/profile/src/main/java/com/goalpanzi/mission_mate/feature/profile/ProfileNavigation.kt
new file mode 100644
index 00000000..14647d87
--- /dev/null
+++ b/feature/profile/src/main/java/com/goalpanzi/mission_mate/feature/profile/ProfileNavigation.kt
@@ -0,0 +1,45 @@
+package com.goalpanzi.mission_mate.feature.profile
+
+import androidx.compose.animation.AnimatedContentTransitionScope
+import androidx.compose.animation.core.tween
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.compose.composable
+
+enum class ProfileSettingType {
+ CREATE, SETTING
+}
+
+fun NavController.navigateToProfileCreate() {
+ this.navigate("RouteModel.Profile.Create")
+}
+
+fun NavController.navigateToProfileSetting() {
+ this.navigate("RouteModel.Profile.Setting")
+}
+
+fun NavGraphBuilder.profileNavGraph(
+ onSaveSuccess: () -> Unit,
+ onBackClick: () -> Unit
+) {
+ composable("RouteModel.Profile.Create",
+ enterTransition = {
+ slideIntoContainer(
+ towards = AnimatedContentTransitionScope.SlideDirection.Left,
+ animationSpec = tween(300)
+ )
+ }
+ ) {
+ ProfileRoute(
+ profileSettingType = ProfileSettingType.CREATE,
+ onSaveSuccess = onSaveSuccess
+ )
+ }
+ composable("RouteModel.Profile.Setting") {
+ ProfileRoute(
+ profileSettingType = ProfileSettingType.SETTING,
+ onSaveSuccess = onBackClick,
+ onBackClick = onBackClick
+ )
+ }
+}
\ No newline at end of file
diff --git a/feature/profile/src/main/java/com/goalpanzi/mission_mate/feature/profile/ProfileScreen.kt b/feature/profile/src/main/java/com/goalpanzi/mission_mate/feature/profile/ProfileScreen.kt
new file mode 100644
index 00000000..e917f81c
--- /dev/null
+++ b/feature/profile/src/main/java/com/goalpanzi/mission_mate/feature/profile/ProfileScreen.kt
@@ -0,0 +1,411 @@
+package com.goalpanzi.mission_mate.feature.profile
+
+import android.app.Activity
+import android.content.res.Configuration
+import android.widget.Toast
+import androidx.annotation.DrawableRes
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.systemBarsPadding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.CircularProgressIndicator
+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.alpha
+import androidx.compose.ui.draw.paint
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+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.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.goalpanzi.core.model.CharacterType
+import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateButtonType
+import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateTextButton
+import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateTextFieldGroup
+import com.goalpanzi.mission_mate.core.designsystem.ext.clickableWithoutRipple
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray5_FFF5F6F9
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF
+import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography
+import com.goalpanzi.mission_mate.core.designsystem.theme.component.MissionMateTopAppBar
+import com.goalpanzi.mission_mate.core.designsystem.theme.component.NavigationType
+import com.goalpanzi.mission_mate.feature.profile.model.CharacterListItem
+import com.goalpanzi.mission_mate.feature.profile.model.CharacterListItem.Companion.createDefaultList
+import com.goalpanzi.mission_mate.feature.profile.model.ProfileUiState
+import com.luckyoct.feature.profile.R
+import dagger.hilt.android.EntryPointAccessors
+import kotlinx.coroutines.flow.collectLatest
+
+@Composable
+fun profileViewModel(profileSettingType: ProfileSettingType): ProfileViewModel {
+ val factory = EntryPointAccessors.fromActivity(
+ LocalContext.current as Activity,
+ ProfileViewModelFactoryProvider::class.java
+ ).profileViewModelFactory()
+
+ return viewModel(factory = ProfileViewModel.provideFactory(factory, profileSettingType))
+}
+
+@Composable
+fun ProfileRoute(
+ modifier: Modifier = Modifier,
+ profileSettingType: ProfileSettingType,
+ onSaveSuccess: () -> Unit,
+ onBackClick: (() -> Unit)? = null
+) {
+ val viewModel = profileViewModel(profileSettingType = profileSettingType)
+ val context = LocalContext.current
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+ val isNicknameDuplicated by viewModel.isNicknameDuplicated.collectAsStateWithLifecycle()
+ val isNotChangedProfileInput by viewModel.isNotChangedProfileInput.collectAsStateWithLifecycle()
+
+ val keyboardController = LocalSoftwareKeyboardController.current
+ val localFocusManager = LocalFocusManager.current
+
+ LaunchedEffect(true) {
+ viewModel.isSaveSuccess.collectLatest {
+ if (it) {
+ if (profileSettingType == ProfileSettingType.SETTING) {
+ Toast.makeText(
+ context,
+ context.getString(R.string.profile_update_success),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ onSaveSuccess()
+ }
+ }
+ }
+
+ Box(
+ modifier = Modifier
+ .clickableWithoutRipple {
+ keyboardController?.hide()
+ localFocusManager.clearFocus()
+ }
+ .fillMaxSize()
+ .background(color = ColorWhite_FFFFFFFF)
+ .systemBarsPadding()
+ .navigationBarsPadding()
+ .imePadding()
+ ) {
+ ProfileContent(
+ modifier = modifier,
+ uiState = uiState,
+ profileSettingType = profileSettingType,
+ isNotChangedProfileInput = isNotChangedProfileInput,
+ onClickCharacter = { viewModel.selectCharacter(it) },
+ onClickSave = {
+ keyboardController?.hide()
+ viewModel.saveProfile(it)
+ },
+ onBackClick = onBackClick,
+ isNicknameDuplicated = isNicknameDuplicated
+ )
+ }
+}
+
+@Composable
+fun ProfileContent(
+ modifier: Modifier = Modifier,
+ uiState: ProfileUiState,
+ profileSettingType: ProfileSettingType,
+ isNotChangedProfileInput: Boolean,
+ onClickCharacter: (CharacterListItem) -> Unit = {},
+ onClickSave: (String) -> Unit = {},
+ onBackClick: (() -> Unit)? = null,
+ isNicknameDuplicated: Boolean
+) {
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .background(color = ColorWhite_FFFFFFFF)
+ ) {
+ if (profileSettingType == ProfileSettingType.SETTING) {
+ MissionMateTopAppBar(
+ navigationType = NavigationType.BACK,
+ containerColor = ColorWhite_FFFFFFFF,
+ onNavigationClick = { onBackClick?.invoke() }
+ )
+ }
+ when (uiState) {
+ ProfileUiState.Loading -> {
+ ProfileLoading()
+ }
+
+ is ProfileUiState.Success -> {
+ ProfileScreen(
+ modifier = modifier,
+ profileSettingType = profileSettingType,
+ initialNickname = uiState.nickname,
+ characters = uiState.characterList,
+ isNotChangedProfileInput = isNotChangedProfileInput,
+ onClickCharacter = onClickCharacter,
+ onClickSave = onClickSave,
+ isNicknameDuplicated = isNicknameDuplicated
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun ColumnScope.ProfileScreen(
+ modifier: Modifier = Modifier,
+ initialNickname: String,
+ profileSettingType: ProfileSettingType,
+ characters: List,
+ isNotChangedProfileInput: Boolean,
+ onClickCharacter: (CharacterListItem) -> Unit,
+ onClickSave: (String) -> Unit,
+ isNicknameDuplicated: Boolean,
+) {
+ var nicknameInput by remember { mutableStateOf(initialNickname) }
+ val scrollState = rememberScrollState()
+ val regex = Regex("^[가-힣ㅏ-ㅣㄱ-ㅎa-zA-Z0-9]{1,6}$")
+ var invalidNicknameError by remember { mutableStateOf(false) }
+ val configuration = LocalConfiguration.current
+
+ LaunchedEffect(nicknameInput) {
+ if (nicknameInput.isEmpty()) return@LaunchedEffect
+ invalidNicknameError = (nicknameInput.length > 6 || regex.matches(nicknameInput).not())
+ }
+
+ Column(
+ modifier = modifier
+ .padding(bottom = 18.dp)
+ .weight(1f)
+ .fillMaxWidth()
+ .verticalScroll(scrollState)
+ .imePadding()
+ ) {
+ Text(
+ text = stringResource(
+ id = when (profileSettingType) {
+ ProfileSettingType.CREATE -> R.string.profile_create
+ ProfileSettingType.SETTING -> R.string.profile_setting_title
+ }
+ ),
+ modifier = modifier
+ .align(Alignment.CenterHorizontally)
+ .padding(top = if (profileSettingType == ProfileSettingType.SETTING) 0.dp else 56.dp),
+ style = MissionMateTypography.heading_sm_bold,
+ color = ColorGray1_FF404249
+ )
+
+ characters.find { it.isSelected }?.let {
+ Box(
+ modifier = modifier
+ .padding(top = 32.dp)
+ .size(configuration.screenWidthDp.dp * 0.55f)
+ .align(Alignment.CenterHorizontally)
+ ) {
+ CharacterLargeImage(
+ imageResId = it.defaultImageResId,
+ backgroundResId = it.backgroundResId
+ )
+ }
+ }
+ CharacterRow(
+ characters = characters,
+ configuration = configuration,
+ onClick = onClickCharacter
+ )
+
+ MissionMateTextFieldGroup(
+ modifier = modifier
+ .padding(top = 38.dp, start = 24.dp, end = 24.dp)
+ .fillMaxWidth()
+ .wrapContentHeight(),
+ text = nicknameInput,
+ onValueChange = { nicknameInput = it },
+ hintId = R.string.nickname_hint,
+ guidanceId = if (isNicknameDuplicated) R.string.err_duplicated_nickname else R.string.nickname_input_guide,
+ isError = invalidNicknameError || isNicknameDuplicated
+ )
+ }
+
+ MissionMateTextButton(
+ modifier = modifier
+ .padding(bottom = 36.dp, start = 24.dp, end = 24.dp)
+ .fillMaxWidth(),
+ textId = R.string.save,
+ buttonType = when (profileSettingType) {
+ ProfileSettingType.CREATE -> {
+ if (nicknameInput.trim().isEmpty() || invalidNicknameError) {
+ MissionMateButtonType.DISABLED
+ } else {
+ MissionMateButtonType.ACTIVE
+ }
+ }
+
+ ProfileSettingType.SETTING -> {
+ if ((initialNickname == nicknameInput && isNotChangedProfileInput) ||
+ nicknameInput.trim().isEmpty() || invalidNicknameError
+ ) {
+ MissionMateButtonType.DISABLED
+ } else {
+ MissionMateButtonType.ACTIVE
+ }
+ }
+ },
+ onClick = { onClickSave(nicknameInput) }
+ )
+}
+
+@Composable
+fun CharacterLargeImage(
+ modifier: Modifier = Modifier,
+ @DrawableRes imageResId: Int,
+ @DrawableRes backgroundResId: Int,
+) {
+ Image(
+ painter = painterResource(id = imageResId),
+ contentDescription = null,
+ modifier = modifier
+ .fillMaxSize()
+ .paint(
+ painter = painterResource(backgroundResId),
+ contentScale = ContentScale.FillWidth,
+ ).padding(20.dp)
+ )
+}
+
+@Composable
+fun CharacterRow(
+ modifier: Modifier = Modifier,
+ configuration: Configuration,
+ characters: List,
+ onClick: (CharacterListItem) -> Unit
+) {
+ val scrollState = rememberLazyListState()
+
+ LaunchedEffect(key1 = characters) {
+ characters.indexOfFirst { it.isSelected }.takeIf { it > 0 }?.let {
+ scrollState.animateScrollToItem(it - 1)
+ }
+ }
+
+ LazyRow(
+ modifier = modifier
+ .padding(top = 18.dp),
+ horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
+ contentPadding = PaddingValues(horizontal = 24.dp),
+ state = scrollState
+ ) {
+ items(items = characters, key = { it.selectedImageResId }) {
+ CharacterElement(
+ character = it,
+ configuration = configuration,
+ onClick = onClick
+ )
+ }
+ }
+}
+
+@Composable
+fun CharacterElement(
+ modifier: Modifier = Modifier,
+ character: CharacterListItem,
+ configuration: Configuration = LocalConfiguration.current,
+ onClick: (CharacterListItem) -> Unit = {}
+) {
+ Box(
+ modifier = modifier
+ .size(
+ width = configuration.screenWidthDp.dp * 100f / 390f,
+ height = configuration.screenWidthDp.dp * 100f / 390f * 1.24f
+ )
+ .alpha(if (character.isSelected) 1f else 0.3f)
+ .clickable(
+ indication = null,
+ interactionSource = remember { MutableInteractionSource() },
+ onClick = { onClick(character) }
+ )
+ ) {
+ Image(
+ painter = painterResource(id = character.selectedImageResId),
+ contentDescription = null,
+ )
+
+ Text(
+ text = stringResource(id = character.nameResId),
+ style = MissionMateTypography.body_md_bold,
+ modifier = modifier
+ .align(Alignment.BottomCenter)
+ .fillMaxWidth()
+ .background(color = ColorGray5_FFF5F6F9, shape = RoundedCornerShape(10.dp)),
+ color = ColorGray1_FF404249,
+ textAlign = TextAlign.Center
+ )
+ }
+}
+
+@Composable
+private fun ProfileLoading() {
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ CircularProgressIndicator()
+ }
+}
+
+@Preview
+@Composable
+fun ColumnScope.ProfileScreenPreview() {
+ ProfileScreen(
+ profileSettingType = ProfileSettingType.CREATE,
+ initialNickname = "",
+ characters = createDefaultList(),
+ onClickCharacter = {},
+ onClickSave = {},
+ isNicknameDuplicated = false,
+ isNotChangedProfileInput = false
+ )
+}
+
+@Preview
+@Composable
+fun CharacterElementPreview() {
+ CharacterElement(
+ character = CharacterListItem(
+ type = CharacterType.CAT,
+ selectedImageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_cat_selected,
+ defaultImageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_cat_default,
+ nameResId = R.string.cat_name,
+ isSelected = false,
+ backgroundResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_cat
+ )
+ )
+}
\ No newline at end of file
diff --git a/feature/profile/src/main/java/com/goalpanzi/mission_mate/feature/profile/ProfileViewModel.kt b/feature/profile/src/main/java/com/goalpanzi/mission_mate/feature/profile/ProfileViewModel.kt
new file mode 100644
index 00000000..f9b455c5
--- /dev/null
+++ b/feature/profile/src/main/java/com/goalpanzi/mission_mate/feature/profile/ProfileViewModel.kt
@@ -0,0 +1,149 @@
+package com.goalpanzi.mission_mate.feature.profile
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import com.goalpanzi.mission_mate.core.domain.usecase.ProfileUseCase
+import com.goalpanzi.core.model.CharacterType
+import com.goalpanzi.core.model.UserProfile
+import com.goalpanzi.core.model.base.NetworkResult
+import com.goalpanzi.mission_mate.feature.profile.model.CharacterListItem
+import com.goalpanzi.mission_mate.feature.profile.model.ProfileUiState
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+
+class ProfileViewModel @AssistedInject constructor(
+ @Assisted private val profileSettingType: ProfileSettingType,
+ private val profileUseCase: ProfileUseCase
+) : ViewModel() {
+
+ @AssistedFactory
+ interface Factory {
+ fun create(profileSettingType: ProfileSettingType): ProfileViewModel
+ }
+
+ companion object {
+ @Suppress("UNCHECKED_CAST")
+ fun provideFactory(
+ assistedFactory: Factory,
+ profileSettingType: ProfileSettingType
+ ): ViewModelProvider.Factory = object : ViewModelProvider.Factory {
+ override fun create(modelClass: Class): T {
+ return assistedFactory.create(profileSettingType) as T
+ }
+ }
+ }
+
+ private val defaultCharacters = CharacterListItem.createDefaultList()
+
+ private val _uiState = MutableStateFlow(ProfileUiState.Loading)
+ val uiState = _uiState.asStateFlow()
+
+ private val _isNicknameDuplicated = MutableStateFlow(false)
+ val isNicknameDuplicated = _isNicknameDuplicated.asStateFlow()
+
+ private val _isNotChangedProfileInput = MutableStateFlow(true)
+ val isNotChangedProfileInput: StateFlow = _isNotChangedProfileInput.asStateFlow()
+
+ private val _isSaveSuccess = MutableSharedFlow()
+ val isSaveSuccess = _isSaveSuccess.asSharedFlow()
+
+ init {
+ viewModelScope.launch {
+ when (profileSettingType) {
+ ProfileSettingType.CREATE -> {
+ _uiState.value = ProfileUiState.Success(
+ nickname = "",
+ characterList = defaultCharacters.toMutableList().apply {
+ set(0, get(0).copy(isSelected = true))
+ }
+ )
+ _isNotChangedProfileInput.emit(false)
+ }
+
+ ProfileSettingType.SETTING -> {
+ val userProfile = profileUseCase.getProfile()
+ ?: UserProfile(
+ nickname = "",
+ characterType = CharacterType.RABBIT
+ ) // TODO : API
+ _uiState.value = ProfileUiState.Success(
+ nickname = userProfile.nickname,
+ characterList = defaultCharacters.toMutableList().apply {
+ val index = indexOfFirst { it.type == userProfile.characterType }
+ set(index, get(index).copy(isSelected = true))
+ }
+ )
+ _isNotChangedProfileInput.emit(true)
+ }
+ }
+ }
+ }
+
+// fun updateNickname(input: String) {
+// viewModelScope.launch {
+// _invalidNicknameError.emit(
+// if (input.length > 6) {
+// InvalidNicknameError.TooLong
+// } else if (input.contains(Regex("[^가-힣a-zA-Z0-9]"))) {
+// InvalidNicknameError.IncludeSpecial
+// } else {
+// InvalidNicknameError.Duplicated
+// }
+// )
+// }
+// }
+
+ fun selectCharacter(character: CharacterListItem) {
+ val state = uiState.value as? ProfileUiState.Success ?: return
+ _uiState.value = state.copy(
+ characterList = state.characterList.map {
+ it.copy(isSelected = it == character)
+ }
+ )
+ viewModelScope.launch {
+ _isNotChangedProfileInput.emit(profileUseCase.getProfile()?.characterType == character.type)
+ }
+
+ }
+
+ fun resetNicknameErrorState() {
+ _isNicknameDuplicated.value = false
+ }
+
+ fun saveProfile(nickname: String) {
+ if (nickname.isEmpty()) return
+ viewModelScope.launch {
+ val selectedItem = (uiState.value as? ProfileUiState.Success)?.characterList?.find {
+ it.isSelected
+ } ?: return@launch
+
+ when (
+ val response = profileUseCase
+ .saveProfile(
+ nickname = nickname,
+ type = selectedItem.type,
+ isEqualNickname = profileUseCase.getProfile()?.nickname == nickname
+ )
+ ) {
+ is NetworkResult.Success -> {
+ _isSaveSuccess.emit(true)
+ }
+
+ is NetworkResult.Exception -> {}
+ is NetworkResult.Error -> {
+ if (response.code == 409) {
+ _isNicknameDuplicated.emit(true)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/feature/profile/src/main/java/com/goalpanzi/mission_mate/feature/profile/ProfileViewModelFactoryProvider.kt b/feature/profile/src/main/java/com/goalpanzi/mission_mate/feature/profile/ProfileViewModelFactoryProvider.kt
new file mode 100644
index 00000000..06f9ec5e
--- /dev/null
+++ b/feature/profile/src/main/java/com/goalpanzi/mission_mate/feature/profile/ProfileViewModelFactoryProvider.kt
@@ -0,0 +1,12 @@
+package com.goalpanzi.mission_mate.feature.profile
+
+import dagger.hilt.EntryPoint
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ActivityComponent
+
+@EntryPoint
+@InstallIn(ActivityComponent::class)
+interface ProfileViewModelFactoryProvider {
+
+ fun profileViewModelFactory(): ProfileViewModel.Factory
+}
\ No newline at end of file
diff --git a/feature/profile/src/main/java/com/goalpanzi/mission_mate/feature/profile/model/CharacterListItem.kt b/feature/profile/src/main/java/com/goalpanzi/mission_mate/feature/profile/model/CharacterListItem.kt
new file mode 100644
index 00000000..8f27c0b5
--- /dev/null
+++ b/feature/profile/src/main/java/com/goalpanzi/mission_mate/feature/profile/model/CharacterListItem.kt
@@ -0,0 +1,62 @@
+package com.goalpanzi.mission_mate.feature.profile.model
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import com.goalpanzi.core.model.CharacterType
+import com.luckyoct.feature.profile.R
+
+data class CharacterListItem(
+ val type: CharacterType,
+ @DrawableRes val selectedImageResId: Int,
+ @DrawableRes val defaultImageResId: Int,
+ @StringRes val nameResId: Int,
+ val isSelected: Boolean = false,
+ @DrawableRes val backgroundResId: Int
+) {
+ companion object {
+ fun createDefaultList() = listOf(
+ CharacterListItem(
+ type = CharacterType.RABBIT,
+ selectedImageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_rabbit_selected,
+ defaultImageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_rabbit_default,
+ nameResId = R.string.rabbit_name,
+ backgroundResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_rabbit
+ ),
+ CharacterListItem(
+ type = CharacterType.CAT,
+ selectedImageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_cat_selected,
+ defaultImageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_cat_default,
+ nameResId = R.string.cat_name,
+ backgroundResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_cat
+ ),
+ CharacterListItem(
+ type = CharacterType.DOG,
+ selectedImageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_dog_selected,
+ defaultImageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_dog_default,
+ nameResId = R.string.dog_name,
+ backgroundResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_dog
+ ),
+ CharacterListItem(
+ type = CharacterType.PANDA,
+ selectedImageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_panda_selected,
+ defaultImageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_panda_default,
+ nameResId = R.string.panda_name,
+ backgroundResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_panda
+ ),
+ CharacterListItem(
+ type = CharacterType.BEAR,
+ selectedImageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_bear_selected,
+ defaultImageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_bear_default,
+ nameResId = R.string.bear_name,
+ backgroundResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_bear
+ ),
+ CharacterListItem(
+ type = CharacterType.BIRD,
+ selectedImageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_bird_selected,
+ defaultImageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_bird_default,
+ nameResId = R.string.bird_name,
+ backgroundResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_bird
+ )
+ )
+ }
+}
diff --git a/feature/profile/src/main/java/com/goalpanzi/mission_mate/feature/profile/model/ProfileUiState.kt b/feature/profile/src/main/java/com/goalpanzi/mission_mate/feature/profile/model/ProfileUiState.kt
new file mode 100644
index 00000000..3f668756
--- /dev/null
+++ b/feature/profile/src/main/java/com/goalpanzi/mission_mate/feature/profile/model/ProfileUiState.kt
@@ -0,0 +1,11 @@
+package com.goalpanzi.mission_mate.feature.profile.model
+
+sealed interface ProfileUiState {
+
+ data object Loading : ProfileUiState
+
+ data class Success(
+ val nickname: String,
+ val characterList: List
+ ) : ProfileUiState
+}
\ No newline at end of file
diff --git a/feature/profile/src/main/res/values/arrays.xml b/feature/profile/src/main/res/values/arrays.xml
new file mode 100644
index 00000000..8e6024ca
--- /dev/null
+++ b/feature/profile/src/main/res/values/arrays.xml
@@ -0,0 +1,29 @@
+
+
+
+ - @drawable/img_rabbit_selected
+ - @drawable/img_cat_selected
+ - @drawable/img_dog_selected
+ - @drawable/img_panda_selected
+ - @drawable/img_bear_selected
+ - @drawable/img_bird_selected
+
+
+
+ - @string/rabbit_name
+ - @string/cat_name
+ - @string/dog_name
+ - @string/panda_name
+ - @string/bear_name
+ - @string/bird_name
+
+
+
+ - @color/rabbit_color
+ - @color/cat_color
+ - @color/dog_color
+ - @color/panda_color
+ - @color/bear_color
+ - @color/bird_color
+
+
\ No newline at end of file
diff --git a/feature/profile/src/main/res/values/strings.xml b/feature/profile/src/main/res/values/strings.xml
new file mode 100644
index 00000000..98f3d20e
--- /dev/null
+++ b/feature/profile/src/main/res/values/strings.xml
@@ -0,0 +1,18 @@
+
+
+ 프로필 만들기
+ 프로필 수정
+ 닉네임 입력
+ 1~6자, 한글, 영문 또는 숫자를 입력하세요.
+ 저장하기
+ 이미 존재하는 회원 닉네임이에요.
+
+ 뚝심토끼
+ 포기란없다냥
+ 끝까지해볼개
+ 하나만팬다
+ 할건끝내곰
+ 할때까지해뱁새
+
+ 프로필이 저장되었어요.
+
\ No newline at end of file
diff --git a/feature/profile/src/test/java/com/goalpanzi/feature/profile/ExampleUnitTest.kt b/feature/profile/src/test/java/com/goalpanzi/feature/profile/ExampleUnitTest.kt
new file mode 100644
index 00000000..8a24415c
--- /dev/null
+++ b/feature/profile/src/test/java/com/goalpanzi/feature/profile/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.goalpanzi.feature.profile
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/feature/setting/.gitignore b/feature/setting/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/feature/setting/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/feature/setting/build.gradle.kts b/feature/setting/build.gradle.kts
new file mode 100644
index 00000000..a1febe6c
--- /dev/null
+++ b/feature/setting/build.gradle.kts
@@ -0,0 +1,78 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.jetbrains.kotlin.android)
+ alias(libs.plugins.compose.compiler)
+ alias(libs.plugins.kotlin.ksp)
+ alias(libs.plugins.hilt.android)
+ alias(libs.plugins.kotlin.plugin.serialization)
+}
+
+android {
+ namespace = "com.luckyoct.feature.setting"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 26
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ kotlin {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_17)
+ }
+ }
+ buildFeatures {
+ compose = true
+ }
+ composeCompiler {
+ enableStrongSkippingMode = true
+ }
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+}
+
+dependencies {
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.bundles.lifecycle)
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.bundles.compose)
+ implementation(libs.androidx.activity.compose)
+ implementation(libs.bundles.coroutines)
+
+ testImplementation(libs.bundles.test)
+ androidTestImplementation(libs.bundles.android.test)
+ androidTestImplementation(platform(libs.androidx.compose.bom))
+ debugImplementation(libs.androidx.ui.tooling)
+ debugImplementation(libs.androidx.ui.test.manifest)
+
+ implementation(libs.androidx.hilt.navigation.compose)
+ implementation(libs.hilt.android)
+ ksp(libs.hilt.compiler)
+
+ implementation(libs.kotlin.serialization.json)
+
+ implementation(project(":core:designsystem"))
+ implementation(project(":core:navigation"))
+ implementation(project(":core:domain"))
+ implementation(project(":core:model"))
+ implementation(project(":feature:profile"))
+}
\ No newline at end of file
diff --git a/feature/setting/proguard-rules.pro b/feature/setting/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/feature/setting/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/core/domain/src/androidTest/java/com/goalpanzi/mission_mate/core/domain/ExampleInstrumentedTest.kt b/feature/setting/src/androidTest/java/com/goalpanzi/feature/setting/ExampleInstrumentedTest.kt
similarity index 80%
rename from core/domain/src/androidTest/java/com/goalpanzi/mission_mate/core/domain/ExampleInstrumentedTest.kt
rename to feature/setting/src/androidTest/java/com/goalpanzi/feature/setting/ExampleInstrumentedTest.kt
index ff04cf42..a2784b35 100644
--- a/core/domain/src/androidTest/java/com/goalpanzi/mission_mate/core/domain/ExampleInstrumentedTest.kt
+++ b/feature/setting/src/androidTest/java/com/goalpanzi/feature/setting/ExampleInstrumentedTest.kt
@@ -1,4 +1,4 @@
-package com.goalpanzi.mission_mate.core.domain
+package com.goalpanzi.feature.setting
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.goalpanzi.mission_mate.core.domain.test", appContext.packageName)
+ assertEquals("com.luckyoct.feature.setting.test", appContext.packageName)
}
}
\ No newline at end of file
diff --git a/feature/setting/src/main/AndroidManifest.xml b/feature/setting/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..a5918e68
--- /dev/null
+++ b/feature/setting/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/feature/setting/src/main/java/com/goalpanzi/mission_mate/feature/setting/Event.kt b/feature/setting/src/main/java/com/goalpanzi/mission_mate/feature/setting/Event.kt
new file mode 100644
index 00000000..00a07620
--- /dev/null
+++ b/feature/setting/src/main/java/com/goalpanzi/mission_mate/feature/setting/Event.kt
@@ -0,0 +1,5 @@
+package com.goalpanzi.mission_mate.feature.setting
+
+sealed interface Event {
+ data object GoToLogin : Event
+}
\ No newline at end of file
diff --git a/feature/setting/src/main/java/com/goalpanzi/mission_mate/feature/setting/Util.kt b/feature/setting/src/main/java/com/goalpanzi/mission_mate/feature/setting/Util.kt
new file mode 100644
index 00000000..a7076f32
--- /dev/null
+++ b/feature/setting/src/main/java/com/goalpanzi/mission_mate/feature/setting/Util.kt
@@ -0,0 +1,22 @@
+package com.goalpanzi.mission_mate.feature.setting
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.Build
+
+object Util {
+ fun getAppVersionName(context: Context): String {
+ return try {
+ val packageManager = context.packageManager
+ val packageName = context.packageName
+ val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ packageManager.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0))
+ } else {
+ packageManager.getPackageInfo(packageName, 0)
+ }
+ packageInfo.versionName
+ } catch (e: Exception) {
+ ""
+ }
+ }
+}
\ No newline at end of file
diff --git a/feature/setting/src/main/java/com/goalpanzi/mission_mate/feature/setting/navigation/SettingNavigation.kt b/feature/setting/src/main/java/com/goalpanzi/mission_mate/feature/setting/navigation/SettingNavigation.kt
new file mode 100644
index 00000000..221dd5fc
--- /dev/null
+++ b/feature/setting/src/main/java/com/goalpanzi/mission_mate/feature/setting/navigation/SettingNavigation.kt
@@ -0,0 +1,74 @@
+package com.goalpanzi.mission_mate.feature.setting.navigation
+
+import androidx.compose.animation.AnimatedContentTransitionScope
+import androidx.compose.animation.core.tween
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.compose.composable
+import com.goalpanzi.mission_mate.feature.setting.screen.SettingRoute
+import com.goalpanzi.mission_mate.feature.setting.screen.WebViewScreen
+
+fun NavController.navigateToSetting() {
+ this.navigate("RouteModel.Setting")
+}
+
+fun NavController.navigateToInquiry() {
+ this.navigate("SettingRouteModel.Inquiry")
+}
+
+fun NavController.navigateToServicePolicy() {
+ this.navigate("SettingRouteModel.ServicePolicy")
+}
+
+fun NavController.navigateToPrivacyPolicy() {
+ this.navigate("SettingRouteModel.PrivacyPolicy")
+}
+
+fun NavGraphBuilder.settingNavGraph(
+ onBackClick: () -> Unit,
+ onClickProfileSetting: () -> Unit,
+ onClickServicePolicy: () -> Unit,
+ onClickPrivacyPolicy: () -> Unit,
+ onClickLogout: () -> Unit
+) {
+ composable("RouteModel.Setting",
+ enterTransition = {
+ slideIntoContainer(
+ towards = AnimatedContentTransitionScope.SlideDirection.Start,
+ animationSpec = tween(300)
+ )
+ },
+ popEnterTransition = null,
+ ) {
+ SettingRoute(
+ onBackClick = onBackClick,
+ onClickProfileSetting = onClickProfileSetting,
+ onClickServicePolicy = onClickServicePolicy,
+ onClickPrivacyPolicy = onClickPrivacyPolicy,
+ onLogout = onClickLogout
+ )
+ }
+}
+
+
+fun NavGraphBuilder.servicePolicyNavGraph(
+ onBackClick: () -> Unit
+) {
+ composable("SettingRouteModel.ServicePolicy") {
+ WebViewScreen(
+ onBackClick = onBackClick,
+ url = "https://missionmate.notion.site/f638866edeaf45b58ef63d1000f30c15?pvs=73"
+ )
+ }
+}
+
+fun NavGraphBuilder.privacyPolicyNavGraph(
+ onBackClick: () -> Unit
+) {
+ composable("SettingRouteModel.PrivacyPolicy") {
+ WebViewScreen(
+ onBackClick = onBackClick,
+ url = "https://missionmate.notion.site/c79e9e6990de466490c584f351b364b7?pvs=4"
+ )
+ }
+}
\ No newline at end of file
diff --git a/feature/setting/src/main/java/com/goalpanzi/mission_mate/feature/setting/screen/SettingScreen.kt b/feature/setting/src/main/java/com/goalpanzi/mission_mate/feature/setting/screen/SettingScreen.kt
new file mode 100644
index 00000000..15be806e
--- /dev/null
+++ b/feature/setting/src/main/java/com/goalpanzi/mission_mate/feature/setting/screen/SettingScreen.kt
@@ -0,0 +1,302 @@
+package com.goalpanzi.mission_mate.feature.setting.screen
+
+import androidx.annotation.StringRes
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+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.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+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 com.goalpanzi.mission_mate.core.designsystem.component.MissionMateDialog
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray3_FF727484
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray4_FFE5E5E5
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF
+import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography
+import com.goalpanzi.mission_mate.core.designsystem.theme.component.MissionMateTopAppBar
+import com.goalpanzi.mission_mate.core.designsystem.theme.component.NavigationType
+import com.goalpanzi.mission_mate.feature.setting.Event
+import com.goalpanzi.mission_mate.feature.setting.Util
+import com.luckyoct.feature.setting.R
+import kotlinx.coroutines.flow.collectLatest
+
+@Composable
+fun SettingRoute(
+ modifier: Modifier = Modifier,
+ onClickProfileSetting: () -> Unit,
+ onClickServicePolicy: () -> Unit,
+ onClickPrivacyPolicy: () -> Unit,
+ onBackClick: () -> Unit,
+ onLogout: () -> Unit,
+ viewModel: SettingViewModel = hiltViewModel()
+) {
+ val showLogoutDialog = remember { mutableStateOf(false) }
+ val showDeleteAccountDialog = remember { mutableStateOf(false) }
+
+ LaunchedEffect(true) {
+ viewModel.event.collectLatest { event ->
+ when (event) {
+ Event.GoToLogin -> {
+ showLogoutDialog.value = false
+ onLogout()
+ }
+ }
+ }
+ }
+
+ if (showLogoutDialog.value) {
+ LogoutDialog(
+ onDismissRequest = { showLogoutDialog.value = false },
+ onClickOk = { viewModel.logout() }
+ )
+ }
+
+ if (showDeleteAccountDialog.value) {
+ AccountDeleteDialog(
+ onDismissRequest = { showDeleteAccountDialog.value = false },
+ onClickOk = { viewModel.deleteAccount() }
+ )
+ }
+
+ SettingScreen(
+ modifier = modifier,
+ onBackClick = { onBackClick() },
+ onClickProfileSetting = { onClickProfileSetting() },
+ onClickServicePolicy = { onClickServicePolicy() },
+ onClickPrivacyPolicy = { onClickPrivacyPolicy() },
+ onClickLogout = { showLogoutDialog.value = true },
+ onClickDeleteAccount = { showDeleteAccountDialog.value = true }
+ )
+}
+
+@Composable
+fun SettingScreen(
+ modifier: Modifier = Modifier,
+ onBackClick: () -> Unit,
+ onClickProfileSetting: () -> Unit,
+ onClickServicePolicy: () -> Unit,
+ onClickPrivacyPolicy: () -> Unit,
+ onClickLogout: () -> Unit,
+ onClickDeleteAccount: () -> Unit
+) {
+ val scrollState = rememberScrollState()
+
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .background(ColorWhite_FFFFFFFF)
+ .statusBarsPadding()
+ .navigationBarsPadding()
+ ) {
+ MissionMateTopAppBar(
+ navigationType = NavigationType.BACK,
+ onNavigationClick = { onBackClick() },
+ containerColor = ColorWhite_FFFFFFFF
+ )
+ Text(
+ text = stringResource(id = R.string.setting_title),
+ modifier = Modifier
+ .wrapContentWidth()
+ .wrapContentHeight()
+ .padding(start = 24.dp, bottom = 16.dp),
+ style = MissionMateTypography.heading_sm_bold,
+ color = ColorGray1_FF404249
+ )
+
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .weight(1f)
+ .verticalScroll(scrollState)
+ ) {
+ SettingHeader(titleRes = R.string.my_info)
+ SettingContent(
+ titleRes = R.string.setting_profile,
+ onClick = { onClickProfileSetting() }
+ )
+ Divider()
+ SettingHeader(titleRes = R.string.version_info)
+ SettingContent(
+ titleRes = R.string.current_version,
+ subContent = {
+ Text(
+ text = Util.getAppVersionName(LocalContext.current),
+ style = MissionMateTypography.body_xl_regular,
+ color = ColorGray1_FF404249
+ )
+ }
+ )
+ Divider()
+ SettingHeader(titleRes = R.string.help_desk)
+ SettingContent(
+ titleRes = R.string.inquiry,
+ subContent = {
+ Text(
+ text = stringResource(id = R.string.inquiry_email),
+ style = MissionMateTypography.body_md_regular,
+ color = ColorGray1_FF404249
+ )
+ }
+ )
+ Divider()
+ SettingHeader(titleRes = R.string.policy)
+ SettingContent(
+ titleRes = R.string.service_policy,
+ onClick = { onClickServicePolicy() }
+ )
+ SettingContent(
+ titleRes = R.string.privacy_policy,
+ onClick = { onClickPrivacyPolicy() }
+ )
+ Divider()
+ SettingHeader(titleRes = R.string.login_info)
+ SettingContent(
+ titleRes = R.string.logout,
+ onClick = { onClickLogout() }
+ )
+ SettingContent(
+ titleRes = R.string.delete_account,
+ onClick = { onClickDeleteAccount() }
+ )
+ }
+ }
+}
+
+@Composable
+fun SettingHeader(
+ @StringRes titleRes: Int,
+ modifier: Modifier = Modifier
+) {
+ Text(
+ text = stringResource(id = titleRes),
+ modifier = modifier
+ .wrapContentWidth()
+ .wrapContentHeight()
+ .padding(start = 24.dp, top = 18.dp),
+ style = MissionMateTypography.body_lg_regular,
+ color = ColorGray3_FF727484
+ )
+}
+
+@Composable
+fun SettingContent(
+ @StringRes titleRes: Int,
+ modifier: Modifier = Modifier,
+ onClick: (() -> Unit)? = null,
+ subContent: (@Composable () -> Unit)? = null
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .run {
+ onClick?.let {
+ clickable { it() }
+ } ?: run { this }
+ },
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Spacer(modifier = modifier.width(24.dp))
+ Text(
+ text = stringResource(id = titleRes),
+ style = MissionMateTypography.body_xl_regular,
+ color = ColorGray1_FF404249,
+ modifier = modifier
+ .wrapContentHeight()
+ .weight(1f)
+ .padding(vertical = 16.dp)
+ )
+ onClick?.let {
+ Image(
+ imageVector = ImageVector.vectorResource(id = com.goalpanzi.mission_mate.core.designsystem.R.drawable.ic_arrow_right),
+ contentDescription = ""
+ )
+ }
+ subContent?.let {
+ it()
+ }
+ Spacer(modifier = modifier.width(24.dp))
+ }
+}
+
+@Composable
+fun Divider(
+ modifier: Modifier = Modifier
+) {
+ Box(
+ modifier = modifier
+ .padding(horizontal = 24.dp)
+ .fillMaxWidth()
+ .height(1.dp)
+ .background(ColorGray4_FFE5E5E5)
+ .padding(bottom = 10.dp)
+ )
+}
+
+@Composable
+fun LogoutDialog(
+ onDismissRequest: () -> Unit,
+ onClickOk: () -> Unit
+) {
+ MissionMateDialog(
+ titleId = R.string.confirm_logout,
+ onDismissRequest = onDismissRequest,
+ onClickOk = onClickOk,
+ okTextId = R.string.logout,
+ cancelTextId = R.string.cancel
+ )
+}
+
+@Composable
+fun AccountDeleteDialog(
+ onDismissRequest: () -> Unit,
+ onClickOk: () -> Unit
+) {
+ MissionMateDialog(
+ titleId = R.string.confirm_delete_account,
+ onDismissRequest = onDismissRequest,
+ descriptionId = R.string.confirm_delete_account_content,
+ onClickOk = onClickOk,
+ okTextId = R.string.require_delete_account,
+ cancelTextId = R.string.cancel
+ )
+}
+
+@Preview
+@Composable
+fun SettingScreenPreview() {
+ SettingScreen(
+ onBackClick = {},
+ onClickProfileSetting = {},
+ onClickServicePolicy = {},
+ onClickPrivacyPolicy = {},
+ onClickLogout = {},
+ onClickDeleteAccount = {}
+ )
+}
\ No newline at end of file
diff --git a/feature/setting/src/main/java/com/goalpanzi/mission_mate/feature/setting/screen/SettingViewModel.kt b/feature/setting/src/main/java/com/goalpanzi/mission_mate/feature/setting/screen/SettingViewModel.kt
new file mode 100644
index 00000000..9471dc05
--- /dev/null
+++ b/feature/setting/src/main/java/com/goalpanzi/mission_mate/feature/setting/screen/SettingViewModel.kt
@@ -0,0 +1,39 @@
+package com.goalpanzi.mission_mate.feature.setting.screen
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.goalpanzi.mission_mate.feature.setting.Event
+import com.goalpanzi.mission_mate.core.domain.usecase.AccountDeleteUseCase
+import com.goalpanzi.mission_mate.core.domain.usecase.LogoutUseCase
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class SettingViewModel @Inject constructor(
+ private val logoutUseCase: LogoutUseCase,
+ private val accountDeleteUseCase: AccountDeleteUseCase
+) : ViewModel() {
+
+ private val _event = MutableSharedFlow()
+ val event = _event.asSharedFlow()
+
+ fun logout() {
+ viewModelScope.launch {
+ logoutUseCase.invoke().collectLatest {
+ _event.emit(Event.GoToLogin)
+ }
+ }
+ }
+
+ fun deleteAccount() {
+ viewModelScope.launch {
+ accountDeleteUseCase.invoke().collectLatest {
+ _event.emit(Event.GoToLogin)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/feature/setting/src/main/java/com/goalpanzi/mission_mate/feature/setting/screen/WebViewScreen.kt b/feature/setting/src/main/java/com/goalpanzi/mission_mate/feature/setting/screen/WebViewScreen.kt
new file mode 100644
index 00000000..03146b73
--- /dev/null
+++ b/feature/setting/src/main/java/com/goalpanzi/mission_mate/feature/setting/screen/WebViewScreen.kt
@@ -0,0 +1,53 @@
+package com.goalpanzi.mission_mate.feature.setting.screen
+
+import android.view.ViewGroup
+import android.webkit.WebChromeClient
+import android.webkit.WebView
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.viewinterop.AndroidView
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF
+import com.goalpanzi.mission_mate.core.designsystem.theme.component.MissionMateTopAppBar
+import com.goalpanzi.mission_mate.core.designsystem.theme.component.NavigationType
+
+@Composable
+fun WebViewScreen(
+ onBackClick: () -> Unit,
+ url: String
+) {
+ Column(
+ modifier = Modifier
+ .background(ColorWhite_FFFFFFFF)
+ .statusBarsPadding()
+ ) {
+ MissionMateTopAppBar(
+ navigationType = NavigationType.BACK,
+ containerColor = ColorWhite_FFFFFFFF,
+ onNavigationClick = { onBackClick() }
+ )
+
+ AndroidView(
+ factory = {
+ WebView(it).apply {
+ layoutParams = ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT
+ )
+ settings.javaScriptEnabled = true
+ settings.domStorageEnabled = true
+ webChromeClient = CustomWebChromeClient()
+ }
+ }, update = {
+ it.loadUrl(url)
+ }
+ )
+ }
+
+}
+
+class CustomWebChromeClient : WebChromeClient() {
+ override fun onCloseWindow(window: WebView?) {}
+}
\ No newline at end of file
diff --git a/feature/setting/src/main/res/values/strings.xml b/feature/setting/src/main/res/values/strings.xml
new file mode 100644
index 00000000..ef1de721
--- /dev/null
+++ b/feature/setting/src/main/res/values/strings.xml
@@ -0,0 +1,22 @@
+
+
+ 설정
+ 내정보
+ 프로필 수정하기
+ 버전정보
+ 현재버전
+ 고객지원
+ 문의하기
+ 약관 및 정책
+ 서비스 이용약관
+ 개인정보처리방침
+ 로그인 정보
+ 로그아웃
+ 계정탈퇴
+ 로그아웃\n하시겠습니까?
+ 취소
+ 계정탈퇴\n하시겠습니까?
+ 탈퇴하면 저장된 데이터가\n모두 초기화돼요.
+ 탈퇴하기
+ missionmateteam@gmail.com
+
\ No newline at end of file
diff --git a/feature/setting/src/test/java/com/goalpanzi/feature/setting/ExampleUnitTest.kt b/feature/setting/src/test/java/com/goalpanzi/feature/setting/ExampleUnitTest.kt
new file mode 100644
index 00000000..5adf66d0
--- /dev/null
+++ b/feature/setting/src/test/java/com/goalpanzi/feature/setting/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.goalpanzi.feature.setting
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 2b2125b5..224dff18 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,6 +1,6 @@
[versions]
## Android gradle plugin
-agp = "8.4.0"
+agp = "8.4.2"
## Kotlin
kotlin = "2.0.0"
@@ -11,9 +11,9 @@ coroutine = "1.9.0-RC"
## AndroidX
androidxCoreKtx = "1.13.1"
-androidxLifecycleKtx = "2.8.3"
+androidxLifecycleKtx = "2.8.4"
androidxAppcompat = "1.7.0"
-androidxActivity = "1.9.0"
+androidxActivity = "1.9.1"
## Compose
composeBom = "2024.06.00"
@@ -30,7 +30,7 @@ kotest = "5.9.0"
mockk = "1.13.11"
## Hilt
-hilt = "2.51"
+hilt = "2.51.1"
hilt-navigation-compose = "1.2.0"
## Network
@@ -43,6 +43,21 @@ dataStore-preferences = "1.1.1"
## Image Loader
coil-compose = "2.5.0"
+## Google OAuth
+credential = "1.2.2"
+identity = "1.1.1"
+material = "1.12.0"
+
+## Google Service
+google-service = "4.4.2"
+firebase = "33.1.2"
+
+## Lottie
+lottie-compose = "6.5.0"
+
+# ETC
+balloon = "1.6.6"
+
[libraries]
## Koitln
kotlin-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
@@ -69,7 +84,7 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
-androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" }
+androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation-compose" }
## Test
junit = { group = "junit", name = "junit", version.ref = "junit" }
@@ -97,6 +112,21 @@ dataStore = { module = "androidx.datastore:datastore-preferences", version.ref =
## Image Loader
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil-compose" }
+## Google OAuth
+credentials = { group = "androidx.credentials", name = "credentials", version.ref = "credential" }
+credentials-auth = { group = "androidx.credentials", name = "credentials-play-services-auth", version.ref = "credential" }
+google-id = { group = "com.google.android.libraries.identity.googleid", name = "googleid", version = "identity" }
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+
+## Firebase
+firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebase" }
+
+## Lottie
+lottie-compose = { group = "com.airbnb.android", name = "lottie-compose", version.ref = "lottie-compose"}
+
+## ETC
+balloon = { group = "com.github.skydoves", name = "balloon", version.ref = "balloon" }
+
[plugins]
## Android gradle plugin
android-application = { id = "com.android.application", version.ref = "agp" }
@@ -107,6 +137,7 @@ jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref =
kotlin-plugin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
+kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
## KSP
kotlin-ksp = { id = "com.google.devtools.ksp" , version.ref = "ksp"}
@@ -114,6 +145,9 @@ kotlin-ksp = { id = "com.google.devtools.ksp" , version.ref = "ksp"}
## Hilt
hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
+## Google Service
+google-service = { id = "com.google.gms.google-services", version.ref = "google-service" }
+
[bundles]
coroutines = ["coroutines-core", "coroutines-android"]
lifecycle = ["androidx-lifecycle-runtime-ktx","androidx-lifecycle-runtime-compose","androidx-lifecycle-viewmodel-compose"]
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 1789af6b..60df1575 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,4 +1,4 @@
-#Sat Jul 13 15:21:07 KST 2024
+#Sat Jul 20 15:16:45 KST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 1808e810..aa7f0e9d 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -30,3 +30,7 @@ include(":core:navigation")
include(":feature:login")
include(":feature:main")
include(":feature:board")
+include(":core:model")
+include(":feature:onboarding")
+include(":feature:profile")
+include(":feature:setting")