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")