diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 00000000..4ef10f8b --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,24 @@ +coverage: + status: + project: + default: + target: 60% + threshold: 2% + +comment: + layout: "reach, diff, flags, files" + behavior: default + require_changes: true + +parsers: + gcov: + branch_detection: + conditional: true + loop: true + method: true + macro: true + +ignore: + - "**/di/**" + - "**/BuildConfig.*" + - "**/generated/**" diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 00000000..e31fc13e --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,19 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +language: "ko-KR" +early_access: false +reviews: + profile: "chill" + request_changes_workflow: true + high_level_summary: true + poem: false + review_status: true + collapse_walkthrough: false + abort_on_close: true + auto_review: + enabled: true + drafts: false + finishing_touches: + unit_tests: + enabled: true +chat: + auto_reply: true diff --git a/.github/workflows/android_cd.yml b/.github/workflows/android_cd.yml index c00a6bba..af61cefd 100644 --- a/.github/workflows/android_cd.yml +++ b/.github/workflows/android_cd.yml @@ -3,10 +3,10 @@ name: Orbit CD on: push: branches: - - main + - 'release/**' pull_request: branches: - - main + - 'release/**' jobs: cd: @@ -14,11 +14,11 @@ jobs: runs-on: ubuntu-latest steps: - # 1. Code Checkout + # 1. Checkout - name: Checkout code uses: actions/checkout@v4 - # 2. Gradle Cache + # 2. Cache Gradle - name: Cache Gradle dependencies uses: actions/cache@v4 with: @@ -29,7 +29,7 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- - # 3. JDK 17 + # 3. Set up JDK 17 - name: Set up JDK 17 uses: actions/setup-java@v4 with: @@ -37,131 +37,74 @@ jobs: distribution: 'corretto' cache: gradle - # 4. Grant Execute Permission + # 4. Change gradlew permissions - name: Change gradlew permissions run: chmod +x gradlew - # 5. Install Firebase CLI - - name: Install Firebase CLI - run: curl -sL https://firebase.tools | bash - - # 6. Decode google-services.json for debug - - name: Decode google-services.json (debug) - env: - FIREBASE_SECRET: ${{ secrets.FIREBASE_SECRET_DEBUG }} - run: | - mkdir -p app/src/dev - echo $FIREBASE_SECRET | base64 --decode > app/src/dev/google-services.json - - # 7. Decode google-services.json for release + # 5. Decode google-services.json (release) - name: Decode google-services.json (release) env: - FIREBASE_SECRET: ${{ secrets.FIREBASE_SECRET_RELEASE }} + FIREBASE_SECRET: ${{ secrets.FIREBASE_SECRET_RELEASE }} run: echo $FIREBASE_SECRET | base64 --decode > app/google-services.json - # 8. Add Local Properties + # 6. Add Local Properties - name: Add Local Properties env: BASE_URL: ${{ secrets.BASE_URL }} AMPLITUDE_API_KEY: ${{ secrets.AMPLITUDE_API_KEY }} - ADMOB_APP_ID_DEBUG: ${{ secrets.ADMOB_APP_ID_DEBUG }} ADMOB_APP_ID_RELEASE: ${{ secrets.ADMOB_APP_ID_RELEASE }} - ADMOB_AD_UNIT_ID_DEBUG: ${{ secrets.ADMOB_AD_UNIT_ID_DEBUG }} ADMOB_AD_UNIT_ID_RELEASE: ${{ secrets.ADMOB_AD_UNIT_ID_RELEASE }} run: | - echo -e "baseUrl=$BASE_URL" > local.properties - echo -e "amplitudeApiKey=$AMPLITUDE_API_KEY" >> local.properties - echo -e "admobAppIdDebug=$ADMOB_APP_ID_DEBUG" >> local.properties - echo -e "admobAppIdRelease=$ADMOB_APP_ID_RELEASE" >> local.properties - echo -e "admobAdUnitIdDebug=$ADMOB_AD_UNIT_ID_DEBUG" >> local.properties - echo -e "admobAdUnitIdRelease=$ADMOB_AD_UNIT_ID_RELEASE" >> local.properties - - # 9. Debug Local Properties Check - - name: Debug Local Properties - run: cat local.properties - - # 10. Ktlint - - name: Run Ktlint Check - run: ./gradlew ktlintCheck --stacktrace - - # 11. Debug APK Build - - name: Build Debug APK - run: ./gradlew assembleDebug --stacktrace - - # 12. Release AAB Build - - name: Build Release AAB - run: ./gradlew bundleRelease --stacktrace + echo "baseUrl=$BASE_URL" > local.properties + echo "amplitudeApiKey=$AMPLITUDE_API_KEY" >> local.properties + echo "admobAppIdRelease=$ADMOB_APP_ID_RELEASE" >> local.properties + echo "admobAdUnitIdRelease=$ADMOB_AD_UNIT_ID_RELEASE" >> local.properties - # 13. Release APK Build + # 7. Build Release APK - name: Build Release APK run: ./gradlew assembleRelease --stacktrace - # 14. AAB Artifact Upload - - name: Upload Release AAB - uses: actions/upload-artifact@v4 - with: - name: release-aab - path: app/build/outputs/bundle/release/app-release.aab - - # 15. APK Artifact Upload + # 8. Upload Release APK - name: Upload Release APK uses: actions/upload-artifact@v4 with: name: release-apk path: app/build/outputs/apk/release/app-release.apk - # 16. Set up Firebase Service Account Credentials - - name: Set up Firebase Service Account Credentials + # 9. Set up Firebase Credentials + - name: Set up Firebase Credentials env: GOOGLE_APPLICATION_CREDENTIALS_JSON: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS_JSON }} run: | echo "$GOOGLE_APPLICATION_CREDENTIALS_JSON" | base64 --decode > $HOME/firebase-credentials.json - echo "๐Ÿ”ฅ Firebase Credentials JSON ์ƒ์„ฑ ์™„๋ฃŒ!" - ls -l $HOME/firebase-credentials.json - export GOOGLE_APPLICATION_CREDENTIALS=$HOME/firebase-credentials.json - echo "GOOGLE_APPLICATION_CREDENTIALS=$GOOGLE_APPLICATION_CREDENTIALS" - - # 17. Firebase CLI ์ธ์ฆ ํ™•์ธ - - name: Check Firebase CLI Authentication - run: | - export GOOGLE_APPLICATION_CREDENTIALS=$HOME/firebase-credentials.json + echo "GOOGLE_APPLICATION_CREDENTIALS=$HOME/firebase-credentials.json" >> $GITHUB_ENV - echo "๐Ÿ“Œ GOOGLE_APPLICATION_CREDENTIALS ์„ค์ • ๊ฐ’:" - echo $GOOGLE_APPLICATION_CREDENTIALS - ls -l $GOOGLE_APPLICATION_CREDENTIALS - - echo "๐Ÿ“Œ ํ˜„์žฌ Firebase ํ”„๋กœ์ ํŠธ ๋ชฉ๋ก ํ™•์ธ:" - firebase projects:list || (echo "โŒ Firebase ์ธ์ฆ ์‹คํŒจ!"; exit 1) + # 10. Install Firebase CLI + - name: Install Firebase CLI + run: curl -sL https://firebase.tools | bash - # 18. Firebase App Distribution Upload + # 11. Upload to Firebase App Distribution - name: Upload APK to Firebase App Distribution env: - GOOGLE_APPLICATION_CREDENTIALS: $HOME/firebase-credentials.json + GOOGLE_APPLICATION_CREDENTIALS: ${{ env.GOOGLE_APPLICATION_CREDENTIALS }} FIREBASE_APP_ID: ${{ secrets.FIREBASE_APP_ID }} run: | - echo "๐Ÿ”ฅ FIREBASE_APP_ID ํ™•์ธ: $FIREBASE_APP_ID" - - # ๋งŒ์•ฝ FIREBASE_APP_ID๊ฐ€ ์—†์œผ๋ฉด ์—๋Ÿฌ ์ถœ๋ ฅ ํ›„ ์ข…๋ฃŒ if [ -z "$FIREBASE_APP_ID" ]; then - echo "โŒ ERROR: FIREBASE_APP_ID๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. GitHub Secrets์—์„œ ํ™•์ธํ•˜์„ธ์š”." + echo "โŒ ERROR: FIREBASE_APP_ID is missing!" exit 1 fi - # GOOGLE_APPLICATION_CREDENTIALS๋ฅผ ๋‹ค์‹œ ์„ค์ • - export GOOGLE_APPLICATION_CREDENTIALS=$HOME/firebase-credentials.json - echo "GOOGLE_APPLICATION_CREDENTIALS=$GOOGLE_APPLICATION_CREDENTIALS" - firebase appdistribution:distribute app/build/outputs/apk/release/app-release.apk \ - --app "$FIREBASE_APP_ID" \ - --release-notes "๐Ÿš€ ์ƒˆ๋กœ์šด ๋ฐ๋ชจ ๋ฒ„์ „์ด ๋ฐฐํฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!" \ - --groups "orbit-tester-group" + --app "$FIREBASE_APP_ID" \ + --release-notes "๐Ÿš€ release ๋ธŒ๋žœ์น˜์—์„œ ์ƒˆ ๋นŒ๋“œ๊ฐ€ ์—…๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!" \ + --groups "orbit-tester-group" - # 19. Notify Discord + # 12. Notify Discord - name: Notify Discord env: DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} run: | curl -H "Content-Type: application/json" \ - -X POST \ - -d '{"content": "๐Ÿš€ ์ƒˆ๋กœ์šด ๋ฐ๋ชจ ๋ฒ„์ „์ด Firebase App Distribution์— ์—…๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!\nAPK ๋‹ค์šด๋กœ๋“œ: https://appdistribution.firebase.google.com"}' \ - $DISCORD_WEBHOOK_URL + -X POST \ + -d '{"content": "๐Ÿš€ Firebase App Distribution์— ์ƒˆ APK๊ฐ€ ์—…๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!\n๐Ÿ”— ๋‹ค์šด๋กœ๋“œ ๋งํฌ: https://appdistribution.firebase.google.com"}' \ + $DISCORD_WEBHOOK_URL diff --git a/.github/workflows/android_ci.yml b/.github/workflows/android_ci.yml index b8974a0d..3e073b75 100644 --- a/.github/workflows/android_ci.yml +++ b/.github/workflows/android_ci.yml @@ -1,12 +1,12 @@ name: Orbit CI on: - pull_request: - branches: [develop] - paths: - - 'app/**' - - 'build.gradle' - - '**/*.kt' + pull_request: + branches: [develop] + paths: + - '**/*.kt' + - 'build.gradle' + - 'app/**' jobs: build: @@ -80,3 +80,16 @@ jobs: # Run Lint and Build - name: Run lint and build run: ./gradlew ktlintCheck assembleDebug + + # Run Unit Test and Generate Coverage + - name: Run unit tests and generate coverage + run: ./gradlew generateTestCoverageReport + + # Upload Coverage to Codecov + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: data/build/reports/jacoco/testDebugUnitTestCoverage/testDebugUnitTestCoverage.xml + name: codecov-report + fail_ci_if_error: true diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4acc31ae..6c350ede 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -8,11 +8,12 @@ plugins { android { namespace = "com.yapp.orbit" + compileSdk = 35 defaultConfig { - versionCode = 5 - versionName = "1.0.3" - targetSdk = 34 + versionCode = 6 + versionName = "1.1.3" + targetSdk = 35 } buildTypes { @@ -29,22 +30,30 @@ android { dependencies { implementation(projects.core.common) + implementation(projects.core.analytics) implementation(projects.core.buildconfig) implementation(projects.core.network) implementation(projects.core.designsystem) implementation(projects.core.datastore) implementation(projects.core.alarm) implementation(projects.core.media) + implementation(projects.core.ui) implementation(projects.data) implementation(projects.domain) + implementation(projects.feature.splash) implementation(projects.feature.onboarding) implementation(projects.feature.home) implementation(projects.feature.alarmInteraction) implementation(projects.feature.fortune) implementation(projects.feature.mission) implementation(projects.feature.setting) - implementation(projects.feature.navigator) + implementation(projects.feature.webview) + implementation(platform(libs.firebase.bom)) + implementation(libs.compose.material) implementation(libs.firebase.analytics) implementation(libs.firebase.crashlytics) implementation(libs.play.services.ads) + implementation(libs.kotlin.reflect) + implementation(libs.hilt.worker) + implementation(libs.androidx.work.runtime) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 51ec3303..e7abd75a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,6 +11,7 @@ + + tools:targetApi="33"> - + @@ -79,5 +79,15 @@ + + + + diff --git a/feature/navigator/src/main/java/com/yapp/navigator/MainActivity.kt b/app/src/main/java/com/yapp/orbit/MainActivity.kt similarity index 74% rename from feature/navigator/src/main/java/com/yapp/navigator/MainActivity.kt rename to app/src/main/java/com/yapp/orbit/MainActivity.kt index 7728e44f..35358445 100644 --- a/feature/navigator/src/main/java/com/yapp/navigator/MainActivity.kt +++ b/app/src/main/java/com/yapp/orbit/MainActivity.kt @@ -1,10 +1,9 @@ -package com.yapp.navigator +package com.yapp.orbit import android.annotation.SuppressLint import android.content.pm.ActivityInfo import android.os.Bundle import androidx.activity.ComponentActivity -import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.runtime.CompositionLocalProvider @@ -25,16 +24,7 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT - enableEdgeToEdge( - statusBarStyle = SystemBarStyle.light( - android.graphics.Color.TRANSPARENT, - android.graphics.Color.TRANSPARENT, - ), - navigationBarStyle = SystemBarStyle.light( - android.graphics.Color.BLACK, - android.graphics.Color.BLACK, - ), - ) + enableEdgeToEdge() setContent { val navigator = rememberOrbitNavigator() diff --git a/app/src/main/java/com/yapp/orbit/OrbitApplication.kt b/app/src/main/java/com/yapp/orbit/OrbitApplication.kt index 7391cf6e..b06538f8 100644 --- a/app/src/main/java/com/yapp/orbit/OrbitApplication.kt +++ b/app/src/main/java/com/yapp/orbit/OrbitApplication.kt @@ -1,13 +1,24 @@ package com.yapp.orbit import android.app.Application +import androidx.hilt.work.HiltWorkerFactory +import androidx.work.Configuration import com.google.android.gms.ads.MobileAds import dagger.hilt.android.HiltAndroidApp +import javax.inject.Inject @HiltAndroidApp -class OrbitApplication : Application() { +class OrbitApplication() : Application(), Configuration.Provider { + + @Inject lateinit var workerFactory: HiltWorkerFactory + override fun onCreate() { super.onCreate() MobileAds.initialize(this) } + + override val workManagerConfiguration: Configuration + get() = Configuration.Builder() + .setWorkerFactory(workerFactory) + .build() } diff --git a/feature/navigator/src/main/java/com/yapp/navigator/OrbitNavHost.kt b/app/src/main/java/com/yapp/orbit/OrbitNavHost.kt similarity index 63% rename from feature/navigator/src/main/java/com/yapp/navigator/OrbitNavHost.kt rename to app/src/main/java/com/yapp/orbit/OrbitNavHost.kt index 082c2ca5..5dd9ce4b 100644 --- a/feature/navigator/src/main/java/com/yapp/navigator/OrbitNavHost.kt +++ b/app/src/main/java/com/yapp/orbit/OrbitNavHost.kt @@ -1,4 +1,4 @@ -package com.yapp.navigator +package com.yapp.orbit import android.annotation.SuppressLint import androidx.compose.animation.AnimatedVisibility @@ -6,6 +6,7 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold @@ -25,6 +26,9 @@ import com.yapp.mission.missionScreen import com.yapp.onboarding.onboardingNavGraph import com.yapp.setting.settingNavGraph import com.yapp.splash.splashScreen +import com.yapp.ui.component.bottomsheet.OrbitBottomSheetState +import com.yapp.ui.component.bottomsheet.rememberOrbitBottomSheetState +import com.yapp.ui.component.navigation.NavigationBarScrim import com.yapp.ui.component.snackbar.CustomSnackBarVisuals import com.yapp.ui.component.snackbar.OrbitSnackBar import com.yapp.webview.webViewScreen @@ -34,35 +38,45 @@ import com.yapp.webview.webViewScreen internal fun OrbitNavHost( modifier: Modifier = Modifier, navigator: OrbitNavigator = rememberOrbitNavigator(), + bottomSheetState: OrbitBottomSheetState = rememberOrbitBottomSheetState(), ) { val snackBarHostState = remember { SnackbarHostState() } - Scaffold( - modifier = modifier, - snackbarHost = { - OrbitSnackBarHost(snackBarHostState = snackBarHostState) - }, - containerColor = OrbitTheme.colors.gray_900, - ) { - NavHost( - navController = navigator.navController, - startDestination = navigator.startDestination, - modifier = Modifier.navigationBarsPadding(), + Box { + Scaffold( + modifier = modifier, + snackbarHost = { OrbitSnackBarHost(snackBarHostState) }, + containerColor = OrbitTheme.colors.gray_900, ) { - splashScreen(navigator = navigator) - onboardingNavGraph(navigator = navigator) - homeNavGraph( - navigator = navigator, - snackBarHostState = snackBarHostState, - ) - missionScreen(navigator = navigator) - fortuneNavGraph( + OrbitNavigationGraph( navigator = navigator, + bottomSheetState = bottomSheetState, snackBarHostState = snackBarHostState, ) - settingNavGraph(navigator = navigator) - webViewScreen(navigator = navigator) } + + NavigationBarScrim() + } +} + +@Composable +private fun OrbitNavigationGraph( + navigator: OrbitNavigator, + bottomSheetState: OrbitBottomSheetState, + snackBarHostState: SnackbarHostState, +) { + NavHost( + modifier = Modifier.navigationBarsPadding(), + navController = navigator.navController, + startDestination = navigator.startDestination, + ) { + splashScreen(navigator) + onboardingNavGraph(navigator, bottomSheetState) + homeNavGraph(navigator, bottomSheetState, snackBarHostState) + missionScreen(navigator) + fortuneNavGraph(navigator, snackBarHostState) + settingNavGraph(navigator) + webViewScreen(navigator) } } @@ -73,9 +87,7 @@ private fun OrbitSnackBarHost( AnimatedVisibility( visible = snackBarHostState.currentSnackbarData != null, enter = slideInVertically(initialOffsetY = { it }) + fadeIn(), - exit = slideOutVertically( - targetOffsetY = { it }, - ) + fadeOut(), + exit = slideOutVertically(targetOffsetY = { it }) + fadeOut(), ) { SnackbarHost( hostState = snackBarHostState, @@ -88,9 +100,9 @@ private fun OrbitSnackBarHost( end = 20.dp, bottom = visuals?.bottomPadding ?: 12.dp, ), - label = visuals?.actionLabel ?: "", + label = visuals?.actionLabel.orEmpty(), iconRes = visuals?.iconRes, - message = visuals?.message ?: "", + message = visuals?.message.orEmpty(), onAction = { snackBarHostState.currentSnackbarData?.performAction() }, ) }, diff --git a/app/src/main/java/com/yapp/orbit/di/AppVersionModule.kt b/app/src/main/java/com/yapp/orbit/di/AppVersionModule.kt new file mode 100644 index 00000000..6297ecc3 --- /dev/null +++ b/app/src/main/java/com/yapp/orbit/di/AppVersionModule.kt @@ -0,0 +1,18 @@ +package com.yapp.orbit.di + +import com.yapp.orbit.BuildConfig +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Named +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object AppVersionModule { + @Provides + @Singleton + @Named("appVersion") + fun provideAppVersion(): String = BuildConfig.VERSION_NAME +} diff --git a/build-logic/src/main/java/com/yapp/convention/ComposeAndroid.kt b/build-logic/src/main/java/com/yapp/convention/ComposeAndroid.kt index 64edd3f7..f705f061 100644 --- a/build-logic/src/main/java/com/yapp/convention/ComposeAndroid.kt +++ b/build-logic/src/main/java/com/yapp/convention/ComposeAndroid.kt @@ -16,6 +16,8 @@ internal fun Project.configureComposeAndroid() { val bom = libs.findLibrary("compose.bom").get() add("implementation", platform(bom)) + add("implementation", libs.findLibrary("activity.compose").get()) + add("implementation", libs.findLibrary("compose.material3").get()) add("implementation", libs.findLibrary("compose.ui").get()) add("implementation", libs.findLibrary("compose.ui.tooling.preview").get()) diff --git a/build-logic/src/main/java/com/yapp/convention/HiltAndroid.kt b/build-logic/src/main/java/com/yapp/convention/HiltAndroid.kt index 6ccba65c..15db09ff 100644 --- a/build-logic/src/main/java/com/yapp/convention/HiltAndroid.kt +++ b/build-logic/src/main/java/com/yapp/convention/HiltAndroid.kt @@ -14,6 +14,9 @@ internal fun Project.configureHiltAndroid() { dependencies { "implementation"(libs.findLibrary("hilt.android").get()) "ksp"(libs.findLibrary("hilt.android.compiler").get()) + "ksp"(libs.findLibrary("androidx-hilt-compiler").get()) + "implementation"(libs.findLibrary("hilt-navigation-compose").get()) + "implementation"(libs.findLibrary("hilt-worker").get()) } } diff --git a/build-logic/src/main/java/com/yapp/convention/TestAndroid.kt b/build-logic/src/main/java/com/yapp/convention/TestAndroid.kt index 9fe992f6..e4fcee6b 100644 --- a/build-logic/src/main/java/com/yapp/convention/TestAndroid.kt +++ b/build-logic/src/main/java/com/yapp/convention/TestAndroid.kt @@ -1,16 +1,33 @@ package com.yapp.convention import org.gradle.api.Project +import org.gradle.kotlin.dsl.dependencies internal fun Project.configureTestAndroid() { configureJUnitAndroid() + // feature ๋ชจ๋“ˆ์—๋งŒ UI ํ…Œ์ŠคํŠธ ๊ด€๋ จ ์„ค์ • ์ ์šฉ + if (path.startsWith(":feature:")) { + configureComposeUiTest() + } +} + +internal fun Project.configureComposeUiTest() { + val libs = extensions.libs + dependencies { + "androidTestImplementation"(libs.findLibrary("compose-ui-test-junit4").get()) + "debugImplementation"(libs.findLibrary("compose-ui-test-manifest").get()) + } } @Suppress("UnstableApiUsage") internal fun Project.configureJUnitAndroid() { androidExtension.apply { - testOptions { - unitTests.all { it.useJUnitPlatform() } + defaultConfig { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } + + val libs = extensions.libs + dependencies { + "androidTestImplementation"(libs.findLibrary("androidx-test-ext-junit").get()) + "androidTestImplementation"(libs.findLibrary("androidx-test-runner").get()) } } } diff --git a/build-logic/src/main/java/com/yapp/convention/TestCoverage.kt b/build-logic/src/main/java/com/yapp/convention/TestCoverage.kt new file mode 100644 index 00000000..70510798 --- /dev/null +++ b/build-logic/src/main/java/com/yapp/convention/TestCoverage.kt @@ -0,0 +1,58 @@ +package com.yapp.convention + +import com.android.build.api.dsl.ApplicationExtension +import com.android.build.api.dsl.LibraryExtension +import org.gradle.api.Project +import org.gradle.api.tasks.testing.Test +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.withType +import org.gradle.testing.jacoco.plugins.JacocoPluginExtension +import org.gradle.testing.jacoco.plugins.JacocoTaskExtension +import org.gradle.testing.jacoco.tasks.JacocoReport + +internal fun Project.configureTestCoverage() { + pluginManager.apply("jacoco") + + val libs = extensions.libs + extensions.configure { + toolVersion = libs.findVersion("jacoco").get().toString() + } + + // ๋ชจ๋“  ์œ ๋‹› ํ…Œ์ŠคํŠธ์— Jacoco ์„ค์ • ์ ์šฉ + tasks.withType().configureEach { + extensions.configure { + isIncludeNoLocationClasses = true + excludes = listOf("jdk.internal.*") + } + } + + // Android ๋ชจ๋“ˆ์ด๋ฉด ์ปค๋ฒ„๋ฆฌ์ง€ ์„ค์ • ์ถ”๊ฐ€ + extensions.findByType(ApplicationExtension::class.java)?.buildTypes?.configureEach { + enableUnitTestCoverage = true + } + + extensions.findByType(LibraryExtension::class.java)?.buildTypes?.configureEach { + enableUnitTestCoverage = true + } + + // ์ปค๋ฒ„๋ฆฌ์ง€ ๋ฆฌํฌํŠธ Task ๋“ฑ๋ก + tasks.register("generateTestCoverageReport") { + group = "verification" + description = "Run unit tests and generate coverage report." + + dependsOn("testDebugUnitTest") + dependsOn("createDebugUnitTestCoverageReport") + } + + // .exec ํŒŒ์ผ ์—†์„ ๊ฒฝ์šฐ createDebugUnitTestCoverageReport task ์Šคํ‚ต + tasks.matching { it.name == "createDebugUnitTestCoverageReport" }.configureEach { + onlyIf { + val execFile = layout.buildDirectory + .file("outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec") + .get().asFile + execFile.exists() + } + + (this as? JacocoReport)?.reports?.xml?.required?.set(true) + } +} diff --git a/build-logic/src/main/java/com/yapp/convention/TestKotlin.kt b/build-logic/src/main/java/com/yapp/convention/TestKotlin.kt index 3b7d98c7..790833c0 100644 --- a/build-logic/src/main/java/com/yapp/convention/TestKotlin.kt +++ b/build-logic/src/main/java/com/yapp/convention/TestKotlin.kt @@ -1,23 +1,16 @@ package com.yapp.convention import org.gradle.api.Project -import org.gradle.api.tasks.testing.Test import org.gradle.kotlin.dsl.dependencies -import org.gradle.kotlin.dsl.withType -internal fun Project.configureTest() { - configureJUnit() +internal fun Project.configureTestKotlin() { val libs = extensions.libs dependencies { + // JUnit4 ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ "testImplementation"(libs.findLibrary("junit4").get()) - "testImplementation"(libs.findLibrary("junit-jupiter").get()) - "testImplementation"(libs.findLibrary("coroutines-test").get()) + // ์ฝ”๋ฃจํ‹ด ๊ด€๋ จ ํ…Œ์ŠคํŠธ ๋„๊ตฌ (TestCoroutineScope, runTest ๋“ฑ..) + "testImplementation"(libs.findLibrary("kotlinx-coroutines-test").get()) + // Kotlin ๊ธฐ๋ฐ˜ mock ๊ฐ์ฒด ์ƒ์„ฑ, ํ–‰์œ„ ๊ฒ€์ฆ "testImplementation"(libs.findLibrary("mockk").get()) } } - -internal fun Project.configureJUnit() { - tasks.withType().configureEach { - useJUnitPlatform() - } -} diff --git a/build-logic/src/main/java/orbit.android.feature.gradle.kts b/build-logic/src/main/java/orbit.android.feature.gradle.kts index d9568c85..47d5c072 100644 --- a/build-logic/src/main/java/orbit.android.feature.gradle.kts +++ b/build-logic/src/main/java/orbit.android.feature.gradle.kts @@ -1,4 +1,3 @@ -import com.yapp.convention.configureHiltAndroid import com.yapp.convention.libs plugins { @@ -6,20 +5,11 @@ plugins { id("orbit.android.compose") } -android { - defaultConfig { - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - } -} - -configureHiltAndroid() - dependencies { implementation(project(":core:designsystem")) implementation(project(":core:ui")) val libs = project.extensions.libs - implementation(libs.findLibrary("hilt-navigation-compose").get()) implementation(libs.findLibrary("compose-navigation").get()) implementation(libs.findLibrary("lifecycle-viewmodel").get()) implementation(libs.findLibrary("lifecycle-runtime").get()) diff --git a/build-logic/src/main/java/orbit.android.library.gradle.kts b/build-logic/src/main/java/orbit.android.library.gradle.kts index 3ee18d12..f63f2be4 100644 --- a/build-logic/src/main/java/orbit.android.library.gradle.kts +++ b/build-logic/src/main/java/orbit.android.library.gradle.kts @@ -1,6 +1,9 @@ import com.yapp.convention.configureCoroutine import com.yapp.convention.configureHiltAndroid import com.yapp.convention.configureKotlinAndroid +import com.yapp.convention.configureTestAndroid +import com.yapp.convention.configureTestCoverage +import com.yapp.convention.configureTestKotlin plugins { id("com.android.library") @@ -9,3 +12,6 @@ plugins { configureKotlinAndroid() configureCoroutine() configureHiltAndroid() +configureTestAndroid() +configureTestKotlin() +configureTestCoverage() diff --git a/build.gradle.kts b/build.gradle.kts index 46e42947..3f1d3f7f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,12 +7,12 @@ plugins { alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.kotlin.serialization) apply false alias(libs.plugins.ksp) apply false + alias(libs.plugins.room) apply false alias(libs.plugins.hilt) apply false alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.google.service) apply false alias(libs.plugins.firebase.app.distribution) apply false alias(libs.plugins.firebase.crashlytics) apply false -// alias(libs.plugins.sentry) apply false } apply { diff --git a/core/alarm/build.gradle.kts b/core/alarm/build.gradle.kts index 0747052e..c5ab38e0 100644 --- a/core/alarm/build.gradle.kts +++ b/core/alarm/build.gradle.kts @@ -11,7 +11,7 @@ android { dependencies { implementation(projects.core.analytics) - implementation(projects.core.datastore) + implementation(projects.core.common) implementation(projects.core.designsystem) implementation(projects.core.media) implementation(projects.domain) diff --git a/core/alarm/src/main/java/com/yapp/alarm/AlarmConstants.kt b/core/alarm/src/main/java/com/yapp/alarm/AlarmConstants.kt index 67d359f1..3c2541bc 100644 --- a/core/alarm/src/main/java/com/yapp/alarm/AlarmConstants.kt +++ b/core/alarm/src/main/java/com/yapp/alarm/AlarmConstants.kt @@ -7,6 +7,8 @@ object AlarmConstants { const val ACTION_ALARM_INTERACTION_ACTIVITY_CLOSE = "com.yapp.orbit.ACTION_ALERT_INTERACTION_CLOSE" const val EXTRA_NOTIFICATION_ID = "com.yapp.orbit.EXTRA_NOTIFICATION_ID" + const val EXTRA_MISSION_TYPE = "com.yapp.orbit.EXTRA_MISSION_TYPE" + const val EXTRA_MISSION_COUNT = "com.yapp.orbit.EXTRA_MISSION_COUNT" const val EXTRA_ALARM = "com.yapp.orbit.EXTRA_ALARM" const val EXTRA_ALARM_DAY = "com.yapp.orbit.EXTRA_ALARM_DAY" @@ -16,8 +18,6 @@ object AlarmConstants { const val SNOOZE_ID_OFFSET = 10000 - const val WEEK_INTERVAL_MILLIS: Long = 7 * 24 * 60 * 60 * 1000 - val HOLIDAYS_2025 = setOf( "2025-01-01", "2025-01-27", "2025-01-28", "2025-01-29", "2025-01-30", "2025-03-01", "2025-03-03", "2025-05-05", "2025-05-06", "2025-06-06", diff --git a/core/alarm/src/main/java/com/yapp/alarm/AlarmHelper.kt b/core/alarm/src/main/java/com/yapp/alarm/AlarmHelper.kt deleted file mode 100644 index 4823278e..00000000 --- a/core/alarm/src/main/java/com/yapp/alarm/AlarmHelper.kt +++ /dev/null @@ -1,156 +0,0 @@ -package com.yapp.alarm - -import android.app.AlarmManager -import android.app.Application -import android.util.Log -import com.yapp.alarm.pendingIntent.schedule.createAlarmReceiverPendingIntentForSchedule -import com.yapp.alarm.pendingIntent.schedule.createAlarmReceiverPendingIntentForUnSchedule -import com.yapp.domain.model.Alarm -import com.yapp.domain.model.AlarmDay -import com.yapp.domain.model.toAlarmDays -import com.yapp.domain.model.toDayOfWeek -import java.time.Instant -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.format.DateTimeFormatter -import javax.inject.Inject - -class AlarmHelper @Inject constructor( - private val app: Application, - private val alarmManager: AlarmManager, -) { - fun scheduleAlarm(alarm: Alarm) { - val selectedDays = alarm.repeatDays.toAlarmDays() - - if (selectedDays.isEmpty()) { - setNonRepeatingAlarm(alarm) - } else { - selectedDays.forEach { day -> - setRepeatingAlarm(day, alarm) - } - } - } - - fun scheduleWeeklyAlarm(alarm: Alarm, day: AlarmDay) { - val initialTriggerMillis = getNextAlarmTimeMillis(alarm, day) + AlarmConstants.WEEK_INTERVAL_MILLIS - val triggerMillis = findNextNonHolidayDate(initialTriggerMillis) - - val pendingIntent = createAlarmReceiverPendingIntentForSchedule(app, alarm, day) - - alarmManager.setExactAndAllowWhileIdle( - AlarmManager.RTC_WAKEUP, - triggerMillis, - pendingIntent, - ) - - Log.d("AlarmHelper", "Scheduled weekly alarm for $day at: $triggerMillis") - } - - fun unScheduleAlarm(alarm: Alarm) { - val selectedDays = alarm.repeatDays.toAlarmDays() - - if (selectedDays.isEmpty()) { - val pendingIntent = createAlarmReceiverPendingIntentForUnSchedule( - app, - alarm, - null, - ) - alarmManager.cancel(pendingIntent) - } else { - selectedDays.forEach { day -> - val pendingIntent = createAlarmReceiverPendingIntentForUnSchedule( - app, - alarm, - day, - ) - alarmManager.cancel(pendingIntent) - } - } - } - - fun cancelSnoozedAlarm(alarmId: Long) { - val snoozedAlarmId = alarmId + AlarmConstants.SNOOZE_ID_OFFSET - val pendingIntent = createAlarmReceiverPendingIntentForUnSchedule(app, Alarm(id = snoozedAlarmId)) - alarmManager.cancel(pendingIntent) - Log.d("AlarmHelper", "Canceled snoozed alarm with id: $snoozedAlarmId") - } - - private fun setRepeatingAlarm(day: AlarmDay, alarm: Alarm) { - val alarmReceiverPendingIntent = - createAlarmReceiverPendingIntentForSchedule(app, alarm, day) - val firstAlarmTriggerMillis = getNextAlarmTimeMillis(alarm, day) - - Log.d("AlarmHelper", "Setting repeating alarm id: ${alarm.id} at: $firstAlarmTriggerMillis") - - alarmManager.setExactAndAllowWhileIdle( - AlarmManager.RTC_WAKEUP, - firstAlarmTriggerMillis, - alarmReceiverPendingIntent, - ) - } - - private fun setNonRepeatingAlarm(alarm: Alarm) { - val alarmReceiverPendingIntent = - createAlarmReceiverPendingIntentForSchedule(app, alarm) - - val triggerMillis = getNextAlarmTimeMillis(alarm, null) - - Log.d("AlarmHelper", "Setting one-time alarm at: $triggerMillis") - - alarmManager.setExactAndAllowWhileIdle( - AlarmManager.RTC_WAKEUP, - triggerMillis, - alarmReceiverPendingIntent, - ) - } - - private fun getNextAlarmTimeMillis(alarm: Alarm, day: AlarmDay?): Long { - val now = LocalDateTime.now().withNano(0) // ๋ฐ€๋ฆฌ์ดˆ ์ œ๊ฑฐํ•˜์—ฌ ์ •ํ™•ํ•œ ์ดˆ ๊ธฐ์ค€ ์„ค์ • - - val alarmHour = when { - alarm.isAm && alarm.hour == 12 -> 0 - !alarm.isAm && alarm.hour != 12 -> alarm.hour + 12 - else -> alarm.hour - } - - var alarmDateTime = now.withHour(alarmHour).withMinute(alarm.minute).withSecond(alarm.second) - - if (day != null) { - val targetDayOfWeek = day.toDayOfWeek() - while (alarmDateTime.dayOfWeek != targetDayOfWeek || alarmDateTime.isBefore(now)) { - alarmDateTime = alarmDateTime.plusDays(1) - } - } else { - if (alarmDateTime.isBefore(now)) { - alarmDateTime = alarmDateTime.plusDays(1) - } - } - - val epochMillis = alarmDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() - - Log.d("AlarmHelper", "Alarm scheduled at: $alarmDateTime (epochMillis=$epochMillis)") - - return epochMillis - } - - private fun findNextNonHolidayDate(initialMillis: Long): Long { - val dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") - - var adjustedMillis = initialMillis - - while (true) { - val localDate = Instant.ofEpochMilli(adjustedMillis) - .atZone(ZoneId.systemDefault()) - .toLocalDate() - - val dateString = localDate.format(dateFormatter) - - if (!AlarmConstants.HOLIDAYS_2025.contains(dateString)) { - return adjustedMillis // ๊ณตํœด์ผ์ด ์•„๋‹ˆ๋ผ๋ฉด ํ•ด๋‹น ๋‚ ์งœ ๋ฐ˜ํ™˜ - } - - // ๊ณตํœด์ผ์ด๋ผ๋ฉด ๋‹ค์Œ 1์ฃผ ๋’ค๋กœ ์ด๋™ - adjustedMillis += AlarmConstants.WEEK_INTERVAL_MILLIS - } - } -} diff --git a/core/alarm/src/main/java/com/yapp/alarm/AlarmModule.kt b/core/alarm/src/main/java/com/yapp/alarm/AlarmModule.kt deleted file mode 100644 index d126a638..00000000 --- a/core/alarm/src/main/java/com/yapp/alarm/AlarmModule.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.yapp.alarm - -import android.app.AlarmManager -import android.content.Context -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -object AlarmModule { - - @Provides - @Singleton - fun provideAlarmManager(@ApplicationContext context: Context): AlarmManager { - return context.getSystemService(Context.ALARM_SERVICE) as AlarmManager - } -} diff --git a/core/alarm/src/main/java/com/yapp/alarm/AlarmTimeCalculator.kt b/core/alarm/src/main/java/com/yapp/alarm/AlarmTimeCalculator.kt new file mode 100644 index 00000000..f7d6e590 --- /dev/null +++ b/core/alarm/src/main/java/com/yapp/alarm/AlarmTimeCalculator.kt @@ -0,0 +1,98 @@ +package com.yapp.alarm + +import com.yapp.domain.model.Alarm +import com.yapp.domain.model.AlarmDay +import com.yapp.domain.model.toDayOfWeek +import java.time.Clock +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import javax.inject.Inject + +class AlarmTimeCalculator @Inject constructor( + private val clock: Clock, +) { + private val holidayDateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + + private fun isHoliday(dateToCheck: LocalDateTime): Boolean { + if (dateToCheck.year == 2025) { + val dateString = dateToCheck.format(holidayDateFormatter) + return AlarmConstants.HOLIDAYS_2025.contains(dateString) + } + return false + } + + private fun skipHolidaysIfEnabled(initialDateTime: LocalDateTime, alarm: Alarm): LocalDateTime { + if (!alarm.isHolidayAlarmOff) return initialDateTime + + var adjustedDateTime = initialDateTime + while (isHoliday(adjustedDateTime)) { + adjustedDateTime = adjustedDateTime.plusWeeks(1) + } + + return adjustedDateTime + } + + private fun getAlarmDateTimeOnDate(alarm: Alarm, now: LocalDateTime): LocalDateTime { + return now + .withHour(alarm.hour) + .withMinute(alarm.minute) + .withSecond(alarm.second) + .withNano(0) + } + + fun calculateNextRepeatingTimeMillis( + alarm: Alarm, + alarmDay: AlarmDay, + zoneId: ZoneId = clock.zone, + ): Long { + val now = LocalDateTime.now(clock) + val targetDayOfWeek = alarmDay.toDayOfWeek() + + val alarmDateTimeToday = getAlarmDateTimeOnDate(alarm, now) + + var nextAlarmDateTimeCandidate = alarmDateTimeToday + + while (nextAlarmDateTimeCandidate.dayOfWeek != targetDayOfWeek || nextAlarmDateTimeCandidate.isBefore(now)) { + nextAlarmDateTimeCandidate = nextAlarmDateTimeCandidate.plusDays(1) + } + + nextAlarmDateTimeCandidate = skipHolidaysIfEnabled(nextAlarmDateTimeCandidate, alarm) + + return nextAlarmDateTimeCandidate.atZone(zoneId).toInstant().toEpochMilli() + } + + fun calculateNonRepeatingTimeMillis( + alarm: Alarm, + zoneId: ZoneId = clock.zone, + ): Long { + val now = LocalDateTime.now(clock) + var alarmDateTime = getAlarmDateTimeOnDate(alarm, now) + + if (alarmDateTime.isBefore(now)) { + alarmDateTime = alarmDateTime.plusDays(1) + } + + return alarmDateTime.atZone(zoneId).toInstant().toEpochMilli() + } + + fun calculateNextWeeklyRescheduledTimeMillis( + alarm: Alarm, + alarmTargetDay: AlarmDay, + zoneId: ZoneId = clock.zone, + ): Long { + val now = LocalDateTime.now(clock) + val targetDayOfWeek = alarmTargetDay.toDayOfWeek() + + var initialAlarmDateTimeCandidate = getAlarmDateTimeOnDate(alarm, now) + + while (initialAlarmDateTimeCandidate.dayOfWeek != targetDayOfWeek || initialAlarmDateTimeCandidate.isBefore(now)) { + initialAlarmDateTimeCandidate = initialAlarmDateTimeCandidate.plusDays(1) + } + + val nextWeeklyAlarmDateTimeCandidate = initialAlarmDateTimeCandidate.plusWeeks(1) + val nextWeeklyAlarmDateTime = skipHolidaysIfEnabled(nextWeeklyAlarmDateTimeCandidate, alarm) + + return nextWeeklyAlarmDateTime.atZone(zoneId).toInstant().toEpochMilli() + } +} diff --git a/core/alarm/src/main/java/com/yapp/alarm/AndroidAlarmScheduler.kt b/core/alarm/src/main/java/com/yapp/alarm/AndroidAlarmScheduler.kt new file mode 100644 index 00000000..49c4581a --- /dev/null +++ b/core/alarm/src/main/java/com/yapp/alarm/AndroidAlarmScheduler.kt @@ -0,0 +1,101 @@ +package com.yapp.alarm + +import android.app.AlarmManager +import android.app.Application +import android.util.Log +import com.yapp.alarm.pendingIntent.schedule.createAlarmReceiverPendingIntentForSchedule +import com.yapp.alarm.pendingIntent.schedule.createAlarmReceiverPendingIntentForUnSchedule +import com.yapp.domain.model.Alarm +import com.yapp.domain.model.AlarmDay +import com.yapp.domain.model.toAlarmDays +import com.yapp.domain.scheduler.AlarmScheduler +import javax.inject.Inject + +class AndroidAlarmScheduler @Inject constructor( + private val app: Application, + private val alarmManager: AlarmManager, + private val alarmTimeCalculator: AlarmTimeCalculator, +) : AlarmScheduler { + + private fun logSchedule(tag: String, alarm: Alarm, triggerMillis: Long, extra: String = "") { + Log.d("ScheduleTrace", "scheduleAlarm Called", Throwable()) + Log.d( + "AlarmSchedule", + "[$tag] id=${alarm.id}, repeatDays=${alarm.repeatDays}, " + + "time=${java.time.Instant.ofEpochMilli(triggerMillis)} $extra", + ) + } + + override fun scheduleAlarm(alarm: Alarm) { + val selectedDays = alarm.repeatDays.toAlarmDays() + + if (selectedDays.isEmpty()) { + setNonRepeatingAlarm(alarm) + } else { + selectedDays.forEach { day -> + setRepeatingAlarm(day, alarm) + } + } + } + + private fun setRepeatingAlarm(day: AlarmDay, alarm: Alarm) { + val triggerMillis = alarmTimeCalculator.calculateNextRepeatingTimeMillis(alarm, day) + val pendingIntent = createAlarmReceiverPendingIntentForSchedule(app, alarm, day) + logSchedule("REPEAT", alarm, triggerMillis, "day=$day") + alarmManager.setExactAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, + triggerMillis, + pendingIntent, + ) + } + + private fun setNonRepeatingAlarm(alarm: Alarm) { + val triggerMillis = alarmTimeCalculator.calculateNonRepeatingTimeMillis(alarm) + val pendingIntent = createAlarmReceiverPendingIntentForSchedule(app, alarm) + logSchedule("NON_REPEAT", alarm, triggerMillis) + alarmManager.setExactAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, + triggerMillis, + pendingIntent, + ) + } + + fun rescheduleUpcomingWeeklyAlarm(alarm: Alarm, day: AlarmDay) { + val triggerMillis = alarmTimeCalculator.calculateNextWeeklyRescheduledTimeMillis(alarm, day) + val pendingIntent = createAlarmReceiverPendingIntentForSchedule(app, alarm, day) + logSchedule("RESCHEDULE_WEEKLY", alarm, triggerMillis, "day=$day") + alarmManager.setExactAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, + triggerMillis, + pendingIntent, + ) + } + + override fun unScheduleAlarm(alarm: Alarm) { + val selectedDays = alarm.repeatDays.toAlarmDays() + + if (selectedDays.isEmpty()) { + val pendingIntent = createAlarmReceiverPendingIntentForUnSchedule( + app, + alarm, + null, + ) + alarmManager.cancel(pendingIntent) + } else { + selectedDays.forEach { day -> + val pendingIntent = createAlarmReceiverPendingIntentForUnSchedule( + app, + alarm, + day, + ) + alarmManager.cancel(pendingIntent) + } + } + } + + fun cancelSnoozedAlarm(alarmId: Long) { + val snoozedAlarmId = alarmId + AlarmConstants.SNOOZE_ID_OFFSET + val pendingIntent = createAlarmReceiverPendingIntentForUnSchedule(app, Alarm(id = snoozedAlarmId)) + alarmManager.cancel(pendingIntent) + } +} diff --git a/core/alarm/src/main/java/com/yapp/alarm/di/AlarmModule.kt b/core/alarm/src/main/java/com/yapp/alarm/di/AlarmModule.kt new file mode 100644 index 00000000..d5b5d624 --- /dev/null +++ b/core/alarm/src/main/java/com/yapp/alarm/di/AlarmModule.kt @@ -0,0 +1,39 @@ +package com.yapp.alarm.di + +import android.app.AlarmManager +import android.content.Context +import com.yapp.alarm.AlarmTimeCalculator +import com.yapp.alarm.AndroidAlarmScheduler +import com.yapp.domain.scheduler.AlarmScheduler +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import java.time.Clock +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class AlarmModule { + @Binds + @Singleton + abstract fun bindsAlarmScheduler( + alarmScheduler: AndroidAlarmScheduler, + ): AlarmScheduler + + companion object { + @Provides + @Singleton + fun provideAlarmTimeCalculator(clock: Clock): AlarmTimeCalculator { + return AlarmTimeCalculator(clock) + } + + @Provides + @Singleton + fun provideAlarmManager(@ApplicationContext context: Context): AlarmManager { + return context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + } + } +} diff --git a/core/alarm/src/main/java/com/yapp/alarm/pendingIntent/interaction/AlarmDismissPendingIntent.kt b/core/alarm/src/main/java/com/yapp/alarm/pendingIntent/interaction/AlarmDismissPendingIntent.kt index 3bd687df..3d702fb1 100644 --- a/core/alarm/src/main/java/com/yapp/alarm/pendingIntent/interaction/AlarmDismissPendingIntent.kt +++ b/core/alarm/src/main/java/com/yapp/alarm/pendingIntent/interaction/AlarmDismissPendingIntent.kt @@ -12,8 +12,10 @@ import com.yapp.alarm.receivers.AlarmReceiver fun createAlarmDismissPendingIntent( applicationContext: Context, pendingIntentId: Long, + missionType: Int, + missionCount: Int, ): PendingIntent { - val alarmDismissIntent = createAlarmDismissIntent(applicationContext, pendingIntentId) + val alarmDismissIntent = createAlarmDismissIntent(applicationContext, pendingIntentId, missionType, missionCount) return PendingIntent.getBroadcast( applicationContext, pendingIntentId.toInt(), @@ -25,18 +27,29 @@ fun createAlarmDismissPendingIntent( fun createAlarmDismissIntent( context: Context, notificationId: Long, + missionType: Int, + missionCount: Int, ): Intent { return Intent(AlarmConstants.ACTION_ALARM_DISMISSED).apply { setClass(context, AlarmReceiver::class.java) putExtra(AlarmConstants.EXTRA_NOTIFICATION_ID, notificationId) + putExtra(AlarmConstants.EXTRA_MISSION_TYPE, missionType) + putExtra(AlarmConstants.EXTRA_MISSION_COUNT, missionCount) } } fun createNavigateToMissionPendingIntent( applicationContext: Context, notificationId: Long, + missionType: Int, + missionCount: Int, ): PendingIntent { - val navigateToMissionIntent = createNavigateToMissionIntent(applicationContext, notificationId) + val navigateToMissionIntent = createNavigateToMissionIntent( + context = applicationContext, + notificationId = notificationId, + missionType = missionType, + missionCount = missionCount, + ) return PendingIntent.getActivity( applicationContext, notificationId.toInt(), @@ -48,8 +61,11 @@ fun createNavigateToMissionPendingIntent( fun createNavigateToMissionIntent( context: Context, notificationId: Long, + missionType: Int, + missionCount: Int, ): Intent { - return Intent(Intent.ACTION_VIEW, "orbitapp://mission?notificationId=$notificationId".toUri()).apply { + val uriString = "orbitapp://mission?notificationId=$notificationId&missionType=$missionType&missionCount=$missionCount" + return Intent(Intent.ACTION_VIEW, uriString.toUri()).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) setPackage(context.packageName) } diff --git a/core/alarm/src/main/java/com/yapp/alarm/receivers/AlarmInteractionActivityReceiver.kt b/core/alarm/src/main/java/com/yapp/alarm/receivers/AlarmInteractionActivityReceiver.kt index 4b703bf9..029072a4 100644 --- a/core/alarm/src/main/java/com/yapp/alarm/receivers/AlarmInteractionActivityReceiver.kt +++ b/core/alarm/src/main/java/com/yapp/alarm/receivers/AlarmInteractionActivityReceiver.kt @@ -6,21 +6,22 @@ import android.content.Intent import androidx.activity.ComponentActivity import androidx.core.net.toUri import com.yapp.alarm.AlarmConstants -import com.yapp.datastore.UserPreferences +import com.yapp.domain.model.FortuneCreateStatus +import com.yapp.domain.model.MissionType +import com.yapp.domain.repository.FortuneRepository import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import java.time.LocalDate -import java.time.format.DateTimeFormatter +import kotlinx.coroutines.withContext import javax.inject.Inject @AndroidEntryPoint class AlarmInteractionActivityReceiver(private val activity: ComponentActivity) : BroadcastReceiver() { @Inject - lateinit var userPreferences: UserPreferences + lateinit var fortuneRepository: FortuneRepository override fun onReceive(context: Context?, intent: Intent?) { val isSnoozed = intent?.getBooleanExtra(AlarmConstants.EXTRA_IS_SNOOZED, false) ?: false @@ -29,19 +30,70 @@ class AlarmInteractionActivityReceiver(private val activity: ComponentActivity) activity.finish() if (!isSnoozed) { - CoroutineScope(Dispatchers.IO).launch { - val fortuneDate = userPreferences.fortuneDateFlow.firstOrNull() - val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE) - - if (fortuneDate != todayDate) { - context?.let { - val missionIntent = - Intent(Intent.ACTION_VIEW, "orbitapp://mission".toUri()).apply { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - setPackage(context.packageName) + val notificationId = intent.getLongExtra(AlarmConstants.EXTRA_NOTIFICATION_ID, -1L) + val missionTypeRaw = intent.getIntExtra(AlarmConstants.EXTRA_MISSION_TYPE, -1) + val missionCount = intent.getIntExtra(AlarmConstants.EXTRA_MISSION_COUNT, -1) + + val missionType = MissionType.fromInt(missionTypeRaw) + + val hasValidMissionData = ( + notificationId != -1L && + missionType != MissionType.NONE && + missionCount != -1 + ) + + val pending = goAsync() + CoroutineScope(Dispatchers.Main).launch { + try { + if (!hasValidMissionData) { + val (fortuneCreateStatus, hasUnseenFortune) = withContext(Dispatchers.IO) { + val status = fortuneRepository.fortuneCreateStatusFlow.first() + val unseen = fortuneRepository.hasUnseenFortuneFlow.first() + status to unseen + } + + when (fortuneCreateStatus) { + is FortuneCreateStatus.Creating -> { + context?.let { ctx -> + val uri = "orbitapp://fortune".toUri() + val fortuneIntent = Intent(Intent.ACTION_VIEW, uri).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + setPackage(ctx.packageName) + } + ctx.startActivity(fortuneIntent) + } } - it.startActivity(missionIntent) + + is FortuneCreateStatus.Success -> { + if (hasUnseenFortune) { + context?.let { ctx -> + val uri = "orbitapp://fortune".toUri() + val fortuneIntent = + Intent(Intent.ACTION_VIEW, uri).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + setPackage(ctx.packageName) + } + ctx.startActivity(fortuneIntent) + } + } + } + + FortuneCreateStatus.Failure, FortuneCreateStatus.Idle -> { } + } + } else { + context?.let { ctx -> + val uriString = + "orbitapp://mission?notificationId=$notificationId&missionType=${missionType.value}&missionCount=$missionCount" + val missionIntent = + Intent(Intent.ACTION_VIEW, uriString.toUri()).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + setPackage(ctx.packageName) + } + ctx.startActivity(missionIntent) + } } + } finally { + pending.finish() } } } diff --git a/core/alarm/src/main/java/com/yapp/alarm/receivers/AlarmReceiver.kt b/core/alarm/src/main/java/com/yapp/alarm/receivers/AlarmReceiver.kt index 2e97ee68..011dadef 100644 --- a/core/alarm/src/main/java/com/yapp/alarm/receivers/AlarmReceiver.kt +++ b/core/alarm/src/main/java/com/yapp/alarm/receivers/AlarmReceiver.kt @@ -7,20 +7,21 @@ import android.os.Build import android.util.Log import android.widget.Toast import com.yapp.alarm.AlarmConstants -import com.yapp.alarm.AlarmHelper +import com.yapp.alarm.AndroidAlarmScheduler import com.yapp.alarm.services.AlarmService import com.yapp.analytics.AnalyticsEvent import com.yapp.analytics.AnalyticsHelper -import com.yapp.datastore.UserPreferences import com.yapp.domain.model.Alarm +import com.yapp.domain.model.toAlarmDay import com.yapp.domain.model.toTimeString +import com.yapp.domain.repository.FortuneRepository import com.yapp.domain.usecase.AlarmUseCase import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch +import java.time.LocalDate import java.time.LocalDateTime import javax.inject.Inject @@ -31,10 +32,10 @@ class AlarmReceiver : BroadcastReceiver() { lateinit var analyticsHelper: AnalyticsHelper @Inject - lateinit var alarmHelper: AlarmHelper + lateinit var androidAlarmScheduler: AndroidAlarmScheduler @Inject - lateinit var userPreferences: UserPreferences + lateinit var fortuneRepository: FortuneRepository @Inject lateinit var alarmUseCase: AlarmUseCase @@ -46,10 +47,11 @@ class AlarmReceiver : BroadcastReceiver() { val alarmServiceIntent = createAlarmServiceIntent(context, intent) when (intent.action) { AlarmConstants.ACTION_ALARM_TRIGGERED -> { - Log.d("AlarmReceiver", "Alarm Triggered") - val alarm: Alarm? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - alarmServiceIntent.getParcelableExtra(AlarmConstants.EXTRA_ALARM, Alarm::class.java) + alarmServiceIntent.getParcelableExtra( + AlarmConstants.EXTRA_ALARM, + Alarm::class.java, + ) } else { @Suppress("DEPRECATION") alarmServiceIntent.getParcelableExtra(AlarmConstants.EXTRA_ALARM) @@ -68,8 +70,6 @@ class AlarmReceiver : BroadcastReceiver() { } AlarmConstants.ACTION_ALARM_SNOOZED -> { - Log.d("AlarmReceiver", "Alarm Snoozed") - val alarm: Alarm? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { intent.getParcelableExtra(AlarmConstants.EXTRA_ALARM, Alarm::class.java) } else { @@ -86,44 +86,68 @@ class AlarmReceiver : BroadcastReceiver() { ) alarm?.let { handleSnooze(context, it) } - Toast.makeText(context, "์•Œ๋žŒ์ด ${alarm?.snoozeInterval}๋ถ„ ํ›„ ๋‹ค์‹œ ์šธ๋ ค์š”", Toast.LENGTH_SHORT).show() + Toast.makeText( + context, + "์•Œ๋žŒ์ด ${alarm?.snoozeInterval}๋ถ„ ํ›„ ๋‹ค์‹œ ์šธ๋ ค์š”", + Toast.LENGTH_SHORT, + ).show() } AlarmConstants.ACTION_ALARM_DISMISSED -> { - Log.d("AlarmReceiver", "Alarm Dismissed") - - val alarmId = intent.getLongExtra(AlarmConstants.EXTRA_NOTIFICATION_ID, -1L) - if (alarmId != -1L) { - CoroutineScope(Dispatchers.IO).launch { - val alarms = alarmUseCase.getAllAlarms().first().sortedBy { it.isAlarmActive } - val isFirstAlarm = alarms.firstOrNull()?.id == alarmId - - analyticsHelper.logEvent( - AnalyticsEvent( - type = "alarm_dismiss", - properties = mapOf( - AnalyticsEvent.AlarmPropertiesKeys.ALARM_ID to "$alarmId", - AnalyticsEvent.AlarmPropertiesKeys.DISMISS_IS_FIRST_ALARM to isFirstAlarm, - ), - ), - ) - val existingId = userPreferences.firstDismissedAlarmIdFlow.firstOrNull() - if (existingId == null) { - // ์ฒซ ๋ฒˆ์งธ ์•Œ๋žŒ ํ•ด์ œ ๊ธฐ๋ก - userPreferences.saveFirstDismissedAlarmId(alarmId) - } else if (existingId != alarmId) { - // ๋‘ ๋ฒˆ์งธ ์•Œ๋žŒ ํ•ด์ œ ๊ฐ์ง€ - ๊ธฐ์กด ๊ธฐ๋ก ์‚ญ์ œ - userPreferences.clearDismissedAlarmId() - } - } + val notificationId = intent.getLongExtra(AlarmConstants.EXTRA_NOTIFICATION_ID, -1L) + val missionType = intent.getIntExtra(AlarmConstants.EXTRA_MISSION_TYPE, -1) + val missionCount = intent.getIntExtra(AlarmConstants.EXTRA_MISSION_COUNT, -1) - alarmHelper.cancelSnoozedAlarm(alarmId) - } else { - Log.e("AlarmReceiver", "์•Œ๋žŒ ID ์ˆ˜์‹  ์‹คํŒจ") + if (notificationId == -1L) { + Log.e("AlarmReceiver", "notificationId ์ˆ˜์‹  ์‹คํŒจ") + return } - alarmHelper.cancelSnoozedAlarm(alarmId) + + androidAlarmScheduler.cancelSnoozedAlarm(notificationId) context.stopService(alarmServiceIntent) - sendBroadCastToCloseAlarmInteractionActivity(context) + + CoroutineScope(Dispatchers.IO).launch { + val alarms = alarmUseCase.getAllAlarms().first() + + val isSnoozeId = notificationId >= AlarmConstants.SNOOZE_ID_OFFSET + + fun Alarm.ringsToday(): Boolean { + if (repeatDays == 0) return true + + val todayAlarmDay = LocalDate.now().dayOfWeek.toAlarmDay() + return (repeatDays and todayAlarmDay.bitValue) != 0 + } + + val earliestIdToday: Long? = alarms + .asSequence() + .filter { (it.isAlarmActive || it.id == notificationId) && it.ringsToday() } + .sortedWith(compareBy({ it.hour }, { it.minute }, { it.second })) + .firstOrNull() + ?.id + + val isEarliestAlarmDismissedToday = + !isSnoozeId && (earliestIdToday == notificationId) + + if (isEarliestAlarmDismissedToday) fortuneRepository.markFirstAlarmDismissedToday() + + val isFirstAlarm = earliestIdToday == notificationId + analyticsHelper.logEvent( + AnalyticsEvent( + type = "alarm_dismiss", + properties = mapOf( + AnalyticsEvent.AlarmPropertiesKeys.ALARM_ID to "$notificationId", + AnalyticsEvent.AlarmPropertiesKeys.DISMISS_IS_FIRST_ALARM to isFirstAlarm, + ), + ), + ) + + sendBroadCastToCloseAlarmInteractionActivity( + context = context, + notificationId = notificationId, + missionType = missionType, + missionCount = missionCount, + ) + } Toast.makeText(context, "์•Œ๋žŒ์ด ํ•ด์ œ๋˜์—ˆ์–ด์š”", Toast.LENGTH_SHORT).show() } @@ -152,8 +176,7 @@ class AlarmReceiver : BroadcastReceiver() { .plusMinutes(alarm.snoozeInterval.toLong()) val updatedAlarm = alarm.copy( - isAm = snoozeDateTime.hour < 12, - hour = if (snoozeDateTime.hour == 0) 12 else if (snoozeDateTime.hour > 12) snoozeDateTime.hour - 12 else snoozeDateTime.hour, + hour = snoozeDateTime.hour, minute = snoozeDateTime.minute, second = snoozeDateTime.second, repeatDays = 0, @@ -161,21 +184,22 @@ class AlarmReceiver : BroadcastReceiver() { id = alarm.id + AlarmConstants.SNOOZE_ID_OFFSET, ) - Log.d( - "AlarmReceiver", - "Scheduling snooze alarm: alarmId=${updatedAlarm.id}, newTime=${updatedAlarm.hour}:${updatedAlarm.minute}, remaining snoozeCount=$newSnoozeCount", - ) - context.stopService(Intent(context, AlarmService::class.java)) - alarmHelper.scheduleAlarm(updatedAlarm) + androidAlarmScheduler.scheduleAlarm(updatedAlarm) } - private fun sendBroadCastToCloseAlarmInteractionActivity(context: Context) { - Log.d("AlarmReceiver", "Send Broadcast to close Alarm Interaction Activity") - val alarmAlertActivityCloseIntent = - Intent(AlarmConstants.ACTION_ALARM_INTERACTION_ACTIVITY_CLOSE).apply { - putExtra(AlarmConstants.EXTRA_IS_SNOOZED, false) - } - context.sendBroadcast(alarmAlertActivityCloseIntent) + private fun sendBroadCastToCloseAlarmInteractionActivity( + context: Context, + notificationId: Long, + missionType: Int, + missionCount: Int, + ) { + val intent = Intent(AlarmConstants.ACTION_ALARM_INTERACTION_ACTIVITY_CLOSE).apply { + putExtra(AlarmConstants.EXTRA_IS_SNOOZED, false) + putExtra(AlarmConstants.EXTRA_NOTIFICATION_ID, notificationId) + putExtra(AlarmConstants.EXTRA_MISSION_TYPE, missionType) + putExtra(AlarmConstants.EXTRA_MISSION_COUNT, missionCount) + } + context.sendBroadcast(intent) } } diff --git a/core/alarm/src/main/java/com/yapp/alarm/receivers/RescheduleAlarmReceiver.kt b/core/alarm/src/main/java/com/yapp/alarm/receivers/RescheduleAlarmReceiver.kt index b26ce622..b42e9fab 100644 --- a/core/alarm/src/main/java/com/yapp/alarm/receivers/RescheduleAlarmReceiver.kt +++ b/core/alarm/src/main/java/com/yapp/alarm/receivers/RescheduleAlarmReceiver.kt @@ -3,11 +3,12 @@ package com.yapp.alarm.receivers import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import com.yapp.alarm.AlarmHelper +import com.yapp.alarm.AndroidAlarmScheduler import com.yapp.domain.usecase.AlarmUseCase import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import javax.inject.Inject @@ -18,7 +19,7 @@ class RescheduleAlarmReceiver : BroadcastReceiver() { lateinit var alarmUseCase: AlarmUseCase @Inject - lateinit var alarmHelper: AlarmHelper + lateinit var androidAlarmScheduler: AndroidAlarmScheduler override fun onReceive(context: Context?, intent: Intent?) { context ?: return @@ -31,11 +32,10 @@ class RescheduleAlarmReceiver : BroadcastReceiver() { private fun rescheduleAlarm() { CoroutineScope(Dispatchers.IO).launch { - alarmUseCase.getAllAlarms().collect { alarms -> - alarms.forEach { alarm -> - alarmHelper.scheduleAlarm(alarm) - } - } + val alarms = alarmUseCase.getAllAlarms().first() + alarms + .filter { it.isAlarmActive } + .forEach { alarm -> androidAlarmScheduler.scheduleAlarm(alarm) } } } } diff --git a/core/alarm/src/main/java/com/yapp/alarm/scheduler/PostFortuneTaskScheduler.kt b/core/alarm/src/main/java/com/yapp/alarm/scheduler/PostFortuneTaskScheduler.kt new file mode 100644 index 00000000..e16e8dd4 --- /dev/null +++ b/core/alarm/src/main/java/com/yapp/alarm/scheduler/PostFortuneTaskScheduler.kt @@ -0,0 +1,5 @@ +package com.yapp.alarm.scheduler + +interface PostFortuneTaskScheduler { + fun enqueueOnceForToday() +} diff --git a/core/alarm/src/main/java/com/yapp/alarm/services/AlarmService.kt b/core/alarm/src/main/java/com/yapp/alarm/services/AlarmService.kt index c982fe7e..56141679 100644 --- a/core/alarm/src/main/java/com/yapp/alarm/services/AlarmService.kt +++ b/core/alarm/src/main/java/com/yapp/alarm/services/AlarmService.kt @@ -18,14 +18,15 @@ import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.net.toUri import com.yapp.alarm.AlarmConstants -import com.yapp.alarm.AlarmHelper +import com.yapp.alarm.AndroidAlarmScheduler import com.yapp.alarm.pendingIntent.interaction.createAlarmAlertPendingIntent import com.yapp.alarm.pendingIntent.interaction.createAlarmDismissPendingIntent import com.yapp.alarm.pendingIntent.interaction.createAlarmSnoozePendingIntent import com.yapp.alarm.pendingIntent.interaction.createNavigateToMissionPendingIntent -import com.yapp.datastore.UserPreferences +import com.yapp.alarm.scheduler.PostFortuneTaskScheduler import com.yapp.domain.model.Alarm import com.yapp.domain.model.AlarmDay +import com.yapp.domain.model.MissionType import com.yapp.domain.usecase.AlarmUseCase import com.yapp.media.sound.SoundPlayer import dagger.hilt.android.AndroidEntryPoint @@ -33,10 +34,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch -import java.time.LocalDate -import java.time.format.DateTimeFormatter import javax.inject.Inject @AndroidEntryPoint @@ -51,10 +49,10 @@ class AlarmService : Service() { private lateinit var vibrator: Vibrator @Inject - lateinit var alarmHelper: AlarmHelper + lateinit var androidAlarmScheduler: AndroidAlarmScheduler @Inject - lateinit var userPreferences: UserPreferences + lateinit var postFortuneTaskScheduler: PostFortuneTaskScheduler private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) @@ -81,7 +79,7 @@ class AlarmService : Service() { super.onDestroy() } - private suspend fun handleIntent(intent: Intent) { + private fun handleIntent(intent: Intent) { val alarm: Alarm? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { intent.getParcelableExtra(AlarmConstants.EXTRA_ALARM, Alarm::class.java) } else { @@ -103,7 +101,7 @@ class AlarmService : Service() { // ๋ฐ˜๋ณต ์š”์ผ ์•Œ๋žŒ ์‹œ, ๋‹ค์Œ ์ฃผ ๋™์ผ ์š”์ผ ์•Œ๋žŒ ์˜ˆ์•ฝ if (!isOneTimeAlarm) { intent.getStringExtra(AlarmConstants.EXTRA_ALARM_DAY)?.let { - alarmHelper.scheduleWeeklyAlarm(alarm, AlarmDay.valueOf(it)) + androidAlarmScheduler.rescheduleUpcomingWeeklyAlarm(alarm, AlarmDay.valueOf(it)) } } @@ -113,7 +111,7 @@ class AlarmService : Service() { false -> { startForeground( notificationId.toInt(), - createNotification(alarm, shouldNavigateToMission()), + createNotification(alarm, shouldNavigateToMission(alarm.missionType)), ) if (alarm.isVibrationEnabled) startVibration() if (alarm.isSoundEnabled) startSound(alarm.soundUri, alarm.soundVolume) @@ -123,12 +121,14 @@ class AlarmService : Service() { if (isOneTimeAlarm) { turnOffAlarm(alarmId = notificationId) } + + postFortuneTaskScheduler.enqueueOnceForToday() } - private suspend fun shouldNavigateToMission(): Boolean { - val fortuneDate = userPreferences.fortuneDateFlow.firstOrNull() - val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE) - return fortuneDate != todayDate + private fun shouldNavigateToMission( + missionType: MissionType, + ): Boolean { + return missionType != MissionType.NONE } private fun createNotification(alarm: Alarm, shouldNavigateToMission: Boolean): Notification { @@ -139,11 +139,15 @@ class AlarmService : Service() { createNavigateToMissionPendingIntent( applicationContext = applicationContext, notificationId = alarm.id, + missionType = alarm.missionType.value, + missionCount = alarm.missionCount, ) } else { createAlarmDismissPendingIntent( applicationContext = applicationContext, pendingIntentId = alarm.id, + missionType = alarm.missionType.value, + missionCount = alarm.missionCount, ) } diff --git a/core/alarm/src/test/kotlin/com/yapp/alarm/AlarmTimeCalculatorTest.kt b/core/alarm/src/test/kotlin/com/yapp/alarm/AlarmTimeCalculatorTest.kt new file mode 100644 index 00000000..a4055be2 --- /dev/null +++ b/core/alarm/src/test/kotlin/com/yapp/alarm/AlarmTimeCalculatorTest.kt @@ -0,0 +1,468 @@ +import com.yapp.alarm.AlarmTimeCalculator +import com.yapp.domain.model.Alarm +import com.yapp.domain.model.AlarmDay +import org.junit.Assert.assertEquals +import org.junit.Test +import java.time.Clock +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.ZoneId + +class AlarmTimeCalculatorTest { + + private val testZoneId: ZoneId = ZoneId.of("Asia/Seoul") + + // --- ๊ธฐ์ค€ ์‹œ๊ฐ (Fixed Clocks) --- + private val MONDAY_2024_07_22_10AM: LocalDateTime = LocalDateTime.of(2024, 7, 22, 10, 0, 0) + private val clockMonday2024_10am: Clock = Clock.fixed( + MONDAY_2024_07_22_10AM.toInstant( + testZoneId.rules.getOffset(MONDAY_2024_07_22_10AM) + ), testZoneId + ) + + private val FRIDAY_2024_07_26_3PM: LocalDateTime = LocalDateTime.of(2024, 7, 26, 15, 0, 0) + private val clockFriday2024_3pm: Clock = Clock.fixed( + FRIDAY_2024_07_26_3PM.toInstant(testZoneId.rules.getOffset(FRIDAY_2024_07_26_3PM)), + testZoneId + ) + + private val MONDAY_2025_01_20_10AM: LocalDateTime = LocalDateTime.of(2025, 1, 20, 10, 0, 0) + private val clockMonday2025_01_20_10am: Clock = Clock.fixed( + MONDAY_2025_01_20_10AM.toInstant( + testZoneId.rules.getOffset(MONDAY_2025_01_20_10AM) + ), testZoneId + ) + + private val MONDAY_2025_01_20_2_01PM: LocalDateTime = LocalDateTime.of(2025, 1, 20, 14, 1, 0) + private val clockMonday2025_PrevHoliday_2_01pm: Clock = Clock.fixed( + MONDAY_2025_01_20_2_01PM.toInstant( + testZoneId.rules.getOffset(MONDAY_2025_01_20_2_01PM) + ), testZoneId + ) + + private val MONDAY_HOLIDAY_2025_01_27_10AM: LocalDateTime = + LocalDateTime.of(2025, 1, 27, 10, 0, 0) + private val clockMondayHoliday2025_10am: Clock = Clock.fixed( + MONDAY_HOLIDAY_2025_01_27_10AM.toInstant( + testZoneId.rules.getOffset(MONDAY_HOLIDAY_2025_01_27_10AM) + ), testZoneId + ) + + private val MONDAY_2025_02_17_10AM: LocalDateTime = LocalDateTime.of(2025, 2, 17, 10, 0, 0) + private val clockMonday2025_02_17_10am: Clock = Clock.fixed( + MONDAY_2025_02_17_10AM.toInstant( + testZoneId.rules.getOffset(MONDAY_2025_02_17_10AM) + ), testZoneId + ) + + private fun createTestAlarm( + hour: Int, + minute: Int, + second: Int = 0, + isHolidayAlarmOff: Boolean = false, + repeatDays: Int = 0, // ๊ธฐ๋ณธ๊ฐ’์€ ๋น„๋ฐ˜๋ณต + ): Alarm { + return Alarm( + hour = hour, + minute = minute, + second = second, + repeatDays = repeatDays, + isHolidayAlarmOff = isHolidayAlarmOff, + ) + } + + private fun getExpectedMillis(dateTime: LocalDateTime, zone: ZoneId = testZoneId): Long { + return dateTime.atZone(zone).toInstant().toEpochMilli() + } + + // --- ๋น„๋ฐ˜๋ณต ์•Œ๋žŒ ์‹œ๊ฐ„ ๊ณ„์‚ฐ (calculateNonRepeatingTimeMillis) ํ…Œ์ŠคํŠธ --- + @Test + fun `๋น„๋ฐ˜๋ณต_์•Œ๋žŒ์‹œ๊ฐ„์ด_์˜ค๋Š˜_๋ฏธ๋ž˜์ด๋ฉด_์˜ค๋Š˜_์•Œ๋žŒ์‹œ๊ฐ„์œผ๋กœ_๊ณ„์‚ฐ๋œ๋‹ค`() { + // ํ˜„์žฌ: 2024-07-22 (์›”) 10:00:00 + // ์•Œ๋žŒ: ์˜ค๋Š˜ 14:00:00, ๋น„๋ฐ˜๋ณต + // ๊ธฐ๋Œ€: 2024-07-22 (์›”) 14:00:00 + + // given + val calculator = AlarmTimeCalculator(clockMonday2024_10am) + val alarmTime = LocalTime.of(14, 0) + val alarm = createTestAlarm(alarmTime.hour, alarmTime.minute) // repeatDays = 0 (๋น„๋ฐ˜๋ณต) + + // when + val actualMillis = calculator.calculateNonRepeatingTimeMillis(alarm, testZoneId) + + // then + val expectedDateTime = MONDAY_2024_07_22_10AM.with(alarmTime) + val expectedMillis = getExpectedMillis(expectedDateTime) + assertEquals(expectedMillis, actualMillis) + } + + @Test + fun `๋น„๋ฐ˜๋ณต_์•Œ๋žŒ์‹œ๊ฐ„์ด_์˜ค๋Š˜_๊ณผ๊ฑฐ์ด๋ฉด_๋‚ด์ผ_์•Œ๋žŒ์‹œ๊ฐ„์œผ๋กœ_๊ณ„์‚ฐ๋œ๋‹ค`() { + // ํ˜„์žฌ: 2024-07-22 (์›”) 10:00:00 + // ์•Œ๋žŒ: ์˜ค๋Š˜ 08:00:00 (์ด๋ฏธ ์ง€๋‚จ), ๋น„๋ฐ˜๋ณต + // ๊ธฐ๋Œ€: 2024-07-23 (ํ™”) 08:00:00 + + // given + val calculator = AlarmTimeCalculator(clockMonday2024_10am) + val alarmTime = LocalTime.of(8, 0) + val alarm = createTestAlarm(alarmTime.hour, alarmTime.minute) // repeatDays = 0 (๋น„๋ฐ˜๋ณต) + + // when + val actualMillis = calculator.calculateNonRepeatingTimeMillis(alarm, testZoneId) + + // then + val expectedDateTime = MONDAY_2024_07_22_10AM.plusDays(1).with(alarmTime) + val expectedMillis = getExpectedMillis(expectedDateTime) + assertEquals(expectedMillis, actualMillis) + } + + // --- ๋‹ค์Œ ๋ฐ˜๋ณต ์š”์ผ ์•Œ๋žŒ ์‹œ๊ฐ„ ๊ณ„์‚ฐ (calculateNextRepeatingTimeMillis) ํ…Œ์ŠคํŠธ --- + @Test + fun `๋ฐ˜๋ณต์š”์ผ์•Œ๋žŒ_์˜ค๋Š˜์ด_๋Œ€์ƒ์š”์ผ์ด๊ณ _์•Œ๋žŒ์‹œ๊ฐ„์ด_๋ฏธ๋ž˜์ด๋ฉฐ_๊ณตํœด์ผ๊ฑด๋„ˆ๋›ฐ๊ธฐ_๋น„ํ™œ์„ฑ์‹œ_์˜ค๋Š˜๋กœ_๊ณ„์‚ฐ๋œ๋‹ค`() { + // ํ˜„์žฌ: 2024-07-22 (์›”) 10:00:00 + // ์•Œ๋žŒ: ๋งค์ฃผ ์›”์š”์ผ 14:00:00, ๊ณตํœด์ผ ๊ฑด๋„ˆ๋›ฐ๊ธฐ ๋น„ํ™œ์„ฑ + // ๊ธฐ๋Œ€: 2024-07-22 (์›”) 14:00:00 + + // given + val calculator = AlarmTimeCalculator(clockMonday2024_10am) + val alarmTime = LocalTime.of(14, 0) + val alarm = createTestAlarm( + hour = alarmTime.hour, + minute = alarmTime.minute, + isHolidayAlarmOff = false, + repeatDays = AlarmDay.MON.bitValue // ์›”์š”์ผ ๋ฐ˜๋ณต + ) + + // when + val actualMillis = + calculator.calculateNextRepeatingTimeMillis(alarm, AlarmDay.MON, testZoneId) + + // then + val expectedDateTime = MONDAY_2024_07_22_10AM.with(alarmTime) + val expectedMillis = getExpectedMillis(expectedDateTime) + assertEquals(expectedMillis, actualMillis) + } + + @Test + fun `๋ฐ˜๋ณต์š”์ผ์•Œ๋žŒ_์˜ค๋Š˜์ด_๊ณตํœด์ผ์ธ_์šธ๋ฆด์š”์ผ์ด๊ณ _์•Œ๋žŒ์‹œ๊ฐ„์ด_๋ฏธ๋ž˜์ด๋ฉฐ_๊ณตํœด์ผ๊ฑด๋„ˆ๋›ฐ๊ธฐ_๋น„ํ™œ์„ฑ์‹œ_์˜ค๋Š˜_๊ณตํœด์ผ๋กœ_๊ณ„์‚ฐ๋œ๋‹ค`() { + // ํ˜„์žฌ: 2025-01-27 (์›”, ๊ณตํœด์ผ) 10:00:00 + // ์•Œ๋žŒ: ๋งค์ฃผ ์›”์š”์ผ 14:00:00, ๊ณตํœด์ผ ๊ฑด๋„ˆ๋›ฐ๊ธฐ ๋น„ํ™œ์„ฑ + // ๊ธฐ๋Œ€: 2025-01-27 (์›”, ๊ณตํœด์ผ) 14:00:00 (๊ฑด๋„ˆ๋›ฐ๊ธฐ ๋น„ํ™œ์„ฑ์ด๋ฏ€๋กœ ์˜ค๋Š˜ ๊ณตํœด์ผ์ด์–ด๋„ ์šธ๋ฆผ) + + // given + val calculator = AlarmTimeCalculator(clockMondayHoliday2025_10am) + val alarmTime = LocalTime.of(14, 0) + val alarm = createTestAlarm( + hour = alarmTime.hour, + minute = alarmTime.minute, + isHolidayAlarmOff = false, + repeatDays = AlarmDay.MON.bitValue // ์›”์š”์ผ ๋ฐ˜๋ณต + ) + + // when + val actualMillis = + calculator.calculateNextRepeatingTimeMillis(alarm, AlarmDay.MON, testZoneId) + + // then + val expectedDateTime = MONDAY_HOLIDAY_2025_01_27_10AM.with(alarmTime) + val expectedMillis = getExpectedMillis(expectedDateTime) + assertEquals(expectedMillis, actualMillis) + } + + @Test + fun `๋ฐ˜๋ณต์š”์ผ์•Œ๋žŒ_๋‹ค์Œ์ฃผ_์šธ๋ฆด์š”์ผ์ด_๊ณตํœด์ผ์ด๊ณ _๊ณตํœด์ผ๊ฑด๋„ˆ๋›ฐ๊ธฐ_๋น„ํ™œ์„ฑ์‹œ_๋‹ค์Œ์ฃผ_๊ณตํœด์ผ๋กœ_๊ณ„์‚ฐ๋œ๋‹ค`() { + // ํ˜„์žฌ: 2025-01-20 (์›”) 10:00:00 (๊ณตํœด์ผ ์•„๋‹Œ ์›”์š”์ผ) + // ์•Œ๋žŒ: ๋งค์ฃผ ์›”์š”์ผ 09:00:00, ๊ณตํœด์ผ ๊ฑด๋„ˆ๋›ฐ๊ธฐ ๋น„ํ™œ์„ฑ + // ๋‹ค์Œ ์ฃผ ์›”์š”์ผ: 2025-01-27 (๊ณตํœด์ผ) + // ๊ธฐ๋Œ€: 2025-01-27 (์›”, ๊ณตํœด์ผ) 09:00:00 (๊ฑด๋„ˆ๋›ฐ๊ธฐ ๋น„ํ™œ์„ฑ์ด๋ฏ€๋กœ ๋‹ค์Œ ์ฃผ ๊ณตํœด์ผ์ด์–ด๋„ ์šธ๋ฆผ) + + // given + val calculator = AlarmTimeCalculator(clockMonday2025_01_20_10am) + val alarmTime = LocalTime.of(9, 0) + val alarm = createTestAlarm( + hour = alarmTime.hour, + minute = alarmTime.minute, + isHolidayAlarmOff = false, + repeatDays = AlarmDay.MON.bitValue // ์›”์š”์ผ ๋ฐ˜๋ณต + ) + + // when + val actualMillis = + calculator.calculateNextRepeatingTimeMillis(alarm, AlarmDay.MON, testZoneId) + + // then + val expectedDateTime = LocalDateTime.of(2025, 1, 27, 9, 0, 0) + val expectedMillis = getExpectedMillis(expectedDateTime) + assertEquals(expectedMillis, actualMillis) + } + + @Test + fun `๋ฐ˜๋ณต์š”์ผ์•Œ๋žŒ_๋Œ€์ƒ์š”์ผ์ด_์ด๋ฒˆ์ฃผ_๋ฏธ๋ž˜์š”์ผ์ด๊ณ _๊ณตํœด์ผ๊ฑด๋„ˆ๋›ฐ๊ธฐ_๋น„ํ™œ์„ฑ์‹œ_ํ•ด๋‹น์š”์ผ๋กœ_๊ณ„์‚ฐ๋œ๋‹ค`() { + // ํ˜„์žฌ: 2024-07-22 (์›”) 10:00:00 + // ์•Œ๋žŒ: ๋งค์ฃผ ์ˆ˜์š”์ผ 11:00:00, ๊ณตํœด์ผ ๊ฑด๋„ˆ๋›ฐ๊ธฐ ๋น„ํ™œ์„ฑ + // ๊ธฐ๋Œ€: 2024-07-24 (์ˆ˜) 11:00:00 + + // given + val calculator = AlarmTimeCalculator(clockMonday2024_10am) + val alarmTime = LocalTime.of(11, 0) + val alarm = createTestAlarm( + hour = alarmTime.hour, + minute = alarmTime.minute, + isHolidayAlarmOff = false, + repeatDays = AlarmDay.WED.bitValue // ์ˆ˜์š”์ผ ๋ฐ˜๋ณต + ) + + // when + val actualMillis = + calculator.calculateNextRepeatingTimeMillis(alarm, AlarmDay.WED, testZoneId) + + // then + val expectedDateTime = + MONDAY_2024_07_22_10AM.plusDays(2).with(alarmTime) // 2024-07-24 (์ˆ˜) 11:00 + val expectedMillis = getExpectedMillis(expectedDateTime) + assertEquals(expectedMillis, actualMillis) + } + + @Test + fun `๋ฐ˜๋ณต์š”์ผ์•Œ๋žŒ_์˜ค๋Š˜์ด_๋Œ€์ƒ์š”์ผ์ด๋‚˜_์‹œ๊ฐ„์ด_์ง€๋‚ฌ๊ณ _๋‹ค์Œ์ฃผ_ํ•ด๋‹น์š”์ผ์ด_๊ณตํœด์ผ์ด๋ฉฐ_๊ณตํœด์ผ_๋น„ํ™œ์„ฑ์‹œ_๋‹ค์Œ์ฃผ_๊ณตํœด์ผ๋กœ_๊ณ„์‚ฐ๋œ๋‹ค`() { + // ํ˜„์žฌ: 2025-01-20 (์›”) 14:01 (์›”์š”์ผ 14:00 ์•Œ๋žŒ ํ›„) + // ์•Œ๋žŒ: ๋งค์ฃผ ์›”์š”์ผ 14:00, isHolidayAlarmOff = false + // ๋‹ค์Œ์ฃผ ์›”์š”์ผ: 2025-01-27 (๊ณตํœด์ผ) + // ๊ธฐ๋Œ€: 2025-01-27 (์›”) 14:00 (์˜ต์…˜ Off์ด๋ฏ€๋กœ ๊ณตํœด์ผ์ด์–ด๋„ ์šธ๋ฆผ) + + // given + val calculator = AlarmTimeCalculator(clockMonday2025_PrevHoliday_2_01pm) + val alarmTime = LocalTime.of(14, 0) + val alarm = createTestAlarm( + hour = alarmTime.hour, + minute = alarmTime.minute, + isHolidayAlarmOff = false, + repeatDays = AlarmDay.MON.bitValue // ์›”์š”์ผ ๋ฐ˜๋ณต + ) + + // when + val actualMillis = + calculator.calculateNextRepeatingTimeMillis(alarm, AlarmDay.MON, testZoneId) + + // then + val expectedDateTime = LocalDateTime.of(2025, 1, 27, 14, 0, 0) + val expectedMillis = getExpectedMillis(expectedDateTime) + assertEquals(expectedMillis, actualMillis) + } + + @Test + fun `๋ฐ˜๋ณต์š”์ผ์•Œ๋žŒ_์˜ค๋Š˜์ด_๋Œ€์ƒ์š”์ผ์ด๋‚˜_์‹œ๊ฐ„์ด_์ง€๋‚ฌ๊ณ _๋‹ค์Œ์ฃผ_ํ•ด๋‹น์š”์ผ์ด_๊ณตํœด์ผ์ด๋ฉฐ_๊ณตํœด์ผ๊ฑด๋„ˆ๋›ฐ๊ธฐ_ํ™œ์„ฑ์‹œ_๋‹ค๋‹ค์Œ์ฃผ_ํ•ด๋‹น์š”์ผ๋กœ_๊ณ„์‚ฐ๋œ๋‹ค`() { + // ํ˜„์žฌ: 2025-01-20 (์›”) 14:01 (์›”์š”์ผ 14:00 ์•Œ๋žŒ ํ›„) + // ์•Œ๋žŒ: ๋งค์ฃผ ์›”์š”์ผ 14:00, isHolidayAlarmOff = true + // ๋‹ค์Œ์ฃผ ์›”์š”์ผ: 2025-01-27 (๊ณตํœด์ผ) + // ๊ธฐ๋Œ€: ๋‹ค๋‹ค์Œ์ฃผ ์›”์š”์ผ 2025-02-03 (์›”) 14:00 + + // given + val calculator = AlarmTimeCalculator(clockMonday2025_PrevHoliday_2_01pm) + val alarmTime = LocalTime.of(14, 0) + val alarm = createTestAlarm( + hour = alarmTime.hour, + minute = alarmTime.minute, + isHolidayAlarmOff = true, + repeatDays = AlarmDay.MON.bitValue // ์›”์š”์ผ ๋ฐ˜๋ณต + ) + + // when + val actualMillis = + calculator.calculateNextRepeatingTimeMillis(alarm, AlarmDay.MON, testZoneId) + + // then + val expectedDateTime = LocalDateTime.of(2025, 2, 3, 14, 0, 0) + val expectedMillis = getExpectedMillis(expectedDateTime) + assertEquals(expectedMillis, actualMillis) + } + + @Test + fun `๋ฐ˜๋ณต์š”์ผ์•Œ๋žŒ_์˜ค๋Š˜์ด_๊ณตํœด์ผ์ธ_๋Œ€์ƒ์š”์ผ์ด๊ณ _์•Œ๋žŒ์‹œ๊ฐ„์ด_๋ฏธ๋ž˜์ด๋ฉฐ_๊ณตํœด์ผ๊ฑด๋„ˆ๋›ฐ๊ธฐ_ํ™œ์„ฑ์‹œ_๋‹ค์Œ์ฃผ_ํ•ด๋‹น์š”์ผ๋กœ_๊ณ„์‚ฐ๋œ๋‹ค`() { + // ํ˜„์žฌ: 2025-01-27 (์›”, ๊ณตํœด์ผ) 10:00 + // ์•Œ๋žŒ: ๋งค์ฃผ ์›”์š”์ผ 14:00, isHolidayAlarmOff = true + // ๊ธฐ๋Œ€: ๋‹ค์Œ์ฃผ ์›”์š”์ผ 2025-02-03 (์›”) 14:00 + + // given + val calculator = AlarmTimeCalculator(clockMondayHoliday2025_10am) + val alarmTime = LocalTime.of(14, 0) + val alarm = createTestAlarm( + hour = alarmTime.hour, + minute = alarmTime.minute, + isHolidayAlarmOff = true, + repeatDays = AlarmDay.MON.bitValue // ์›”์š”์ผ ๋ฐ˜๋ณต + ) + + // when + val actualMillis = + calculator.calculateNextRepeatingTimeMillis(alarm, AlarmDay.MON, testZoneId) + + // then + val expectedDateTime = LocalDateTime.of(2025, 2, 3, 14, 0, 0) + val expectedMillis = getExpectedMillis(expectedDateTime) + assertEquals(expectedMillis, actualMillis) + } + + @Test + fun `๋ฐ˜๋ณต์š”์ผ์•Œ๋žŒ_์˜ค๋Š˜์ด_๊ณตํœด์ผ์ธ_๋Œ€์ƒ์š”์ผ์ด๊ณ _์•Œ๋žŒ์‹œ๊ฐ„์ด_๋ฏธ๋ž˜์ด๋ฉฐ_๊ณตํœด์ผ๊ฑด๋„ˆ๋›ฐ๊ธฐ_๋น„ํ™œ์„ฑ์‹œ_์˜ค๋Š˜_๊ณตํœด์ผ๋กœ_๊ณ„์‚ฐ๋œ๋‹ค`() { + // ํ˜„์žฌ: 2025-01-27 (์›”, ๊ณตํœด์ผ) 10:00 + // ์•Œ๋žŒ: ๋งค์ฃผ ์›”์š”์ผ 14:00, isHolidayAlarmOff = false + // ๊ธฐ๋Œ€: ์˜ค๋Š˜ 2025-01-27 (์›”) 14:00 + + // given + val calculator = AlarmTimeCalculator(clockMondayHoliday2025_10am) + val alarmTime = LocalTime.of(14, 0) + val alarm = createTestAlarm( + hour = alarmTime.hour, + minute = alarmTime.minute, + isHolidayAlarmOff = false, + repeatDays = AlarmDay.MON.bitValue // ์›”์š”์ผ ๋ฐ˜๋ณต + ) + + // when + val actualMillis = + calculator.calculateNextRepeatingTimeMillis(alarm, AlarmDay.MON, testZoneId) + + // then + val expectedDateTime = MONDAY_HOLIDAY_2025_01_27_10AM.with(alarmTime) + val expectedMillis = getExpectedMillis(expectedDateTime) + assertEquals(expectedMillis, actualMillis) + } + + + // --- ๋‹ค์Œ ์ฃผ๊ฐ„ ์žฌ์˜ˆ์•ฝ ์•Œ๋žŒ ์‹œ๊ฐ„ ๊ณ„์‚ฐ (calculateNextWeeklyRescheduledTimeMillis) ํ…Œ์ŠคํŠธ --- + @Test + fun `์ฃผ๊ฐ„์žฌ์˜ˆ์•ฝ_ํ˜„์žฌ_์›”์š”์ผ์˜ค์ „_๋Œ€์ƒ๋„_์›”์š”์ผ_๊ณตํœด์ผ๊ฑด๋„ˆ๋›ฐ๊ธฐ_๋น„ํ™œ์„ฑ_๋‹ค์Œ์ฃผ_์›”์š”์ผ์ด_๊ณตํœด์ผ์•„๋‹๋•Œ_๋‹ค์Œ์ฃผ_์›”์š”์ผ๋กœ_๊ณ„์‚ฐ๋œ๋‹ค`() { + // ํ˜„์žฌ: 2024-07-22 (์›”) 10:00 + // ์•Œ๋žŒ: ๋งค์ฃผ ์›”์š”์ผ 14:00, isHolidayAlarmOff = false + // ๋‹ค์Œ์ฃผ ์›”์š”์ผ: 2024-07-29 (๊ณตํœด์ผ ์•„๋‹˜) + // ๊ธฐ๋Œ€: 2024-07-29 (์›”) 14:00 + + // given + val calculator = AlarmTimeCalculator(clockMonday2024_10am) + val alarmTime = LocalTime.of(14, 0) + val alarm = createTestAlarm( + hour = alarmTime.hour, + minute = alarmTime.minute, + isHolidayAlarmOff = false, + repeatDays = AlarmDay.MON.bitValue // ์›”์š”์ผ ๋ฐ˜๋ณต + ) + + // when + val actualMillis = + calculator.calculateNextWeeklyRescheduledTimeMillis(alarm, AlarmDay.MON, testZoneId) + + // then + val expectedDateTime = LocalDateTime.of(2024, 7, 29, 14, 0, 0) + val expectedMillis = getExpectedMillis(expectedDateTime) + assertEquals(expectedMillis, actualMillis) + } + + @Test + fun `์ฃผ๊ฐ„์žฌ์˜ˆ์•ฝ_๋‹ค์Œ์ฃผ_์šธ๋ฆด์š”์ผ์ด_๊ณตํœด์ผ์ด๊ณ _๊ณตํœด์ผ๊ฑด๋„ˆ๋›ฐ๊ธฐ_๋น„ํ™œ์„ฑ์‹œ_๋‹ค์Œ์ฃผ_๊ณตํœด์ผ๋กœ_๊ณ„์‚ฐ๋œ๋‹ค`() { + // ํ˜„์žฌ: 2025-01-20 (์›”) 10:00:00 (์„ค ์—ฐํœด ์ „ ์ฃผ ์›”์š”์ผ ์˜ค์ „) + // ์•Œ๋žŒ: ๋งค์ฃผ ์›”์š”์ผ 14:00:00, ๊ณตํœด์ผ ๊ฑด๋„ˆ๋›ฐ๊ธฐ ๋น„ํ™œ์„ฑ + // ๋‹ค์Œ์ฃผ ์›”์š”์ผ: 2025-01-27 (๊ณตํœด์ผ) + // ๊ธฐ๋Œ€: 2025-01-27 (์›”, ๊ณตํœด์ผ) 14:00:00 (๊ฑด๋„ˆ๋›ฐ๊ธฐ ๋น„ํ™œ์„ฑ์ด๋ฏ€๋กœ ๋‹ค์Œ์ฃผ ๊ณตํœด์ผ์ด์–ด๋„ ์šธ๋ฆผ) + + // given + val calculator = AlarmTimeCalculator(clockMonday2025_01_20_10am) + val alarmTime = LocalTime.of(14, 0) + val alarm = createTestAlarm( + hour = alarmTime.hour, + minute = alarmTime.minute, + isHolidayAlarmOff = false, + repeatDays = AlarmDay.MON.bitValue // ์›”์š”์ผ ๋ฐ˜๋ณต + ) + + // when + val actualMillis = + calculator.calculateNextWeeklyRescheduledTimeMillis(alarm, AlarmDay.MON, testZoneId) + + // then + val expectedDateTime = LocalDateTime.of(2025, 1, 27, 14, 0, 0) + val expectedMillis = getExpectedMillis(expectedDateTime) + assertEquals(expectedMillis, actualMillis) + } + + @Test + fun `์ฃผ๊ฐ„์žฌ์˜ˆ์•ฝ_ํ˜„์žฌ_๊ธˆ์š”์ผ์˜คํ›„_๋Œ€์ƒ์€_์›”์š”์ผ_๊ณตํœด์ผ๊ฑด๋„ˆ๋›ฐ๊ธฐ_๋น„ํ™œ์„ฑ์‹œ_๋‹ค๋‹ค์Œ์ฃผ_์›”์š”์ผ๋กœ_๊ณ„์‚ฐ๋œ๋‹ค`() { + // ํ˜„์žฌ: 2024-07-26 (๊ธˆ) 15:00 + // ์•Œ๋žŒ: ๋งค์ฃผ ์›”์š”์ผ 14:00, isHolidayAlarmOff = false + // ๋กœ์ง: ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ๋‹ค์Œ ์›”์š”์ผ(29์ผ)์˜ ๊ทธ ๋‹ค์Œ ์ฃผ ์›”์š”์ผ(5์ผ) + // ๊ธฐ๋Œ€: 2024-08-05 (์›”) 14:00 + + // given + val calculator = AlarmTimeCalculator(clockFriday2024_3pm) + val alarmTime = LocalTime.of(14, 0) + val alarm = createTestAlarm( + hour = alarmTime.hour, + minute = alarmTime.minute, + isHolidayAlarmOff = false, + repeatDays = AlarmDay.MON.bitValue // ์›”์š”์ผ ๋ฐ˜๋ณต + ) + + // when + val actualMillis = + calculator.calculateNextWeeklyRescheduledTimeMillis(alarm, AlarmDay.MON, testZoneId) + + // then + val expectedDateTime = LocalDateTime.of(2024, 8, 5, 14, 0, 0) + val expectedMillis = getExpectedMillis(expectedDateTime) + assertEquals(expectedMillis, actualMillis) + } + + @Test + fun `์ฃผ๊ฐ„์žฌ์˜ˆ์•ฝ_ํ˜„์žฌ_์›”์š”์ผ_๋Œ€์ƒ๋„_์›”์š”์ผ_๊ณตํœด์ผ๊ฑด๋„ˆ๋›ฐ๊ธฐ_ํ™œ์„ฑ_๋‹ค์Œ์ฃผ_์›”์š”์ผ์ด_๊ณตํœด์ผ์ผ๋•Œ_๋‹ค๋‹ค์Œ์ฃผ_์›”์š”์ผ๋กœ_๊ณ„์‚ฐ๋œ๋‹ค`() { + // ํ˜„์žฌ: 2025-01-20 (์›”) 10:00 (์„ค ์—ฐํœด ์ „ ์ฃผ ์›”์š”์ผ ์˜ค์ „) + // ์•Œ๋žŒ: ๋งค์ฃผ ์›”์š”์ผ 14:00, isHolidayAlarmOff = true + // ๋‹ค์Œ์ฃผ ์›”์š”์ผ: 2025-01-27 (๊ณตํœด์ผ) + // ๊ธฐ๋Œ€: ๋‹ค๋‹ค์Œ์ฃผ ์›”์š”์ผ 2025-02-03 (์›”) 14:00 + + // given + val calculator = AlarmTimeCalculator(clockMonday2025_01_20_10am) + val alarmTime = LocalTime.of(14, 0) + val alarm = createTestAlarm( + hour = alarmTime.hour, + minute = alarmTime.minute, + isHolidayAlarmOff = true, + repeatDays = AlarmDay.MON.bitValue // ์›”์š”์ผ ๋ฐ˜๋ณต + ) + + // when + val actualMillis = + calculator.calculateNextWeeklyRescheduledTimeMillis(alarm, AlarmDay.MON, testZoneId) + + // then + val expectedDateTime = LocalDateTime.of(2025, 2, 3, 14, 0, 0) + val expectedMillis = getExpectedMillis(expectedDateTime) + assertEquals(expectedMillis, actualMillis) + } + + @Test + fun `์ฃผ๊ฐ„์žฌ์˜ˆ์•ฝ_ํ˜„์žฌ_์›”์š”์ผ_๋Œ€์ƒ๋„_์›”์š”์ผ_๊ณตํœด์ผ๊ฑด๋„ˆ๋›ฐ๊ธฐ_ํ™œ์„ฑ_๋‹ค์Œ์ฃผ_์›”์š”์ผ์ด_๊ณตํœด์ผ์•„๋‹๋•Œ_๋‹ค์Œ์ฃผ_์›”์š”์ผ๋กœ_๊ณ„์‚ฐ๋œ๋‹ค`() { + // ํ˜„์žฌ: 2025-02-17 (์›”) 10:00 (์‚ผ์ผ์ ˆ ์—ฐํœด ์ „์ „ ์ฃผ ์›”์š”์ผ) + // ์•Œ๋žŒ: ๋งค์ฃผ ์›”์š”์ผ 14:00, isHolidayAlarmOff = true + // ๋‹ค์Œ์ฃผ ์›”์š”์ผ: 2025-02-24 (๊ณตํœด์ผ ์•„๋‹˜) + // ๊ธฐ๋Œ€: 2025-02-24 (์›”) 14:00 + + // given + val calculator = AlarmTimeCalculator(clockMonday2025_02_17_10am) + val alarmTime = LocalTime.of(14, 0) + val alarm = createTestAlarm( + hour = alarmTime.hour, + minute = alarmTime.minute, + isHolidayAlarmOff = true, + repeatDays = AlarmDay.MON.bitValue // ์›”์š”์ผ ๋ฐ˜๋ณต + ) + + // when + val actualMillis = + calculator.calculateNextWeeklyRescheduledTimeMillis(alarm, AlarmDay.MON, testZoneId) + + // then + val expectedDateTime = LocalDateTime.of(2025, 2, 24, 14, 0, 0) + val expectedMillis = getExpectedMillis(expectedDateTime) + assertEquals(expectedMillis, actualMillis) + } +} diff --git a/core/common/src/main/java/com/yapp/common/di/ClockModule.kt b/core/common/src/main/java/com/yapp/common/di/ClockModule.kt new file mode 100644 index 00000000..50158f3a --- /dev/null +++ b/core/common/src/main/java/com/yapp/common/di/ClockModule.kt @@ -0,0 +1,17 @@ +package com.yapp.common.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import java.time.Clock +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object ClockModule { + + @Provides + @Singleton + fun provideClock(): Clock = Clock.systemDefaultZone() +} diff --git a/core/common/src/main/java/com/yapp/common/di/LocaleModule.kt b/core/common/src/main/java/com/yapp/common/di/LocaleModule.kt new file mode 100644 index 00000000..839a5a8f --- /dev/null +++ b/core/common/src/main/java/com/yapp/common/di/LocaleModule.kt @@ -0,0 +1,17 @@ +package com.yapp.common.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import java.util.Locale +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object LocaleModule { + + @Provides + @Singleton + fun provideLocale(): Locale = Locale.getDefault() +} diff --git a/core/common/src/main/java/com/yapp/common/navigation/OrbitNavigator.kt b/core/common/src/main/java/com/yapp/common/navigation/OrbitNavigator.kt index 34ceed6f..ec884f97 100644 --- a/core/common/src/main/java/com/yapp/common/navigation/OrbitNavigator.kt +++ b/core/common/src/main/java/com/yapp/common/navigation/OrbitNavigator.kt @@ -11,6 +11,7 @@ import com.yapp.common.navigation.route.FortuneBaseRoute import com.yapp.common.navigation.route.FortuneDestination import com.yapp.common.navigation.route.HomeBaseRoute import com.yapp.common.navigation.route.HomeDestination +import com.yapp.common.navigation.route.MissionRoute import com.yapp.common.navigation.route.OnboardingBaseRoute import com.yapp.common.navigation.route.OnboardingDestination import com.yapp.common.navigation.route.SettingBaseRoute @@ -57,6 +58,21 @@ class OrbitNavigator( navController.navigate(AlarmInteractionDestination.AlarmSnoozeTimer(alarm), navOptions) } + fun navigateToMissionPreview( + missionType: Int, + missionCount: Int, + navOptions: NavOptions? = null, + ) { + navController.navigate( + MissionRoute( + missionType = "$missionType", + missionCount = "$missionCount", + missionMode = "PREVIEW", + ), + navOptions, + ) + } + fun navigateToFortune(navOptions: NavOptions? = null) { navController.navigate(FortuneBaseRoute, navOptions) } diff --git a/core/common/src/main/java/com/yapp/common/navigation/route/MissionRoute.kt b/core/common/src/main/java/com/yapp/common/navigation/route/MissionRoute.kt index d5a04949..111c7788 100644 --- a/core/common/src/main/java/com/yapp/common/navigation/route/MissionRoute.kt +++ b/core/common/src/main/java/com/yapp/common/navigation/route/MissionRoute.kt @@ -1,6 +1,11 @@ package com.yapp.common.navigation.route +import com.yapp.domain.MissionMode import kotlinx.serialization.Serializable @Serializable -data object MissionRoute +data class MissionRoute( + val missionType: String, + val missionCount: String, + val missionMode: String = MissionMode.REAL.name, +) diff --git a/core/common/src/main/java/com/yapp/common/security/CryptoManager.kt b/core/common/src/main/java/com/yapp/common/security/CryptoManager.kt deleted file mode 100644 index d3995727..00000000 --- a/core/common/src/main/java/com/yapp/common/security/CryptoManager.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.yapp.common.security - -interface CryptoManager { - fun encryptData(keyAlias: String, text: String): Pair - - fun decryptData(keyAlias: String, encryptedData: ByteArray, iv: ByteArray): ByteArray -} diff --git a/core/security/.gitignore b/core/database/.gitignore similarity index 100% rename from core/security/.gitignore rename to core/database/.gitignore diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts new file mode 100644 index 00000000..5796214b --- /dev/null +++ b/core/database/build.gradle.kts @@ -0,0 +1,26 @@ +import com.yapp.convention.setNamespace + +plugins { + id("orbit.android.library") + id("androidx.room") +} + +android { + setNamespace("core.database") + + sourceSets { getByName("androidTest").assets.srcDir("$projectDir/schemas") } +} + +room { + schemaDirectory("$projectDir/schemas") +} + +dependencies { + implementation(projects.domain) + + ksp(libs.androidx.room.compiler) + implementation(libs.androidx.room.ktx) + implementation(libs.androidx.room.runtime) + + androidTestImplementation(libs.androidx.room.testing) +} diff --git a/core/security/consumer-rules.pro b/core/database/consumer-rules.pro similarity index 100% rename from core/security/consumer-rules.pro rename to core/database/consumer-rules.pro diff --git a/core/security/proguard-rules.pro b/core/database/proguard-rules.pro similarity index 100% rename from core/security/proguard-rules.pro rename to core/database/proguard-rules.pro diff --git a/core/database/schemas/com.yapp.database.AlarmDatabase/1.json b/core/database/schemas/com.yapp.database.AlarmDatabase/1.json new file mode 100644 index 00000000..f700d6a5 --- /dev/null +++ b/core/database/schemas/com.yapp.database.AlarmDatabase/1.json @@ -0,0 +1,118 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "d9643e982a8885da158bcd94c55931ff", + "entities": [ + { + "tableName": "alarm_database", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `isAm` INTEGER NOT NULL, `hour` INTEGER NOT NULL, `minute` INTEGER NOT NULL, `second` INTEGER NOT NULL, `repeatDays` INTEGER NOT NULL, `isHolidayAlarmOff` INTEGER NOT NULL, `isSnoozeEnabled` INTEGER NOT NULL, `snoozeInterval` INTEGER NOT NULL, `snoozeCount` INTEGER NOT NULL, `isVibrationEnabled` INTEGER NOT NULL, `isSoundEnabled` INTEGER NOT NULL, `soundUri` TEXT NOT NULL, `soundVolume` INTEGER NOT NULL, `isAlarmActive` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAm", + "columnName": "isAm", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hour", + "columnName": "hour", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minute", + "columnName": "minute", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "second", + "columnName": "second", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatDays", + "columnName": "repeatDays", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isHolidayAlarmOff", + "columnName": "isHolidayAlarmOff", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isSnoozeEnabled", + "columnName": "isSnoozeEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "snoozeInterval", + "columnName": "snoozeInterval", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "snoozeCount", + "columnName": "snoozeCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVibrationEnabled", + "columnName": "isVibrationEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isSoundEnabled", + "columnName": "isSoundEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "soundUri", + "columnName": "soundUri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "soundVolume", + "columnName": "soundVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAlarmActive", + "columnName": "isAlarmActive", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd9643e982a8885da158bcd94c55931ff')" + ] + } +} diff --git a/core/database/schemas/com.yapp.database.AlarmDatabase/2.json b/core/database/schemas/com.yapp.database.AlarmDatabase/2.json new file mode 100644 index 00000000..9d84a14a --- /dev/null +++ b/core/database/schemas/com.yapp.database.AlarmDatabase/2.json @@ -0,0 +1,123 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "3d2a568f32fed54188f8a57463eddcf1", + "entities": [ + { + "tableName": "alarm_database", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `hour` INTEGER NOT NULL, `minute` INTEGER NOT NULL, `second` INTEGER NOT NULL, `repeatDays` INTEGER NOT NULL, `isHolidayAlarmOff` INTEGER NOT NULL, `isSnoozeEnabled` INTEGER NOT NULL, `snoozeInterval` INTEGER NOT NULL, `snoozeCount` INTEGER NOT NULL, `isVibrationEnabled` INTEGER NOT NULL, `isSoundEnabled` INTEGER NOT NULL, `soundUri` TEXT NOT NULL, `soundVolume` INTEGER NOT NULL, `isAlarmActive` INTEGER NOT NULL, `missionType` INTEGER NOT NULL DEFAULT 1, `missionCount` INTEGER NOT NULL DEFAULT 10)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hour", + "columnName": "hour", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minute", + "columnName": "minute", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "second", + "columnName": "second", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatDays", + "columnName": "repeatDays", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isHolidayAlarmOff", + "columnName": "isHolidayAlarmOff", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isSnoozeEnabled", + "columnName": "isSnoozeEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "snoozeInterval", + "columnName": "snoozeInterval", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "snoozeCount", + "columnName": "snoozeCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVibrationEnabled", + "columnName": "isVibrationEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isSoundEnabled", + "columnName": "isSoundEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "soundUri", + "columnName": "soundUri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "soundVolume", + "columnName": "soundVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAlarmActive", + "columnName": "isAlarmActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "missionType", + "columnName": "missionType", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "missionCount", + "columnName": "missionCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "10" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3d2a568f32fed54188f8a57463eddcf1')" + ] + } +} \ No newline at end of file diff --git a/core/database/src/androidTest/java/com/yapp/database/MigrationTest.kt b/core/database/src/androidTest/java/com/yapp/database/MigrationTest.kt new file mode 100644 index 00000000..3798f783 --- /dev/null +++ b/core/database/src/androidTest/java/com/yapp/database/MigrationTest.kt @@ -0,0 +1,133 @@ +package com.yapp.database + +import androidx.room.testing.MigrationTestHelper +import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.io.IOException + +@RunWith(AndroidJUnit4::class) +class MigrationTest { + + private val testDbName = "test_alarm_database" + + @get:Rule + val helper: MigrationTestHelper = MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AlarmDatabase::class.java, + emptyList(), + FrameworkSQLiteOpenHelperFactory(), + ) + + @After + fun tearDown() { + InstrumentationRegistry.getInstrumentation() + .targetContext.deleteDatabase(testDbName) + } + + @Test + @Throws(IOException::class) + fun `๋ฒ„์ „1์—์„œ_๋ฒ„์ „2๋กœ_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์‹œ_์ƒˆ_์ปฌ๋Ÿผ์ด_๊ธฐ๋ณธ๊ฐ’์œผ๋กœ_์ฑ„์›Œ์ง`() { + helper.createDatabase(testDbName, 1).apply { + execSQL( + """ + INSERT INTO alarm_database ( + id, + isAm, + hour, + minute, + second, + repeatDays, + isHolidayAlarmOff, + isSnoozeEnabled, + snoozeInterval, + snoozeCount, + isVibrationEnabled, + isSoundEnabled, + soundUri, + soundVolume, + isAlarmActive + ) VALUES ( + null, -- id (autoGenerate) + 0, -- isAm = false + 11, -- hour + 30, -- minute + 0, -- second + 0, -- repeatDays + 0, -- isHolidayAlarmOff = false + 1, -- isSnoozeEnabled = true + 5, -- snoozeInterval + 3, -- snoozeCount + 1, -- isVibrationEnabled = true + 1, -- isSoundEnabled = true + 'alarm.mp3', -- soundUri + 70, -- soundVolume + 1 -- isAlarmActive = true + ) + """.trimIndent(), + ) + close() + } + + val db = helper.runMigrationsAndValidate(testDbName, 2, true, DatabaseMigrations.MIGRATION_1_2) + + val cursor = db.query("SELECT hour, missionType, missionCount FROM ${AlarmDatabase.DATABASE_NAME}") + cursor.use { + assertEquals(1, it.count) + it.moveToFirst() + assertEquals(1, it.getInt(it.getColumnIndexOrThrow("missionType"))) + assertEquals(10, it.getInt(it.getColumnIndexOrThrow("missionCount"))) + } + + db.close() + } + + @Test + @Throws(IOException::class) + fun `๋ฒ„์ „1์—์„œ_๋ฒ„์ „2๋กœ_๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์‹œ_12์‹œ๊ฐ„_ํฌ๋งท์ด_24์‹œ๊ฐ„_ํฌ๋งท์œผ๋กœ_์ •ํ™•ํžˆ_๋ณ€ํ™˜๋˜๋Š”์ง€_ํ™•์ธ`() { + helper.createDatabase(testDbName, 1).apply { + // 4๊ฐ€์ง€ ์ผ€์ด์Šค ์‚ฝ์ž… + listOf( + Triple(1, 12, 0), // ์˜ค์ „ 12์‹œ โ†’ 0์‹œ + Triple(0, 12, 12), // ์˜คํ›„ 12์‹œ โ†’ 12์‹œ + Triple(1, 7, 7), // ์˜ค์ „ 7์‹œ โ†’ 7์‹œ + Triple(0, 7, 19), // ์˜คํ›„ 7์‹œ โ†’ 19์‹œ + ).forEach { (isAm, hour12, _) -> + execSQL( + """ + INSERT INTO alarm_database ( + id, isAm, hour, minute, second, repeatDays, isHolidayAlarmOff, + isSnoozeEnabled, snoozeInterval, snoozeCount, isVibrationEnabled, + isSoundEnabled, soundUri, soundVolume, isAlarmActive + ) VALUES ( + null, $isAm, $hour12, 0, 0, 0, 0, 1, 5, 3, 1, 1, 'alarm.mp3', 70, 1 + ) + """.trimIndent(), + ) + } + close() + } + + val db = + helper.runMigrationsAndValidate(testDbName, 2, true, DatabaseMigrations.MIGRATION_1_2) + + val expected = listOf(0, 12, 7, 19) // ๊ธฐ๋Œ€ ๊ฒฐ๊ณผ: ๋ณ€ํ™˜๋œ hour ์ˆœ์„œ + val cursor = db.query("SELECT hour FROM ${AlarmDatabase.DATABASE_NAME}") + cursor.use { + assertEquals(4, it.count) + var idx = 0 + while (it.moveToNext()) { + val actual = it.getInt(it.getColumnIndexOrThrow("hour")) + assertEquals(expected[idx], actual) + idx++ + } + } + + db.close() + } +} diff --git a/feature/navigator/src/main/AndroidManifest.xml b/core/database/src/main/AndroidManifest.xml similarity index 98% rename from feature/navigator/src/main/AndroidManifest.xml rename to core/database/src/main/AndroidManifest.xml index 76073216..e1000761 100644 --- a/feature/navigator/src/main/AndroidManifest.xml +++ b/core/database/src/main/AndroidManifest.xml @@ -1,3 +1,4 @@ + diff --git a/data/src/main/java/com/yapp/data/local/AlarmDao.kt b/core/database/src/main/java/com/yapp/database/AlarmDao.kt similarity index 66% rename from data/src/main/java/com/yapp/data/local/AlarmDao.kt rename to core/database/src/main/java/com/yapp/database/AlarmDao.kt index 41f0291a..9d2b4e9f 100644 --- a/data/src/main/java/com/yapp/data/local/AlarmDao.kt +++ b/core/database/src/main/java/com/yapp/database/AlarmDao.kt @@ -1,4 +1,4 @@ -package com.yapp.data.local +package com.yapp.database import androidx.room.Dao import androidx.room.Insert @@ -22,17 +22,11 @@ interface AlarmDao { @Query("SELECT * FROM ${AlarmDatabase.DATABASE_NAME} WHERE id = :id") suspend fun getAlarm(id: Long): AlarmEntity? - @Query("SELECT * FROM ${AlarmDatabase.DATABASE_NAME} ORDER BY isAm DESC, hour ASC, minute ASC LIMIT :limit OFFSET :offset") - fun getPagedAlarms(limit: Int, offset: Int): Flow> - - @Query("SELECT * FROM ${AlarmDatabase.DATABASE_NAME} ORDER BY isAm DESC, hour ASC, minute ASC") + @Query("SELECT * FROM ${AlarmDatabase.DATABASE_NAME} ORDER BY hour ASC, minute ASC") fun getAllAlarms(): Flow> - @Query("SELECT * FROM ${AlarmDatabase.DATABASE_NAME} WHERE hour = :hour AND minute = :minute AND isAm = :isAm") - fun getAlarmsByTime(hour: Int, minute: Int, isAm: Boolean): Flow> - - @Query("SELECT COUNT(*) FROM ${AlarmDatabase.DATABASE_NAME}") - fun getAlarmCount(): Flow + @Query("SELECT * FROM ${AlarmDatabase.DATABASE_NAME} WHERE hour = :hour AND minute = :minute") + fun getAlarmsByTime(hour: Int, minute: Int): Flow> @Query("DELETE FROM ${AlarmDatabase.DATABASE_NAME} WHERE id = :id") suspend fun deleteAlarm(id: Long): Int diff --git a/data/src/main/java/com/yapp/data/local/AlarmDatabase.kt b/core/database/src/main/java/com/yapp/database/AlarmDatabase.kt similarity index 56% rename from data/src/main/java/com/yapp/data/local/AlarmDatabase.kt rename to core/database/src/main/java/com/yapp/database/AlarmDatabase.kt index 027f62d3..988912e2 100644 --- a/data/src/main/java/com/yapp/data/local/AlarmDatabase.kt +++ b/core/database/src/main/java/com/yapp/database/AlarmDatabase.kt @@ -1,9 +1,11 @@ -package com.yapp.data.local +package com.yapp.database import androidx.room.Database import androidx.room.RoomDatabase +import androidx.room.TypeConverters -@Database(entities = [AlarmEntity::class], version = 1, exportSchema = false) +@Database(entities = [AlarmEntity::class], version = 2, exportSchema = true) +@TypeConverters(MissionTypeConverter::class) abstract class AlarmDatabase : RoomDatabase() { abstract fun alarmDao(): AlarmDao diff --git a/data/src/main/java/com/yapp/data/local/AlarmEntity.kt b/core/database/src/main/java/com/yapp/database/AlarmEntity.kt similarity index 81% rename from data/src/main/java/com/yapp/data/local/AlarmEntity.kt rename to core/database/src/main/java/com/yapp/database/AlarmEntity.kt index 56ce2472..72320fbd 100644 --- a/data/src/main/java/com/yapp/data/local/AlarmEntity.kt +++ b/core/database/src/main/java/com/yapp/database/AlarmEntity.kt @@ -1,16 +1,16 @@ -package com.yapp.data.local +package com.yapp.database +import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey import com.yapp.domain.model.Alarm +import com.yapp.domain.model.MissionType @Entity(tableName = AlarmDatabase.DATABASE_NAME) data class AlarmEntity( @PrimaryKey(autoGenerate = true) val id: Long = 0, - val isAm: Boolean = true, - val hour: Int = 6, val minute: Int = 0, val second: Int = 0, @@ -31,11 +31,15 @@ data class AlarmEntity( val soundVolume: Int = 70, val isAlarmActive: Boolean = true, + + @ColumnInfo(defaultValue = "1") + val missionType: MissionType = MissionType.TAP, + @ColumnInfo(defaultValue = "10") + val missionCount: Int = 10, ) fun AlarmEntity.toDomain() = Alarm( id = id, - isAm = isAm, hour = hour, minute = minute, second = second, @@ -49,11 +53,12 @@ fun AlarmEntity.toDomain() = Alarm( soundUri = soundUri, soundVolume = soundVolume, isAlarmActive = isAlarmActive, + missionType = missionType, + missionCount = missionCount, ) fun Alarm.toEntity() = AlarmEntity( id = id, - isAm = isAm, hour = hour, minute = minute, second = second, @@ -67,4 +72,6 @@ fun Alarm.toEntity() = AlarmEntity( soundUri = soundUri, soundVolume = soundVolume, isAlarmActive = isAlarmActive, + missionType = missionType, + missionCount = missionCount, ) diff --git a/core/database/src/main/java/com/yapp/database/DatabaseMigrations.kt b/core/database/src/main/java/com/yapp/database/DatabaseMigrations.kt new file mode 100644 index 00000000..8d163fb2 --- /dev/null +++ b/core/database/src/main/java/com/yapp/database/DatabaseMigrations.kt @@ -0,0 +1,86 @@ +package com.yapp.database + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import com.yapp.database.AlarmDatabase.Companion.DATABASE_NAME + +internal object DatabaseMigrations { + + val MIGRATION_1_2 = object : Migration(1, 2) { + override fun migrate(database: SupportSQLiteDatabase) { + database.beginTransaction() + try { + // 1. ์ƒˆ ์Šคํ‚ค๋งˆ๋กœ ์ž„์‹œ ํ…Œ์ด๋ธ” ์ƒ์„ฑ (isAm ์ปฌ๋Ÿผ ์ œ์™ธ, missionType, missionCount ์ถ”๊ฐ€ ๋ฐ ๊ธฐ๋ณธ๊ฐ’ ๋ณ€๊ฒฝ) + database.execSQL( + """ + CREATE TABLE ${DATABASE_NAME}_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + hour INTEGER NOT NULL, + minute INTEGER NOT NULL, + second INTEGER NOT NULL, + repeatDays INTEGER NOT NULL, + isHolidayAlarmOff INTEGER NOT NULL, + isSnoozeEnabled INTEGER NOT NULL, + snoozeInterval INTEGER NOT NULL, + snoozeCount INTEGER NOT NULL, + isVibrationEnabled INTEGER NOT NULL, + isSoundEnabled INTEGER NOT NULL, + soundUri TEXT NOT NULL, + soundVolume INTEGER NOT NULL, + isAlarmActive INTEGER NOT NULL, + missionType INTEGER NOT NULL DEFAULT 1, -- ํƒ€์ž… INTEGER, ๊ธฐ๋ณธ๊ฐ’ 1 + missionCount INTEGER NOT NULL DEFAULT 10 -- ํƒ€์ž… INTEGER, ๊ธฐ๋ณธ๊ฐ’ 10 + ) + """.trimIndent(), + ) + + // 2. ๊ธฐ์กด ํ…Œ์ด๋ธ”์—์„œ ์ƒˆ ์ž„์‹œ ํ…Œ์ด๋ธ”๋กœ ๋ฐ์ดํ„ฐ ๋ณต์‚ฌ (isAm ์ปฌ๋Ÿผ์€ ๋ณต์‚ฌํ•˜์ง€ ์•Š์Œ) + database.execSQL( + """ + INSERT INTO ${DATABASE_NAME}_new ( + id, hour, minute, second, repeatDays, isHolidayAlarmOff, + isSnoozeEnabled, snoozeInterval, snoozeCount, isVibrationEnabled, + isSoundEnabled, soundUri, soundVolume, isAlarmActive + -- missionType, missionCount๋Š” CREATE TABLE์—์„œ ์ •์˜๋œ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์ž๋™ ์ฑ„์›Œ์ง + ) + SELECT + id, + -- hour๋ฅผ 24์‹œ๊ฐ„ ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + -- ์˜ˆ์‹œ: isAm ์ปฌ๋Ÿผ์ด 0 (PM)์ด๊ณ  hour๊ฐ€ 12๊ฐ€ ์•„๋‹ˆ๋ฉด hour + 12 + -- ์˜ˆ์‹œ: isAm ์ปฌ๋Ÿผ์ด 1 (AM)์ด๊ณ  hour๊ฐ€ 12 (์ž์ •)์ด๋ฉด 0์œผ๋กœ ๋ณ€ํ™˜ + -- ์‹ค์ œ isAm ์ปฌ๋Ÿผ์˜ ์˜๋ฏธ์™€ ๊ฐ’์— ๋”ฐ๋ผ ์•„๋ž˜ ๋กœ์ง์„ ์กฐ์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. + CASE + WHEN isAm = 0 AND hour != 12 THEN hour + 12 -- ์˜คํ›„ 1์‹œ ~ 11์‹œ -> 13 ~ 23์‹œ + WHEN isAm = 1 AND hour = 12 THEN 0 -- ์˜ค์ „ 12์‹œ (์ž์ •) -> 0์‹œ + ELSE hour -- ๊ทธ ์™ธ (์˜ค์ „ 1์‹œ ~ 11์‹œ, ์˜คํ›„ 12์‹œ(์ •์˜ค)) + END AS hour_24, + minute, + second, + repeatDays, + isHolidayAlarmOff, + isSnoozeEnabled, + snoozeInterval, + snoozeCount, + isVibrationEnabled, + isSoundEnabled, + soundUri, + soundVolume, + isAlarmActive + FROM $DATABASE_NAME + """.trimIndent(), + ) + + // 3. ๊ธฐ์กด ํ…Œ์ด๋ธ” ์‚ญ์ œ + database.execSQL("DROP TABLE $DATABASE_NAME") + + // 4. ์ž„์‹œ ํ…Œ์ด๋ธ”์˜ ์ด๋ฆ„์„ ๊ธฐ์กด ํ…Œ์ด๋ธ” ์ด๋ฆ„์œผ๋กœ ๋ณ€๊ฒฝ + database.execSQL("ALTER TABLE ${DATABASE_NAME}_new RENAME TO $DATABASE_NAME") + + // 5. ์ปค๋ฐ‹ + database.setTransactionSuccessful() + } finally { + database.endTransaction() + } + } + } +} diff --git a/core/database/src/main/java/com/yapp/database/MissionTypeConverter.kt b/core/database/src/main/java/com/yapp/database/MissionTypeConverter.kt new file mode 100644 index 00000000..aeb59503 --- /dev/null +++ b/core/database/src/main/java/com/yapp/database/MissionTypeConverter.kt @@ -0,0 +1,17 @@ +package com.yapp.database + +import androidx.room.TypeConverter +import com.yapp.domain.model.MissionType + +class MissionTypeConverter { + + @TypeConverter + fun fromInt(value: Int): MissionType { + return MissionType.fromInt(value) + } + + @TypeConverter + fun toInt(missionType: MissionType): Int { + return missionType.value + } +} diff --git a/data/src/main/java/com/yapp/data/local/di/DatabaseModule.kt b/core/database/src/main/java/com/yapp/database/di/DatabaseModule.kt similarity index 76% rename from data/src/main/java/com/yapp/data/local/di/DatabaseModule.kt rename to core/database/src/main/java/com/yapp/database/di/DatabaseModule.kt index 80bcd808..7c6339f2 100644 --- a/data/src/main/java/com/yapp/data/local/di/DatabaseModule.kt +++ b/core/database/src/main/java/com/yapp/database/di/DatabaseModule.kt @@ -1,9 +1,10 @@ -package com.yapp.data.local.di +package com.yapp.database.di import android.content.Context import androidx.room.Room -import com.yapp.data.local.AlarmDao -import com.yapp.data.local.AlarmDatabase +import com.yapp.database.AlarmDao +import com.yapp.database.AlarmDatabase +import com.yapp.database.DatabaseMigrations import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -24,7 +25,9 @@ class DatabaseModule { context.applicationContext, AlarmDatabase::class.java, AlarmDatabase.DATABASE_NAME, - ).build() + ) + .addMigrations(DatabaseMigrations.MIGRATION_1_2) + .build() } @Provides diff --git a/core/database/src/test/java/com/yapp/database/ExampleUnitTest.kt b/core/database/src/test/java/com/yapp/database/ExampleUnitTest.kt new file mode 100644 index 00000000..47e4e45c --- /dev/null +++ b/core/database/src/test/java/com/yapp/database/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package com.yapp.database + +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * 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) + } +} diff --git a/core/datastore/src/main/java/com/yapp/datastore/UserPreferences.kt b/core/datastore/src/main/java/com/yapp/datastore/UserPreferences.kt index 35298556..c9325870 100644 --- a/core/datastore/src/main/java/com/yapp/datastore/UserPreferences.kt +++ b/core/datastore/src/main/java/com/yapp/datastore/UserPreferences.kt @@ -1,6 +1,5 @@ package com.yapp.datastore -import android.util.Log import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey @@ -14,7 +13,6 @@ import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import java.time.LocalDate -import java.time.format.DateTimeFormatter import javax.inject.Inject import javax.inject.Singleton @@ -26,15 +24,25 @@ class UserPreferences @Inject constructor( val USER_ID = longPreferencesKey("user_id") val USER_NAME = stringPreferencesKey("user_name") val ONBOARDING_COMPLETED = booleanPreferencesKey("onboarding_completed") + val FORTUNE_ID = longPreferencesKey("fortune_id") - val FORTUNE_DATE = stringPreferencesKey("fortune_date") + val FORTUNE_DATE_EPOCH = longPreferencesKey("fortune_date_epoch") val FORTUNE_IMAGE_ID = intPreferencesKey("fortune_image_id") val FORTUNE_SCORE = intPreferencesKey("fortune_score") - val FORTUNE_CHECKED = booleanPreferencesKey("fortune_checked") - val FIRST_DISMISSED_ALARM_ID = longPreferencesKey("first_dismissed_alarm_id") - val DISMISSED_DATE = stringPreferencesKey("dismissed_date") + val FORTUNE_SEEN = booleanPreferencesKey("fortune_seen") + val FORTUNE_TOOLTIP_SHOWN = booleanPreferencesKey("fortune_tooltip_shown") + val FORTUNE_CREATING = booleanPreferencesKey("fortune_creating") + val FORTUNE_FAILED = booleanPreferencesKey("fortune_failed") + + val FIRST_ALARM_DISMISSED_TODAY = booleanPreferencesKey("first_alarm_dismissed_today") + val FIRST_ALARM_DISMISSED_DATE_EPOCH = longPreferencesKey("first_alarm_dismissed_date_epoch") + + val UPDATE_NOTICE_DONT_SHOW_VERSION = stringPreferencesKey("update_notice_dont_show_version") + val UPDATE_NOTICE_LAST_SHOWN_DATE_EPOCH = longPreferencesKey("update_notice_last_shown_date_epoch") } + private fun todayEpoch(): Long = LocalDate.now().toEpochDay() + val userIdFlow: Flow = dataStore.data .catch { emit(emptyPreferences()) } .map { it[Keys.USER_ID] } @@ -55,9 +63,9 @@ class UserPreferences @Inject constructor( .map { it[Keys.FORTUNE_ID] } .distinctUntilChanged() - val fortuneDateFlow: Flow = dataStore.data + val fortuneDateEpochFlow: Flow = dataStore.data .catch { emit(emptyPreferences()) } - .map { it[Keys.FORTUNE_DATE] } + .map { it[Keys.FORTUNE_DATE_EPOCH] } .distinctUntilChanged() val fortuneImageIdFlow: Flow = dataStore.data @@ -70,108 +78,143 @@ class UserPreferences @Inject constructor( .map { it[Keys.FORTUNE_SCORE] } .distinctUntilChanged() - val hasNewFortuneFlow: Flow = dataStore.data + val hasUnseenFortuneFlow: Flow = dataStore.data .catch { emit(emptyPreferences()) } - .map { preferences -> - val savedDate = preferences[Keys.FORTUNE_DATE] - val isChecked = preferences[Keys.FORTUNE_CHECKED] ?: true - val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE) - savedDate == todayDate && !isChecked + .map { pref -> + val isToday = pref[Keys.FORTUNE_DATE_EPOCH] == todayEpoch() + isToday && (pref[Keys.FORTUNE_ID] != null) && (pref[Keys.FORTUNE_SEEN] != true) } .distinctUntilChanged() - val firstDismissedAlarmIdFlow: Flow = dataStore.data + val shouldShowFortuneToolTipFlow: Flow = dataStore.data .catch { emit(emptyPreferences()) } - .map { preferences -> - val savedDate = preferences[Keys.DISMISSED_DATE] - val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE) - - if (savedDate == todayDate) { - preferences[Keys.FIRST_DISMISSED_ALARM_ID] - } else { - null - } + .map { pref -> + val hasTodayFortune = (pref[Keys.FORTUNE_DATE_EPOCH] == todayEpoch()) && (pref[Keys.FORTUNE_ID] != null) + val tooltipShown = pref[Keys.FORTUNE_TOOLTIP_SHOWN] ?: false + hasTodayFortune && !tooltipShown } .distinctUntilChanged() - suspend fun saveUserId(userId: Long) { - dataStore.edit { preferences -> - preferences[Keys.USER_ID] = userId + val isFortuneCreatingFlow: Flow = dataStore.data + .catch { emit(emptyPreferences()) } + .map { it[Keys.FORTUNE_CREATING] ?: false } + .distinctUntilChanged() + + val isFortuneFailedFlow: Flow = dataStore.data + .catch { emit(emptyPreferences()) } + .map { it[Keys.FORTUNE_FAILED] ?: false } + .distinctUntilChanged() + + val isFirstAlarmDismissedTodayFlow: Flow = dataStore.data + .catch { emit(emptyPreferences()) } + .map { pref -> + val flag = pref[Keys.FIRST_ALARM_DISMISSED_TODAY] ?: false + val isToday = pref[Keys.FIRST_ALARM_DISMISSED_DATE_EPOCH] == todayEpoch() + flag && isToday } + .distinctUntilChanged() + + val updateNoticeDontShowVersionFlow: Flow = dataStore.data + .catch { emit(emptyPreferences()) } + .map { it[Keys.UPDATE_NOTICE_DONT_SHOW_VERSION] } + .distinctUntilChanged() + + val updateNoticeLastShownDateEpochFlow: Flow = dataStore.data + .catch { emit(emptyPreferences()) } + .map { it[Keys.UPDATE_NOTICE_LAST_SHOWN_DATE_EPOCH] } + .distinctUntilChanged() + + suspend fun saveUserId(userId: Long) { + dataStore.edit { it[Keys.USER_ID] = userId } } suspend fun saveUserName(userName: String) { - dataStore.edit { preferences -> - preferences[Keys.USER_NAME] = userName + dataStore.edit { it[Keys.USER_NAME] = userName } + } + + suspend fun setOnboardingCompleted() { + dataStore.edit { it[Keys.ONBOARDING_COMPLETED] = true } + } + + suspend fun markFortuneCreating() { + dataStore.edit { pref -> + pref[Keys.FORTUNE_CREATING] = true + pref[Keys.FORTUNE_FAILED] = false } } - suspend fun saveFortuneId(fortuneId: Long) { - val currentDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE) - dataStore.edit { preferences -> - preferences[Keys.FORTUNE_ID] = fortuneId - preferences[Keys.FORTUNE_DATE] = currentDate - preferences[Keys.FORTUNE_CHECKED] = false + suspend fun markFortuneCreated(fortuneId: Long) { + dataStore.edit { pref -> + val today = todayEpoch() + val prevDate = pref[Keys.FORTUNE_DATE_EPOCH] + val isNewForToday = (pref[Keys.FORTUNE_ID] != fortuneId) || (prevDate != today) + + pref[Keys.FORTUNE_ID] = fortuneId + pref[Keys.FORTUNE_DATE_EPOCH] = today + pref[Keys.FORTUNE_CREATING] = false + pref[Keys.FORTUNE_FAILED] = false + + if (isNewForToday) { + pref[Keys.FORTUNE_SEEN] = false + pref[Keys.FORTUNE_TOOLTIP_SHOWN] = false + } } } - suspend fun markFortuneAsChecked() { - dataStore.edit { preferences -> - preferences[Keys.FORTUNE_CHECKED] = true + suspend fun markFortuneFailed() { + dataStore.edit { pref -> + pref[Keys.FORTUNE_CREATING] = false + pref[Keys.FORTUNE_FAILED] = true } } + suspend fun markFortuneSeen() { + dataStore.edit { it[Keys.FORTUNE_SEEN] = true } + } + + suspend fun markFortuneTooltipShown() { + dataStore.edit { it[Keys.FORTUNE_TOOLTIP_SHOWN] = true } + } + suspend fun saveFortuneImageId(imageResId: Int) { - dataStore.edit { preferences -> - preferences[Keys.FORTUNE_IMAGE_ID] = imageResId - } + dataStore.edit { it[Keys.FORTUNE_IMAGE_ID] = imageResId } } suspend fun saveFortuneScore(score: Int) { - dataStore.edit { preferences -> - preferences[Keys.FORTUNE_SCORE] = score - } + dataStore.edit { it[Keys.FORTUNE_SCORE] = score } } - suspend fun saveFirstDismissedAlarmId(alarmId: Long) { - dataStore.edit { preferences -> - val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE) - if (preferences[Keys.FIRST_DISMISSED_ALARM_ID] == null) { - preferences[Keys.FIRST_DISMISSED_ALARM_ID] = alarmId - preferences[Keys.DISMISSED_DATE] = todayDate - Log.d("UserPreferences", "์ฒซ ํ•ด์ œ๋œ ์•Œ๋žŒ ID ์ €์žฅ ์™„๋ฃŒ: $alarmId (๋‚ ์งœ: $todayDate)") - } else { - Log.d("UserPreferences", "์ด๋ฏธ ์ฒซ ์•Œ๋žŒ ํ•ด์ œ ID๊ฐ€ ์ €์žฅ๋˜์–ด ์žˆ์Œ)") - } + suspend fun markFirstAlarmDismissedToday() { + dataStore.edit { pref -> + pref[Keys.FIRST_ALARM_DISMISSED_TODAY] = true + pref[Keys.FIRST_ALARM_DISMISSED_DATE_EPOCH] = todayEpoch() } } - suspend fun setOnboardingCompleted() { - dataStore.edit { preferences -> - preferences[Keys.ONBOARDING_COMPLETED] = true - } + suspend fun markUpdateNoticeDontShow(version: String) { + dataStore.edit { it[Keys.UPDATE_NOTICE_DONT_SHOW_VERSION] = version } } - suspend fun clearDismissedAlarmId() { - dataStore.edit { preferences -> - preferences.remove(Keys.FIRST_DISMISSED_ALARM_ID) - preferences.remove(Keys.DISMISSED_DATE) + suspend fun markUpdateNoticeShownToday() { + dataStore.edit { pref -> + pref[Keys.UPDATE_NOTICE_LAST_SHOWN_DATE_EPOCH] = todayEpoch() } } suspend fun clearUserData() { - dataStore.edit { preferences -> - preferences.clear() - } + dataStore.edit { it.clear() } } - suspend fun clearFortuneId() { - dataStore.edit { preferences -> - preferences.remove(Keys.FORTUNE_ID) - preferences.remove(Keys.FORTUNE_DATE) - preferences.remove(Keys.FORTUNE_IMAGE_ID) - preferences.remove(Keys.FORTUNE_SCORE) - preferences.remove(Keys.FORTUNE_CHECKED) + suspend fun clearFortuneData() { + dataStore.edit { pref -> + pref.remove(Keys.FORTUNE_ID) + pref.remove(Keys.FORTUNE_DATE_EPOCH) + pref.remove(Keys.FORTUNE_IMAGE_ID) + pref.remove(Keys.FORTUNE_SCORE) + pref.remove(Keys.FORTUNE_SEEN) + pref.remove(Keys.FORTUNE_TOOLTIP_SHOWN) + pref.remove(Keys.FORTUNE_CREATING) + pref.remove(Keys.FORTUNE_FAILED) } } } diff --git a/core/datastore/src/main/java/com/yapp/datastore/di/DataStoreModule.kt b/core/datastore/src/main/java/com/yapp/datastore/di/DataStoreModule.kt index 41447bb8..895621d9 100644 --- a/core/datastore/src/main/java/com/yapp/datastore/di/DataStoreModule.kt +++ b/core/datastore/src/main/java/com/yapp/datastore/di/DataStoreModule.kt @@ -2,12 +2,9 @@ package com.yapp.datastore.di import android.content.Context import androidx.datastore.core.DataStore -import androidx.datastore.core.DataStoreFactory import androidx.datastore.dataStoreFile import androidx.datastore.preferences.core.PreferenceDataStoreFactory import androidx.datastore.preferences.core.Preferences -import com.yapp.datastore.token.AuthToken -import com.yapp.datastore.token.TokenDataSerializer import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -18,18 +15,6 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) object DataStoreModule { - @Provides - @Singleton - fun providesTokenDataStore( - @ApplicationContext context: Context, - tokenDataSerializer: TokenDataSerializer, - ): DataStore = - DataStoreFactory.create( - serializer = tokenDataSerializer, - ) { - context.dataStoreFile("token.json") - } - @Provides @Singleton fun providesPreferencesDataStore( diff --git a/core/datastore/src/main/java/com/yapp/datastore/token/AuthToken.kt b/core/datastore/src/main/java/com/yapp/datastore/token/AuthToken.kt deleted file mode 100644 index ec40212a..00000000 --- a/core/datastore/src/main/java/com/yapp/datastore/token/AuthToken.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.yapp.datastore.token - -import kotlinx.serialization.Serializable - -@Serializable -data class AuthToken( - val accessToken: String = "", - val refreshToken: String = "", - val isSigned: Boolean = false, -) diff --git a/core/datastore/src/main/java/com/yapp/datastore/token/TokenDataSerializer.kt b/core/datastore/src/main/java/com/yapp/datastore/token/TokenDataSerializer.kt deleted file mode 100644 index ebe7d713..00000000 --- a/core/datastore/src/main/java/com/yapp/datastore/token/TokenDataSerializer.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.yapp.datastore.token - -import androidx.datastore.core.Serializer -import com.yapp.common.security.CryptoManager -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.serialization.json.Json -import java.io.InputStream -import java.io.OutputStream -import javax.inject.Inject - -class TokenDataSerializer @Inject constructor( - private val cryptoManager: CryptoManager, -) : Serializer { - - private val securityKeyAlias = "data-store" - - override val defaultValue: AuthToken - get() = AuthToken() - - override suspend fun readFrom(input: InputStream): AuthToken { - val encryptedDataWithIv = input.readBytes() - - if (encryptedDataWithIv.size < 12) return defaultValue - - val (iv, encryptedData) = encryptedDataWithIv.splitIvAndData() - return runCatching { - val decryptedBytes = cryptoManager.decryptData(securityKeyAlias, encryptedData, iv) - Json.decodeFromString(AuthToken.serializer(), decryptedBytes.decodeToString()) - }.getOrElse { - it.printStackTrace() - defaultValue // ๋ณตํ˜ธํ™” ์‹คํŒจ ์‹œ defaultValue - } - } - - override suspend fun writeTo(t: AuthToken, output: OutputStream) { - val encryptedResult = cryptoManager.encryptData( - securityKeyAlias, - Json.encodeToString(AuthToken.serializer(), t), - ) - withContext(Dispatchers.IO) { - output.write(encryptedResult.toCombinedByteArray()) - } - } - - private fun ByteArray.splitIvAndData(): Pair { - val iv = this.copyOfRange(0, 12) - val encryptedData = this.copyOfRange(12, this.size) - return iv to encryptedData - } - - private fun Pair.toCombinedByteArray(): ByteArray { - return second + first - } -} diff --git a/core/datastore/src/main/java/com/yapp/datastore/token/TokenDataStore.kt b/core/datastore/src/main/java/com/yapp/datastore/token/TokenDataStore.kt deleted file mode 100644 index 75d9ee5d..00000000 --- a/core/datastore/src/main/java/com/yapp/datastore/token/TokenDataStore.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.yapp.datastore.token - -import android.util.Log -import androidx.datastore.core.DataStore -import java.io.IOException -import javax.inject.Inject - -class TokenDataStore @Inject constructor( - private val tokenPreferences: DataStore, -) { - - val token = tokenPreferences.data - - suspend fun setAuthToken(authToken: AuthToken) { - updateDataSafely { copy(authToken.accessToken, authToken.refreshToken, authToken.isSigned) } - } - - suspend fun setAutoLogin(isSigned: Boolean) { - updateDataSafely { copy(isSigned = isSigned) } - } - - suspend fun setAccessToken(accessToken: String) { - updateDataSafely { copy(accessToken = accessToken) } - } - - suspend fun setRefreshToken(refreshToken: String) { - updateDataSafely { copy(refreshToken = refreshToken) } - } - - private suspend fun updateDataSafely(transform: AuthToken.() -> AuthToken) { - runCatching { - tokenPreferences.updateData { it.transform() } - }.onFailure { exception -> - if (exception is IOException) { - Log.e("TokenDataStore", "๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ ์‹คํŒจ: ${exception.message}") - } - } - } -} diff --git a/core/designsystem/src/main/res/drawable-xhdpi/ic_100_buble.png b/core/designsystem/src/main/res/drawable-xhdpi/ic_100_buble.png deleted file mode 100644 index d30beec6..00000000 Binary files a/core/designsystem/src/main/res/drawable-xhdpi/ic_100_buble.png and /dev/null differ diff --git a/core/designsystem/src/main/res/drawable-xhdpi/ic_fortune_delivering_speech_bubble.png b/core/designsystem/src/main/res/drawable-xhdpi/ic_fortune_delivering_speech_bubble.png new file mode 100644 index 00000000..c75d2db3 Binary files /dev/null and b/core/designsystem/src/main/res/drawable-xhdpi/ic_fortune_delivering_speech_bubble.png differ diff --git a/core/designsystem/src/main/res/drawable-xhdpi/ic_fortune_waiting_speech_bubble.png b/core/designsystem/src/main/res/drawable-xhdpi/ic_fortune_waiting_speech_bubble.png new file mode 100644 index 00000000..a119387d Binary files /dev/null and b/core/designsystem/src/main/res/drawable-xhdpi/ic_fortune_waiting_speech_bubble.png differ diff --git a/core/designsystem/src/main/res/drawable-xxhdpi/ic_100_buble.png b/core/designsystem/src/main/res/drawable-xxhdpi/ic_100_buble.png deleted file mode 100644 index 469a7b3f..00000000 Binary files a/core/designsystem/src/main/res/drawable-xxhdpi/ic_100_buble.png and /dev/null differ diff --git a/core/designsystem/src/main/res/drawable-xxhdpi/ic_fortune_delivering_speech_bubble.png b/core/designsystem/src/main/res/drawable-xxhdpi/ic_fortune_delivering_speech_bubble.png new file mode 100644 index 00000000..318d11d6 Binary files /dev/null and b/core/designsystem/src/main/res/drawable-xxhdpi/ic_fortune_delivering_speech_bubble.png differ diff --git a/core/designsystem/src/main/res/drawable-xxhdpi/ic_fortune_waiting_speech_bubble.png b/core/designsystem/src/main/res/drawable-xxhdpi/ic_fortune_waiting_speech_bubble.png new file mode 100644 index 00000000..83b665f4 Binary files /dev/null and b/core/designsystem/src/main/res/drawable-xxhdpi/ic_fortune_waiting_speech_bubble.png differ diff --git a/core/designsystem/src/main/res/drawable/ic_100_buble.png b/core/designsystem/src/main/res/drawable/ic_100_buble.png deleted file mode 100644 index e69978c4..00000000 Binary files a/core/designsystem/src/main/res/drawable/ic_100_buble.png and /dev/null differ diff --git a/core/designsystem/src/main/res/drawable/ic_delete.xml b/core/designsystem/src/main/res/drawable/ic_delete.xml new file mode 100644 index 00000000..d2ed0eb5 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_delete.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_mission_shake.xml b/core/designsystem/src/main/res/drawable/ic_mission_shake.xml new file mode 100644 index 00000000..210b620a --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_mission_shake.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_mission_tap.xml b/core/designsystem/src/main/res/drawable/ic_mission_tap.xml new file mode 100644 index 00000000..0e202de6 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_mission_tap.xml @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/core/designsystem/src/main/res/raw/fortune_loading.json b/core/designsystem/src/main/res/raw/fortune_loading.json new file mode 100644 index 00000000..cb570812 --- /dev/null +++ b/core/designsystem/src/main/res/raw/fortune_loading.json @@ -0,0 +1 @@ +{"nm":"์ปดํฌ์ง€์…˜ 2","h":500,"w":700,"meta":{"g":"@lottiefiles/toolkit-js 0.66.4","tc":"#202f44"},"layers":[{"ty":2,"nm":"Group 1948760243.png","sr":1,"st":1,"op":45,"ip":0,"ln":"2856","hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[151.5,137]},"s":{"a":0,"k":[61.6,61.6,101.65]},"p":{"a":1,"k":[{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[130.5,159,0],"t":0},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[167.139,178.955,0],"t":15},{"o":{"x":0,"y":0},"i":{"x":1,"y":1},"s":[233.8,193.529,0],"t":31},{"s":[285.485,192.267,0],"t":45}]},"r":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[17.4],"t":0},{"s":[32.4],"t":45}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"refId":"1","ind":1},{"ty":2,"nm":"Group 1948760248-1.png","sr":1,"st":0,"op":45,"ip":0,"ln":"2855","hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[22,121.064]},"s":{"a":0,"k":[70.27,70.27,89.655]},"p":{"a":0,"k":[386,148,0]},"r":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[-9.304],"t":0},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":45},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[10],"t":48},{"s":[0],"t":51}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"refId":"2","ind":2},{"ty":2,"nm":"Vector 27856.png","sr":1,"st":0,"op":45,"ip":0,"ln":"2854","hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[93.94,9.052]},"s":{"a":0,"k":[75.628,72.572,100]},"p":{"a":0,"k":[310.5,242.5,0]},"r":{"a":0,"k":2},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"refId":"3","ind":3},{"ty":2,"nm":"Group 1948760248.png","sr":1,"st":0,"op":45,"ip":0,"ln":"2853","hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[132.5,232]},"s":{"a":0,"k":[76.4,76.4,76.4]},"p":{"a":0,"k":[370,256,0]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"refId":"4","ind":4}],"v":"5.7.0","fr":30,"op":45,"ip":0,"assets":[{"id":"1","e":1,"w":303,"h":274,"p":"data:image/webp;base64,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","u":""},{"id":"2","e":1,"w":100,"h":148,"p":"data:image/webp;base64,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","u":""},{"id":"3","e":1,"w":154,"h":124,"p":"data:image/webp;base64,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","u":""},{"id":"4","e":1,"w":265,"h":464,"p":"data:image/webp;base64,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","u":""}]} \ No newline at end of file diff --git a/core/designsystem/src/main/res/raw/mission_shake.json b/core/designsystem/src/main/res/raw/mission_shake.json new file mode 100644 index 00000000..a9598279 --- /dev/null +++ b/core/designsystem/src/main/res/raw/mission_shake.json @@ -0,0 +1 @@ +{"assets":[{"id":"el-5276-8N90","layers":[{"ddd":0,"ind":2,"ty":4,"nm":"Layer 1","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[22.791,16.319]},"o":{"a":0,"k":100},"p":{"a":0,"k":[22.791,16.319]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Path 1 (10) Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1 (10)","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[-0.02,-0.005],[-0.154,-0.262],[-0.131,-0.066],[0.066,-0.569],[0.215,-0.288],[0.257,-0.923],[0.124,-0.17],[0.037,-0.455],[0.442,-0.937],[0.053,-0.047],[0.091,-0.015],[0.276,-0.05],[0.207,0.214],[-0.121,0.311],[0.046,0.294],[-0.169,0.421],[0.107,0.289],[0.72,0.185],[0.156,0.1],[0.117,0.252],[-0.111,0.274],[-0.303,-0.078],[-0.275,0.131],[-0.875,-0.205],[-0.129,0.269],[-0.144,0.246],[-0.006,0.18],[-0.654,0.033]],"o":[[0.147,-0.023],[0.175,0.025],[0.077,0.125],[0.18,0.087],[-0.041,0.554],[-0.124,0.17],[-0.252,0.903],[-0.143,0.165],[-0.069,0.587],[-0.087,0.18],[-0.083,0.04],[-0.147,0.023],[-0.341,0.073],[-0.184,-0.229],[0.097,-0.218],[-0.045,-0.293],[0.38,-0.932],[-0.083,-0.304],[-0.435,-0.112],[-0.137,-0.096],[-0.178,-0.409],[0.11,-0.275],[0.076,0.02],[0.4,-0.14],[0.289,0.053],[0.048,-0.109],[0.143,-0.245],[0.046,-0.493],[0,0]],"v":[[20.493,5.13],[20.743,5.103],[21.237,5.533],[21.556,5.826],[21.727,6.809],[21.342,8.072],[20.77,9.711],[20.206,11.321],[19.936,12.251],[19.169,14.536],[18.96,14.876],[18.697,14.959],[18.063,15.069],[17.24,14.858],[17.145,14.047],[17.221,13.279],[17.406,12.207],[17.816,10.375],[16.611,9.642],[15.725,9.324],[15.344,8.802],[15.244,7.778],[15.864,7.483],[16.391,7.316],[18.303,7.413],[18.931,7.09],[19.219,6.558],[19.443,5.92],[20.493,5.13]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (10)","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[-0.292,-0.197],[-0.089,-0.205],[0.848,-0.63],[0.315,-0.363],[-0.079,-0.242],[-0.744,-0.716],[-0.149,-0.281],[-0.19,-0.049],[-0.079,-0.323],[0.125,-0.33],[0.257,-0.055],[0.233,-0.121],[0.353,0.433],[0.229,0.223],[0.15,0.276],[0.215,0.255],[0.008,0.123],[0.217,0.257],[0.156,0.1],[0.156,-0.061],[0.105,-0.124],[0.291,-0.187],[0.21,-0.188],[0.408,-0.177],[0.151,0.038],[0.202,0.314],[-0.043,0.171],[-0.258,0.296],[-0.205,0.088],[-0.263,0.235],[-0.523,0.309],[-0.339,0.295],[-0.166,0.018],[-0.163,0.321],[-0.391,0.263],[-0.147,0.023]],"o":[[0.185,-0.013],[0.312,0.2],[0.205,0.537],[-0.3,0.226],[-0.372,0.429],[0.08,0.242],[0.193,0.19],[0.15,0.28],[0.264,0.068],[0.078,0.323],[-0.107,0.337],[-0.147,0.023],[-0.413,0.197],[-0.218,-0.235],[-0.224,-0.221],[-0.145,-0.3],[-0.217,-0.258],[0.006,-0.1],[-0.217,-0.257],[-0.085,-0.063],[-0.145,0.072],[-0.123,0.17],[-0.29,0.188],[-0.229,0.184],[-0.404,0.158],[-0.171,-0.044],[-0.197,-0.333],[0.044,-0.171],[0.281,-0.312],[0.247,-0.098],[0.262,-0.236],[1.285,-0.76],[0.257,-0.217],[0.227,-0.022],[0.053,-0.128],[0.39,-0.263],[0,0]],"v":[[12.18,3.087],[12.896,3.362],[13.497,3.97],[12.533,5.72],[11.61,6.603],[11.17,7.61],[12.406,9.047],[12.92,9.754],[13.43,10.248],[13.945,10.834],[13.875,11.814],[13.33,12.402],[12.76,12.618],[11.61,12.263],[10.94,11.576],[10.375,10.826],[9.833,9.991],[9.495,9.42],[9.179,8.884],[8.62,8.348],[8.258,8.345],[7.878,8.642],[7.257,9.178],[6.507,9.742],[5.551,10.284],[4.718,10.464],[4.159,9.927],[3.929,9.171],[4.382,8.471],[5.111,7.871],[5.875,7.371],[7.053,6.553],[9.488,4.97],[10.123,4.618],[10.709,4.103],[11.374,3.517],[12.18,3.088]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (10)","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[-0.965,-0.489],[-0.241,-0.243],[-0.185,-0.068],[-0.094,-0.024],[-0.267,-0.533],[0.002,-0.323],[-0.028,-0.129],[0.168,-0.579],[0.395,-0.282],[0.147,-0.252],[0.417,0.107],[0.266,-0.094],[0.209,-0.027],[0.044,-0.009],[0.763,0.175],[0.299,0.017],[0.26,0.167],[0.128,-0.028],[0.391,0.281],[0.203,0.234],[0.176,0.192],[0.125,0.537],[0.096,0.273],[-0.127,0.572],[-0.053,0.127],[-0.158,0.222],[-0.221,0.388],[-0.171,0.117],[-0.294,0.045],[-0.744,0.07],[-0.155,-0.02],[-0.601,-0.175],[-0.351,-0.051]],"o":[[0.605,0.075],[0.212,0.116],[0.24,0.243],[0.18,0.087],[0.213,0.115],[0.271,0.513],[-0.007,0.26],[0.147,0.585],[-0.17,0.582],[-0.24,0.167],[-0.296,0.449],[-0.189,-0.049],[-0.304,0.083],[-0.269,0.032],[-0.497,0.053],[-0.738,-0.189],[-0.309,0.002],[-0.221,-0.158],[-0.11,0.032],[-0.396,-0.263],[-0.168,-0.199],[-0.442,-0.477],[-0.042,-0.286],[-0.178,-0.409],[0.107,-0.498],[0.058,-0.147],[0.191,-0.273],[0.183,-0.316],[0.177,-0.137],[0.043,-0.01],[0.418,-0.055],[0.176,0.024],[0.809,0.227],[0,0]],"v":[[12.072,14.713],[14.427,15.559],[15.106,16.097],[15.743,16.563],[16.154,16.729],[16.874,17.701],[17.278,18.955],[17.31,19.538],[17.278,21.315],[16.43,22.611],[15.843,23.247],[14.773,23.76],[14.091,23.827],[13.321,23.992],[12.851,24.053],[10.961,23.871],[9.406,23.562],[8.533,23.308],[8.009,23.113],[7.257,22.739],[6.359,21.993],[5.843,21.407],[4.993,19.887],[4.785,19.047],[4.709,17.575],[4.949,16.638],[5.273,16.085],[5.89,15.093],[6.42,14.443],[7.126,14.17],[8.307,14.05],[9.167,13.998],[10.332,14.297],[12.072,14.713]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (10)","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0.207,0.092],[0.459,0.017],[0.346,0.078],[0.348,-0.093],[0.049,-0.189],[0.205,-0.169],[0.001,-0.241],[-0.281,-0.477],[-0.094,-0.186],[-0.047,-0.053],[-0.246,-0.063],[-0.265,-0.149],[-0.763,-0.095],[-0.418,0.216],[-0.157,0.061],[-0.121,0.393],[0.169,0.287],[-0.034,0.133],[0.561,0.406],[0.488,-0.016]],"o":[[-0.226,-0.018],[-0.217,-0.096],[-0.354,-0.021],[-0.353,-0.074],[-0.342,0.073],[-0.024,0.095],[-0.276,0.212],[-0.001,0.241],[0.234,0.423],[0.093,0.185],[0.071,0.038],[0.34,0.087],[0.581,0.331],[0.787,0.081],[0.305,-0.163],[0.271,-0.111],[0.14,-0.387],[-0.141,-0.237],[0.077,-0.303],[-0.543,-0.402],[0,0]],"v":[[12.297,17.373],[11.643,17.206],[10.628,17.036],[9.576,16.887],[8.51,16.916],[7.924,17.31],[7.58,17.706],[7.164,18.386],[7.584,19.463],[8.076,20.376],[8.286,20.733],[8.762,20.885],[9.67,21.239],[11.685,21.877],[13.492,21.675],[14.184,21.338],[14.772,20.581],[14.729,19.571],[14.569,19.016],[13.843,17.952],[12.297,17.373]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (10)","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[-0.52,-0.255],[-0.293,-0.116],[-0.193,-0.19],[-0.075,-0.02],[-0.233,-0.503],[0.063,-0.327],[-0.016,-0.219],[0.133,-0.753],[0.126,-0.411],[0.435,0.113],[0.247,-0.098],[0.208,0.135],[-0.092,0.359],[0.008,0.285],[-0.138,0.221],[-0.025,0.256],[-0.03,0.293],[3.451,-0.002],[0.328,0.077],[0.854,-0.023],[0.373,0.197],[-0.111,0.275],[0.051,0.187],[-0.395,0.363],[-0.322,-0.082],[-0.37,0.026],[-0.076,-0.02],[-0.174,-0.105],[-0.26,-0.007],[-0.453,-0.197],[-0.179,-0.006],[-0.749,-0.151],[-0.043,0.009]],"o":[[0.076,-0.061],[0.269,0.129],[0.217,0.097],[0.17,0.205],[0.285,0.073],[0.239,0.485],[-0.047,0.215],[0.013,0.185],[-0.113,0.758],[-0.185,0.72],[-0.113,-0.029],[-0.399,0.14],[-0.203,-0.153],[0.059,-0.227],[-0.014,-0.26],[0.125,-0.25],[0.059,-0.289],[0.2,-1.724],[-0.337,0.007],[-0.606,-0.155],[-0.644,-0.004],[-0.354,-0.192],[0.085,-0.174],[-0.055,-0.337],[0.396,-0.362],[0.132,0.034],[0.265,-0.013],[0.095,0.025],[0.26,0.167],[0.28,0.012],[0.46,0.179],[0.185,-0.013],[0.728,0.147],[0,0]],"v":[[33.33,7.608],[34.224,7.898],[35.068,8.266],[35.684,8.696],[36.051,9.033],[36.828,9.898],[37.091,11.115],[37.044,11.769],[36.864,13.176],[36.505,14.93],[35.575,15.841],[35.034,15.945],[34.124,15.953],[33.958,15.184],[34.034,14.417],[34.224,13.678],[34.449,12.919],[34.582,12.045],[29.706,9.462],[28.705,9.356],[26.515,9.158],[24.989,8.857],[24.624,8.157],[24.677,7.597],[25.188,6.547],[26.265,6.127],[27.018,6.139],[27.53,6.149],[27.934,6.344],[28.714,6.604],[29.814,6.917],[30.773,7.194],[32.173,7.401],[33.33,7.608]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (10)","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[-0.264,-0.229],[-0.227,-0.139],[0.438,-1.159],[-0.032,-0.109],[-0.293,-0.035],[-1.335,-0.383],[-0.753,-0.133],[-0.26,-0.168],[-0.246,-0.063],[-0.047,-0.133],[0.073,-0.284],[0.134,-0.128],[0.298,0.016],[0.351,-0.031],[0.094,0.024],[0.138,0.081],[0.928,0.237],[0.714,0.203],[0.09,-0.037],[0.866,0.242],[0.455,0.036],[0.213,0.042],[0.302,0.159],[0.247,0.063],[0.109,0.129],[0.064,0.299],[-0.029,0.113],[-0.148,0.184],[-0.166,0.017],[-0.856,-0.2],[-0.223,0.003],[-0.159,0.302],[-0.125,0.169],[-0.021,0.237],[-0.475,0.121]],"o":[[0.323,-0.078],[0.147,0.139],[0.471,0.283],[-0.149,0.426],[0.033,0.11],[0.298,0.016],[0.36,0.092],[0.747,0.151],[0.222,0.158],[0.227,0.058],[0.071,0.119],[-0.087,0.34],[-0.129,0.108],[-0.124,0.009],[-0.228,0.022],[-0.15,-0.053],[-0.283,-0.154],[-1.633,-0.399],[-0.412,-0.126],[-0.205,0.089],[-0.644,-0.165],[-0.217,-0.007],[-0.114,-0.029],[-0.392,-0.201],[-0.227,-0.058],[-0.089,-0.124],[-0.065,-0.3],[0.034,-0.132],[0.248,-0.26],[0.167,-0.018],[1.141,0.273],[0.228,-0.023],[0.114,-0.213],[0.129,-0.188],[0.045,-0.412],[0,0]],"v":[[27.945,12.551],[28.825,12.777],[29.385,13.193],[29.435,15.355],[29.259,16.157],[29.749,16.374],[32.198,16.972],[33.867,17.31],[35.378,17.788],[36.08,18.12],[36.49,18.407],[36.487,19.011],[36.155,19.713],[35.515,19.851],[34.803,19.911],[34.32,19.908],[33.887,19.706],[32.07,19.119],[28.55,18.215],[27.797,18.082],[26.191,17.852],[24.543,17.55],[23.897,17.475],[23.273,17.193],[22.315,16.796],[21.812,16.516],[21.582,15.882],[21.529,15.262],[21.802,14.788],[22.423,14.372],[23.957,14.645],[26.002,15.049],[26.582,14.562],[26.94,13.988],[27.165,13.35],[27.945,12.551]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (10)","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[-0.515,-0.273],[-0.303,-0.078],[-0.707,-0.706],[-0.274,-0.11],[-0.08,-0.081],[-0.146,-0.219],[-0.151,-0.12],[0.017,-0.54],[-0.061,-0.157],[0.17,-0.582],[0.433,-0.353],[0.117,-0.176],[0.568,0.146],[0.493,-0.116],[0.356,0.03],[0.611,0.137],[0.413,-0.035],[0.076,0.02],[0.107,0.076],[0.245,0.043],[0.391,0.282],[0.179,0.247],[0.207,0.19],[0.189,0.755],[-0.103,0.478],[0.013,0.104],[-0.749,0.796],[-0.233,0.122],[-0.337,-0.026],[-0.413,0.196],[-0.413,-0.046]],"o":[[0.581,0.048],[0.288,0.135],[0.909,0.233],[0.24,0.243],[0.218,0.097],[0.085,0.062],[0.098,0.167],[0.283,0.233],[-0.012,0.28],[0.243,0.466],[-0.13,0.543],[-0.164,0.134],[-0.325,0.482],[-0.151,-0.04],[-0.493,0.115],[-0.341,-0.007],[-0.624,-0.16],[-0.308,0.022],[-0.124,-0.043],[-0.21,-0.133],[-0.256,-0.025],[-0.373,-0.277],[-0.144,-0.241],[-0.306,-0.301],[-0.185,-0.774],[0.092,-0.36],[-0.055,-0.257],[0.368,-0.41],[0.233,-0.122],[0.398,0.021],[0.28,-0.151],[0,0]],"v":[[26.162,19.025],[27.823,19.511],[28.71,19.83],[31.133,21.239],[31.904,21.769],[32.351,22.036],[32.697,22.457],[33.071,22.887],[33.469,24.047],[33.543,24.702],[33.653,26.274],[32.785,27.655],[32.362,28.122],[31.022,28.626],[30.055,28.74],[28.781,28.868],[27.353,28.652],[25.797,28.465],[25.221,28.468],[24.874,28.288],[24.186,28.021],[23.215,27.56],[22.388,26.773],[21.858,26.123],[21.115,24.539],[20.992,22.661],[21.11,21.965],[22.15,20.386],[23.052,19.588],[23.906,19.444],[25.123,19.182],[26.163,19.025]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (10)","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0.147,-0.023],[0.549,0.061],[0.257,-0.055],[0.053,-0.208],[0.295,-0.207],[-0.018,-0.165],[-0.417,-0.893],[-0.1,-0.086],[-0.303,-0.078],[-0.349,-0.211],[-0.914,-0.053],[-0.509,0.253],[-0.107,0.337],[0.173,0.347],[0.048,0.25],[0.327,0.225],[0.515,0.111],[0.213,0.116]],"o":[[-0.264,-0.148],[-0.123,0.009],[-0.63,-0.06],[-0.232,0.041],[-0.025,0.095],[-0.29,0.188],[0.037,0.172],[0.153,0.342],[0.099,0.086],[0.284,0.073],[0.482,0.325],[0.92,0.034],[0.371,-0.187],[0.111,-0.355],[-0.133,-0.217],[-0.04,-0.313],[-0.302,-0.24],[-0.554,-0.121],[0,0]],"v":[[27.693,22.141],[27.076,21.953],[26.067,21.875],[24.737,21.867],[24.309,22.241],[23.829,22.693],[23.421,23.223],[24.101,24.821],[24.481,25.463],[25.083,25.709],[26.033,26.134],[28.127,26.702],[30.27,26.374],[30.986,25.589],[30.893,24.536],[30.62,23.831],[30.07,23.024],[28.844,22.497],[27.694,22.141]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (10)","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[-0.259,-0.167],[-0.315,-0.424],[0.331,-1.287],[0.221,-0.387],[0.031,-0.355],[0.126,-0.617],[-0.004,-0.222],[0.162,-0.24],[0.078,-0.279],[0.057,-0.066],[0.233,-0.121],[0.143,-0.004],[0.274,0.191],[0.054,0.417],[-0.239,0.928],[0,0],[0,0],[-0.246,0.724],[0.041,0.151],[-0.23,0.425],[0.051,0.194],[-0.211,0.35],[-0.129,0.27],[-0.26,-0.007]],"o":[[0.37,-0.026],[0.265,0.149],[0.094,0.105],[-0.325,1.268],[-0.11,0.194],[-0.053,0.627],[-0.097,0.379],[-0.012,0.28],[-0.157,0.244],[-0.044,0.17],[-0.058,0.067],[-0.304,0.164],[-0.118,-0.01],[-0.383,-0.24],[-0.05,-0.436],[0,0],[0,0],[0.064,-0.327],[0.169,-0.582],[-0.05,-0.276],[0.172,-0.279],[-0.069,-0.36],[0.096,-0.137],[0.255,-0.52],[0,0]],"v":[[39.82,11.362],[40.764,11.574],[41.634,12.433],[41.279,14.521],[40.46,17.004],[40.249,17.828],[39.981,19.696],[39.841,20.598],[39.58,21.378],[39.226,22.166],[39.075,22.52],[38.639,22.802],[37.969,23.054],[37.381,22.752],[36.726,21.766],[37.009,19.72],[37.242,18.812],[37.388,18.244],[37.853,16.668],[38.045,15.568],[38.315,14.517],[38.497,13.807],[38.709,12.742],[39.047,12.132],[39.82,11.362]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (10)","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[-0.094,-0.024],[-0.345,-0.23],[-0.033,-0.19],[0.116,-0.374],[0.252,-0.117],[0.544,0.161],[0.103,0.147],[-0.027,0.417],[-0.313,0.121],[-0.257,0.136]],"o":[[0.253,-0.117],[0.119,0.01],[0.273,0.191],[0.055,0.175],[-0.174,0.52],[-0.252,0.117],[-0.43,-0.131],[-0.09,-0.124],[0.041,-0.555],[0.152,-0.041],[0,0]],"v":[[36.224,26.904],[36.744,26.765],[37.439,27.125],[37.898,27.697],[37.807,28.521],[37.168,29.477],[35.974,29.412],[35.174,28.995],[35.079,28.184],[35.611,27.17],[36.224,26.904]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[1,1,1]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-5253-8N90","layers":[{"ddd":0,"ind":5,"ty":4,"nm":"Layer 1","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[12.737,11.607]},"o":{"a":0,"k":100},"p":{"a":0,"k":[12.737,11.607]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Path 1 (3) Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1 (3)","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[-0.057,-0.015],[-0.212,-0.116],[-0.253,-0.428],[-0.008,-0.124],[0.145,-0.326],[0.079,-0.545],[-0.021,-0.308],[0.169,-0.421],[-0.013,-0.185],[0.244,-0.321],[0.213,-0.753],[0.093,-0.689],[0.096,-0.217],[0.105,-0.359],[0.237,-0.405],[0.025,-0.094],[-0.009,-0.204],[0.063,-0.166],[0.258,-0.297],[0.138,-0.065],[0.245,0.063],[0.179,0.167],[-0.048,0.189],[0.079,0.323],[-0.048,0.109],[-0.165,0.483],[-0.068,0.265],[-0.207,0.411],[-0.026,0.255],[-0.203,0.634],[-0.072,0.122],[-0.002,0.322],[-0.193,0.677],[0.004,0.143],[0.203,0.073],[0.157,0.485],[-0.211,0.269],[-0.414,0.277],[-0.351,0.111]],"o":[[0.418,-0.135],[0.062,-0.005],[0.208,0.134],[0.207,0.375],[0.009,0.123],[-0.125,0.25],[-0.073,0.525],[0.027,0.29],[-0.168,0.42],[0.036,0.332],[-0.124,0.169],[-0.176,0.673],[-0.055,0.37],[-0.154,0.34],[-0.162,0.44],[-0.153,0.283],[-0.019,0.076],[0.017,0.327],[-0.057,0.147],[-0.262,0.316],[-0.115,0.052],[-0.304,-0.078],[-0.155,-0.181],[0.015,-0.057],[-0.078,-0.322],[0.095,-0.137],[0.164,-0.482],[0.365,-1.582],[0.133,-0.208],[0.005,-0.18],[0.203,-0.635],[0.063,-0.085],[0.002,-0.323],[0.156,-0.605],[-0.004,-0.142],[-0.327,-0.144],[-0.135,-0.498],[0.181,-0.237],[0.433,-0.273],[0,0]],"v":[[6.798,1.742],[7.51,1.562],[7.921,1.728],[8.613,2.571],[8.935,3.32],[8.732,3.994],[8.426,5.187],[8.347,6.437],[8.134,7.503],[7.901,8.411],[7.589,9.391],[7.083,10.774],[6.679,12.819],[6.453,13.699],[6.063,14.749],[5.464,16.019],[5.197,16.585],[5.181,17.005],[5.111,17.744],[4.638,18.409],[4.038,18.981],[3.498,18.964],[2.774,18.597],[2.614,18.041],[2.518,17.471],[2.473,16.824],[2.863,15.895],[3.211,14.775],[4.069,11.786],[4.308,11.091],[4.621,9.87],[5.033,8.735],[5.13,8.124],[5.423,6.625],[5.651,5.503],[5.341,5.181],[4.614,4.238],[4.728,3.088],[5.621,2.318],[6.798,1.742]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (3)","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[-0.321,-0.243],[-0.696,-0.199],[-0.147,-0.057],[-0.236,-0.183],[-0.151,-0.12],[-0.319,-0.485],[-0.051,-0.195],[-0.11,-0.593],[0.01,-0.118],[-0.089,-0.139],[-0.07,-0.118],[-0.069,-0.522],[0.107,-0.174],[-0.083,-0.304],[0.072,-0.123],[0.18,-0.619],[0.196,-0.213],[0.102,-0.226],[0.339,-0.377],[0.106,-0.219],[0.148,-0.104],[0.628,-0.323],[0.341,0.087],[0.404,-0.078],[0.185,-0.094],[0.969,0.309],[0.631,0.687],[0.249,0.133],[0.367,0.376],[-0.043,0.17],[0.201,0.396],[-0.012,0.361],[0.088,0.366],[-0.034,0.328],[0.047,0.261],[-0.076,0.209],[0.021,0.228],[-0.13,0.192],[0.011,0.35],[-0.345,0.637],[-0.243,0.159],[-0.2,0.231],[-0.367,0.329],[-0.167,0.098],[-0.249,0.259],[-0.559,0.287],[-0.341,-0.087],[-0.11,0.033]],"o":[[0.251,-0.036],[0.217,0.177],[0.492,0.127],[0.147,0.057],[0.387,0.3],[0.189,0.13],[0.325,0.467],[0.018,0.167],[0.123,0.617],[0.012,0.165],[0.146,0.22],[0.14,0.238],[0.073,0.503],[-0.23,0.425],[0.037,0.171],[-0.071,0.123],[-0.16,0.625],[-0.164,0.187],[-0.198,0.466],[-0.174,0.17],[-0.1,0.155],[-0.124,0.089],[-0.718,0.36],[-0.113,-0.029],[-0.384,0.083],[-0.6,0.29],[-0.95,-0.305],[-0.181,-0.217],[-0.36,-0.173],[-0.343,-0.391],[0.01,-0.037],[-0.281,-0.555],[0.001,-0.161],[-0.067,-0.323],[0.04,-0.262],[-0.045,-0.218],[0.1,-0.236],[-0.022,-0.23],[0.196,-0.291],[-0.017,-0.247],[0.369,-0.652],[0.1,-0.075],[0.225,-0.245],[0.387,-0.325],[0.161,-0.08],[0.45,-0.438],[0.552,-0.262],[0.34,0.088],[0,0]],"v":[[15.941,3.877],[16.799,4.188],[18.168,4.751],[19.126,5.027],[19.7,5.387],[20.506,6.017],[21.268,6.939],[21.831,7.931],[22.023,9.07],[22.193,10.173],[22.347,10.636],[22.671,11.143],[22.984,12.283],[22.934,13.299],[22.714,14.393],[22.662,14.833],[22.285,15.947],[21.751,17.203],[21.349,17.826],[20.538,19.1],[20.115,19.688],[19.743,20.077],[18.615,20.695],[17.027,21.105],[16.251,21.178],[15.397,21.443],[13.043,21.414],[10.671,19.927],[10.02,19.397],[8.93,18.573],[8.48,17.731],[8.193,17.081],[7.789,15.707],[7.659,14.917],[7.609,13.935],[7.599,13.146],[7.646,12.492],[7.764,11.796],[7.931,11.143],[8.215,10.157],[8.707,8.83],[9.625,7.613],[10.075,7.153],[10.962,6.292],[11.792,5.657],[12.406,5.149],[13.927,4.056],[15.266,3.794],[15.941,3.877]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (3)","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0.91,-0.008],[0.529,-0.41],[0.35,-0.078],[0.083,-0.322],[0.191,-0.273],[0.22,-0.575],[-0.011,-0.427],[0.075,-0.607],[-0.182,-0.47],[-0.343,-0.391],[-0.104,-0.067],[-0.397,-0.102],[-0.283,-0.153],[-0.602,-0.013],[-0.418,0.215],[-0.261,0.075],[-0.086,0.099],[-0.059,0.227],[-0.239,0.222],[-0.101,0.236],[-0.136,0.254],[-0.005,0.1],[-0.145,0.407],[0.195,0.575],[-0.001,0.208],[0.409,0.69],[0.235,0.424],[0.227,0.139],[0.17,0.124]],"o":[[-0.486,-0.387],[-0.91,0.008],[-0.284,0.218],[-0.47,0.101],[-0.033,0.133],[-0.357,0.502],[-0.217,0.61],[0.001,0.706],[-0.045,0.331],[0.187,0.452],[0.466,0.543],[0.123,0.071],[0.479,0.103],[0.439,0.254],[0.602,0.013],[0.252,-0.117],[0.218,-0.065],[0.11,-0.113],[0.058,-0.227],[0.238,-0.221],[0.099,-0.27],[0.153,-0.283],[0.015,-0.056],[0.293,-0.832],[-0.078,-0.192],[-0.031,-0.351],[-0.225,-0.38],[-0.163,-0.305],[-0.203,-0.153],[0,0]],"v":[[17.788,7.529],[15.694,6.961],[13.535,7.588],[12.573,8.037],[11.743,8.671],[11.406,9.281],[10.536,10.903],[10.226,12.459],[10.115,14.428],[10.321,15.63],[11.116,16.894],[11.971,17.81],[12.751,18.07],[13.894,18.454],[15.456,18.855],[16.986,18.552],[17.756,18.265],[18.212,18.019],[18.465,17.509],[18.91,16.836],[19.419,16.15],[19.772,15.363],[20.01,14.788],[20.25,14.093],[20.398,11.983],[20.281,11.377],[19.622,9.816],[18.932,8.61],[18.347,7.945],[17.787,7.529]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[1,1,1]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-5242-8N90","layers":[{"ddd":0,"ind":8,"ty":4,"nm":"Layer 1","hd":true,"sr":1,"ks":{"a":{"a":0,"k":[9.566,11.387]},"o":{"a":0,"k":100},"p":{"a":0,"k":[9.566,11.387]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":true,"nm":"Path 1 (2) Group","bm":0,"it":[{"ty":"sh","hd":true,"nm":"Path 1 (2)","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[-0.545,-0.624],[0.003,-0.483],[0.186,-0.255],[-0.016,-0.327],[0.195,-0.839],[0.25,-0.501],[0.022,-0.245],[-0.061,-0.157],[-0.98,-0.271],[-0.508,-0.381],[0.068,-0.265],[0.285,-0.249],[0.109,-0.033],[0.332,0.045],[0.132,0.114],[0.445,0.073],[0.219,-0.065],[0.051,-0.43],[0.126,-0.411],[-0.041,-0.231],[0.029,-0.114],[0.162,-0.241],[0.063,-0.244],[0.291,-0.269],[0.171,-0.037],[0.213,0.035],[0.157,0.084],[-0.057,0.692],[-0.363,0.471],[-0.136,0.611],[0.007,0.204],[0.595,0.274],[0.217,0.122],[0.526,0.074],[0.458,0.178],[0.123,-0.009],[0.387,0.22],[0.095,0.104],[0.093,0.265],[0.088,0.181],[-0.185,0.638],[-0.273,0.434],[-0.11,0.113],[-0.242,0.079],[-0.32,0.383],[-0.223,0.085],[-0.257,0.217],[-0.247,0.098],[-0.086,0.099],[-0.485,0.157],[-0.167,0.099],[-0.138,0.147],[-0.181,0.075],[-0.138,0.065],[-0.36,-0.013]],"o":[[0.853,-0.023],[0.184,0.229],[-0.003,0.483],[-0.32,0.462],[0.017,0.407],[-0.195,0.837],[-0.098,0.225],[-0.007,0.26],[0.065,0.138],[0.607,0.186],[0.472,0.363],[-0.02,0.076],[-0.267,0.253],[-0.237,0.06],[-0.327,-0.064],[-0.184,-0.149],[-0.445,-0.074],[-0.441,0.15],[0.009,0.204],[-0.097,0.38],[0.051,0.195],[-0.025,0.094],[-0.152,0.201],[-0.039,0.151],[-0.267,0.254],[-0.086,0.018],[-0.173,-0.04],[-0.279,-0.172],[0.08,-0.706],[0.091,-0.118],[0.136,-0.611],[-0.008,-0.284],[-0.231,-0.094],[-0.175,-0.106],[-0.489,-0.045],[-0.44,-0.173],[-0.1,-0.006],[-0.382,-0.24],[-0.08,-0.082],[-0.08,-0.185],[-0.094,-0.185],[0.203,-0.635],[0.292,-0.429],[0.129,-0.108],[0.371,-0.107],[0.119,-0.15],[0.247,-0.098],[0.252,-0.197],[0.228,-0.103],[0.11,-0.113],[0.465,-0.163],[0.185,-0.093],[0.125,-0.169],[0.223,-0.084],[0.204,-0.09],[0,0]],"v":[[12.21,2.568],[14.308,3.47],[14.579,4.538],[14.295,5.646],[13.839,6.83],[13.572,8.699],[12.905,10.707],[12.723,11.417],[12.804,12.043],[14.372,12.657],[16.058,13.514],[16.664,14.456],[16.206,14.944],[15.642,15.374],[14.788,15.397],[14.1,15.13],[13.157,14.797],[12.161,14.783],[11.423,15.653],[11.247,16.576],[11.163,17.493],[11.196,17.956],[10.916,18.459],[10.591,19.132],[10.096,19.762],[9.44,20.199],[8.992,20.174],[8.495,19.986],[8.162,18.69],[8.827,16.924],[9.168,15.831],[9.361,14.609],[8.456,13.772],[7.783,13.448],[6.732,13.178],[5.305,12.842],[4.46,12.595],[3.73,12.256],[3.015,11.74],[2.755,11.219],[2.503,10.669],[2.639,9.434],[3.352,7.831],[3.955,7.017],[4.511,6.737],[5.547,6.003],[6.061,5.651],[6.818,5.179],[7.567,4.736],[8.038,4.433],[8.93,4.027],[9.878,3.634],[10.363,3.274],[10.821,2.908],[11.363,2.684],[12.209,2.568]]}}},{"ty":"sh","hd":true,"nm":"Path 1 (2)","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0.461,-0.144],[0.281,-0.069],[0.162,-0.159],[0.229,-0.086],[0.144,-0.327],[0.271,-0.112],[0.119,-0.151],[0.181,-0.076],[0.145,-0.407],[-0.184,-0.148],[-0.862,-0.181],[-0.378,-0.258],[-0.093,-0.025],[-0.294,0.045],[-0.081,0.16],[-0.243,0.946],[-0.158,0.221],[0.095,0.73]],"o":[[-0.017,-0.247],[-0.27,0.112],[-0.28,0.07],[-0.176,0.169],[-0.351,0.111],[-0.067,0.104],[-0.295,0.126],[-0.135,0.143],[-0.399,0.14],[-0.121,0.392],[0.207,0.134],[0.289,0.054],[0.34,0.249],[0.115,0.029],[0.337,-0.055],[0.106,-0.175],[0.277,-1.08],[0.244,-0.321],[0,0]],"v":[[11.555,5.821],[10.838,5.667],[10.011,5.939],[9.348,6.283],[8.734,6.67],[7.991,7.327],[7.484,7.651],[6.863,8.066],[6.384,8.398],[5.568,9.218],[5.663,10.028],[7.267,10.501],[8.267,10.969],[8.917,11.379],[9.53,11.354],[10.157,11.031],[10.68,9.349],[11.332,7.398],[11.555,5.821]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[1,1,1]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-5200-8N90","layers":[{"ddd":0,"ind":11,"ty":4,"nm":"Layer 1","hd":true,"sr":1,"ks":{"a":{"a":0,"k":[8.354,10.301]},"o":{"a":0,"k":100},"p":{"a":0,"k":[8.354,10.301]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":true,"nm":"Path 1 Group","bm":0,"it":[{"ty":"sh","hd":true,"nm":"Path 1","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[-0.557,-0.264],[-0.311,-0.282],[-0.278,-0.253],[-0.201,-0.475],[-0.003,-0.304],[-0.033,-0.19],[0.025,-0.175],[-0.035,-0.332],[0.049,-0.109],[0.188,-0.416],[0.12,-0.151],[0.215,-0.127],[0.082,-0.241],[-0.207,-0.214],[-0.042,-0.232],[-0.111,-0.271],[0.035,-0.213],[-0.028,-0.251],[0.165,-0.563],[0.259,-0.378],[0.269,-0.269],[0.077,0.019],[0.347,-0.173],[0.209,-0.027],[0.197,-0.06],[0.292,0.029],[0.503,0.008],[0.239,0.122],[0.412,0.132],[0.358,0.253],[0.263,0.471],[0.235,0.343],[0.055,0.257],[-0.229,0.264],[-0.09,0.038],[-0.331,-0.046],[0,0],[0,0],[0,0],[-0.203,-0.072],[-0.368,-0.215],[-0.251,-0.044],[-0.537,0.125],[-0.113,-0.029],[-0.26,1.406],[0.188,0.29],[0.95,0.385],[0.216,0.173],[0.18,0.131],[-0.097,0.46],[-0.399,0.14],[-0.113,0.051],[-0.317,-0.021],[-0.514,0.433],[-0.051,0.673],[0.493,0.591],[0.74,-0.053],[0.294,0.008],[0.367,-0.249],[0.234,1.211],[-0.267,0.416],[-0.275,0.051],[-0.209,0.107],[-0.193,-0.03],[-0.369,0.025],[-0.171,-0.044],[-0.118,-0.01]],"o":[[0.323,0.002],[0.577,0.269],[0.08,0.081],[0.236,0.182],[0.22,0.48],[-0.007,0.18],[0.036,0.171],[-0.005,0.18],[0.018,0.247],[-0.029,0.113],[-0.273,0.596],[-0.1,0.156],[-0.328,0.178],[-0.063,0.246],[0.141,0.158],[0.023,0.228],[0.093,0.267],[-0.036,0.25],[0.055,0.417],[-0.16,0.544],[-0.222,0.31],[-0.295,0.287],[-0.019,-0.005],[-0.342,0.155],[-0.205,0.018],[-0.282,0.082],[-0.123,0.009],[-0.268,0.012],[-0.396,-0.176],[-0.672,-0.213],[-0.335,-0.267],[-0.201,-0.313],[-0.385,-0.543],[-0.037,-0.251],[0.21,-0.269],[0.11,-0.033],[0,0],[0,0],[0,0],[0.094,0.105],[0.27,0.13],[0.401,0.245],[0.255,0.025],[0.323,-0.079],[0.625,0.16],[0.157,-0.928],[-0.164,-0.305],[-0.255,-0.108],[-0.173,-0.141],[-0.307,-0.22],[0.103,-0.477],[0.248,-0.098],[0.138,-0.066],[0.773,0.057],[0.539,-0.447],[0.056,-0.691],[-0.4,-0.487],[-0.294,0.018],[-0.18,-0.006],[-1.223,0.836],[-0.05,-0.275],[0.273,-0.433],[0.176,-0.056],[0.233,-0.122],[0.2,0.01],[0.413,-0.036],[0.232,0.04],[0,0]],"v":[[10.524,1.566],[11.844,1.965],[13.176,2.791],[13.713,3.292],[14.368,4.277],[14.702,5.453],[14.742,6.008],[14.759,6.527],[14.804,7.295],[14.758,7.828],[14.433,8.622],[13.843,9.742],[13.371,10.166],[12.756,10.795],[12.972,11.485],[13.246,12.07],[13.447,12.818],[13.535,13.537],[13.523,14.291],[13.358,15.761],[12.73,17.144],[11.992,18.014],[11.435,18.416],[10.886,18.668],[10.06,18.94],[9.455,19.057],[8.587,19.137],[7.648,19.139],[6.874,18.97],[5.661,18.507],[4.116,17.808],[3.219,16.7],[2.564,15.715],[1.904,14.516],[2.193,13.743],[2.643,13.283],[3.305,13.303],[3.915,13.398],[4.513,14.49],[5.083,15.453],[5.529,15.719],[6.486,16.237],[7.465,16.67],[8.653,16.521],[9.307,16.447],[10.635,14.578],[10.589,12.751],[8.918,11.716],[8.209,11.292],[7.679,10.884],[7.365,9.864],[8.117,8.938],[8.659,8.714],[9.342,8.647],[11.272,8.083],[12.158,6.403],[11.502,4.48],[9.792,3.83],[8.91,3.845],[8.09,4.21],[5.904,3.648],[6.23,2.612],[7.052,1.885],[7.63,1.64],[8.27,1.502],[9.124,1.479],[9.999,1.491],[10.524,1.566]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[1,1,1]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-4715-8N90","layers":[{"ddd":0,"ind":14,"ty":4,"nm":"Layer 1","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[151.899,164.177]},"o":{"a":0,"k":100},"p":{"a":0,"k":[151.899,164.177]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Path 1 Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[-0.644,-0.871],[-0.212,-0.196],[-0.084,-0.223],[-0.103,-0.228],[0.083,-0.483],[-0.012,-0.346],[0.13,-0.269],[0.144,-0.326],[0.167,-0.18],[0.481,-0.3],[0.124,-0.169],[0.43,-0.013],[0.538,-0.447],[0.313,-0.057],[0.128,-0.109],[0.49,-0.257],[0.347,-0.093],[-0.012,-0.347],[-0.602,-0.013],[-0.345,-0.149],[-0.194,-0.029],[-0.26,-0.169],[-0.426,-0.069],[-0.175,-0.105],[-0.417,-0.026],[-0.299,0.065],[-0.089,-0.043],[-0.302,-0.239],[-0.108,-0.048],[0.056,-0.531],[0.233,-0.202],[0.465,-0.002],[0.265,0.148],[0.479,-0.038],[0.213,0.115],[0.209,0.06],[0.154,0.133],[0.099,0.005],[0.507,-0.01],[0.663,0.17],[0.499,0.174],[0.259,0.329],[0.193,0.08],[0.075,0.1],[0.041,0.231],[-0.054,0.369],[-0.11,0.274],[-0.076,-0.02],[-0.152,0.123],[-0.076,0.142],[-0.152,0.042],[-0.357,0.211],[-0.176,0.056],[-0.186,0.174],[-0.171,0.037],[-0.162,0.16],[-0.18,-0.006],[-0.21,0.187],[0,0],[-0.224,0.165],[-0.171,0.036],[-0.367,0.249],[-0.233,0.122],[-0.121,0.392],[0.087,0.527],[0.061,0.076],[0.231,0.2],[1.917,-1.182],[0.204,-0.008],[0.254,0.267],[-0.011,0.28],[-0.297,0.448],[-0.334,0.278],[-0.209,0.027],[-0.427,0.173],[-0.123,0.009],[-0.293,-0.116],[-0.104,0.014],[-0.25,-0.125],[-0.294,0.046]],"o":[[0.869,-0.16],[0.334,0.428],[0.226,0.22],[0.117,0.252],[0.21,0.437],[-0.03,0.275],[0.036,0.332],[-0.13,0.269],[-0.11,0.273],[-0.163,0.16],[-0.367,0.25],[-0.255,0.346],[-0.456,0.044],[-0.25,0.196],[-0.299,0.064],[-0.191,0.193],[-0.49,0.257],[-0.826,0.231],[-0.007,0.26],[0.36,0.012],[0.331,0.125],[0.199,0.01],[0.265,0.148],[0.44,0.093],[0.218,0.097],[0.436,0.031],[0.323,-0.078],[0.014,0.024],[0.213,0.196],[0.171,0.124],[-0.051,0.512],[-0.157,0.141],[-0.465,0.001],[-0.412,-0.248],[-0.332,0.036],[-0.192,-0.103],[-0.196,-0.056],[-0.147,-0.139],[-0.095,-0.023],[-0.18,-0.005],[-0.513,-0.126],[-0.511,-0.213],[-0.148,-0.147],[-0.198,-0.091],[-0.071,-0.12],[-0.051,-0.195],[0.055,-0.37],[0.111,-0.275],[0.076,0.02],[0.158,-0.141],[0.111,-0.193],[0.156,-0.06],[0.314,-0.202],[0.152,-0.042],[0.181,-0.155],[0.152,-0.042],[0.162,-0.16],[0.161,0],[0,0],[0.119,-0.07],[0.229,-0.184],[0.731,-0.176],[0.3,-0.226],[0.323,-0.159],[0.14,-0.388],[-0.056,-0.257],[-0.037,-0.091],[-1.698,-1.404],[-0.429,0.253],[-0.18,-0.005],[-0.269,-0.291],[0.011,-0.28],[0.186,-0.254],[0.353,-0.273],[0.152,-0.042],[0.566,-0.238],[0.128,-0.028],[0.321,0.163],[0.1,0.005],[0.255,0.106],[0,0]],"v":[[172.281,77.704],[174.55,78.771],[175.369,79.707],[175.834,80.372],[176.164,81.092],[176.354,82.472],[176.327,83.404],[176.186,84.306],[175.775,85.199],[175.359,85.879],[174.394,86.569],[173.658,87.198],[172.574,87.768],[171.083,88.504],[170.227,88.89],[169.586,89.15],[168.565,89.825],[167.31,90.351],[166.089,91.218],[166.982,91.628],[168.04,91.87],[168.828,92.102],[169.516,92.37],[170.552,92.696],[171.474,92.993],[172.426,93.177],[173.528,93.127],[174.147,93.074],[174.621,93.468],[175.102,93.834],[175.274,94.816],[174.848,95.887],[173.915,96.102],[172.821,95.882],[171.451,95.56],[170.634,95.441],[170.031,95.196],[169.5,94.908],[169.132,94.692],[168.229,94.672],[166.965,94.409],[165.446,93.959],[164.292,93.147],[163.775,92.803],[163.365,92.516],[163.197,91.989],[163.202,91.143],[163.45,90.177],[163.73,89.795],[164.072,89.641],[164.423,89.216],[164.817,88.863],[165.587,88.456],[166.322,88.069],[166.829,87.745],[167.357,87.457],[167.828,87.154],[168.341,86.923],[168.898,86.642],[169.355,86.275],[169.869,85.923],[170.469,85.593],[172.115,84.956],[172.915,84.434],[173.581,83.607],[173.661,82.235],[173.486,81.736],[173.084,81.3],[167.661,80.967],[166.712,81.359],[166.061,80.95],[165.675,80.094],[166.137,79.002],[166.917,78.204],[167.759,77.754],[168.628,77.432],[169.662,77.062],[170.294,77.194],[170.932,77.418],[171.457,77.614],[172.281,77.704]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[1,1,1]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":0.88,"y":0.77},"o":{"x":0.5,"y":0}},{"t":33.3,"s":[0],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]},{"ddd":0,"ind":16,"ty":4,"nm":"Layer 3","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[191.642,235.5]},"o":{"a":0,"k":100},"p":{"a":0,"k":[191.642,235.5]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":20,"ty":0,"nm":"Mask Group","td":1,"parent":16,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[100000,100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":200000,"refId":"el-4801-8N90-mask","w":200000},{"ddd":0,"ind":24,"ty":0,"nm":"Mask Group","tt":1,"tp":20,"parent":16,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[100000,100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":200000,"refId":"el-4801-8N90-masked","w":200000},{"ddd":0,"ind":25,"ty":4,"nm":"แ„Œแ…ฅแ†ซแ„Žแ…ฆ","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Layer 4 Group","bm":0,"it":[{"ty":"tr","nm":"Transform","a":{"a":0,"k":[191.642,235.5]},"o":{"a":0,"k":100},"p":{"a":0,"k":[191.642,235.5]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]},{"ddd":0,"ind":27,"ty":4,"nm":"Layer 4","parent":25,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[191.642,235.5]},"o":{"a":0,"k":100},"p":{"a":0,"k":[191.642,235.5]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":31,"ty":0,"nm":"Mask Group","td":1,"parent":27,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[100000,100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":200000,"refId":"el-4797-8N90-mask","w":200000},{"ddd":0,"ind":35,"ty":0,"nm":"Mask Group","tt":1,"tp":31,"parent":27,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[100000,100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":200000,"refId":"el-4797-8N90-masked","w":200000},{"ddd":0,"ind":36,"ty":4,"nm":"แ„Œแ…ฅแ†ซแ„Žแ…ฆ","parent":25,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Layer 6 Group","bm":0,"it":[{"ty":"tr","nm":"Transform","a":{"a":0,"k":[143.486,189.189]},"o":{"a":0,"k":60},"p":{"a":0,"k":[143.486,189.189]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]},{"ddd":0,"ind":38,"ty":4,"nm":"Layer 6","parent":25,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[143.486,189.189]},"o":{"a":0,"k":60},"p":{"a":0,"k":[143.486,189.189]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":42,"ty":0,"nm":"Mask Group","td":1,"parent":38,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[100000,100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":200000,"refId":"el-4789-8N90-mask","w":200000},{"ddd":0,"ind":46,"ty":0,"nm":"Mask Group","tt":1,"tp":42,"parent":38,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[100000,100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":200000,"refId":"el-4789-8N90-masked","w":200000},{"ddd":0,"ind":47,"ty":4,"nm":"แ„Œแ…ฅแ†ซแ„Žแ…ฆ","parent":25,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Layer 7 Group","bm":0,"it":[{"ty":"tr","nm":"Transform","a":{"a":0,"k":[143.486,189.189]},"o":{"a":0,"k":60},"p":{"a":0,"k":[143.486,189.189]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]},{"ddd":0,"ind":49,"ty":4,"nm":"Layer 7","parent":25,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[143.486,189.189]},"o":{"a":0,"k":60},"p":{"a":0,"k":[143.486,189.189]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":53,"ty":0,"nm":"Mask Group","td":1,"parent":49,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[100000,100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":200000,"refId":"el-4785-8N90-mask","w":200000},{"ddd":0,"ind":57,"ty":0,"nm":"Mask Group","tt":1,"tp":53,"parent":49,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[100000,100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":200000,"refId":"el-4785-8N90-masked","w":200000},{"ddd":0,"ind":58,"ty":4,"nm":"แ„Œแ…ฅแ†ซแ„Žแ…ฆ","parent":25,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Layer 8 Group","bm":0,"it":[{"ty":"tr","nm":"Transform","a":{"a":0,"k":[143.486,189.189]},"o":{"a":0,"k":60},"p":{"a":0,"k":[143.486,189.189]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]},{"ddd":0,"ind":60,"ty":4,"nm":"Layer 8","parent":25,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[143.486,189.189]},"o":{"a":0,"k":60},"p":{"a":0,"k":[143.486,189.189]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":64,"ty":0,"nm":"Mask Group","td":1,"parent":60,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[100000,100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":200000,"refId":"el-4781-8N90-mask","w":200000},{"ddd":0,"ind":68,"ty":0,"nm":"Mask Group","tt":1,"tp":64,"parent":60,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[100000,100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":200000,"refId":"el-4781-8N90-masked","w":200000},{"ddd":0,"ind":69,"ty":4,"nm":"แ„Œแ…ฅแ†ซแ„Žแ…ฆ","parent":25,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Layer 9 Group","bm":0,"it":[{"ty":"tr","nm":"Transform","a":{"a":0,"k":[143.486,189.189]},"o":{"a":0,"k":60},"p":{"a":0,"k":[143.486,189.189]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]},{"ddd":0,"ind":71,"ty":4,"nm":"Layer 9","parent":25,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[143.486,189.189]},"o":{"a":0,"k":60},"p":{"a":0,"k":[143.486,189.189]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":75,"ty":0,"nm":"Mask Group","td":1,"parent":71,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[100000,100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":200000,"refId":"el-4777-8N90-mask","w":200000},{"ddd":0,"ind":79,"ty":0,"nm":"Mask Group","tt":1,"tp":75,"parent":71,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[100000,100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":200000,"refId":"el-4777-8N90-masked","w":200000},{"ddd":0,"ind":80,"ty":4,"nm":"แ„Œแ…ฅแ†ซแ„Žแ…ฆ","parent":25,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Layer 10 Group","bm":0,"it":[{"ty":"tr","nm":"Transform","a":{"a":0,"k":[143.486,189.189]},"o":{"a":0,"k":60},"p":{"a":0,"k":[143.486,189.189]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]},{"ddd":0,"ind":82,"ty":4,"nm":"Layer 10","parent":25,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[143.486,189.189]},"o":{"a":0,"k":60},"p":{"a":0,"k":[143.486,189.189]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":86,"ty":0,"nm":"Mask Group","td":1,"parent":82,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[100000,100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":200000,"refId":"el-4773-8N90-mask","w":200000},{"ddd":0,"ind":90,"ty":0,"nm":"Mask Group","tt":1,"tp":86,"parent":82,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[100000,100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":200000,"refId":"el-4773-8N90-masked","w":200000},{"ddd":0,"ind":91,"ty":4,"nm":"แ„Œแ…ฅแ†ซแ„Žแ…ฆ","parent":25,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Layer 11 Group","bm":0,"it":[{"ty":"tr","nm":"Transform","a":{"a":0,"k":[143.486,189.189]},"o":{"a":0,"k":60},"p":{"a":0,"k":[143.486,189.189]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]},{"ddd":0,"ind":93,"ty":4,"nm":"Layer 11","parent":25,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[143.486,189.189]},"o":{"a":0,"k":60},"p":{"a":0,"k":[143.486,189.189]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":97,"ty":0,"nm":"Mask Group","td":1,"parent":93,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[100000,100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":200000,"refId":"el-4769-8N90-mask","w":200000},{"ddd":0,"ind":101,"ty":0,"nm":"Mask Group","tt":1,"tp":97,"parent":93,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[100000,100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":200000,"refId":"el-4769-8N90-masked","w":200000},{"ddd":0,"ind":102,"ty":4,"nm":"แ„Œแ…ฅแ†ซแ„Žแ…ฆ","parent":25,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Layer 12 Group","bm":0,"it":[{"ty":"tr","nm":"Transform","a":{"a":0,"k":[143.486,189.189]},"o":{"a":0,"k":60},"p":{"a":0,"k":[143.486,189.189]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]},{"ddd":0,"ind":104,"ty":4,"nm":"Layer 12","parent":25,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[143.486,189.189]},"o":{"a":0,"k":60},"p":{"a":0,"k":[143.486,189.189]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":108,"ty":0,"nm":"Mask Group","td":1,"parent":104,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[100000,100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":200000,"refId":"el-4765-8N90-mask","w":200000},{"ddd":0,"ind":112,"ty":0,"nm":"Mask Group","tt":1,"tp":108,"parent":104,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[100000,100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":200000,"refId":"el-4765-8N90-masked","w":200000},{"ddd":0,"ind":113,"ty":4,"nm":"แ„Œแ…ฅแ†ซแ„Žแ…ฆ","parent":25,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Layer 13 Group","bm":0,"it":[{"ty":"tr","nm":"Transform","a":{"a":0,"k":[143.486,189.189]},"o":{"a":0,"k":60},"p":{"a":0,"k":[143.486,189.189]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]},{"ddd":0,"ind":115,"ty":4,"nm":"Layer 13","parent":25,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[143.486,189.189]},"o":{"a":0,"k":60},"p":{"a":0,"k":[143.486,189.189]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":119,"ty":0,"nm":"Mask Group","td":1,"parent":115,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[100000,100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":200000,"refId":"el-4761-8N90-mask","w":200000},{"ddd":0,"ind":123,"ty":0,"nm":"Mask Group","tt":1,"tp":119,"parent":115,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[100000,100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":200000,"refId":"el-4761-8N90-masked","w":200000},{"ddd":0,"ind":124,"ty":4,"nm":"แ„Œแ…ฅแ†ซแ„Žแ…ฆ","parent":25,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Layer 14 Group","bm":0,"it":[{"ty":"tr","nm":"Transform","a":{"a":0,"k":[143.486,189.189]},"o":{"a":0,"k":60},"p":{"a":0,"k":[143.486,189.189]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]},{"ddd":0,"ind":126,"ty":4,"nm":"Layer 14","parent":25,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[143.486,189.189]},"o":{"a":0,"k":60},"p":{"a":0,"k":[143.486,189.189]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":130,"ty":0,"nm":"Mask Group","td":1,"parent":126,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[100000,100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":200000,"refId":"el-4757-8N90-mask","w":200000},{"ddd":0,"ind":134,"ty":0,"nm":"Mask Group","tt":1,"tp":130,"parent":126,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[100000,100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":200000,"refId":"el-4757-8N90-masked","w":200000},{"ddd":0,"ind":135,"ty":4,"nm":"แ„Œแ…ฅแ†ซแ„Žแ…ฆ","parent":25,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Layer 15 Group","bm":0,"it":[{"ty":"tr","nm":"Transform","a":{"a":0,"k":[143.486,189.189]},"o":{"a":0,"k":60},"p":{"a":0,"k":[143.486,189.189]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]},{"ddd":0,"ind":137,"ty":4,"nm":"Layer 15","parent":25,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[143.486,189.189]},"o":{"a":0,"k":60},"p":{"a":0,"k":[143.486,189.189]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":141,"ty":0,"nm":"Mask Group","td":1,"parent":137,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[100000,100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":200000,"refId":"el-4753-8N90-mask","w":200000},{"ddd":0,"ind":145,"ty":0,"nm":"Mask Group","tt":1,"tp":141,"parent":137,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[100000,100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":200000,"refId":"el-4753-8N90-masked","w":200000},{"ddd":0,"ind":146,"ty":4,"nm":"แ„Œแ…ฅแ†ซแ„Žแ…ฆ","parent":25,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Layer 16 Group","bm":0,"it":[{"ty":"tr","nm":"Transform","a":{"a":0,"k":[143.486,189.189]},"o":{"a":0,"k":60},"p":{"a":0,"k":[143.486,189.189]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]},{"ddd":0,"ind":148,"ty":4,"nm":"Layer 16","parent":25,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[143.486,189.189]},"o":{"a":0,"k":60},"p":{"a":0,"k":[143.486,189.189]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":152,"ty":0,"nm":"Mask Group","td":1,"parent":148,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[100000,100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":200000,"refId":"el-4749-8N90-mask","w":200000},{"ddd":0,"ind":156,"ty":0,"nm":"Mask Group","tt":1,"tp":152,"parent":148,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[100000,100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":200000,"refId":"el-4749-8N90-masked","w":200000},{"ddd":0,"ind":157,"ty":4,"nm":"แ„Œแ…ฅแ†ซแ„Žแ…ฆ","parent":25,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":162,"ty":0,"nm":"Mask Group","td":1,"parent":25,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[100000,100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":200000,"refId":"el-4725-8N90-mask","w":200000},{"ddd":0,"ind":185,"ty":0,"nm":"Mask Group","tt":1,"tp":162,"parent":25,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[100000,100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":200000,"refId":"el-4725-8N90-masked","w":200000},{"ddd":0,"ind":186,"ty":4,"nm":"แ„Œแ…ฅแ†ซแ„Žแ…ฆ","parent":25,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Layer 19 Group","bm":0,"it":[{"ty":"tr","nm":"Transform","a":{"a":0,"k":[191.642,235.5]},"o":{"a":0,"k":60},"p":{"a":0,"k":[191.642,235.5]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]},{"ddd":0,"ind":188,"ty":4,"nm":"Layer 19","parent":25,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[191.642,235.5]},"o":{"a":0,"k":60},"p":{"a":0,"k":[191.642,235.5]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":192,"ty":0,"nm":"Mask Group","td":1,"parent":188,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[100000,100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":200000,"refId":"el-4722-8N90-mask","w":200000},{"ddd":0,"ind":196,"ty":0,"nm":"Mask Group","tt":1,"tp":192,"parent":188,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[100000,100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":200000,"refId":"el-4722-8N90-masked","w":200000},{"ddd":0,"ind":197,"ty":4,"nm":"แ„Œแ…ฅแ†ซแ„Žแ…ฆ","parent":25,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Layer 20 Group","bm":0,"it":[{"ty":"tr","nm":"Transform","a":{"a":0,"k":[191.642,235.5]},"o":{"a":0,"k":100},"p":{"a":0,"k":[191.642,235.5]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]},{"ddd":0,"ind":199,"ty":4,"nm":"Layer 20","parent":25,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[191.642,235.5]},"o":{"a":0,"k":100},"p":{"a":0,"k":[191.642,235.5]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":203,"ty":0,"nm":"Mask Group","td":1,"parent":199,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[100000,100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":200000,"refId":"el-4717-8N90-mask","w":200000},{"ddd":0,"ind":208,"ty":0,"nm":"Mask Group","tt":1,"tp":203,"parent":199,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[100000,100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":200000,"refId":"el-4717-8N90-masked","w":200000}]},{"id":"el-4801-8N90-mask","layers":[{"ddd":0,"ind":17,"ty":4,"nm":"center","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[-100000,-100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":18,"ty":4,"nm":"Mask Group","parent":17,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Mask Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Mask","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[11.142,0],[372.142,0],[372.142,471],[11.142,471]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.85,0.85,0.85]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-4801-8N90-masked","layers":[{"ddd":0,"ind":21,"ty":4,"nm":"center","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[-100000,-100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":22,"ty":4,"nm":"Mask Group","parent":21,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Path 1 Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[143.004,54.134],[141.025,53.052],[138.731,53.179],[136.775,52.007],[134.515,52.004],[132.492,51.091],[130.282,50.891],[128.228,50.088],[126.149,49.364],[124.845,48.459],[124.318,46.782],[125.153,45.519],[125.83,43.983],[127.509,44.066],[129.573,44.833],[131.902,44.551],[134.005,45.167],[136.013,46.153],[138.275,46.133],[140.155,47.618],[142.45,47.484],[144.543,48.139],[145.549,49.416],[146.06,50.886],[145.851,52.309],[144.554,53.323]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":53.1,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[225.279,73.984],[223.3,72.902],[221.006,73.029],[219.05,71.857],[216.79,71.854],[132.492,51.091],[130.282,50.891],[128.228,50.088],[126.149,49.364],[124.845,48.459],[124.318,46.782],[125.153,45.519],[125.83,43.983],[127.509,44.066],[129.573,44.833],[131.902,44.551],[134.005,45.167],[218.288,66.003],[220.55,65.983],[222.43,67.468],[224.725,67.334],[226.818,67.989],[227.824,69.266],[228.335,70.736],[228.126,72.159],[226.829,73.173]]}],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[1,0.969,0.6]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-4797-8N90-mask","layers":[{"ddd":0,"ind":28,"ty":4,"nm":"center","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[-100000,-100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":29,"ty":4,"nm":"Mask Group","parent":28,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Mask Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Mask","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[11.142,0],[372.142,0],[372.142,471],[11.142,471]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.85,0.85,0.85]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-4797-8N90-masked","layers":[{"ddd":0,"ind":32,"ty":4,"nm":"center","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[-100000,-100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":33,"ty":4,"nm":"Mask Group","parent":32,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Path 1 Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[1.045,0.323],[0,0],[0,0],[0,0],[0,0],[0.807,0.706],[0,0],[0.011,0.004],[-0.074,0.02],[0,0],[-0.144,-0.026],[0,0],[0,0],[0,0],[0,0],[-0.275,-0.41],[0,0],[0.535,-0.036],[0.071,0.018],[0.173,-0.151]],"o":[[-0.821,0.722],[0,0],[0,0],[0,0],[0,0],[-1.014,-0.35],[0,0],[-0.009,-0.007],[-0.074,-0.019],[0,0],[0.141,-0.037],[0,0],[0,0],[0,0],[0,0],[0.486,0.086],[0,0],[0.299,0.446],[-0.074,0.005],[-0.223,-0.057],[0,0]],"v":[[226.746,73.751],[223.743,74.393],[201.093,67.353],[176.287,61.641],[151.268,56.741],[128.658,48.968],[125.901,47.369],[124.434,46.082],[124.404,46.066],[124.403,45.922],[127.8,45.022],[128.232,45.005],[153.114,49.549],[177.555,56.687],[201.909,64.174],[226.282,68.478],[227.473,69.254],[229.1,71.684],[228.567,72.767],[228.348,72.747],[227.715,72.898]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[1,1,1]},"r":1,"o":{"a":0,"k":10}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-4789-8N90-mask","layers":[{"ddd":0,"ind":39,"ty":4,"nm":"center","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[-100000,-100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":40,"ty":4,"nm":"Mask Group","parent":39,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Mask Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Mask","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[113.144,114.238],[197.071,128.633],[173.828,264.141],[89.902,249.746]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.85,0.85,0.85]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-4789-8N90-masked","layers":[{"ddd":0,"ind":43,"ty":4,"nm":"center","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[-100000,-100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":44,"ty":4,"nm":"Mask Group","parent":43,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Path 1 (2) Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1 (2)","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[126.766,251.395],[129.038,251.603],[131.121,250.607],[133.641,248.124],[135.411,251.146],[137.062,252.556],[139.007,253.649],[141.458,253.758],[143.852,254.199],[146.272,254.742],[146.271,253.836],[144.161,253.308],[141.906,252.96],[139.704,252.306],[137.85,251.278],[136.169,249.535],[133.993,247.848],[137.237,245.576],[138.115,247.782],[139.061,249.602],[139.717,248.443],[138.937,245.154],[138.123,243.408],[137.047,241.838],[135.844,240.427],[135.521,239.408],[135.096,239.634],[134.816,239.38],[134.154,240.181],[132.338,240.795],[130.76,241.954],[129.592,243.528],[128.311,246.572],[127.96,247.533],[129.557,246.329],[130.791,244.847],[132.896,247.721],[130.203,248.757],[127.621,250.325],[127.862,250.242]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (2)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[131.116,244.563],[132.904,243.341],[134.62,242.415],[136.554,243.472],[137.249,245.648],[135.556,246.608],[133.806,247.152],[132.353,246.039],[131.115,244.564]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.573,0.702,0.859]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-4785-8N90-mask","layers":[{"ddd":0,"ind":50,"ty":4,"nm":"center","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[-100000,-100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":51,"ty":4,"nm":"Mask Group","parent":50,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Mask Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Mask","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[113.144,114.238],[197.071,128.633],[173.828,264.141],[89.902,249.746]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.85,0.85,0.85]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-4785-8N90-masked","layers":[{"ddd":0,"ind":54,"ty":4,"nm":"center","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[-100000,-100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":55,"ty":4,"nm":"Mask Group","parent":54,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Path 1 (2) Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1 (2)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[138.981,253.8],[137.035,252.599],[135.117,251.356],[133.529,248.774],[130.979,250.405],[129.074,251.76],[126.685,251.87],[126.636,251.35],[126.363,250.895],[127.267,250.018],[127.589,250.19],[129.925,248.556],[132.615,247.634],[130.697,245.436],[129.309,246.935],[128.177,248.044],[127.958,248.067],[127.639,247.981],[127.943,246.454],[129.382,243.382],[130.879,242.081],[132.387,240.871],[133.399,240.163],[133.93,239.806],[134.687,239.106],[134.831,239.22],[134.946,239.025],[135.246,238.755],[135.221,238.901],[135.316,239.2],[135.719,239.055],[135.563,239.369],[136.045,240.26],[136.755,240.481],[137.698,241.368],[138.45,243.251],[139.52,244.989],[139.469,248.445],[139.259,249.882],[139.04,249.825],[138.962,249.712],[137.901,248.536],[137.097,245.942],[135.068,248.081],[136.461,249.5],[137.889,251.242],[139.687,252.398],[141.98,252.523],[144.274,252.641],[146.42,253.624],[146.994,253.715],[146.205,254.635],[146.253,254.594],[143.827,254.339],[141.342,254.468],[138.979,253.801]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (2)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[133.845,246.932],[137.09,245.786],[135.884,243.842],[134.643,242.275],[132.741,243.077],[131.638,244.479],[133.844,246.933]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.573,0.702,0.859]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-4781-8N90-mask","layers":[{"ddd":0,"ind":61,"ty":4,"nm":"center","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[-100000,-100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":62,"ty":4,"nm":"Mask Group","parent":61,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Mask Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Mask","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[113.144,114.238],[197.071,128.633],[173.828,264.141],[89.902,249.746]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.85,0.85,0.85]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-4781-8N90-masked","layers":[{"ddd":0,"ind":65,"ty":4,"nm":"center","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[-100000,-100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":66,"ty":4,"nm":"Mask Group","parent":65,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Path 1 (3) Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1 (3)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[167.788,256.147],[168.887,254.332],[169.252,251.468],[170.166,248.69],[170.843,245.868],[170.623,245.636],[171.221,243.153],[171.473,240.61],[172.16,238.141],[172.008,235.529],[172.473,233.022],[173.524,230.617],[173.578,228.04],[174.441,225.601],[174.142,222.963],[174.708,220.474],[175.147,217.963],[175.499,215.436],[176.11,212.961],[176.901,210.518],[176.861,207.931],[177.51,205.463],[177.699,202.917],[178.2,200.424],[178.883,197.962],[179.185,195.458],[178.601,195.267],[178.474,195.528],[178.365,195.093],[178.282,195.393],[178.232,195.622],[178.114,195.25],[178.128,195.277],[177.617,197.768],[177.519,200.331],[176.864,202.797],[176.364,205.29],[176.387,207.873],[175.414,210.286],[175.394,212.861],[174.677,215.318],[174.75,217.889],[173.803,220.285],[173.583,222.806],[173.497,225.35],[173.009,227.824],[172.216,230.247],[172.204,232.803],[171.531,235.246],[171.019,237.717],[170.227,240.139],[169.879,242.637],[169.79,245.179],[169.063,247.183],[168.025,249.688],[166.65,252.132],[164.176,253.456],[161.922,255.148],[159.101,255.135],[156.415,255.152],[154.014,254.693],[151.529,254.677],[149.093,254.391],[146.714,253.794],[143.963,253.47],[142.62,253.136],[141.332,253.453],[141.12,253.351],[143.826,254.215],[146.544,254.781],[149.05,254.803],[151.407,255.723],[153.887,255.897],[156.294,256.518],[158.767,256.744],[161.257,256.873],[163.695,257.339],[166.714,257.535],[168.444,258.144],[168.404,258.207],[167.788,256.144]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (3)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[169.065,250.414],[168.377,254.146],[168.165,254.837],[165.739,253.84],[168.215,251.709]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (3)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[163.939,256.884],[160.614,256.237],[162.905,255.036],[165.108,254.274],[164.791,256.517],[163.94,256.885]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.573,0.702,0.859]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-4777-8N90-mask","layers":[{"ddd":0,"ind":72,"ty":4,"nm":"center","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[-100000,-100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":73,"ty":4,"nm":"Mask Group","parent":72,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Mask Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Mask","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[113.144,114.238],[197.071,128.633],[173.828,264.141],[89.902,249.746]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.85,0.85,0.85]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-4777-8N90-masked","layers":[{"ddd":0,"ind":76,"ty":4,"nm":"center","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[-100000,-100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":77,"ty":4,"nm":"Mask Group","parent":76,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Path 1 (6) Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1 (6)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[122.141,249.677],[119.497,248.718],[117.089,248.494],[114.641,248.528],[112.362,247.533],[109.891,247.669],[108.069,246.733],[106.299,245.875],[104.296,244.38],[102.387,242.685],[101.431,240.315],[100.823,237.899],[100.073,235.418],[100.89,233.778],[100.914,231.488],[101.426,229.279],[101.656,227.022],[101.753,224.748],[102.765,222.631],[103.005,220.381],[102.865,218.068],[103.313,215.592],[104.027,213.163],[104.73,210.732],[104.914,208.211],[105.606,205.778],[105.995,203.293],[106.335,200.799],[106.497,198.275],[107.105,195.827],[107.534,193.349],[108.205,190.912],[108.332,188.382],[108.576,185.871],[108.837,183.48],[108.206,183.254],[107.531,185.781],[107.114,188.353],[107.186,191.008],[106.255,193.491],[106.294,196.141],[105.563,198.659],[105.028,201.212],[104.615,203.681],[103.916,206.101],[104.08,208.67],[103.508,211.113],[102.954,213.559],[102.866,216.085],[102.089,218.493],[101.935,221.008],[101.469,223.469],[100.979,225.926],[100.802,228.437],[99.7,230.788],[99.584,233.167],[99.479,233.521],[98.902,236.14],[98.959,238.868],[98.358,241.484],[97.96,243.842],[98.587,244.342],[97.53,246.071],[97.515,246.038],[99.608,245.488],[102.186,247.13],[104.669,247.277],[107.045,248.062],[109.552,248.097],[111.981,248.584],[114.336,249.483],[116.887,249.281],[119.31,249.789],[122.658,250.823],[124.695,250.168],[124.901,250.275],[122.138,249.676]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (6)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[101.748,242.944],[99.268,242.537],[99.231,241.631],[99.421,238.301],[100.212,240.868]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (6)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[101.054,245.881],[101.958,243.581],[103.472,245.181],[105.62,246.9],[102.404,245.84]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (6)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[146.504,254.984],[143.735,254.666],[143.737,254.136],[140.878,253.684],[140.762,253.206],[140.824,253.109],[141.129,252.958],[142.62,253.064],[143.778,253.228],[144.055,252.803],[146.803,253.237],[149.193,253.79],[151.648,253.972],[154.039,254.517],[156.465,254.848],[159.079,254.896],[161.685,254.473],[163.976,253.157],[166.136,251.637],[168.108,249.751],[169.138,247.215],[169.41,245.122],[169.525,242.583],[170.37,240.17],[170.768,237.68],[171.369,235.224],[171.613,232.708],[171.745,230.171],[172.648,227.766],[172.864,225.245],[173.042,222.717],[173.415,220.222],[173.807,217.731],[174.868,215.355],[174.64,212.737],[175.153,210.246],[176.116,207.831],[176.116,205.252],[176.949,202.815],[176.813,200.212],[177.475,197.747],[178.136,195.282],[178.132,195.258],[178.37,194.818],[179.289,195.124],[179.649,195.518],[179.117,198.006],[179.024,200.569],[178.083,202.986],[177.864,205.528],[177.469,208.039],[176.942,210.528],[176.598,213.049],[176.272,215.573],[175.512,217.004],[175.138,219.618],[174.846,222.248],[174.517,224.87],[174.403,227.53],[173.329,230.026],[172.812,232.617],[172.873,235.307],[172.289,237.886],[171.909,240.501],[171.208,243.06],[170.904,245.687],[170.899,245.882],[170.681,248.784],[170.261,251.65],[169.531,254.462],[169.306,254.445],[168.382,256.421],[169.142,257.882],[168.899,258.638],[168.274,258.781],[168.325,258.484],[168.106,258.504],[166.715,257.811],[163.87,257.655],[163.626,257.65],[160.482,257.379],[157.777,256.885],[155.123,256.077],[152.302,256.281],[149.622,255.631],[146.49,255.085],[146.486,255.097],[146.503,254.986]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (6)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[164.994,254.661],[162.29,256.146],[163.976,256.678],[164.083,256.153],[164.165,256.256],[164.752,256.36]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (6)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[166.141,253.844],[167.084,254.14],[167.948,254.565],[167.682,254],[168.182,251.671],[168.309,251.784],[166.142,253.845]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.573,0.702,0.859]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-4773-8N90-mask","layers":[{"ddd":0,"ind":83,"ty":4,"nm":"center","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[-100000,-100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":84,"ty":4,"nm":"Mask Group","parent":83,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Mask Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Mask","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[113.144,114.238],[197.071,128.633],[173.828,264.141],[89.902,249.746]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.85,0.85,0.85]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-4773-8N90-masked","layers":[{"ddd":0,"ind":87,"ty":4,"nm":"center","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[-100000,-100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":88,"ty":4,"nm":"Mask Group","parent":87,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Path 1 (6) Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1 (6)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[165.285,129.113],[167.929,130.081],[170.387,130.005],[172.69,130.851],[175.093,131.098],[177.466,131.512],[179.499,131.623],[181.242,132.648],[183.463,133.967],[184.742,136.228],[185.802,138.467],[186.857,140.746],[187.441,143.274],[187.237,145.026],[186.671,147.225],[185.913,149.392],[185.408,151.602],[185.353,153.883],[185.351,156.174],[184.53,158.324],[184.485,160.606],[183.891,163.056],[183.41,165.526],[183.182,168.039],[182.401,170.457],[181.952,172.931],[181.472,175.401],[181.499,177.958],[180.637,180.362],[180.344,182.863],[180.046,185.364],[179.719,187.86],[179.08,190.303],[178.83,192.813],[178.558,195.218],[178.9,195.377],[179.877,192.901],[179.878,190.258],[180.522,187.726],[180.772,185.126],[181.434,182.597],[182.221,180.088],[182.545,177.499],[182.701,174.986],[183.327,172.553],[183.441,170.032],[183.755,167.545],[184.395,165.113],[184.997,162.675],[185.358,160.196],[185.744,157.721],[186.114,155.244],[186.515,152.771],[187.011,150.315],[187.404,147.843],[188.063,145.558],[187.605,145.109],[188.245,142.5],[188.636,139.849],[189.186,137.225],[188.865,135.03],[189.008,134.361],[190.031,132.734],[189.871,132.531],[187.918,133.056],[185.222,131.79],[182.741,131.589],[180.308,131.123],[177.94,130.288],[175.536,129.664],[173.071,129.387],[170.613,129.066],[168.166,128.689],[164.753,128.069],[162.574,128.116],[162.524,128.435],[165.282,129.111]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (6)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[185.786,135.796],[188.095,136.225],[188.407,137.093],[187.505,139.926],[187.179,137.845],[185.787,135.796]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (6)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[186.305,132.86],[185.469,135.002],[183.843,133.651],[182.233,132.355],[185.052,132.79]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (6)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[116.271,249.606],[113.52,248.987],[110.637,249.107],[107.861,248.631],[105.152,247.768],[102.178,247.215],[101.901,247.464],[99.444,245.863],[97.804,246.681],[97.401,246.839],[97.338,246.133],[96.922,245.489],[98.176,244.346],[98.148,243.785],[97.731,241.363],[98.576,238.795],[98.966,236.146],[99.428,233.51],[99.288,233.112],[100.1,230.682],[100.524,228.186],[100.597,225.629],[100.968,223.123],[101.274,220.607],[101.807,218.129],[102.033,215.598],[102.626,213.131],[103.067,210.637],[103.317,208.111],[103.897,205.641],[104.527,203.18],[104.661,200.633],[105.336,198.18],[105.913,195.71],[106.391,193.222],[106.834,190.729],[107.027,188.193],[107.132,185.641],[108.003,183.22],[108.086,183.077],[108.22,183.208],[109.145,183.486],[109.498,183.476],[109.159,185.97],[108.252,188.366],[108.247,190.917],[107.557,193.351],[107.287,195.857],[106.975,198.355],[106.841,200.884],[106.278,203.339],[105.952,205.835],[105.251,208.267],[104.59,210.706],[104.187,213.189],[104.061,215.72],[103.166,218.119],[103.298,220.913],[102.419,223.532],[102.26,226.277],[102.325,227.137],[101.943,229.367],[100.942,231.492],[100.903,233.78],[100.636,235.43],[100.788,237.903],[101.849,240.132],[103.034,242.225],[104.286,244.386],[106.473,245.57],[108.225,246.313],[109.986,247.165],[112.353,247.617],[114.716,248.098],[117.159,248.081],[119.569,248.289],[122.232,249.217],[125.027,250.105],[125.475,250.471],[124.958,250.491],[124.852,250.568],[122.677,250.712],[119.179,250.593],[119.217,250.351],[116.271,249.607]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (6)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[101.345,245.627],[102.168,246.046],[102.439,245.456],[102.474,245.451],[104.188,246.051],[101.928,243.736],[101.344,245.628]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (6)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[99.239,242.614],[100.022,243.13],[100.774,242.716],[100.029,240.12],[99.413,241.662]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.573,0.702,0.859]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-4769-8N90-mask","layers":[{"ddd":0,"ind":94,"ty":4,"nm":"center","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[-100000,-100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":95,"ty":4,"nm":"Mask Group","parent":94,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Mask Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Mask","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[113.144,114.238],[197.071,128.633],[173.828,264.141],[89.902,249.746]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.85,0.85,0.85]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-4769-8N90-masked","layers":[{"ddd":0,"ind":98,"ty":4,"nm":"center","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[-100000,-100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":99,"ty":4,"nm":"Mask Group","parent":98,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Path 1 (6) Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1 (6)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[143.578,124.696],[140.906,123.897],[138.461,123.496],[135.977,123.348],[133.513,123.073],[131.111,122.42],[128.726,121.673],[126.254,121.442],[123.721,121.495],[120.802,121.521],[119.188,120.354],[118.557,120.706],[119.594,122.523],[118.478,124.351],[117.941,127.188],[117.3,130.016],[117.109,132.923],[116.79,133.061],[116.554,135.607],[115.717,138.049],[115.156,140.539],[115.016,143.101],[114.728,145.638],[113.998,148.098],[113.664,150.627],[113.117,153.12],[113.047,155.695],[112.676,158.218],[111.946,160.679],[111.72,163.227],[111.335,165.741],[111.108,168.281],[110.18,170.702],[110.191,173.283],[109.832,175.8],[109.102,178.254],[108.504,180.73],[108.18,183.266],[108.794,183.719],[108.876,183.716],[109.146,183.216],[109.175,183.228],[109.183,183.245],[108.897,183.375],[109.083,183.384],[109.404,180.861],[110.381,178.451],[110.334,175.865],[110.77,173.362],[111.251,170.866],[111.99,168.415],[112.002,165.839],[112.399,163.328],[112.926,160.86],[113.271,158.36],[114.208,155.961],[114.65,153.477],[114.579,150.906],[115.384,148.484],[115.723,145.983],[115.96,143.464],[116.743,141.04],[116.531,138.445],[117.224,136.005],[118.061,133.589],[117.881,131.356],[119.331,128.958],[120.985,126.758],[123.295,125.287],[125.757,124.234],[128.287,122.988],[131.046,123.44],[133.492,123.662],[135.937,123.901],[138.286,124.677],[140.736,124.875],[143.455,125.413],[144.805,126.042],[146.219,125.467],[146.238,125.466],[143.574,124.698]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (6)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[118.434,128.285],[119.378,124.613],[119.457,124.285],[121.925,124.865],[119.646,127.322],[118.432,128.285]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (6)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[122.211,124.648],[122.557,122.074],[123.445,122.156],[126.787,122.499],[124.511,123.597]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (6)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[178.284,195.313],[177.954,195.214],[178.067,192.681],[178.519,190.207],[179.588,187.838],[179.973,185.352],[180.012,182.807],[180.428,180.327],[180.987,177.871],[181.615,175.426],[182.055,172.95],[182.49,170.473],[182.4,167.905],[183.297,165.507],[183.481,162.986],[183.842,160.496],[184.739,157.839],[184.78,155.035],[185.248,152.305],[185.745,151.659],[186.02,149.411],[185.873,147.089],[186.774,144.947],[186.417,143.247],[186.456,140.825],[185.844,138.45],[184.727,136.241],[182.893,134.591],[181.092,132.911],[179.275,132.229],[177.466,131.493],[175.077,131.167],[172.734,130.565],[170.294,130.56],[167.996,129.709],[165.271,129.178],[162.502,128.469],[162.264,128.271],[162.683,128.234],[162.558,128.002],[164.827,127.557],[168.203,128.471],[171.198,128.991],[174.007,129.272],[176.79,129.692],[179.545,130.272],[182.324,130.713],[185.3,131.275],[185.565,131.106],[188.069,132.712],[189.814,132.398],[189.999,132.118],[190.104,132.559],[190.008,132.71],[188.932,134.3],[189.128,134.955],[189.54,137.294],[189.299,139.967],[188.792,142.595],[188.062,145.186],[187.847,145.522],[187.542,148.039],[187.289,150.565],[187.02,153.088],[186.518,155.571],[186.119,158.072],[185.433,160.524],[185.034,163.025],[184.74,165.544],[183.927,167.973],[184.028,170.56],[183.061,172.963],[183.264,175.567],[182.618,178.026],[181.738,180.444],[181.752,183.016],[181.575,185.555],[180.471,187.935],[180.266,190.469],[179.993,192.992],[179.767,195.524],[179.484,195.783],[179.131,195.858],[178.286,195.312]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (6)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[186.096,136.036],[187.643,138.751],[187.624,136.957],[187.766,136.327],[187.426,135.54],[186.095,136.037]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (6)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[183.071,132.724],[185.548,134.813],[185.632,133.318],[185.17,133.27],[185.081,132.596],[185.074,132.637]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.573,0.702,0.859]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-4765-8N90-mask","layers":[{"ddd":0,"ind":105,"ty":4,"nm":"center","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[-100000,-100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":106,"ty":4,"nm":"Mask Group","parent":105,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Mask Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Mask","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[113.144,114.238],[197.071,128.633],[173.828,264.141],[89.902,249.746]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.85,0.85,0.85]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-4765-8N90-masked","layers":[{"ddd":0,"ind":109,"ty":4,"nm":"center","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[-100000,-100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":110,"ty":4,"nm":"Mask Group","parent":109,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Path 1 (5) Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1 (5)","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[138.617,252.38],[138.725,252.061],[136.316,250.213],[134.015,247.892],[137.265,245.631],[138.142,247.771],[138.85,249.832],[139.778,248.443],[138.915,245.16],[138.543,243.203],[137.042,241.842],[136.219,240.114],[135.235,239.703],[135.15,239.337],[134.921,239.597],[133.862,239.676],[132.529,241.066],[130.912,242.104],[129.327,243.333],[127.914,246.434],[128.183,248.045],[129.3,246.102],[130.89,244.747],[133.336,247.744],[130.864,248.762],[128.617,249.678],[126.527,250.079],[124.303,249.554],[121.957,249.734],[119.765,249.048],[119.673,249.928],[121.974,250.537],[124.427,250.638],[126.796,251.223],[128.908,251.018],[130.81,250.159],[133.624,248.231],[135.167,251.319],[136.987,252.675],[139.099,253.122]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (5)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[133.741,247.544],[132.596,245.8],[130.891,244.655],[132.479,242.803],[134.708,241.906],[136.239,243.663],[137.163,245.56],[135.362,246.223]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (5)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[108.15,183.612],[108.196,183.254],[108.164,180.669],[108.459,178.141],[109.362,175.717],[109.671,173.191],[109.864,170.645],[110.598,168.192],[111.176,165.711],[111.104,163.118],[111.367,161.747],[111.857,159.138],[112.673,156.586],[113.284,153.998],[113.093,151.272],[113.665,148.678],[113.944,146.033],[114.779,143.483],[114.872,140.806],[115.439,138.21],[115.885,135.594],[116.698,133.042],[116.778,132.861],[117.111,129.979],[117.702,127.143],[118.151,124.283],[118.115,124.25],[118.962,122.229],[118.524,120.722],[118.961,120.389],[119.189,119.818],[119.219,120.325],[120.711,120.797],[123.588,120.967],[123.854,120.852],[126.902,121.67],[129.683,121.747],[132.343,122.536],[135.067,122.944],[137.86,122.933],[140.932,123.744],[143.654,124.321],[143.778,124.205],[146.405,125.243],[146.698,125.499],[146.297,125.539],[146.35,125.862],[144.813,125.966],[143.641,125.622],[143.354,126.143],[140.686,125.205],[138.274,124.787],[135.849,124.442],[133.484,123.745],[130.97,123.924],[128.343,123.572],[125.703,124.064],[123.256,125.203],[121.189,126.948],[119.648,129.145],[118.6,131.585],[117.861,133.554],[117.314,136.019],[117.036,138.53],[116.418,140.983],[116.012,143.473],[116.075,146.042],[115.179,148.448],[114.838,150.949],[114.343,153.424],[114.399,155.993],[114.105,158.502],[113.406,160.942],[113.099,163.449],[112.475,165.919],[111.814,168.384],[111.725,170.947],[111.234,173.44],[110.728,175.933],[110.209,178.421],[110.249,181.007],[109.239,183.411],[109.321,183.449],[109.093,183.82]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (5)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[119.623,124.672],[118.868,126.963],[119.049,126.846],[121.007,124.715],[120.42,124.214],[119.624,124.399]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (5)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[122.682,122.33],[122.415,124.313],[125.155,122.555],[123.358,122.665],[123.433,122.19],[123.304,122.29]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.573,0.702,0.859]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-4761-8N90-mask","layers":[{"ddd":0,"ind":116,"ty":4,"nm":"center","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[-100000,-100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":117,"ty":4,"nm":"Mask Group","parent":116,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Mask Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Mask","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[113.144,114.238],[197.071,128.633],[173.828,264.141],[89.902,249.746]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.85,0.85,0.85]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-4761-8N90-masked","layers":[{"ddd":0,"ind":120,"ty":4,"nm":"center","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[-100000,-100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":121,"ty":4,"nm":"Mask Group","parent":120,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Path 1 (4) Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1 (4)","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[149.116,126.719],[148.913,126.441],[151.166,128.442],[153.176,130.744],[149.996,132.639],[149.446,130.864],[148.374,129.099],[148.101,130.252],[148.102,133.654],[148.9,135.489],[149.951,137.176],[151.588,138.273],[151.832,139.375],[152.235,139.716],[152.598,139.249],[153.359,138.632],[155.222,138.067],[156.477,136.529],[158.052,135.309],[159.375,132.202],[159.465,131.116],[158.145,132.587],[156.3,134.218],[154.674,130.952],[156.53,129.82],[158.602,128.545],[160.927,128.567],[163.143,129.144],[165.472,129.058],[167.672,129.61],[167.809,128.708],[165.461,128.211],[163.008,128.113],[160.693,127.213],[158.467,127.364],[156.418,128.223],[153.91,129.953],[152.179,127.446],[150.282,126.3],[148.422,125.139]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (4)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[153.62,131.639],[155.042,132.702],[156.601,134.016],[154.804,135.685],[152.815,136.331],[151.336,134.947],[150.041,132.909],[151.889,132.081],[153.619,131.639]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (4)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[139.039,253.468],[136.784,252.995],[134.915,251.494],[133.488,249.018],[130.954,250.362],[129.046,251.626],[126.704,251.761],[124.393,250.832],[121.996,250.408],[119.49,250.623],[119.439,250.035],[119.446,248.98],[119.837,248.594],[122.034,249.278],[124.315,249.468],[126.605,249.613],[128.681,249.812],[130.702,248.84],[132.34,247.603],[130.859,245.226],[129.323,246.944],[128.283,248.284],[127.954,248.094],[127.475,248.071],[127.542,246.307],[129.129,243.192],[130.695,241.884],[132.215,240.612],[133.097,239.598],[133.698,239.401],[134.694,239.111],[134.864,239.315],[135.095,239.237],[135.178,239.153],[135.205,238.994],[135.613,238.976],[135.548,239.249],[135.489,239.449],[136.526,239.852],[136.614,240.609],[137.56,241.462],[138.78,243.084],[139.546,244.975],[139.665,248.44],[139.139,249.729],[139.03,249.889],[138.738,249.951],[138.031,248.474],[137.163,246.114],[134.782,247.997],[136.872,249.894],[138.799,251.948],[138.608,252.216],[139.364,253.253],[139.272,253.57],[139.037,253.465]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (4)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[133.791,247.255],[136.797,245.42],[135.677,243.97],[134.603,242.52],[132.715,243.037],[131.539,244.547]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.573,0.702,0.859]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-4757-8N90-mask","layers":[{"ddd":0,"ind":127,"ty":4,"nm":"center","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[-100000,-100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":128,"ty":4,"nm":"Mask Group","parent":127,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Mask Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Mask","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[113.144,114.238],[197.071,128.633],[173.828,264.141],[89.902,249.746]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.85,0.85,0.85]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-4757-8N90-masked","layers":[{"ddd":0,"ind":131,"ty":4,"nm":"center","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[-100000,-100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":132,"ty":4,"nm":"Mask Group","parent":131,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Path 1 (2) Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1 (2)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[152.23,139.746],[151.97,139.612],[151.767,139.597],[151.995,139.201],[151.233,138.574],[150.745,138.157],[149.98,137.158],[149.144,135.373],[148.142,133.645],[147.387,130.247],[148.087,128.696],[148.402,128.877],[148.579,128.879],[149.119,130.331],[150.314,132.669],[152.917,130.726],[150.826,128.549],[148.735,126.654],[148.706,126.536],[147.969,125.49],[148.005,124.993],[148.4,125.265],[150.428,126.069],[151.987,127.583],[153.991,129.47],[156.484,128.321],[158.371,126.942],[160.74,126.933],[163.154,127.262],[165.474,128.137],[167.968,127.992],[168.039,128.663],[167.907,129.696],[167.634,129.948],[165.445,129.216],[163.171,128.981],[160.849,129.028],[158.898,129.154],[156.821,130.016],[155.033,131.084],[156.48,133.601],[158.024,131.665],[159.173,130.435],[159.489,130.616],[159.522,130.888],[159.368,132.203],[158.162,135.39],[156.63,136.686],[155.387,138.309],[154.269,138.953],[153.592,139.029],[152.885,139.874],[152.573,139.354],[152.664,139.922]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (2)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[150.584,133.191],[151.539,134.867],[152.855,136.1],[154.713,135.634],[156.114,134.025],[153.623,131.624]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.573,0.702,0.859]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-4753-8N90-mask","layers":[{"ddd":0,"ind":138,"ty":4,"nm":"center","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[-100000,-100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":139,"ty":4,"nm":"Mask Group","parent":138,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Mask Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Mask","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[113.144,114.238],[197.071,128.633],[173.828,264.141],[89.902,249.746]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.85,0.85,0.85]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-4753-8N90-masked","layers":[{"ddd":0,"ind":142,"ty":4,"nm":"center","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[-100000,-100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":143,"ty":4,"nm":"Mask Group","parent":142,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Path 1 (2) Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1 (2)","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[160.617,127.663],[158.53,127.644],[156.388,128.186],[153.906,129.979],[151.914,127.637],[150.303,126.271],[148.412,125.203],[145.943,125.199],[143.646,124.194],[141.146,123.845],[140.853,125.336],[143.252,125.577],[145.461,126.191],[147.72,126.51],[149.888,127.133],[151.268,129.17],[153.053,130.714],[150.038,132.74],[149.361,130.903],[148.616,128.842],[147.919,130.258],[148.381,133.581],[149.39,135.257],[150.246,136.97],[151.323,138.503],[152.195,139.01],[152.253,139.606],[152.644,139.358],[153.577,139.008],[154.938,137.662],[156.531,136.59],[157.555,134.958],[159.231,132.159],[159.225,130.563],[157.868,132.353],[156.628,133.875],[154.468,130.966],[157.114,129.687],[159.926,128.593],[160.059,128.114]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (2)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[156.661,133.996],[154.89,135.797],[152.805,136.399],[151.404,134.911],[150.087,132.955],[151.822,131.959],[153.664,131.398],[155.261,132.465],[156.662,133.996]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.573,0.702,0.859]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-4749-8N90-mask","layers":[{"ddd":0,"ind":149,"ty":4,"nm":"center","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[-100000,-100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":150,"ty":4,"nm":"Mask Group","parent":149,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Mask Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Mask","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[113.144,114.238],[197.071,128.633],[173.828,264.141],[89.902,249.746]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.85,0.85,0.85]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-4749-8N90-masked","layers":[{"ddd":0,"ind":153,"ty":4,"nm":"center","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[-100000,-100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":154,"ty":4,"nm":"Mask Group","parent":153,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Path 1 (2) Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1 (2)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[152.22,139.81],[152.222,139.43],[151.651,139.733],[151.964,139.245],[151.316,138.509],[150.784,138.136],[149.849,137.259],[149.03,135.433],[148.503,133.546],[147.37,130.252],[148.068,128.683],[148.476,128.446],[148.69,128.764],[149.371,130.234],[150.112,132.071],[152.764,130.724],[151.182,129.007],[149.411,127.601],[147.738,126.408],[145.479,126.085],[143.26,125.533],[140.93,125.62],[141.05,125.093],[140.544,123.951],[141.24,123.816],[143.671,124.04],[146.019,124.75],[148.475,124.832],[150.549,125.881],[152.479,127.239],[153.964,129.64],[156.499,128.345],[158.403,127.084],[160.71,127.121],[161.106,127.257],[160.643,127.383],[160.133,128.641],[159.881,128.566],[157.208,129.506],[154.563,131.011],[156.555,133.514],[158.377,131.976],[159.307,130.749],[159.513,130.494],[159.851,130.696],[159.856,132.37],[158.004,135.279],[156.954,137.019],[155.12,137.923],[154.094,138.628],[153.822,139.42],[152.844,139.787],[152.761,139.886],[152.477,139.65],[152.221,139.811]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (2)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[150.622,133.243],[151.273,135.037],[152.885,135.931],[154.682,135.598],[156.207,133.973],[153.629,131.581]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.573,0.702,0.859]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-4725-8N90-mask","layers":[{"ddd":0,"ind":158,"ty":4,"nm":"center","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[-100000,-100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":159,"ty":4,"nm":"Mask Group","parent":158,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[143.75,189.414]},"o":{"a":0,"k":100},"p":{"a":0,"k":[143.75,189.414]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Layer 17 Group","bm":0,"it":[{"ty":"gr","hd":false,"nm":"Path 1 Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0],[0.689,-4.021],[0,0],[-3.99,-0.684],[0,0],[-0.689,4.021],[0,0],[3.991,0.685]],"o":[[0,0],[-3.991,-0.684],[0,0],[-0.69,4.02],[0,0],[3.991,0.685],[0,0],[0.69,-4.021],[0,0]],"v":[[188.164,127.558],[122.487,116.293],[114.013,122.334],[93.359,242.75],[99.335,251.269],[165.013,262.534],[173.487,256.493],[194.141,136.078],[188.164,127.558]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[1,1,1]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[143.75,189.414]},"o":{"a":0,"k":100},"p":{"a":0,"k":[143.75,189.414]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-4725-8N90-masked","layers":[{"ddd":0,"ind":163,"ty":4,"nm":"center","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[-100000,-100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":164,"ty":4,"nm":"Mask Group","parent":163,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[143.75,189.414]},"o":{"a":0,"k":100},"p":{"a":0,"k":[143.75,189.414]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Layer 18 Group","bm":0,"it":[{"ty":"gr","hd":false,"nm":"Path 1 Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[175.769,254.417],[175.255,253.265],[174.477,252.236],[173.86,251.106],[173.133,250.045],[172.408,248.983],[171.961,247.746],[171.172,246.723],[170.526,245.612],[169.807,244.544],[169.224,243.484],[168.308,244.27],[167.196,244.937],[166.275,245.844],[165.267,246.64],[164.304,247.494],[163.342,248.35],[162.415,249.25],[161.414,250.056],[160.331,250.76],[159.36,251.602],[159.961,252.739],[160.609,253.85],[161.374,254.886],[161.928,256.056],[162.597,257.153],[163.32,258.217],[163.979,259.322],[164.585,260.459],[165.275,261.544],[165.914,262.784],[166.993,261.909],[167.99,261.099],[168.817,260.074],[169.883,259.349],[170.801,258.439],[171.825,257.662],[172.811,256.835],[173.825,256.046],[174.913,255.345]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.925,0.957,0.996]},"r":1,"o":{"a":0,"k":60}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"gr","hd":false,"nm":"Path 1 (2) Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1 (2)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[142.952,248.788],[142.353,247.733],[141.797,246.564],[141.146,245.456],[140.335,244.448],[139.837,243.244],[139.019,242.24],[138.568,241.005],[137.784,239.979],[137.039,238.929],[136.469,237.823],[135.541,238.644],[134.458,239.349],[133.509,240.219],[132.459,240.964],[131.624,241.978],[130.557,242.7],[129.499,243.437],[128.569,244.332],[127.539,245.103],[126.526,245.97],[127.164,247.142],[127.863,248.22],[128.478,249.352],[129.306,250.349],[129.819,251.544],[130.425,252.681],[131.245,253.684],[131.959,254.754],[132.607,255.866],[133.188,256.957],[134.168,256.21],[135.162,255.396],[136.075,254.479],[137.06,253.652],[138.049,252.833],[139.142,252.141],[140.097,251.278],[141.006,250.355],[142.052,249.606]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (2)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[159.556,251.636],[158.78,250.515],[158.194,249.366],[157.346,248.381],[156.817,247.196],[156.241,246.04],[155.506,244.983],[154.834,243.888],[154.097,242.833],[153.481,241.701],[152.873,240.505],[151.787,241.285],[150.895,242.227],[149.936,243.085],[149,243.972],[147.928,244.689],[146.966,245.544],[145.927,246.304],[144.929,247.115],[143.999,248.012],[142.961,248.79],[143.618,249.907],[144.376,250.949],[144.894,252.141],[145.619,253.203],[146.182,254.366],[146.826,255.479],[147.479,256.588],[148.306,257.585],[148.805,258.791],[149.548,259.889],[150.576,259.053],[151.562,258.227],[152.584,257.45],[153.573,256.629],[154.44,255.653],[155.414,254.814],[156.436,254.033],[157.536,253.352],[158.538,252.553]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.925,0.957,0.996]},"r":1,"o":{"a":0,"k":60}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"gr","hd":false,"nm":"Path 1 (4) Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1 (4)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[162.552,232.457],[162.011,231.31],[161.189,230.309],[160.611,229.154],[160.051,227.989],[159.208,227.001],[158.766,225.762],[158.048,224.693],[157.306,223.643],[156.695,222.507],[155.987,221.552],[155.041,222.263],[154.067,223.103],[152.958,223.773],[152.14,224.809],[151.072,225.53],[150.146,226.431],[149.008,227.065],[148.148,228.049],[147.177,228.896],[145.987,229.616],[146.75,230.764],[147.431,231.854],[148.18,232.901],[148.774,234.046],[149.41,235.163],[149.984,236.32],[150.775,237.343],[151.316,238.52],[152.152,239.513],[152.722,240.59],[153.656,239.815],[154.723,239.09],[155.608,238.14],[156.637,237.369],[157.581,236.492],[158.55,235.646],[159.714,235.043],[160.678,234.192],[161.679,233.38]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (4)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[179.246,235.116],[178.527,233.965],[177.844,232.877],[177.047,231.859],[176.579,230.635],[175.756,229.635],[175.151,228.497],[174.49,227.395],[173.766,226.332],[173.305,225.102],[172.575,223.958],[171.641,224.964],[170.547,225.652],[169.638,226.573],[168.621,227.359],[167.618,228.162],[166.634,228.989],[165.706,229.889],[164.577,230.533],[163.631,231.41],[162.692,232.277],[163.215,233.45],[164.108,234.408],[164.691,235.559],[165.379,236.644],[165.859,237.86],[166.629,238.894],[167.289,239.998],[167.859,241.157],[168.58,242.223],[169.231,243.454],[170.195,242.435],[171.227,241.669],[172.188,240.814],[173.15,239.959],[174.285,239.321],[175.224,238.438],[176.222,237.627],[177.149,236.727],[178.215,236.007]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (4)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[110.281,243.185],[109.659,242.072],[108.89,241.037],[108.374,239.843],[107.701,238.749],[106.892,237.74],[106.413,236.523],[105.78,235.402],[105.021,234.362],[104.315,233.286],[103.696,232.271],[102.747,232.985],[101.758,233.807],[100.727,234.577],[99.774,235.441],[98.845,236.338],[97.721,236.988],[96.839,237.945],[95.733,238.619],[94.851,239.577],[93.902,240.375],[94.427,241.508],[95.193,242.544],[95.786,243.689],[96.379,244.834],[97.147,245.869],[97.775,246.992],[98.521,248.042],[98.971,249.277],[99.754,250.304],[100.404,251.468],[101.395,250.578],[102.298,249.648],[103.327,248.878],[104.303,248.041],[105.392,247.346],[106.297,246.419],[107.319,245.638],[108.346,244.865],[109.257,243.945]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (4)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[126.545,245.974],[126.142,244.818],[125.418,243.755],[124.793,242.63],[124.123,241.533],[123.427,240.454],[122.641,239.429],[121.995,238.317],[121.327,237.219],[120.88,235.981],[120.108,234.9],[119.135,235.803],[118.116,236.587],[117.225,237.532],[116.166,238.264],[115.173,239.08],[114.116,239.816],[113.246,240.787],[112.249,241.599],[111.272,242.435],[110.292,243.187],[110.849,244.292],[111.589,245.345],[112.234,246.457],[112.837,247.596],[113.507,248.692],[114.156,249.802],[114.813,250.908],[115.468,252.014],[116.057,253.163],[116.777,254.322],[117.693,253.281],[118.812,252.625],[119.717,251.698],[120.786,250.978],[121.805,250.196],[122.732,249.295],[123.804,248.579],[124.622,247.541],[125.62,246.737]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.925,0.957,0.996]},"r":1,"o":{"a":0,"k":60}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"gr","hd":false,"nm":"Path 1 Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[146.28,229.461],[145.651,228.418],[145.095,227.249],[144.398,226.17],[143.708,225.086],[142.965,224.035],[142.484,222.82],[141.721,221.78],[141.15,220.621],[140.334,219.614],[139.782,218.522],[138.89,219.357],[137.801,220.053],[136.798,220.854],[135.899,221.789],[134.779,222.446],[133.85,223.342],[132.817,224.108],[131.807,224.903],[130.95,225.892],[129.824,226.639],[130.577,227.754],[131.348,228.787],[131.817,230.01],[132.47,231.117],[133.266,232.135],[133.891,233.259],[134.408,234.453],[135.124,235.522],[135.89,236.559],[136.477,237.796],[137.416,236.794],[138.552,236.16],[139.513,235.303],[140.498,234.478],[141.361,233.497],[142.407,232.749],[143.388,231.916],[144.476,231.219],[145.425,230.343]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.925,0.957,0.996]},"r":1,"o":{"a":0,"k":60}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"gr","hd":false,"nm":"Path 1 Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[129.914,226.654],[129.238,225.627],[128.658,224.475],[128.032,223.351],[127.44,222.205],[126.74,221.127],[126.111,220.005],[125.495,218.873],[124.746,217.825],[124.016,216.766],[123.41,215.662],[122.339,216.332],[121.388,217.202],[120.515,218.169],[119.488,218.942],[118.561,219.842],[117.398,220.442],[116.455,221.323],[115.508,222.197],[114.473,222.961],[113.513,223.841],[114.228,224.924],[114.804,226.08],[115.574,227.114],[116.109,228.295],[116.828,229.36],[117.577,230.408],[118.11,231.592],[118.753,232.706],[119.528,233.738],[120.109,234.902],[121.047,234],[122.017,233.155],[123.087,232.437],[124.063,231.599],[124.965,230.668],[126.108,230.041],[126.97,229.059],[128.092,228.406],[128.956,227.429]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.925,0.957,0.996]},"r":1,"o":{"a":0,"k":60}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"gr","hd":false,"nm":"Path 1 (2) Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1 (2)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[182.561,215.789],[181.754,214.693],[181.265,213.482],[180.506,212.441],[179.88,211.317],[179.101,210.289],[178.604,209.083],[177.962,207.969],[177.207,206.925],[176.497,205.853],[175.853,204.85],[174.81,205.451],[173.932,206.413],[172.979,207.278],[171.924,208.016],[170.885,208.774],[169.945,209.658],[168.922,210.437],[168.058,211.416],[167.004,212.157],[165.869,212.925],[166.706,214.013],[167.204,215.217],[168.076,216.187],[168.541,217.414],[169.404,218.388],[169.993,219.536],[170.675,220.625],[171.141,221.85],[171.808,222.951],[172.55,224.105],[173.62,223.248],[174.627,222.448],[175.538,221.529],[176.509,220.687],[177.587,219.978],[178.504,219.065],[179.566,218.336],[180.5,217.446],[181.391,216.493]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (2)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[113.646,223.864],[112.859,222.816],[112.285,221.659],[111.745,220.481],[111.099,219.37],[110.253,218.384],[109.686,217.222],[108.988,216.142],[108.327,215.04],[107.729,213.897],[107.042,212.779],[106.026,213.608],[105.054,214.452],[104.102,215.318],[103.152,216.189],[102.092,216.92],[101.175,217.833],[100.077,218.517],[99.092,219.343],[98.162,220.241],[97.167,221.037],[97.805,222.141],[98.475,223.237],[98.999,224.426],[99.829,225.421],[100.549,226.486],[101.095,227.662],[101.862,228.698],[102.461,229.84],[102.968,231.04],[103.737,232.044],[104.724,231.264],[105.759,230.5],[106.607,229.503],[107.774,228.906],[108.664,227.961],[109.713,227.214],[110.656,226.335],[111.725,225.614],[112.689,224.761]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.925,0.957,0.996]},"r":1,"o":{"a":0,"k":60}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"gr","hd":false,"nm":"Path 1 (2) Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1 (2)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[149.595,210.135],[148.922,209.117],[148.486,207.874],[147.742,206.824],[147.089,205.717],[146.462,204.594],[145.773,203.508],[145.149,202.382],[144.315,201.388],[143.864,200.152],[143.115,199.087],[142.073,199.863],[141.218,200.855],[140.236,201.683],[139.198,202.443],[138.153,203.194],[137.156,204.005],[136.115,204.76],[135.139,205.598],[134.175,206.453],[133.249,207.331],[133.967,208.38],[134.513,209.554],[135.174,210.656],[135.82,211.767],[136.617,212.785],[137.172,213.953],[137.847,215.048],[138.517,216.145],[139.073,217.315],[139.784,218.51],[140.724,217.461],[141.806,216.755],[142.794,215.933],[143.812,215.148],[144.713,214.217],[145.698,213.392],[146.801,212.712],[147.627,211.686],[148.681,210.946]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (2)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[165.965,212.942],[165.434,211.844],[164.702,210.787],[164.175,209.601],[163.328,208.615],[162.839,207.405],[162.104,206.349],[161.551,205.179],[160.702,204.193],[160.219,202.978],[159.518,201.768],[158.49,202.718],[157.496,203.533],[156.527,204.38],[155.615,205.297],[154.525,205.991],[153.623,206.922],[152.594,207.694],[151.667,208.593],[150.558,209.264],[149.658,210.145],[150.16,211.307],[150.905,212.357],[151.577,213.452],[152.147,214.611],[152.96,215.618],[153.645,216.706],[154.131,217.918],[154.873,218.97],[155.643,220.006],[156.214,221.033],[157.095,220.256],[158.193,219.571],[159.221,218.802],[160.191,217.958],[161.169,217.122],[162.149,216.29],[163.167,215.502],[164.187,214.721],[165.009,213.694]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.925,0.957,0.996]},"r":1,"o":{"a":0,"k":60}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"gr","hd":false,"nm":"Path 1 Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[133.259,207.332],[132.647,206.242],[131.986,205.139],[131.263,204.076],[130.667,202.932],[130.023,201.82],[129.301,200.756],[128.673,199.631],[128.167,198.431],[127.335,197.435],[126.744,196.223],[125.823,197.218],[124.786,197.979],[123.737,198.724],[122.863,199.689],[121.875,200.512],[120.821,201.251],[119.819,202.056],[118.749,202.775],[117.77,203.612],[116.8,204.509],[117.41,205.68],[118.285,206.648],[118.707,207.901],[119.353,209.011],[120.047,210.094],[120.686,211.21],[121.39,212.286],[122.163,213.318],[122.689,214.507],[123.405,215.687],[124.355,214.663],[125.423,213.941],[126.404,213.11],[127.455,212.369],[128.443,211.546],[129.275,210.527],[130.344,209.805],[131.357,209.015],[132.369,208.219]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.925,0.957,0.996]},"r":1,"o":{"a":0,"k":60}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"gr","hd":false,"nm":"Path 1 (3) Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1 (3)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[169.257,193.609],[168.83,192.464],[168.05,191.436],[167.288,190.397],[166.793,189.191],[166.134,188.088],[165.463,186.992],[164.823,185.875],[164.131,184.793],[163.504,183.667],[162.787,182.698],[161.869,183.473],[160.92,184.345],[159.839,185.048],[158.832,185.847],[157.901,186.741],[156.967,187.632],[155.923,188.384],[154.812,189.052],[153.869,189.933],[152.895,190.803],[153.637,191.875],[154.25,193.008],[154.989,194.06],[155.64,195.169],[156.247,196.306],[156.746,197.51],[157.407,198.613],[158.153,199.663],[158.766,200.796],[159.481,201.97],[160.452,200.981],[161.407,200.118],[162.528,199.465],[163.421,198.521],[164.425,197.72],[165.427,196.916],[166.398,196.07],[167.387,195.249],[168.342,194.389]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (3)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[185.844,196.454],[185.021,195.394],[184.514,194.195],[183.836,193.103],[183.182,191.996],[182.441,190.945],[181.947,189.737],[181.151,188.719],[180.468,187.631],[179.903,186.466],[179.171,185.496],[178.23,186.255],[177.157,186.969],[176.265,187.913],[175.288,188.75],[174.209,189.456],[173.297,190.375],[172.285,191.168],[171.298,191.992],[170.337,192.85],[169.377,193.63],[169.897,194.762],[170.694,195.779],[171.194,196.982],[172.042,197.966],[172.473,199.214],[173.315,200.202],[173.79,201.421],[174.62,202.419],[175.174,203.589],[175.88,204.685],[176.955,203.944],[177.856,203.013],[178.861,202.212],[179.809,201.339],[180.782,200.498],[181.796,199.709],[182.782,198.883],[183.73,198.009],[184.742,197.214]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (3)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[116.865,204.52],[116.247,203.443],[115.602,202.331],[114.942,201.228],[114.284,200.123],[113.699,198.974],[113.07,197.851],[112.277,196.831],[111.76,195.638],[111.014,194.587],[110.369,193.376],[109.286,194.213],[108.443,195.217],[107.461,196.048],[106.339,196.7],[105.447,197.643],[104.407,198.401],[103.428,199.234],[102.485,200.114],[101.542,200.996],[100.482,201.71],[101.22,202.749],[101.701,203.965],[102.478,204.995],[103.161,206.083],[103.664,207.285],[104.406,208.337],[104.973,209.498],[105.687,210.567],[106.27,211.721],[107.042,212.772],[107.978,211.86],[109.057,211.152],[110.049,210.334],[110.922,209.368],[111.985,208.64],[112.921,207.752],[113.953,206.984],[114.957,206.182],[116.019,205.447]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.925,0.957,0.996]},"r":1,"o":{"a":0,"k":60}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"gr","hd":false,"nm":"Path 1 Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[152.988,190.819],[152.3,189.749],[151.741,188.582],[150.924,187.578],[150.348,186.423],[149.783,185.26],[149.151,184.14],[148.505,183.026],[147.819,181.939],[147.006,180.932],[146.442,179.689],[145.401,180.552],[144.506,181.491],[143.417,182.186],[142.46,183.047],[141.439,183.828],[140.469,184.672],[139.518,185.543],[138.534,186.371],[137.603,187.265],[136.431,187.979],[137.318,189.027],[137.8,190.243],[138.51,191.314],[139.126,192.445],[139.792,193.543],[140.46,194.641],[141.268,195.652],[141.931,196.754],[142.464,197.937],[143.148,198.894],[144.192,198.324],[145.047,197.334],[146.079,196.567],[147.097,195.783],[148.003,194.857],[148.963,193.999],[150.103,193.368],[151.115,192.575],[152.019,191.645]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.925,0.957,0.996]},"r":1,"o":{"a":0,"k":60}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"gr","hd":false,"nm":"Path 1 (3) Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1 (3)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[189.185,177.131],[188.532,175.944],[187.776,174.901],[187.112,173.801],[186.38,172.743],[185.671,171.671],[185.113,170.503],[184.373,169.451],[183.896,168.232],[183.044,167.249],[182.505,166.067],[181.543,166.923],[180.59,167.791],[179.584,168.589],[178.607,169.425],[177.514,170.115],[176.577,171.001],[175.646,171.897],[174.538,172.569],[173.543,173.384],[172.614,174.288],[173.402,175.316],[173.826,176.567],[174.638,177.574],[175.134,178.78],[176.001,179.754],[176.495,180.96],[177.328,181.955],[177.871,183.132],[178.646,184.165],[179.187,185.412],[180.267,184.611],[181.16,183.67],[182.159,182.861],[183.13,182.017],[184.189,181.286],[185.046,180.298],[186.158,179.63],[187.157,178.822],[188.07,177.9]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (3)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[120.125,185.182],[119.687,184.035],[118.821,183.062],[118.279,181.885],[117.668,180.753],[117.037,179.631],[116.345,178.548],[115.747,177.406],[115.007,176.352],[114.249,175.309],[113.68,174.07],[112.703,175.015],[111.761,175.893],[110.758,176.696],[109.771,177.52],[108.76,178.314],[107.662,178.996],[106.77,179.941],[105.693,180.652],[104.824,181.626],[103.716,182.368],[104.531,183.424],[105.048,184.617],[105.86,185.624],[106.289,186.872],[107.011,187.936],[107.841,188.932],[108.326,190.146],[109.111,191.171],[109.795,192.26],[110.375,193.336],[111.277,192.512],[112.304,191.738],[113.383,191.031],[114.25,190.057],[115.385,189.419],[116.281,188.481],[117.273,187.664],[118.313,186.906],[119.198,185.958]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (3)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[136.61,188.01],[135.942,186.925],[135.36,185.773],[134.759,184.632],[133.979,183.604],[133.347,182.485],[132.638,181.412],[132.071,180.25],[131.371,179.172],[130.601,178.137],[130.065,176.853],[129.127,177.877],[127.992,178.513],[127.113,179.473],[126.155,180.333],[125.076,181.042],[124.088,181.862],[123.064,182.64],[122.13,183.533],[121.159,184.377],[120.258,185.205],[120.725,186.35],[121.396,187.447],[122.249,188.429],[122.709,189.657],[123.436,190.717],[124.018,191.87],[124.885,192.843],[125.356,194.065],[125.969,195.199],[126.719,196.357],[127.737,195.421],[128.753,194.634],[129.628,193.669],[130.626,192.86],[131.688,192.13],[132.738,191.387],[133.746,190.588],[134.575,189.565],[135.631,188.826]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.925,0.957,0.996]},"r":1,"o":{"a":0,"k":60}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"gr","hd":false,"nm":"Path 1 Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[172.734,174.309],[172.17,173.122],[171.31,172.145],[170.67,171.029],[170.105,169.866],[169.309,168.849],[168.722,167.699],[168.186,166.518],[167.343,165.529],[166.809,164.346],[166.114,163.311],[165.172,164.127],[164.067,164.801],[163.174,165.744],[162.159,166.531],[161.208,167.4],[160.202,168.2],[159.204,169.011],[158.299,169.938],[157.176,170.592],[156.199,171.473],[156.943,172.554],[157.654,173.625],[158.306,174.734],[158.983,175.826],[159.473,177.034],[160.203,178.094],[160.95,179.143],[161.516,180.305],[162.149,181.427],[162.817,182.533],[163.839,181.742],[164.728,180.795],[165.704,179.958],[166.817,179.294],[167.808,178.476],[168.815,177.676],[169.673,176.69],[170.642,175.842],[171.65,175.043]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.925,0.957,0.996]},"r":1,"o":{"a":0,"k":60}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"gr","hd":false,"nm":"Path 1 Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[156.312,171.492],[155.674,170.384],[155.106,169.222],[154.368,168.169],[153.787,167.017],[152.909,166.051],[152.331,164.896],[151.61,163.831],[151.178,162.584],[150.501,161.49],[149.745,160.436],[148.681,161.178],[147.757,162.08],[146.75,162.88],[145.857,163.821],[144.812,164.57],[143.861,165.44],[142.76,166.122],[141.879,167.079],[140.843,167.843],[139.882,168.674],[140.495,169.786],[141.072,170.941],[141.906,171.935],[142.386,173.151],[143.168,174.178],[143.808,175.293],[144.385,176.449],[145.046,177.551],[145.68,178.672],[146.46,179.587],[147.353,178.8],[148.481,178.154],[149.462,177.324],[150.359,176.385],[151.336,175.552],[152.46,174.9],[153.316,173.91],[154.33,173.12],[155.413,172.416]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.925,0.957,0.996]},"r":1,"o":{"a":0,"k":60}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"gr","hd":false,"nm":"Path 1 Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[139.907,168.678],[139.32,167.557],[138.575,166.508],[137.955,165.38],[137.278,164.288],[136.633,163.176],[135.914,162.11],[135.237,161.016],[134.594,159.902],[133.995,158.76],[133.383,157.513],[132.427,158.529],[131.438,159.35],[130.353,160.048],[129.486,161.024],[128.384,161.702],[127.494,162.648],[126.438,163.386],[125.512,164.286],[124.476,165.05],[123.441,165.854],[124.12,166.973],[124.853,168.031],[125.392,169.21],[126.148,170.251],[126.722,171.409],[127.325,172.547],[128.189,173.524],[128.829,174.639],[129.378,175.813],[130.081,176.765],[131.131,176.189],[131.994,175.211],[133.003,174.416],[134.071,173.693],[135.002,172.799],[135.963,171.942],[137.035,171.224],[137.968,170.334],[139.033,169.605]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.925,0.957,0.996]},"r":1,"o":{"a":0,"k":60}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"gr","hd":false,"nm":"Path 1 (2) Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1 (2)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[192.457,157.795],[191.807,156.64],[191.197,155.506],[190.33,154.533],[189.83,153.33],[189.216,152.198],[188.374,151.209],[187.687,150.122],[187.106,148.969],[186.512,147.823],[185.838,146.631],[184.873,147.615],[183.925,148.488],[182.785,149.119],[181.898,150.068],[180.839,150.802],[179.87,151.648],[178.926,152.526],[177.891,153.29],[177.009,154.247],[176.034,154.978],[176.614,156.051],[177.347,157.109],[177.963,158.239],[178.447,159.452],[179.323,160.42],[179.959,161.538],[180.497,162.719],[181.249,163.764],[181.764,164.96],[182.496,166.116],[183.468,165.14],[184.408,164.257],[185.496,163.562],[186.542,162.813],[187.496,161.946],[188.441,161.07],[189.405,160.218],[190.399,159.403],[191.466,158.68]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (2)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[123.626,165.886],[122.841,164.81],[122.346,163.603],[121.561,162.578],[120.879,161.49],[120.346,160.307],[119.543,159.294],[118.952,158.147],[118.39,156.982],[117.575,155.975],[116.961,154.947],[115.935,155.581],[114.96,156.419],[114.062,157.354],[113.049,158.144],[112.01,158.903],[111.016,159.717],[110.034,160.548],[109.104,161.443],[108.03,162.16],[107.122,163.055],[107.853,164.092],[108.462,165.227],[108.992,166.412],[109.849,167.391],[110.464,168.522],[111.14,169.615],[111.754,170.747],[112.471,171.815],[112.927,173.047],[113.68,174.073],[114.629,173.23],[115.66,172.463],[116.666,171.662],[117.569,170.733],[118.6,169.966],[119.58,169.132],[120.566,168.307],[121.574,167.509],[122.575,166.701]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.925,0.957,0.996]},"r":1,"o":{"a":0,"k":60}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"gr","hd":false,"nm":"Path 1 (3) Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1 (3)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[143.141,149.336],[142.701,148.188],[141.854,147.202],[141.311,146.026],[140.665,144.914],[139.928,143.861],[139.362,142.698],[138.652,141.626],[138.12,140.442],[137.316,139.427],[136.667,138.364],[135.636,139.067],[134.67,139.916],[133.659,140.709],[132.638,141.49],[131.69,142.362],[130.76,143.258],[129.723,144.02],[128.783,144.904],[127.724,145.638],[126.893,146.549],[127.421,147.654],[128.175,148.698],[128.822,149.809],[129.429,150.946],[130.185,151.987],[130.827,153.102],[131.365,154.282],[131.989,155.408],[132.674,156.497],[133.386,157.498],[134.425,156.836],[135.397,155.994],[136.298,155.062],[137.407,154.392],[138.404,153.582],[139.3,152.643],[140.216,151.728],[141.221,150.928],[142.275,150.186]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (3)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[159.524,152.146],[158.859,151.138],[158.297,149.974],[157.55,148.926],[157.024,147.739],[156.412,146.605],[155.721,145.522],[155.146,144.365],[154.402,143.314],[153.669,142.255],[153.061,141.101],[152.142,142.033],[151.083,142.766],[150.038,143.517],[149.11,144.414],[148.156,145.28],[147.128,146.052],[146.226,146.983],[145.201,147.76],[144.165,148.522],[143.154,149.339],[143.733,150.508],[144.454,151.571],[145.184,152.631],[145.894,153.702],[146.384,154.912],[147.224,155.903],[147.762,157.083],[148.409,158.193],[149.057,159.306],[149.745,160.434],[150.827,159.673],[151.765,158.788],[152.798,158.022],[153.785,157.199],[154.662,156.238],[155.668,155.437],[156.671,154.632],[157.703,153.866],[158.674,153.017]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (3)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[176.054,154.982],[175.458,153.811],[174.618,152.821],[174.047,151.662],[173.418,150.54],[172.797,149.412],[171.997,148.397],[171.41,147.247],[170.834,146.091],[170.129,145.016],[169.425,144.006],[168.518,144.838],[167.441,145.55],[166.531,146.469],[165.431,147.149],[164.511,148.057],[163.59,148.963],[162.505,149.665],[161.464,150.421],[160.505,151.282],[159.632,152.165],[160.177,153.277],[160.861,154.365],[161.402,155.542],[162.301,156.494],[162.814,157.69],[163.47,158.796],[164.034,159.961],[164.691,161.065],[165.556,162.041],[166.126,163.24],[167.072,162.312],[168.079,161.512],[169.185,160.84],[170.062,159.877],[171.085,159.099],[172.123,158.34],[173.053,157.444],[174.129,156.732],[175.042,155.814]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.925,0.957,0.996]},"r":1,"o":{"a":0,"k":60}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"gr","hd":false,"nm":"Path 1 (2) Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1 (2)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[195.698,138.454],[195.021,137.376],[194.426,136.233],[193.727,135.155],[193.026,134.077],[192.509,132.884],[191.865,131.772],[191.004,130.794],[190.555,129.558],[189.87,128.47],[189.129,127.452],[188.173,128.266],[187.18,129.083],[186.181,129.89],[185.152,130.661],[184.212,131.542],[183.289,132.449],[182.135,133.062],[181.187,133.935],[180.318,134.91],[179.289,135.64],[179.842,136.779],[180.532,137.863],[181.184,138.971],[181.759,140.128],[182.631,141.098],[183.22,142.245],[183.738,143.438],[184.553,144.444],[185.163,145.578],[185.82,146.743],[186.792,145.821],[187.803,145.027],[188.776,144.187],[189.868,143.496],[190.829,142.639],[191.718,141.692],[192.678,140.833],[193.785,140.161],[194.773,139.337]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (2)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[126.963,146.561],[126.265,145.412],[125.673,144.266],[124.924,143.22],[124.12,142.207],[123.472,141.097],[122.858,139.965],[122.295,138.801],[121.553,137.749],[120.882,136.651],[120.317,135.375],[119.286,136.296],[118.257,137.066],[117.305,137.934],[116.311,138.749],[115.314,139.56],[114.44,140.526],[113.363,141.238],[112.318,141.988],[111.363,142.853],[110.438,143.727],[111.11,144.8],[111.802,145.883],[112.363,147.048],[113.036,148.142],[113.633,149.284],[114.304,150.38],[115.105,151.397],[115.551,152.634],[116.275,153.698],[116.973,154.874],[118.052,154.036],[119.04,153.215],[119.874,152.199],[120.972,151.514],[121.967,150.702],[122.925,149.841],[123.943,149.056],[124.984,148.299],[125.919,147.41]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.925,0.957,0.996]},"r":1,"o":{"a":0,"k":60}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"gr","hd":false,"nm":"Path 1 Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[179.472,135.671],[178.685,134.539],[177.98,133.464],[177.379,132.324],[176.82,131.158],[176.116,130.082],[175.425,128.999],[174.726,127.92],[174.125,126.778],[173.492,125.658],[172.765,124.539],[171.702,125.343],[170.859,126.348],[169.779,127.055],[168.821,127.915],[167.806,128.704],[166.82,129.527],[165.769,130.27],[164.807,131.127],[163.873,132.02],[162.767,132.806],[163.575,133.897],[164.065,135.107],[164.823,136.149],[165.478,137.255],[166.036,138.421],[166.709,139.517],[167.516,140.528],[168.036,141.718],[168.849,142.727],[169.467,143.766],[170.477,143.095],[171.348,142.125],[172.485,141.49],[173.313,140.467],[174.362,139.722],[175.398,138.96],[176.451,138.218],[177.285,137.202],[178.357,136.485]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.925,0.957,0.996]},"r":1,"o":{"a":0,"k":60}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"gr","hd":false,"nm":"Path 1 (3) Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1 (3)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[130.234,127.226],[129.582,126.085],[128.817,125.047],[128.26,123.88],[127.464,122.863],[126.908,121.695],[126.293,120.564],[125.482,119.554],[124.887,118.411],[124.343,117.233],[123.598,116.255],[122.588,116.951],[121.559,117.722],[120.691,118.697],[119.61,119.399],[118.705,120.327],[117.708,121.138],[116.662,121.888],[115.758,122.817],[114.751,123.619],[113.779,124.404],[114.499,125.427],[115.011,126.622],[115.686,127.717],[116.291,128.854],[116.922,129.974],[117.799,130.942],[118.281,132.157],[118.98,133.235],[119.768,134.26],[120.29,135.541],[121.388,134.734],[122.35,133.879],[123.331,133.049],[124.303,132.206],[125.163,131.223],[126.328,130.623],[127.3,129.78],[128.158,128.794],[129.281,128.142]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (3)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[146.49,130.014],[145.961,128.895],[145.165,127.878],[144.52,126.765],[143.924,125.623],[143.311,124.489],[142.708,123.351],[141.967,122.298],[141.348,121.17],[140.557,120.147],[140.016,118.847],[138.929,119.71],[138.068,120.693],[137.011,121.427],[135.976,122.191],[135.068,123.113],[134.123,123.991],[133.067,124.728],[132.07,125.539],[131.164,126.466],[130.238,127.226],[130.778,128.301],[131.481,129.376],[132.021,130.554],[132.857,131.547],[133.412,132.715],[133.995,133.868],[134.723,134.928],[135.331,136.063],[135.969,137.183],[136.672,138.343],[137.684,137.436],[138.723,136.68],[139.629,135.753],[140.646,134.967],[141.711,134.242],[142.586,133.277],[143.653,132.554],[144.548,131.615],[145.505,130.755]]}}},{"ty":"sh","hd":false,"nm":"Path 1 (3)","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[162.984,132.843],[162.344,131.704],[161.727,130.573],[161.016,129.503],[160.38,128.385],[159.616,127.347],[159.125,126.139],[158.422,125.061],[157.726,123.98],[157.032,122.897],[156.373,121.796],[155.459,122.708],[154.376,123.411],[153.401,124.25],[152.475,125.151],[151.356,125.805],[150.395,126.663],[149.526,127.635],[148.38,128.26],[147.427,129.129],[146.525,130.02],[147.033,131.189],[147.697,132.289],[148.452,133.333],[149.06,134.468],[149.833,135.5],[150.55,136.568],[151.02,137.791],[151.627,138.927],[152.436,139.937],[153.094,140.915],[153.965,140.12],[155.051,139.423],[155.984,138.531],[157.082,137.846],[158.051,137.002],[158.948,136.064],[159.908,135.205],[160.916,134.408],[161.929,133.614]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.925,0.957,0.996]},"r":1,"o":{"a":0,"k":60}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[144.8,189.52]},"o":{"a":0,"k":60},"p":{"a":0,"k":[144.8,189.52]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-4722-8N90-mask","layers":[{"ddd":0,"ind":189,"ty":4,"nm":"center","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[-100000,-100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":190,"ty":4,"nm":"Mask Group","parent":189,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Mask Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Mask","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[11.142,0],[372.142,0],[372.142,471],[11.142,471]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.85,0.85,0.85]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-4722-8N90-masked","layers":[{"ddd":0,"ind":193,"ty":4,"nm":"center","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[-100000,-100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":194,"ty":4,"nm":"Mask Group","parent":193,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Path 1 Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[165.623,262.632],[164.303,262.566],[162.979,262.537],[161.743,261.983],[160.456,261.736],[159.155,261.558],[157.83,261.528],[156.575,261.09],[155.255,261.029],[153.971,260.754],[152.656,260.66],[151.401,260.228],[150.104,260.029],[148.798,259.886],[147.488,259.766],[146.172,259.674],[144.884,259.428],[143.648,258.882],[142.322,258.854],[141.069,258.398],[139.747,258.35],[138.442,258.197],[137.191,257.734],[135.842,257.836],[134.546,257.636],[133.309,257.088],[131.999,256.968],[130.669,256.962],[129.437,256.388],[128.126,256.267],[126.786,256.323],[125.552,255.761],[124.21,255.827],[122.942,255.461],[121.654,255.206],[120.324,255.204],[119.068,254.766],[117.798,254.411],[116.5,254.223],[115.153,254.317],[113.858,254.107],[112.581,253.793],[111.308,253.459],[110.017,253.217],[108.73,252.961],[107.44,252.724],[106.139,252.552],[104.814,252.516],[103.544,252.157],[102.236,252.025],[100.99,251.522],[99.64,251.624],[98.404,251.07],[97.106,250.929],[95.988,250.263],[94.735,249.774],[94.061,248.607],[93.189,247.654],[92.711,246.458],[92.091,245.279],[92.087,243.96],[92.377,242.694],[92.657,241.429],[92.664,240.117],[92.838,238.835],[92.906,237.533],[93.502,236.323],[93.347,234.984],[93.893,233.765],[93.898,232.453],[94.004,231.158],[94.356,229.906],[94.658,228.645],[94.684,227.337],[95.274,226.125],[95.266,224.811],[95.337,223.51],[95.85,222.285],[95.788,220.962],[96.312,219.739],[96.372,218.437],[96.61,217.165],[96.788,215.883],[96.851,214.581],[97.361,213.355],[97.297,212.032],[97.841,210.812],[98.117,209.547],[98.205,208.249],[98.567,206.999],[98.533,205.679],[98.72,204.398],[99.181,203.165],[99.418,201.892],[99.484,200.591],[99.838,199.34],[99.787,198.019],[100.106,196.762],[100.3,195.482],[100.48,194.2],[100.762,192.936],[100.969,191.659],[101.286,190.4],[101.432,189.114],[101.437,187.802],[101.964,186.578],[101.867,185.25],[102.139,183.984],[102.364,182.71],[102.881,181.486],[102.951,180.185],[103.158,178.908],[103.255,177.612],[103.494,176.34],[103.626,175.05],[103.958,173.794],[104.458,172.567],[104.477,171.257],[104.551,169.957],[104.882,168.7],[105.009,167.409],[105.351,166.155],[105.44,164.858],[105.616,163.575],[105.886,162.309],[106.248,161.057],[106.435,159.776],[106.814,158.529],[106.833,157.219],[107.236,155.974],[107.171,154.651],[107.676,153.424],[107.938,152.156],[108.103,150.871],[108.223,149.579],[108.267,148.273],[108.581,147.014],[108.783,145.736],[109.136,144.482],[109.352,143.207],[109.417,141.905],[109.899,140.675],[109.886,139.36],[110.005,138.067],[110.414,136.824],[110.599,135.542],[110.818,134.267],[111.149,133.01],[111.467,131.751],[111.296,130.408],[111.496,129.129],[112.039,127.909],[112.047,126.598],[112.489,125.36],[112.502,124.048],[112.961,122.813],[112.953,121.497],[113.153,120.187],[113.785,119.019],[114.68,118.054],[115.684,117.25],[116.54,116.218],[117.789,115.771],[119.043,115.366],[120.362,115.334],[121.668,115.435],[122.973,115.585],[124.264,115.808],[125.57,115.955],[126.854,116.223],[128.144,116.461],[129.387,116.97],[130.743,116.825],[131.989,117.31],[133.317,117.33],[134.599,117.615],[135.905,117.759],[137.166,118.162],[138.437,118.508],[139.774,118.472],[141.065,118.706],[142.315,119.177],[143.629,119.267],[144.897,119.632],[146.234,119.595],[147.549,119.691],[148.811,120.092],[150.116,120.241],[151.388,120.585],[152.677,120.828],[153.986,120.956],[155.292,121.101],[156.599,121.239],[157.889,121.477],[159.171,121.761],[160.426,122.205],[161.709,122.484],[163.024,122.571],[164.315,122.805],[165.602,123.062],[166.902,123.24],[168.221,123.311],[169.474,123.766],[170.809,123.739],[172.085,124.057],[173.341,124.497],[174.652,124.608],[175.958,124.753],[177.257,124.95],[178.553,125.149],[179.804,125.617],[181.095,125.855],[182.401,125.997],[183.749,125.9],[185.04,126.137],[186.308,126.504],[187.597,126.757],[188.882,127.029],[190.124,127.399],[191.245,128.028],[192.467,128.54],[193.383,129.482],[194.095,130.576],[194.826,131.684],[195.12,132.974],[195.154,134.283],[194.884,135.55],[194.923,136.869],[194.391,138.09],[194.17,139.365],[194.338,140.707],[193.907,141.946],[193.626,143.21],[193.55,144.51],[193.288,145.778],[193.045,147.049],[192.925,148.341],[192.824,149.636],[192.553,150.903],[192.303,152.173],[192.053,153.443],[191.633,154.683],[191.455,155.965],[191.143,157.225],[191.154,158.539],[190.98,159.822],[190.637,161.076],[190.533,162.371],[190.168,163.621],[190.087,164.92],[189.708,166.168],[189.496,167.444],[189.442,168.748],[189.232,170.024],[189.085,171.312],[188.733,172.565],[188.66,173.866],[188.228,175.104],[188.188,176.41],[187.632,177.628],[187.44,178.906],[187.396,180.211],[187.222,181.494],[186.784,182.731],[186.817,184.049],[186.642,185.332],[186.391,186.602],[186.136,187.871],[186.031,189.164],[185.684,190.418],[185.543,191.707],[185.389,192.992],[184.924,194.225],[184.858,195.527],[184.462,196.772],[184.43,198.079],[184.215,199.355],[184.003,200.631],[183.834,201.915],[183.573,203.183],[183.313,204.451],[183.13,205.732],[182.836,206.996],[182.689,208.282],[182.438,209.553],[181.946,210.781],[181.905,212.087],[181.569,213.342],[181.449,214.634],[181.075,215.883],[181.082,217.198],[181.012,218.499],[180.439,219.713],[180.345,221.01],[179.95,222.256],[180.142,223.602],[179.643,224.829],[179.56,226.128],[179.355,227.406],[178.876,228.637],[178.986,229.969],[178.802,231.251],[178.545,232.52],[178.023,233.743],[177.831,235.023],[177.734,236.319],[177.347,237.566],[177.411,238.89],[176.963,240.127],[176.913,241.431],[176.544,242.681],[176.467,243.981],[176.098,245.231],[175.881,246.507],[175.848,247.815],[175.536,249.075],[175.355,250.357],[175.333,251.666],[175.019,252.926],[174.659,254.178],[174.424,255.452],[174.264,256.739],[173.977,258.01],[173.216,259.077],[172.627,260.228],[171.636,261.062],[170.639,261.887],[169.518,262.598],[168.234,262.961],[166.9,263.079]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[1,1,1]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-4717-8N90-mask","layers":[{"ddd":0,"ind":200,"ty":4,"nm":"center","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[-100000,-100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":201,"ty":4,"nm":"Mask Group","parent":200,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Mask Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Mask","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[11.142,0],[372.142,0],[372.142,471],[11.142,471]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.85,0.85,0.85]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]},{"id":"el-4717-8N90-masked","layers":[{"ddd":0,"ind":204,"ty":4,"nm":"center","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[-100000,-100000]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":205,"ty":4,"nm":"Mask Group","parent":204,"hd":false,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Path 1 Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[167.859,310.286],[165.721,309.657],[163.491,309.387],[161.408,308.546],[159.131,308.447],[157.014,307.736],[154.964,306.766],[152.648,306.833],[150.602,305.849],[148.31,305.804],[146.11,305.419],[144.101,304.291],[141.955,303.694],[139.687,303.571],[137.631,302.611],[135.331,302.614],[133.284,301.649],[131.171,300.923],[128.856,300.986],[126.763,300.182],[124.726,299.161],[122.423,299.178],[120.386,298.158],[118.282,297.381],[115.955,297.491],[113.932,296.417],[111.657,296.323],[109.512,295.723],[107.432,294.856],[105.156,294.765],[103.023,294.118],[100.867,293.546],[98.637,293.276],[96.632,292.134],[94.455,291.644],[92.351,290.882],[90.218,290.22],[87.836,290.545],[85.858,289.28],[83.658,288.895],[81.368,288.858],[79.418,287.482],[77.299,286.766],[75.106,286.352],[72.936,285.837],[70.756,285.377],[68.416,285.519],[66.499,284.001],[64.316,283.533],[62.001,283.597],[60.263,282.064],[58.329,281.101],[56.29,280.237],[54.256,279.237],[53.229,277.118],[51.851,275.444],[50.498,273.75],[48.977,272.101],[48.523,269.888],[47.403,267.97],[46.973,265.81],[46.303,263.683],[46.666,261.45],[46.656,259.274],[46.715,257.074],[46.921,254.858],[47.164,252.618],[47.776,250.472],[49.112,248.507],[49.202,246.227],[50.314,244.21],[50.453,241.946],[50.843,239.743],[51.977,237.731],[51.755,235.371],[52.256,233.197],[53.594,231.237],[53.906,229.014],[53.941,226.72],[55.345,224.773],[55.653,222.549],[55.542,220.217],[56.912,218.266],[56.78,215.929],[57.681,213.857],[58.315,211.717],[58.344,209.421],[59.417,207.393],[60.29,205.314],[60.763,203.129],[60.563,200.774],[61.089,198.606],[61.865,196.502],[62.688,194.41],[63.289,192.262],[63.927,190.118],[64.521,187.968],[64.954,185.776],[65.063,183.497],[65.95,181.421],[66.286,179.204],[67.513,177.216],[67.292,174.856],[68.644,172.9],[68.745,170.619],[69.636,168.545],[69.459,166.192],[70.868,164.251],[71.051,161.995],[71.805,159.885],[71.699,157.551],[72.533,155.462],[73.524,153.413],[74.223,151.286],[74.273,148.995],[74.727,146.809],[74.918,144.551],[76.374,142.622],[76.043,140.229],[77.373,138.268],[77.42,135.977],[77.707,133.744],[78.577,131.664],[79.329,129.55],[79.429,127.272],[80.865,125.334],[80.883,123.035],[81.084,120.78],[82.34,118.799],[82.226,116.467],[83.507,114.489],[83.687,112.232],[84.288,110.079],[84.747,107.894],[84.93,105.634],[86.175,103.65],[86.061,101.318],[87.388,99.356],[88.079,97.226],[88.323,94.986],[89.221,92.913],[89.171,90.597],[89.651,88.413],[90.781,86.4],[91.381,84.247],[91.574,81.993],[92.451,79.912],[92.366,77.587],[93.157,75.483],[93.651,73.306],[94.117,71.119],[94.823,68.993],[95.349,66.825],[96.14,64.722],[96.52,62.512],[96.572,60.218],[97.857,58.241],[97.664,55.889],[98.345,53.756],[98.914,51.599],[100.178,49.617],[100.379,47.362],[100.91,45.191],[101.172,42.951],[101.777,40.8],[102.129,38.583],[102.954,36.483],[104.178,34.491],[104.347,32.258],[104.86,30.068],[106.134,28.222],[107.153,26.23],[108.758,24.671],[110.058,22.843],[111.673,21.291],[113.554,20.08],[115.635,19.252],[117.589,18.235],[119.776,17.855],[121.766,16.816],[124.001,16.9],[126.146,16.047],[128.329,16.794],[130.467,17.14],[132.613,17.454],[134.83,17.774],[137.093,17.911],[139.192,18.691],[141.359,19.205],[143.438,20.079],[145.596,20.626],[147.847,20.813],[149.843,21.995],[152.142,21.996],[154.364,22.31],[156.405,23.316],[158.581,23.794],[160.739,24.344],[162.827,25.167],[164.926,25.958],[167.318,25.582],[169.487,26.089],[171.445,27.418],[173.731,27.468],[175.751,28.553],[178.035,28.615],[180.045,29.74],[182.344,29.756],[184.585,29.979],[186.692,30.73],[188.74,31.707],[190.811,32.595],[193.187,32.31],[195.267,33.161],[197.464,33.561],[199.569,34.315],[201.735,34.848],[203.936,35.229],[206.097,35.784],[208.299,36.162],[210.431,36.827],[212.578,37.417],[214.562,38.661],[216.945,38.335],[218.938,39.524],[221.224,39.59],[223.342,40.298],[225.55,40.666],[227.598,41.659],[229.679,42.507],[231.998,42.444],[234.155,43.028],[236.162,44.182],[238.335,44.506],[240.208,45.66],[242.441,46.1],[244.479,47.055],[245.947,48.779],[247.669,50.177],[248.892,52.04],[250.129,53.846],[251.445,55.623],[252.563,57.551],[253.521,59.583],[254.055,61.767],[254.267,63.998],[253.875,66.239],[253.688,68.41],[253.765,70.618],[253.29,72.765],[252.65,74.904],[252.199,77.091],[252.006,79.345],[250.892,81.365],[250.932,83.675],[250.149,85.777],[249.072,87.804],[248.782,90.033],[248.413,92.241],[247.912,94.416],[247.415,96.591],[246.27,98.601],[245.676,100.751],[245.31,102.964],[245.521,105.321],[244.931,107.473],[244.029,109.545],[243.399,111.686],[242.723,113.816],[242.065,115.95],[241.26,118.046],[241.345,120.372],[240.723,122.515],[240.035,124.645],[239.91,126.916],[239.205,129.038],[238.443,131.146],[237.502,133.207],[237.562,135.526],[236.26,137.495],[235.701,139.658],[236.066,142.055],[235.007,144.087],[234.304,146.213],[234.086,148.46],[233.428,150.594],[232.816,152.74],[232.497,154.962],[232.221,157.198],[231.541,159.326],[230.915,161.469],[230.284,163.614],[229.25,165.652],[228.792,167.837],[228.018,169.946],[228.008,172.246],[227.56,174.434],[226.708,176.522],[226.429,178.754],[225.523,180.825],[225.293,183.072],[224.36,185.136],[223.823,187.301],[223.657,189.566],[223.124,191.732],[222.737,193.939],[221.867,196.019],[221.66,198.269],[220.596,200.303],[220.466,202.577],[219.11,204.532],[218.617,206.708],[218.48,208.98],[218.029,211.167],[216.954,213.199],[216.997,215.513],[216.545,217.704],[215.916,219.845],[215.271,221.987],[214.988,224.217],[214.129,226.3],[213.757,228.508],[213.355,230.711],[212.218,232.722],[212.024,234.976],[211.048,237.028],[210.939,239.308],[210.388,241.469],[209.85,243.638],[209.414,245.829],[208.758,247.968],[208.107,250.104],[207.634,252.289],[206.904,254.405],[206.516,256.613],[205.886,258.758],[204.684,260.752],[204.547,263.024],[203.716,265.118],[203.393,267.342],[202.47,269.408],[202.451,271.71],[202.246,273.964],[200.854,275.91],[200.595,278.151],[199.621,280.208],[200.039,282.622],[198.818,284.615],[198.584,286.862],[198.061,289.035],[196.886,291.044],[197.113,293.409],[196.537,295.58],[195.606,297.617],[193.928,299.226],[192.806,301.084],[191.688,302.989],[189.887,304.287],[188.693,306.253],[186.587,307.09],[184.912,308.565],[182.774,309.171],[180.837,310.254],[178.626,310.46],[176.485,310.825],[174.334,311.439],[172.135,311.169],[169.942,311.008]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.176,0.227,0.29]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"gr","hd":false,"nm":"Path 1 Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[38.394,253.098],[42.186,240.196],[46.196,227.349],[49.616,214.351],[52.676,201.256],[55.8,188.182],[60.286,175.457],[62.794,162.225],[66.655,149.336],[69.507,136.192],[72.047,122.968],[75.31,109.929],[78.66,96.909],[82.818,84.096],[86.892,71.266],[89.183,57.974],[92.283,44.89],[96.29,32.038],[101.394,20.605],[110.52,11.818],[122.686,8.743],[134.793,9.926],[147.939,12.076],[160.611,16.078],[173.744,18.283],[186.398,22.353],[199.112,26.202],[212.349,28],[224.837,32.732],[237.831,35.494],[248.775,41.145],[257.983,49.668],[260.545,61.938],[260.486,74.184],[257.181,87.211],[253.546,100.155],[250.841,113.336],[247.867,126.453],[245.087,139.616],[240.64,152.35],[237.792,165.495],[234.117,178.432],[230.042,191.262],[227.624,204.518],[223.366,217.301],[219.748,230.252],[217.029,243.43],[214.009,256.535],[210.43,269.496],[206.98,282.491],[203.549,295.49],[198.486,306.777],[189.569,315.626],[177.489,318.768],[164.96,319.394],[152.532,314.444],[139.622,311.371],[126.644,308.564],[113.986,304.508],[100.663,303.028],[88.046,298.817],[75.19,295.518],[62.53,291.453],[51.617,286.204],[42.008,278.067],[37.934,265.927]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.651,0.769,0.91]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]}],"ddd":0,"fr":30,"h":180,"ip":0,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"ํ”๋“ค","hd":true,"sr":1,"ks":{"a":{"a":1,"k":[{"t":0,"s":[126.775,93.172],"i":{"x":0.88,"y":0.77},"o":{"x":0.5,"y":0}},{"t":21,"s":[126.765,93.172],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"o":{"a":0,"k":100},"p":{"a":1,"k":[{"t":0,"s":[159.63,88.876],"i":{"x":0.88,"y":0.77},"o":{"x":0.5,"y":0},"ti":[0,0],"to":[0,0]},{"t":12,"s":[151.916,97.447],"i":{"x":0.88,"y":0.77},"o":{"x":0.5,"y":0},"ti":[0,0],"to":[0,0]},{"t":21,"s":[155.63,86.876],"i":{"x":0.88,"y":0.77},"o":{"x":0.5,"y":0},"ti":[0,0],"to":[0,0]},{"t":33.3,"s":[149.63,94.876],"i":{"x":0.88,"y":0.77},"o":{"x":0.5,"y":0},"ti":[0,0],"to":[0,0]},{"t":48,"s":[148.63,94.876],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"r":{"a":1,"k":[{"t":0,"s":[-7],"i":{"x":0.88,"y":0.77},"o":{"x":0.5,"y":0}},{"t":12,"s":[-13.571],"i":{"x":0.88,"y":0.77},"o":{"x":0.5,"y":0}},{"t":21,"s":[-7],"i":{"x":0.88,"y":0.77},"o":{"x":0.5,"y":0}},{"t":33.3,"s":[-17],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"s":{"a":0,"k":[92.398,92.398]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"sh","hd":true,"nm":"ํ”๋“ค","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[189.598,25.821],[191.285,25.848],[192.797,26.559],[194.108,27.613],[195.842,27.924],[197.126,28.987],[198.435,30.006],[199.521,31.258],[200.742,32.387],[201.219,34.058],[202.38,35.246],[203.241,36.638],[203.773,38.19],[204.309,39.72],[205.164,41.155],[205.431,42.772],[205.834,44.354],[206.136,45.954]]}],"i":{"x":0.88,"y":0.77},"o":{"x":0.5,"y":0}},{"t":21,"s":[{"c":false,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[189.527,25.821],[191.214,25.848],[192.725,26.559],[194.036,27.613],[195.769,27.924],[197.053,28.987],[198.361,30.006],[199.447,31.258],[200.667,32.387],[201.144,34.058],[202.305,35.246],[203.165,36.638],[203.697,38.19],[204.233,39.72],[205.088,41.155],[205.354,42.772],[205.758,44.354],[206.059,45.954]]}],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]}},{"ty":"sh","hd":true,"nm":"ํ”๋“ค","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[201.102,18.631],[202.643,18.652],[204.023,19.211],[205.221,20.038],[206.803,20.283],[207.976,21.119],[209.171,21.919],[210.163,22.903],[211.278,23.791],[211.714,25.104],[212.773,26.035],[213.56,27.129],[214.046,28.348],[214.534,29.551],[215.316,30.679],[215.559,31.949],[215.927,33.192],[216.202,34.449]]}],"i":{"x":0.88,"y":0.77},"o":{"x":0.5,"y":0}},{"t":21,"s":[{"c":false,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[201.027,18.631],[202.567,18.652],[203.947,19.211],[205.144,20.038],[206.726,20.283],[207.899,21.119],[209.093,21.919],[210.085,22.903],[211.199,23.791],[211.635,25.104],[212.693,26.035],[213.48,27.129],[213.966,28.348],[214.455,29.551],[215.236,30.679],[215.478,31.949],[215.847,33.192],[216.122,34.449]]}],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]}},{"ty":"sh","hd":true,"nm":"ํ”๋“ค","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[63.951,160.524],[62.264,160.497],[60.753,159.786],[59.441,158.732],[57.707,158.421],[56.423,157.358],[55.115,156.339],[54.028,155.087],[52.808,153.958],[52.33,152.287],[51.169,151.1],[50.308,149.708],[49.775,148.155],[49.24,146.625],[48.385,145.19],[48.118,143.573],[47.715,141.992],[47.413,140.391]]}],"i":{"x":0.88,"y":0.77},"o":{"x":0.5,"y":0}},{"t":21,"s":[{"c":false,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[63.927,160.524],[62.241,160.497],[60.73,159.786],[59.419,158.732],[57.685,158.421],[56.402,157.358],[55.094,156.339],[54.008,155.087],[52.788,153.958],[52.311,152.287],[51.15,151.1],[50.289,149.708],[49.757,148.155],[49.222,146.625],[48.367,145.19],[48.1,143.573],[47.697,141.992],[47.396,140.391]]}],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]}},{"ty":"sh","hd":true,"nm":"ํ”๋“ค","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":false,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[52.447,167.714],[50.907,167.694],[49.526,167.135],[48.328,166.307],[46.745,166.063],[45.573,165.227],[44.379,164.427],[43.386,163.443],[42.272,162.555],[41.836,161.242],[40.777,160.31],[39.99,159.216],[39.504,157.997],[39.016,156.794],[38.233,155.666],[37.991,154.396],[37.622,153.153],[37.347,151.896]]}],"i":{"x":0.88,"y":0.77},"o":{"x":0.5,"y":0}},{"t":21,"s":[{"c":false,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[52.428,167.714],[50.888,167.694],[49.508,167.135],[48.31,166.307],[46.728,166.063],[45.556,165.227],[44.362,164.427],[43.37,163.443],[42.257,162.555],[41.82,161.242],[40.761,160.31],[39.975,159.216],[39.489,157.997],[39.001,156.794],[38.219,155.666],[37.977,154.396],[37.608,153.153],[37.333,151.896]]}],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]}},{"ty":"st","hd":false,"bm":0,"c":{"a":0,"k":[0.996,1,0.396]},"lc":2,"lj":2,"ml":4,"o":{"a":0,"k":60},"w":{"a":0,"k":5.42197}}]},{"ddd":0,"ind":4,"ty":0,"nm":"แ„‰แ…ฅแ†ผแ„€แ…ฉแ†ผ","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[21.5,16]},"o":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":0.88,"y":0.77},"o":{"x":0.5,"y":0}},{"t":48,"s":[0],"i":{"x":0.88,"y":0.77},"o":{"x":0.5,"y":0}},{"t":53.1,"s":[100],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"p":{"a":1,"k":[{"t":0,"s":[169.383,50.519],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":48,"s":[157.007,57.519],"i":{"x":0.88,"y":0.77},"o":{"x":0.5,"y":0},"ti":[0,0],"to":[0,0]},{"t":53.1,"s":[156.383,57.519],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":60.6,"s":[156.383,57.519],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":65.1,"s":[156.383,57.519],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"r":{"a":1,"k":[{"t":0,"s":[-1],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":48,"s":[-9.642],"i":{"x":0.88,"y":0.77},"o":{"x":0.5,"y":0}},{"t":53.1,"s":[-11],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"s":{"a":1,"k":[{"t":0,"s":[80,80],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":29.1,"s":[60,60],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":48,"s":[70,70],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":53.1,"s":[70,70],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":65.1,"s":[60,60],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":32,"refId":"el-5276-8N90","w":43},{"ddd":0,"ind":7,"ty":0,"nm":"10","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[12.5,12]},"o":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":0.88,"y":0.77},"o":{"x":0.5,"y":0}},{"t":29.1,"s":[0],"i":{"x":0.88,"y":0.77},"o":{"x":0.5,"y":0}},{"t":33.3,"s":[100],"i":{"x":0.88,"y":0.77},"o":{"x":0.5,"y":0}},{"t":48,"s":[0],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"p":{"a":1,"k":[{"t":0,"s":[170.383,49.519],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":29.1,"s":[165.085,53.519],"i":{"x":0.88,"y":0.77},"o":{"x":0.5,"y":0},"ti":[0,0],"to":[0,0]},{"t":33.3,"s":[156.383,56.519],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":48,"s":[156.383,56.212],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":60.6,"s":[156.383,57.438],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":65.1,"s":[156.383,56.519],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"r":{"a":1,"k":[{"t":0,"s":[-7],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":29.1,"s":[3],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":33.3,"s":[-9],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"s":{"a":1,"k":[{"t":0,"s":[80,80],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":29.1,"s":[60,60],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":33.3,"s":[60,60],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":48,"s":[60,60],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":60.6,"s":[60,60],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":65.1,"s":[60,60],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":24,"refId":"el-5253-8N90","w":25},{"ddd":0,"ind":10,"ty":0,"nm":"4","hd":true,"sr":1,"ks":{"a":{"a":0,"k":[10,11]},"o":{"a":0,"k":100},"p":{"a":0,"k":[185.5,155]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":22,"refId":"el-5242-8N90","w":20},{"ddd":0,"ind":13,"ty":0,"nm":"3","hd":true,"sr":1,"ks":{"a":{"a":0,"k":[8.5,10.5]},"o":{"a":0,"k":100},"p":{"a":0,"k":[214.5,154]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":21,"refId":"el-5200-8N90","w":17},{"ddd":0,"ind":209,"ty":0,"nm":"แ„Œแ…ฅแ†ซแ„Žแ…ฆ","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[210.07,235.5]},"o":{"a":0,"k":100},"p":{"a":0,"k":[189.906,122.019]},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":0.88,"y":0.77},"o":{"x":0.5,"y":0}},{"t":12,"s":[-12],"i":{"x":0.88,"y":0.77},"o":{"x":0.5,"y":0}},{"t":21,"s":[0],"i":{"x":0.88,"y":0.77},"o":{"x":0.5,"y":0}},{"t":33.3,"s":[-12],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"s":{"a":0,"k":[47.567,47.567]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"h":471,"refId":"el-4715-8N90","w":420.1394958496094},{"ddd":0,"ind":210,"ty":4,"nm":"Screen","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[163.5,90]},"o":{"a":0,"k":100},"p":{"a":0,"k":[163.5,177]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":91,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Screen Group","bm":0,"it":[{"ty":"rc","hd":false,"nm":"Screen","d":1,"p":{"a":0,"k":[163.5,90]},"r":{"a":0,"k":0},"s":{"a":0,"k":[327,180]}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[1,1,1]},"r":1,"o":{"a":0,"k":0}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}],"meta":{"g":"@phase-software/lottie-exporter 0.7.0"},"nm":"","op":90,"v":"5.6.0","w":327} \ No newline at end of file diff --git a/core/designsystem/src/main/res/raw/mission_tap.json b/core/designsystem/src/main/res/raw/mission_tap.json new file mode 100644 index 00000000..f8572947 --- /dev/null +++ b/core/designsystem/src/main/res/raw/mission_tap.json @@ -0,0 +1 @@ +{"assets":[{"id":"el-159-_-Uo","layers":[{"ddd":0,"ind":19,"ty":4,"nm":"Layer 1","hd":true,"sr":1,"ks":{"a":{"a":0,"k":[38.633,28.203]},"o":{"a":0,"k":100},"p":{"a":0,"k":[38.633,28.203]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":37,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":true,"nm":"Path 1 (4) Group","bm":0,"it":[{"ty":"sh","hd":true,"nm":"Path 1 (4)","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[-0.485,-0.112],[-0.137,-0.272],[0.121,-0.528],[0.287,-0.128],[0.097,-0.085],[0.364,0.016],[0.151,0.08],[-0.015,0.688],[-0.561,0.304]],"o":[[0.318,-0.192],[0.5,0.096],[0.181,0.384],[-0.107,0.512],[-0.115,0.059],[-0.06,0.064],[-0.364,-0.016],[-0.546,-0.256],[0.015,-0.688],[0,0]],"v":[[50.177,32.693],[51.382,32.573],[52.337,33.125],[52.427,34.493],[51.837,35.453],[51.518,35.669],[50.882,35.741],[50.109,35.597],[49.313,34.181],[50.177,32.693]]}}},{"ty":"sh","hd":true,"nm":"Path 1 (4)","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[-0.121,0.08],[-0.333,0],[-0.288,-0.08],[-0.257,0],[-0.273,-0.112],[-0.09,-0.128],[-0.303,-0.352],[-0.197,-0.144],[-0.046,-0.384],[-0.075,-0.176],[0.122,-0.23],[0.008,-0.143],[0.091,-0.208],[0.03,-0.352],[0.137,-0.16],[0.455,-0.336],[0.47,-0.096],[0.682,0.176],[0.292,0.02],[0.212,0.16],[0.182,0.091],[0.122,0.12],[0,0.048],[0.257,0.288],[0.11,0.161],[0.091,0.416],[-0.137,0.544],[0,0.208],[-0.045,0.176],[-0.061,0.192],[-0.293,0.117],[-0.091,0.114],[-0.132,0.039],[-0.319,0.304]],"o":[[0.304,-0.288],[0.121,-0.08],[0.333,0],[0.273,0.08],[0.227,0],[0.273,0.112],[0.425,0.544],[0.061,0.096],[0.379,0.272],[0,0.16],[0.093,0.243],[-0.067,0.126],[0,0.112],[-0.243,0.512],[-0.03,0.192],[-0.136,0.144],[-0.606,0.432],[-0.455,0.08],[-0.283,-0.076],[-0.318,-0.016],[-0.166,-0.117],[-0.136,-0.104],[-0.167,-0.176],[0,-0.032],[-0.132,-0.143],[-0.045,-0.112],[-0.091,-0.416],[0.075,-0.336],[0.015,-0.208],[0.06,-0.16],[0.079,-0.305],[0.133,-0.059],[0.091,-0.103],[0.106,-0.016],[0,0]],"v":[[41.76,27.461],[42.397,26.909],[43.079,26.789],[44.011,26.909],[44.807,27.029],[45.557,27.197],[46.102,27.557],[47.194,28.901],[47.58,29.261],[48.217,30.245],[48.33,30.749],[48.285,31.493],[48.171,31.901],[48.035,32.381],[47.625,33.677],[47.375,34.205],[46.489,34.925],[44.875,35.717],[43.17,35.573],[42.306,35.429],[41.511,35.165],[40.988,34.853],[40.601,34.517],[40.351,34.181],[39.965,33.701],[39.601,33.245],[39.396,32.453],[39.465,31.013],[39.578,30.197],[39.669,29.621],[39.851,29.093],[40.442,28.421],[40.783,28.157],[41.124,27.941],[41.761,27.461]]}}},{"ty":"sh","hd":true,"nm":"Path 1 (4)","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0.454,-0.112],[0.288,-0.224],[0.052,-0.086],[0.016,-0.129],[0.077,-0.144],[0.031,-0.096],[0,-0.272],[-0.485,-0.496],[-0.485,0.112],[-0.167,0],[-0.091,0.048],[-0.227,0],[-0.106,0.336],[-0.091,0.128],[-0.03,0.416],[0.849,0.272],[0.154,0.104],[0.137,0.075]],"o":[[-0.228,-0.112],[-0.455,0.096],[-0.083,0.056],[-0.029,0.127],[-0.03,0.16],[-0.136,0.208],[-0.03,0.096],[0,0.64],[0.485,0.496],[0.273,-0.064],[0.151,-0.016],[0.091,-0.064],[0.425,0],[0.045,-0.16],[0.107,-0.128],[0.091,-1.248],[-0.178,-0.055],[-0.12,-0.1],[0,0]],"v":[[44.217,28.877],[43.194,28.877],[42.08,29.357],[41.875,29.573],[41.807,29.957],[41.647,30.413],[41.397,30.869],[41.352,31.421],[42.08,33.125],[43.535,33.701],[44.194,33.605],[44.558,33.509],[45.035,33.413],[45.831,32.909],[46.035,32.477],[46.24,31.661],[45.103,29.381],[44.603,29.141],[44.217,28.877]]}}},{"ty":"sh","hd":true,"nm":"Path 1 (4)","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[-0.348,-0.08],[-0.288,0.048],[-0.227,-0.064],[-0.712,0],[-0.454,0.048],[-0.182,-0.096],[-0.212,0.032],[-0.227,-0.08],[-0.151,0.064],[-0.379,-0.112],[-0.106,-0.224],[0.303,-0.288],[0.652,0.144],[0.194,-0.049],[0.772,0.032],[0.152,-0.112],[0.061,-0.24],[-0.03,-0.192],[0.058,-0.153],[0,-0.224],[0.031,-0.352],[-0.061,-0.768],[0.045,-0.224],[-0.045,-0.286],[0.106,-1.216],[-0.06,-0.48],[0.075,-0.224],[0.287,-0.064],[0.121,0.416],[0.079,0.163],[-0.046,0.16],[0,1.072],[0.076,0.144],[0.005,0.102],[-0.045,0.288],[-0.016,0.432],[-0.015,0.128],[-0.015,0.544],[-0.061,0.56],[0.03,0.208],[0.175,0.102],[0.53,0.032],[0.288,-0.048],[0.197,0.064],[0.545,-0.112],[0.273,0.32],[0.015,0.304],[-0.045,0.144],[-0.289,0.112],[-0.181,-0.08],[-0.318,-0.016],[-0.197,0.048],[-0.182,0.016]],"o":[[0.455,-0.064],[0.288,0.064],[0.288,-0.048],[0.182,0.048],[0.728,0],[0.243,-0.016],[0.197,0.08],[0.212,-0.032],[0.227,0.064],[0.258,-0.112],[0.379,0.112],[0.242,0.512],[-0.303,0.272],[-0.194,-0.049],[-0.197,0.064],[-1.319,-0.08],[-0.09,0.08],[-0.06,0.224],[0.013,0.163],[-0.06,0.192],[0,0.176],[-0.045,0.304],[0.06,0.784],[-0.045,0.286],[0.03,0.384],[-0.06,0.688],[0.06,0.496],[-0.076,0.224],[-0.607,0.176],[-0.056,-0.172],[-0.107,-0.176],[0.06,-0.24],[0.016,-1.088],[-0.054,-0.087],[0,-0.08],[0.06,-0.416],[0.03,-0.624],[0.015,-0.192],[0,-0.4],[0.061,-0.624],[-0.025,-0.201],[-0.107,-0.08],[-0.531,-0.048],[-0.288,0.048],[-0.258,-0.096],[-0.424,0.096],[-0.045,-0.048],[-0.015,-0.32],[0.06,-0.176],[0.288,-0.112],[0.137,0.064],[0.333,0],[0.243,-0.064],[0,0]],"v":[[28.159,20.693],[29.363,20.717],[30.227,20.741],[31,20.765],[32.341,20.837],[34.114,20.765],[34.751,20.885],[35.365,20.957],[36.024,21.029],[36.592,21.029],[37.547,21.029],[38.275,21.533],[38.184,22.733],[36.752,22.925],[36.16,22.925],[34.706,22.973],[32.5,23.021],[32.273,23.501],[32.228,24.125],[32.159,24.605],[32.069,25.229],[32.023,26.021],[32.046,27.629],[32.069,29.141],[32.069,30.005],[31.955,32.405],[31.955,34.157],[31.932,35.237],[31.387,35.669],[30.295,35.309],[30.091,34.805],[30,34.301],[30.09,32.333],[30,30.485],[29.909,30.197],[29.977,29.645],[30.091,28.373],[30.159,27.245],[30.204,26.141],[30.295,24.701],[30.341,23.453],[30.023,22.973],[29.068,22.805],[27.84,22.805],[27.113,22.781],[25.908,22.805],[24.862,22.469],[24.772,21.941],[24.817,21.245],[25.34,20.813],[26.044,20.765],[26.726,20.885],[27.522,20.813],[28.159,20.693]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.239,0.259,0.294]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"gr","hd":true,"nm":"Path 1 Group","bm":0,"it":[{"ty":"sh","hd":true,"nm":"Path 1","d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[-4.091,0.264],[-0.792,1.308],[-0.424,1.179],[-0.952,3.38],[-1.926,-0.633],[-5.175,-3.921],[-1.642,0.295],[-3.385,0.078],[-3.023,-0.025],[0,-2.811],[0.803,-3.235],[1.714,-2.38],[-0.125,-2.702],[-0.096,-1.036],[1.571,-2.667],[2.879,0.554],[5.408,2.839],[1.645,-1.726],[3.323,-2.424],[5.002,0.47],[0,2.701],[-0.282,1.373],[1.978,1.221],[3.056,2.308],[0,1.972],[-1.682,1.258]],"o":[[3.35,-2.506],[1.108,-1.057],[0.638,-1.053],[1.182,-3.281],[0.575,-2.044],[6.016,1.978],[1.291,0.977],[3.325,-0.6],[3.023,-0.07],[2.153,0.018],[0,3.331],[-0.747,3.005],[-1.486,2.063],[0.048,1.03],[0.285,3.075],[-1.763,2.993],[-5.936,-1.143],[-1.794,-0.942],[-2.877,3.022],[-4.26,3.106],[-1.904,-0.18],[0,-1.396],[0.416,-2.02],[-3.242,-2.001],[-1.583,-1.195],[0,-2.147],[0,0]],"v":[[3.407,23.315],[14.353,18.27],[17.51,15.165],[18.782,11.232],[22.424,1.447],[31.175,0.57],[48.305,8.768],[53,12.18],[63.045,10.568],[72.126,10.568],[76.732,12.961],[75.724,22.676],[70.898,30.281],[68.046,37.01],[68.814,40.019],[67.871,50.847],[59.054,53.027],[41.111,47.008],[36.286,45.539],[26.898,53.548],[14.22,56.178],[8.43,53.618],[8.451,49.165],[12.378,37.342],[2.99,30.708],[0.533,27.178],[3.407,23.315]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[1,0.949,0.49]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}]}],"ddd":0,"fr":30,"h":180,"ip":0,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"ํŽธ์ง€","hd":false,"sr":1,"ks":{"a":{"a":1,"k":[{"t":0,"s":[0,-21.883],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[0,-21.883],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":12.6,"s":[0,-21.883],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":16.2,"s":[0,-21.883],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":22.8,"s":[0,-21.883],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"o":{"a":0,"k":100},"p":{"a":1,"k":[{"t":0,"s":[163.518,85.533],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":4.2,"s":[163.518,75.533],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":8.4,"s":[163.585,61.68],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":12.6,"s":[164.317,82.27],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":16.2,"s":[163.849,82.27],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":22.8,"s":[162.71,82.701],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":4.2,"s":[1],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":12.6,"s":[-3],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":16.2,"s":[0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":19.2,"s":[-3],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":22.8,"s":[-1],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":24.6,"s":[0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":27,"s":[2],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":30,"s":[1],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":30.6,"s":[0],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"s":{"a":1,"k":[{"t":0,"s":[58.252,58.252],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":16.2,"s":[64.707,64.707],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":19.2,"s":[70.488,70.488],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":30.6,"s":[84.855,84.855],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":37,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"์—ด๋ฆฌ๋Š” ๋ถ€๋ถ„ Group","bm":0,"it":[{"ty":"gr","hd":false,"nm":"์—ด๋ฆฌ๋Š”๋ถ€๋ถ„_์„  Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"์—ด๋ฆฌ๋Š”๋ถ€๋ถ„_์„ ","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[152.96,607.943],[149.567,611.868],[173.751,622.035],[194.591,636.937],[215.222,652.147],[233.018,671.363],[257.422,681.221],[275.009,700.736],[298.095,712.471],[320.153,725.655],[340.305,741.535],[360.177,757.794],[379.95,774.223],[403.156,785.779],[423.208,801.799],[441.693,820.046],[464.21,832.6],[484.522,848.26],[506.68,861.314],[526.203,878.092],[549.868,889.009],[569.521,905.618],[594.334,918.541],[622.44,917.882],[649.499,908.634],[669.561,893.753],[690.352,879.89],[710.963,865.788],[729.797,849.139],[748.921,832.919],[772.057,822.393],[792.358,807.831],[808.977,788.046],[832.562,778.159],[849.989,759.522],[872.856,748.616],[893.457,734.484],[912.052,717.515],[935.916,708.007],[951.966,687.413],[976.33,678.624],[996.022,663.194],[1014.418,645.925],[1035.308,632.193],[1058.155,621.246],[1050.948,605.966],[1026.006,602.2],[1001.063,604.198],[976.12,607.574],[951.187,605.127],[926.245,603.858],[901.302,606.805],[876.359,601.881],[851.427,601.681],[826.474,604.837],[801.541,607.014],[776.598,601.971],[751.656,602.68],[726.713,603.069],[701.77,607.024],[676.827,603.888],[651.885,604.987],[626.942,603.239],[601.999,605.516],[577.056,605.297],[552.114,604.957],[527.161,600.932],[502.218,601.122],[477.276,608.013],[452.333,606.006],[427.38,602.79],[402.437,601.132],[377.495,600.423],[352.552,600.513],[327.609,606.964],[302.656,606.285],[277.714,603.689],[252.761,601.292],[227.818,606.115],[202.866,601.232],[177.743,601.911]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[152.96,406.676],[149.567,409.302],[173.751,416.103],[194.591,426.071],[215.222,436.246],[233.018,449.1],[257.422,455.694],[275.009,468.749],[298.095,476.599],[320.153,485.418],[340.305,496.041],[360.177,506.917],[379.95,517.908],[403.156,525.638],[423.208,536.354],[441.693,548.56],[464.21,556.958],[484.522,567.433],[506.68,576.165],[526.203,587.389],[549.868,594.692],[569.521,605.802],[594.334,614.447],[622.44,614.006],[649.499,607.82],[669.561,597.865],[690.352,588.592],[710.963,579.159],[729.797,568.021],[748.921,557.171],[772.057,550.13],[792.358,540.389],[808.977,527.154],[832.562,520.54],[849.989,508.073],[872.856,500.778],[893.457,491.324],[912.052,479.973],[935.916,473.613],[951.966,459.837],[976.33,453.958],[996.022,443.635],[1014.418,432.084],[1035.308,422.897],[1058.155,415.575],[1050.948,405.353],[1026.006,402.835],[1001.063,404.171],[976.12,406.429],[951.187,404.792],[926.245,403.944],[901.302,405.915],[876.359,402.621],[851.427,402.487],[826.474,404.598],[801.541,406.055],[776.598,402.681],[751.656,403.155],[726.713,403.416],[701.77,406.062],[676.827,403.964],[651.885,404.699],[626.942,403.53],[601.999,405.053],[577.056,404.906],[552.114,404.679],[527.161,401.986],[502.218,402.113],[477.276,406.723],[452.333,405.38],[427.38,403.229],[402.437,402.12],[377.495,401.645],[352.552,401.706],[327.609,406.021],[302.656,405.567],[277.714,403.83],[252.761,402.227],[227.818,405.454],[202.866,402.187],[177.743,402.641]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":12.6,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[134.343,406.676],[131.362,409.302],[152.603,416.103],[170.907,426.071],[189.027,436.246],[204.657,449.1],[226.091,455.694],[241.537,468.749],[261.813,476.599],[281.187,485.418],[298.886,496.041],[316.339,506.917],[333.705,517.908],[354.087,525.638],[371.698,536.354],[387.934,548.56],[407.71,556.958],[425.55,567.433],[445.011,576.165],[462.158,587.389],[482.942,594.692],[500.203,605.802],[521.996,614.447],[546.682,614.006],[570.447,607.82],[588.067,597.865],[606.328,588.592],[624.43,579.159],[640.972,568.021],[657.768,557.171],[678.088,550.13],[695.919,540.389],[710.515,527.154],[731.229,520.54],[746.535,508.073],[766.619,500.778],[784.712,491.324],[801.044,479.973],[822.004,473.613],[836.1,459.837],[857.499,453.958],[874.794,443.635],[890.951,432.084],[909.298,422.897],[929.364,415.575],[923.035,405.353],[901.128,402.835],[879.221,404.171],[857.314,406.429],[835.416,404.792],[813.509,403.944],[791.603,405.915],[769.696,402.621],[747.797,402.487],[725.882,404.598],[703.984,406.055],[682.077,402.681],[660.17,403.155],[638.263,403.416],[616.356,406.062],[594.449,403.964],[572.542,404.699],[550.635,403.53],[528.729,405.053],[506.822,404.906],[484.915,404.679],[462.999,401.986],[441.092,402.113],[419.185,406.723],[397.278,405.38],[375.363,403.229],[353.456,402.12],[331.549,401.645],[309.642,401.706],[287.735,406.021],[265.819,405.567],[243.913,403.83],[221.997,402.227],[200.09,405.454],[178.174,402.187],[156.11,402.641]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":16.2,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[146.512,505.491],[143.261,508.755],[166.426,517.209],[186.388,529.599],[206.149,542.246],[223.195,558.224],[246.57,566.42],[263.415,582.646],[285.528,592.404],[306.657,603.366],[325.959,616.569],[344.994,630.089],[363.933,643.75],[386.16,653.358],[405.367,666.678],[423.073,681.85],[444.641,692.289],[464.096,705.309],[485.32,716.163],[504.02,730.114],[526.688,739.191],[545.512,753.001],[569.279,763.747],[596.201,763.199],[622.119,755.509],[641.335,743.135],[661.249,731.609],[680.991,719.884],[699.031,706.04],[717.349,692.554],[739.51,683.801],[758.956,671.694],[774.873,655.243],[797.464,647.021],[814.157,631.526],[836.059,622.458],[855.792,610.707],[873.603,596.598],[896.462,588.692],[911.835,571.569],[935.171,564.261],[954.034,551.431],[971.654,537.073],[991.663,525.654],[1013.547,516.552],[1006.644,503.847],[982.753,500.716],[958.862,502.377],[934.97,505.184],[911.089,503.149],[887.198,502.095],[863.306,504.544],[839.415,500.451],[815.533,500.285],[791.633,502.908],[767.751,504.719],[743.86,500.525],[719.969,501.115],[696.077,501.439],[672.186,504.727],[648.295,502.12],[624.404,503.033],[600.512,501.58],[576.621,503.473],[552.73,503.291],[528.839,503.008],[504.938,499.662],[481.047,499.82],[457.155,505.549],[433.264,503.88],[409.363,501.206],[385.472,499.827],[361.581,499.238],[337.69,499.313],[313.798,504.677],[289.898,504.113],[266.006,501.954],[242.106,499.961],[218.214,503.972],[194.313,499.911],[170.25,500.475]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":19.2,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[141.098,430.387],[137.967,433.165],[160.276,440.363],[179.5,450.912],[198.531,461.681],[214.947,475.284],[237.458,482.263],[253.681,496.078],[274.977,504.386],[295.325,513.719],[313.913,524.961],[332.245,536.472],[350.484,548.103],[371.89,556.284],[390.387,567.625],[407.438,580.542],[428.209,589.43],[446.946,600.516],[467.385,609.758],[485.394,621.636],[507.224,629.364],[525.353,641.122],[548.241,650.271],[574.168,649.805],[599.129,643.257],[617.635,632.722],[636.813,622.909],[655.825,612.925],[673.199,601.139],[690.84,589.656],[712.182,582.204],[730.909,571.895],[746.239,557.889],[767.995,550.889],[784.07,537.695],[805.163,529.974],[824.167,519.97],[841.319,507.957],[863.333,501.226],[878.138,486.647],[900.613,480.425],[918.778,469.501],[935.747,457.276],[955.017,447.554],[976.092,439.804],[969.444,428.987],[946.436,426.321],[923.428,427.735],[900.419,430.125],[877.42,428.393],[854.412,427.495],[831.403,429.581],[808.395,426.095],[785.396,425.954],[762.378,428.188],[739.379,429.729],[716.371,426.159],[693.362,426.66],[670.354,426.936],[647.346,429.736],[624.337,427.516],[601.329,428.294],[578.321,427.057],[555.312,428.669],[532.304,428.513],[509.296,428.273],[486.278,425.423],[463.27,425.558],[440.261,430.436],[417.253,429.015],[394.236,426.738],[371.227,425.564],[348.219,425.063],[325.211,425.126],[302.202,429.694],[279.185,429.213],[256.176,427.375],[233.159,425.678],[210.15,429.093],[187.133,425.635],[163.959,426.116]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":22.8,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[135.509,390.319],[132.503,392.839],[153.928,399.367],[172.391,408.934],[190.668,418.7],[206.434,431.037],[228.054,437.366],[243.634,449.895],[264.086,457.43],[283.628,465.894],[301.48,476.089],[319.086,486.529],[336.602,497.077],[357.161,504.496],[374.925,514.781],[391.301,526.496],[411.25,534.557],[429.244,544.611],[448.874,552.992],[466.17,563.764],[487.135,570.773],[504.545,581.436],[526.528,589.734],[551.428,589.311],[575.399,583.373],[593.173,573.819],[611.591,564.918],[629.851,555.865],[646.536,545.175],[663.478,534.761],[683.975,528.003],[701.96,518.654],[716.683,505.951],[737.577,499.603],[753.016,487.638],[773.274,480.636],[791.525,471.563],[807.998,460.668],[829.14,454.564],[843.359,441.342],[864.943,435.699],[882.389,425.792],[898.685,414.705],[917.192,405.888],[937.432,398.86],[931.048,389.05],[908.951,386.632],[886.854,387.915],[864.757,390.082],[842.669,388.511],[820.571,387.697],[798.475,389.588],[776.378,386.427],[754.289,386.299],[732.183,388.325],[710.095,389.723],[687.998,386.485],[665.901,386.94],[643.804,387.19],[621.707,389.73],[599.61,387.716],[577.513,388.421],[555.416,387.299],[533.318,388.761],[511.221,388.62],[489.124,388.402],[467.018,385.818],[444.921,385.94],[422.824,390.364],[400.727,389.076],[378.621,387.011],[356.524,385.946],[334.427,385.491],[312.33,385.549],[290.233,389.691],[268.127,389.255],[246.03,387.588],[223.924,386.049],[201.827,389.146],[179.721,386.011],[157.465,386.446]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":24.6,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[135.509,348.79],[132.503,351.042],[153.928,356.876],[172.391,365.425],[190.668,374.151],[206.434,385.176],[228.054,390.831],[243.634,402.028],[264.086,408.761],[283.628,416.324],[301.48,425.435],[319.086,434.763],[336.602,444.189],[357.161,450.819],[374.925,460.01],[391.301,470.478],[411.25,477.681],[429.244,486.665],[448.874,494.154],[466.17,503.781],[487.135,510.044],[504.545,519.573],[526.528,526.987],[551.428,526.609],[575.399,521.303],[593.173,512.766],[611.591,504.812],[629.851,496.722],[646.536,487.17],[663.478,477.864],[683.975,471.825],[701.96,463.47],[716.683,452.119],[737.577,446.447],[753.016,435.755],[773.274,429.497],[791.525,421.389],[807.998,411.654],[829.14,406.199],[843.359,394.384],[864.943,389.342],[882.389,380.489],[898.685,370.582],[917.192,362.703],[937.432,356.423],[931.048,347.656],[908.951,345.496],[886.854,346.642],[864.757,348.578],[842.669,347.174],[820.571,346.447],[798.475,348.137],[776.378,345.312],[754.289,345.198],[732.183,347.008],[710.095,348.258],[687.998,345.364],[665.901,345.771],[643.804,345.994],[621.707,348.263],[599.61,346.464],[577.513,347.094],[555.416,346.092],[533.318,347.398],[511.221,347.272],[489.124,347.077],[467.018,344.768],[444.921,344.877],[422.824,348.831],[400.727,347.679],[378.621,345.834],[356.524,344.882],[334.427,344.476],[312.33,344.527],[290.233,348.229],[268.127,347.839],[246.03,346.349],[223.924,344.974],[201.827,347.742],[179.721,344.94],[157.465,345.329]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":27,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[135.509,403.714],[132.503,406.32],[153.928,413.072],[172.391,422.967],[190.668,433.068],[206.434,445.829],[228.054,452.375],[243.634,465.334],[264.086,473.127],[283.628,481.882],[301.48,492.427],[319.086,503.225],[336.602,514.135],[357.161,521.809],[374.925,532.447],[391.301,544.564],[411.25,552.901],[429.244,563.3],[448.874,571.968],[466.17,583.111],[487.135,590.36],[504.545,601.389],[526.528,609.971],[551.428,609.533],[575.399,603.392],[593.173,593.51],[611.591,584.304],[629.851,574.94],[646.536,563.883],[663.478,553.112],[683.975,546.122],[701.96,536.452],[716.683,523.314],[737.577,516.748],[753.016,504.372],[773.274,497.13],[791.525,487.745],[807.998,476.477],[829.14,470.163],[843.359,456.487],[864.943,450.651],[882.389,440.404],[898.685,428.937],[917.192,419.817],[937.432,412.548],[931.048,402.401],[908.951,399.9],[886.854,401.227],[864.757,403.468],[842.669,401.843],[820.571,401.001],[798.475,402.957],[776.378,399.688],[754.289,399.555],[732.183,401.651],[710.095,403.097],[687.998,399.748],[665.901,400.218],[643.804,400.477],[621.707,403.104],[599.61,401.021],[577.513,401.751],[555.416,400.59],[533.318,402.102],[511.221,401.956],[489.124,401.73],[467.018,399.058],[444.921,399.184],[422.824,403.76],[400.727,402.427],[378.621,400.292],[356.524,399.19],[334.427,398.72],[312.33,398.779],[290.233,403.064],[268.127,402.613],[246.03,400.888],[223.924,399.297],[201.827,402.5],[179.721,399.257],[157.465,399.708]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":28.2,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[135.509,417.452],[132.503,420.147],[153.928,427.129],[172.391,437.361],[190.668,447.805],[206.434,461],[228.054,467.769],[243.634,481.169],[264.086,489.228],[283.628,498.28],[301.48,509.184],[319.086,520.35],[336.602,531.631],[357.161,539.566],[374.925,550.566],[391.301,563.095],[411.25,571.716],[429.244,582.469],[448.874,591.432],[466.17,602.954],[487.135,610.45],[504.545,621.854],[526.528,630.729],[551.428,630.276],[575.399,623.925],[593.173,613.707],[611.591,604.188],[629.851,594.505],[646.536,583.072],[663.478,571.935],[683.975,564.707],[701.96,554.708],[716.683,541.122],[737.577,534.333],[753.016,521.536],[773.274,514.047],[791.525,504.343],[807.998,492.691],[829.14,486.163],[843.359,472.021],[864.943,465.986],[882.389,455.391],[898.685,443.533],[917.192,434.103],[937.432,426.587],[931.048,416.094],[908.951,413.509],[886.854,414.88],[864.757,417.198],[842.669,415.518],[820.571,414.647],[798.475,416.67],[776.378,413.289],[754.289,413.152],[732.183,415.319],[710.095,416.814],[687.998,413.351],[665.901,413.838],[643.804,414.105],[621.707,416.821],[599.61,414.668],[577.513,415.422],[555.416,414.222],[533.318,415.786],[511.221,415.635],[489.124,415.401],[467.018,412.638],[444.921,412.768],[422.824,417.5],[400.727,416.122],[378.621,413.913],[356.524,412.775],[334.427,412.288],[312.33,412.35],[290.233,416.78],[268.127,416.314],[246.03,414.531],[223.924,412.885],[201.827,416.197],[179.721,412.844],[157.465,413.31]]}],"i":{"x":0.88,"y":0.17},"o":{"x":0.69,"y":0.25}},{"t":30,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[135.509,449.76],[132.503,452.663],[153.928,460.185],[172.391,471.209],[190.668,482.462],[206.434,496.678],[228.054,503.97],[243.634,518.408],[264.086,527.09],[283.628,536.843],[301.48,548.591],[319.086,560.62],[336.602,572.774],[357.161,581.323],[374.925,593.175],[391.301,606.674],[411.25,615.962],[429.244,627.547],[448.874,637.204],[466.17,649.617],[487.135,657.693],[504.545,669.98],[526.528,679.542],[551.428,679.054],[575.399,672.212],[593.173,661.203],[611.591,650.947],[629.851,640.515],[646.536,628.197],[663.478,616.197],[683.975,608.41],[701.96,597.637],[716.683,583],[737.577,575.686],[753.016,561.898],[773.274,553.83],[791.525,543.375],[807.998,530.821],[829.14,523.787],[843.359,508.552],[864.943,502.05],[882.389,490.634],[898.685,477.859],[917.192,467.699],[937.432,459.601],[931.048,448.296],[908.951,445.511],[886.854,446.988],[864.757,449.486],[842.669,447.676],[820.571,446.737],[798.475,448.917],[776.378,445.274],[754.289,445.127],[732.183,447.462],[710.095,449.072],[687.998,445.341],[665.901,445.865],[643.804,446.154],[621.707,449.08],[599.61,446.759],[577.513,447.572],[555.416,446.279],[533.318,447.964],[511.221,447.801],[489.124,447.55],[467.018,444.573],[444.921,444.713],[422.824,449.811],[400.727,448.326],[378.621,445.947],[356.524,444.72],[334.427,444.196],[312.33,444.262],[290.233,449.035],[268.127,448.533],[246.03,446.612],[223.924,444.838],[201.827,448.407],[179.721,444.794],[157.465,445.296]]}],"i":{"x":0.88,"y":0.17},"o":{"x":0.69,"y":0.25}},{"t":30.6,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[135.509,593.414],[132.503,597.245],[153.928,607.169],[172.391,621.714],[190.668,636.561],[206.434,655.318],[228.054,664.939],[243.634,683.988],[264.086,695.444],[283.628,708.312],[301.48,723.812],[319.086,739.683],[336.602,755.72],[357.161,766.999],[374.925,782.636],[391.301,800.447],[411.25,812.701],[429.244,827.986],[448.874,840.728],[466.17,857.106],[487.135,867.762],[504.545,883.973],[526.528,896.589],[551.428,895.945],[575.399,886.918],[593.173,872.392],[611.591,858.861],[629.851,845.096],[646.536,828.845],[663.478,813.012],[683.975,802.738],[701.96,788.524],[716.683,769.212],[737.577,759.561],[753.016,741.37],[773.274,730.724],[791.525,716.93],[807.998,700.367],[829.14,691.086],[843.359,670.984],[864.943,662.405],[882.389,647.344],[898.685,630.488],[917.192,617.083],[937.432,606.399],[931.048,591.483],[908.951,587.808],[886.854,589.757],[864.757,593.053],[842.669,590.664],[820.571,589.426],[798.475,592.302],[776.378,587.496],[754.289,587.301],[732.183,590.382],[710.095,592.507],[687.998,587.584],[665.901,588.276],[643.804,588.656],[621.707,592.517],[599.61,589.455],[577.513,590.528],[555.416,588.822],[533.318,591.045],[511.221,590.83],[489.124,590.498],[467.018,586.57],[444.921,586.755],[422.824,593.482],[400.727,591.522],[378.621,588.383],[356.524,586.764],[334.427,586.073],[312.33,586.16],[290.233,592.458],[268.127,591.795],[246.03,589.26],[223.924,586.921],[201.827,591.629],[179.721,586.862],[157.465,587.525]]}],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]}},{"ty":"st","hd":false,"bm":0,"c":{"a":0,"k":[0.537,0.702,0.871]},"lc":2,"lj":2,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":10.96}},{"ty":"tr","nm":"Transform","a":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":12.6,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":16.2,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":19.2,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":22.8,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":24.6,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":27,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":28.2,"s":[0,0],"i":{"x":0.88,"y":0.17},"o":{"x":0.69,"y":0.25}},{"t":30,"s":[0,0],"i":{"x":0.88,"y":0.17},"o":{"x":0.69,"y":0.25}},{"t":30.6,"s":[0,0],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"o":{"a":0,"k":100},"p":{"a":1,"k":[{"t":0,"s":[-603.861,-759.482],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[-603.861,-508.046],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":12.6,"s":[-530.363,-508.046],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":16.2,"s":[-578.404,-631.493],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":19.2,"s":[-557.029,-537.667],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":22.8,"s":[-534.968,-487.612],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":24.6,"s":[-534.968,-435.732],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":27,"s":[-534.968,-504.346],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":28.2,"s":[-534.968,-521.508],"i":{"x":0.88,"y":0.17},"o":{"x":0.69,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":30,"s":[-534.968,-561.869],"i":{"x":0.88,"y":0.17},"o":{"x":0.69,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":30.6,"s":[-534.968,-741.331],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"gr","hd":false,"nm":"์—ด๋ฆฌ๋Š”๋ถ€๋ถ„_๋ฉด Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"์—ด๋ฆฌ๋Š”๋ถ€๋ถ„_๋ฉด","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[152.96,607.943],[149.567,611.868],[173.751,622.035],[194.591,636.937],[215.222,652.147],[233.018,671.363],[257.422,681.221],[275.009,700.736],[298.095,712.471],[320.153,725.655],[340.305,741.535],[360.177,757.794],[379.95,774.223],[403.156,785.779],[423.208,801.799],[441.693,820.046],[464.21,832.6],[484.522,848.26],[506.68,861.314],[526.203,878.092],[549.868,889.009],[569.521,905.618],[594.334,918.541],[622.44,917.882],[649.499,908.634],[669.561,893.753],[690.352,879.89],[710.963,865.788],[729.797,849.139],[748.921,832.919],[772.057,822.393],[792.358,807.831],[808.977,788.046],[832.562,778.159],[849.989,759.522],[872.856,748.616],[893.457,734.484],[912.052,717.515],[935.916,708.007],[951.966,687.413],[976.33,678.624],[996.022,663.194],[1014.418,645.925],[1035.308,632.193],[1058.155,621.246],[1050.948,605.966],[1026.006,602.2],[1001.063,604.198],[976.12,607.574],[951.187,605.127],[926.245,603.858],[901.302,606.805],[876.359,601.881],[851.427,601.681],[826.474,604.837],[801.541,607.014],[776.598,601.971],[751.656,602.68],[726.713,603.069],[701.77,607.024],[676.827,603.888],[651.885,604.987],[626.942,603.239],[601.999,605.516],[577.056,605.297],[552.114,604.957],[527.161,600.932],[502.218,601.122],[477.276,608.013],[452.333,606.006],[427.38,602.79],[402.437,601.132],[377.495,600.423],[352.552,600.513],[327.609,606.964],[302.656,606.285],[277.714,603.689],[252.761,601.292],[227.818,606.115],[202.866,601.232],[177.743,601.911]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[152.96,406.676],[149.567,409.302],[173.751,416.103],[194.591,426.071],[215.222,436.246],[233.018,449.1],[257.422,455.694],[275.009,468.749],[298.095,476.599],[320.153,485.418],[340.305,496.041],[360.177,506.917],[379.95,517.908],[403.156,525.638],[423.208,536.354],[441.693,548.56],[464.21,556.958],[484.522,567.433],[506.68,576.165],[526.203,587.389],[549.868,594.692],[569.521,605.802],[594.334,614.447],[622.44,614.006],[649.499,607.82],[669.561,597.865],[690.352,588.592],[710.963,579.159],[729.797,568.021],[748.921,557.171],[772.057,550.13],[792.358,540.389],[808.977,527.154],[832.562,520.54],[849.989,508.073],[872.856,500.778],[893.457,491.324],[912.052,479.973],[935.916,473.613],[951.966,459.837],[976.33,453.958],[996.022,443.635],[1014.418,432.084],[1035.308,422.897],[1058.155,415.575],[1050.948,405.353],[1026.006,402.835],[1001.063,404.171],[976.12,406.429],[951.187,404.792],[926.245,403.944],[901.302,405.915],[876.359,402.621],[851.427,402.487],[826.474,404.598],[801.541,406.055],[776.598,402.681],[751.656,403.155],[726.713,403.416],[701.77,406.062],[676.827,403.964],[651.885,404.699],[626.942,403.53],[601.999,405.053],[577.056,404.906],[552.114,404.679],[527.161,401.986],[502.218,402.113],[477.276,406.723],[452.333,405.38],[427.38,403.229],[402.437,402.12],[377.495,401.645],[352.552,401.706],[327.609,406.021],[302.656,405.567],[277.714,403.83],[252.761,402.227],[227.818,405.454],[202.866,402.187],[177.743,402.641]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":12.6,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[134.343,406.676],[131.362,409.302],[152.603,416.103],[170.907,426.071],[189.027,436.246],[204.657,449.1],[226.091,455.694],[241.537,468.749],[261.813,476.599],[281.187,485.418],[298.886,496.041],[316.339,506.917],[333.705,517.908],[354.087,525.638],[371.698,536.354],[387.934,548.56],[407.71,556.958],[425.55,567.433],[445.011,576.165],[462.158,587.389],[482.942,594.692],[500.203,605.802],[521.996,614.447],[546.682,614.006],[570.447,607.82],[588.067,597.865],[606.328,588.592],[624.43,579.159],[640.972,568.021],[657.768,557.171],[678.088,550.13],[695.919,540.389],[710.515,527.154],[731.229,520.54],[746.535,508.073],[766.619,500.778],[784.712,491.324],[801.044,479.973],[822.004,473.613],[836.1,459.837],[857.499,453.958],[874.794,443.635],[890.951,432.084],[909.298,422.897],[929.364,415.575],[923.035,405.353],[901.128,402.835],[879.221,404.171],[857.314,406.429],[835.416,404.792],[813.509,403.944],[791.603,405.915],[769.696,402.621],[747.797,402.487],[725.882,404.598],[703.984,406.055],[682.077,402.681],[660.17,403.155],[638.263,403.416],[616.356,406.062],[594.449,403.964],[572.542,404.699],[550.635,403.53],[528.729,405.053],[506.822,404.906],[484.915,404.679],[462.999,401.986],[441.092,402.113],[419.185,406.723],[397.278,405.38],[375.363,403.229],[353.456,402.12],[331.549,401.645],[309.642,401.706],[287.735,406.021],[265.819,405.567],[243.913,403.83],[221.997,402.227],[200.09,405.454],[178.174,402.187],[156.11,402.641]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":16.2,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[146.512,505.491],[143.261,508.755],[166.426,517.209],[186.388,529.599],[206.149,542.246],[223.195,558.224],[246.57,566.42],[263.415,582.646],[285.528,592.404],[306.657,603.366],[325.959,616.569],[344.994,630.089],[363.933,643.75],[386.16,653.358],[405.367,666.678],[423.073,681.85],[444.641,692.289],[464.096,705.309],[485.32,716.163],[504.02,730.114],[526.688,739.191],[545.512,753.001],[569.279,763.747],[596.201,763.199],[622.119,755.509],[641.335,743.135],[661.249,731.609],[680.991,719.884],[699.031,706.04],[717.349,692.554],[739.51,683.801],[758.956,671.694],[774.873,655.243],[797.464,647.021],[814.157,631.526],[836.059,622.458],[855.792,610.707],[873.603,596.598],[896.462,588.692],[911.835,571.569],[935.171,564.261],[954.034,551.431],[971.654,537.073],[991.663,525.654],[1013.547,516.552],[1006.644,503.847],[982.753,500.716],[958.862,502.377],[934.97,505.184],[911.089,503.149],[887.198,502.095],[863.306,504.544],[839.415,500.451],[815.533,500.285],[791.633,502.908],[767.751,504.719],[743.86,500.525],[719.969,501.115],[696.077,501.439],[672.186,504.727],[648.295,502.12],[624.404,503.033],[600.512,501.58],[576.621,503.473],[552.73,503.291],[528.839,503.008],[504.938,499.662],[481.047,499.82],[457.155,505.549],[433.264,503.88],[409.363,501.206],[385.472,499.827],[361.581,499.238],[337.69,499.313],[313.798,504.677],[289.898,504.113],[266.006,501.954],[242.106,499.961],[218.214,503.972],[194.313,499.911],[170.25,500.475]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":19.2,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[141.098,430.387],[137.967,433.165],[160.276,440.363],[179.5,450.912],[198.531,461.681],[214.947,475.284],[237.458,482.263],[253.681,496.078],[274.977,504.386],[295.325,513.719],[313.913,524.961],[332.245,536.472],[350.484,548.103],[371.89,556.284],[390.387,567.625],[407.438,580.542],[428.209,589.43],[446.946,600.516],[467.385,609.758],[485.394,621.636],[507.224,629.364],[525.353,641.122],[548.241,650.271],[574.168,649.805],[599.129,643.257],[617.635,632.722],[636.813,622.909],[655.825,612.925],[673.199,601.139],[690.84,589.656],[712.182,582.204],[730.909,571.895],[746.239,557.889],[767.995,550.889],[784.07,537.695],[805.163,529.974],[824.167,519.97],[841.319,507.957],[863.333,501.226],[878.138,486.647],[900.613,480.425],[918.778,469.501],[935.747,457.276],[955.017,447.554],[976.092,439.804],[969.444,428.987],[946.436,426.321],[923.428,427.735],[900.419,430.125],[877.42,428.393],[854.412,427.495],[831.403,429.581],[808.395,426.095],[785.396,425.954],[762.378,428.188],[739.379,429.729],[716.371,426.159],[693.362,426.66],[670.354,426.936],[647.346,429.736],[624.337,427.516],[601.329,428.294],[578.321,427.057],[555.312,428.669],[532.304,428.513],[509.296,428.273],[486.278,425.423],[463.27,425.558],[440.261,430.436],[417.253,429.015],[394.236,426.738],[371.227,425.564],[348.219,425.063],[325.211,425.126],[302.202,429.694],[279.185,429.213],[256.176,427.375],[233.159,425.678],[210.15,429.093],[187.133,425.635],[163.959,426.116]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":22.8,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[135.509,390.319],[132.503,392.839],[153.928,399.367],[172.391,408.934],[190.668,418.7],[206.434,431.037],[228.054,437.366],[243.634,449.895],[264.086,457.43],[283.628,465.894],[301.48,476.089],[319.086,486.529],[336.602,497.077],[357.161,504.496],[374.925,514.781],[391.301,526.496],[411.25,534.557],[429.244,544.611],[448.874,552.992],[466.17,563.764],[487.135,570.773],[504.545,581.436],[526.528,589.734],[551.428,589.311],[575.399,583.373],[593.173,573.819],[611.591,564.918],[629.851,555.865],[646.536,545.175],[663.478,534.761],[683.975,528.003],[701.96,518.654],[716.683,505.951],[737.577,499.603],[753.016,487.638],[773.274,480.636],[791.525,471.563],[807.998,460.668],[829.14,454.564],[843.359,441.342],[864.943,435.699],[882.389,425.792],[898.685,414.705],[917.192,405.888],[937.432,398.86],[931.048,389.05],[908.951,386.632],[886.854,387.915],[864.757,390.082],[842.669,388.511],[820.571,387.697],[798.475,389.588],[776.378,386.427],[754.289,386.299],[732.183,388.325],[710.095,389.723],[687.998,386.485],[665.901,386.94],[643.804,387.19],[621.707,389.73],[599.61,387.716],[577.513,388.421],[555.416,387.299],[533.318,388.761],[511.221,388.62],[489.124,388.402],[467.018,385.818],[444.921,385.94],[422.824,390.364],[400.727,389.076],[378.621,387.011],[356.524,385.946],[334.427,385.491],[312.33,385.549],[290.233,389.691],[268.127,389.255],[246.03,387.588],[223.924,386.049],[201.827,389.146],[179.721,386.011],[157.465,386.446]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":24.6,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[135.509,348.79],[132.503,351.042],[153.928,356.876],[172.391,365.425],[190.668,374.151],[206.434,385.176],[228.054,390.831],[243.634,402.028],[264.086,408.761],[283.628,416.324],[301.48,425.435],[319.086,434.763],[336.602,444.189],[357.161,450.819],[374.925,460.01],[391.301,470.478],[411.25,477.681],[429.244,486.665],[448.874,494.154],[466.17,503.781],[487.135,510.044],[504.545,519.573],[526.528,526.987],[551.428,526.609],[575.399,521.303],[593.173,512.766],[611.591,504.812],[629.851,496.722],[646.536,487.17],[663.478,477.864],[683.975,471.825],[701.96,463.47],[716.683,452.119],[737.577,446.447],[753.016,435.755],[773.274,429.497],[791.525,421.389],[807.998,411.654],[829.14,406.199],[843.359,394.384],[864.943,389.342],[882.389,380.489],[898.685,370.582],[917.192,362.703],[937.432,356.423],[931.048,347.656],[908.951,345.496],[886.854,346.642],[864.757,348.578],[842.669,347.174],[820.571,346.447],[798.475,348.137],[776.378,345.312],[754.289,345.198],[732.183,347.008],[710.095,348.258],[687.998,345.364],[665.901,345.771],[643.804,345.994],[621.707,348.263],[599.61,346.464],[577.513,347.094],[555.416,346.092],[533.318,347.398],[511.221,347.272],[489.124,347.077],[467.018,344.768],[444.921,344.877],[422.824,348.831],[400.727,347.679],[378.621,345.834],[356.524,344.882],[334.427,344.476],[312.33,344.527],[290.233,348.229],[268.127,347.839],[246.03,346.349],[223.924,344.974],[201.827,347.742],[179.721,344.94],[157.465,345.329]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":27,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[135.509,403.714],[132.503,406.32],[153.928,413.072],[172.391,422.967],[190.668,433.068],[206.434,445.829],[228.054,452.375],[243.634,465.334],[264.086,473.127],[283.628,481.882],[301.48,492.427],[319.086,503.225],[336.602,514.135],[357.161,521.809],[374.925,532.447],[391.301,544.564],[411.25,552.901],[429.244,563.3],[448.874,571.968],[466.17,583.111],[487.135,590.36],[504.545,601.389],[526.528,609.971],[551.428,609.533],[575.399,603.392],[593.173,593.51],[611.591,584.304],[629.851,574.94],[646.536,563.883],[663.478,553.112],[683.975,546.122],[701.96,536.452],[716.683,523.314],[737.577,516.748],[753.016,504.372],[773.274,497.13],[791.525,487.745],[807.998,476.477],[829.14,470.163],[843.359,456.487],[864.943,450.651],[882.389,440.404],[898.685,428.937],[917.192,419.817],[937.432,412.548],[931.048,402.401],[908.951,399.9],[886.854,401.227],[864.757,403.468],[842.669,401.843],[820.571,401.001],[798.475,402.957],[776.378,399.688],[754.289,399.555],[732.183,401.651],[710.095,403.097],[687.998,399.748],[665.901,400.218],[643.804,400.477],[621.707,403.104],[599.61,401.021],[577.513,401.751],[555.416,400.59],[533.318,402.102],[511.221,401.956],[489.124,401.73],[467.018,399.058],[444.921,399.184],[422.824,403.76],[400.727,402.427],[378.621,400.292],[356.524,399.19],[334.427,398.72],[312.33,398.779],[290.233,403.064],[268.127,402.613],[246.03,400.888],[223.924,399.297],[201.827,402.5],[179.721,399.257],[157.465,399.708]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":28.2,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[135.509,417.452],[132.503,420.147],[153.928,427.129],[172.391,437.361],[190.668,447.805],[206.434,461],[228.054,467.769],[243.634,481.169],[264.086,489.228],[283.628,498.28],[301.48,509.184],[319.086,520.35],[336.602,531.631],[357.161,539.566],[374.925,550.566],[391.301,563.095],[411.25,571.716],[429.244,582.469],[448.874,591.432],[466.17,602.954],[487.135,610.45],[504.545,621.854],[526.528,630.729],[551.428,630.276],[575.399,623.925],[593.173,613.707],[611.591,604.188],[629.851,594.505],[646.536,583.072],[663.478,571.935],[683.975,564.707],[701.96,554.708],[716.683,541.122],[737.577,534.333],[753.016,521.536],[773.274,514.047],[791.525,504.343],[807.998,492.691],[829.14,486.163],[843.359,472.021],[864.943,465.986],[882.389,455.391],[898.685,443.533],[917.192,434.103],[937.432,426.587],[931.048,416.094],[908.951,413.509],[886.854,414.88],[864.757,417.198],[842.669,415.518],[820.571,414.647],[798.475,416.67],[776.378,413.289],[754.289,413.152],[732.183,415.319],[710.095,416.814],[687.998,413.351],[665.901,413.838],[643.804,414.105],[621.707,416.821],[599.61,414.668],[577.513,415.422],[555.416,414.222],[533.318,415.786],[511.221,415.635],[489.124,415.401],[467.018,412.638],[444.921,412.768],[422.824,417.5],[400.727,416.122],[378.621,413.913],[356.524,412.775],[334.427,412.288],[312.33,412.35],[290.233,416.78],[268.127,416.314],[246.03,414.531],[223.924,412.885],[201.827,416.197],[179.721,412.844],[157.465,413.31]]}],"i":{"x":0.88,"y":0.17},"o":{"x":0.69,"y":0.25}},{"t":30,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[135.509,449.76],[132.503,452.663],[153.928,460.185],[172.391,471.209],[190.668,482.462],[206.434,496.678],[228.054,503.97],[243.634,518.408],[264.086,527.09],[283.628,536.843],[301.48,548.591],[319.086,560.62],[336.602,572.774],[357.161,581.323],[374.925,593.175],[391.301,606.674],[411.25,615.962],[429.244,627.547],[448.874,637.204],[466.17,649.617],[487.135,657.693],[504.545,669.98],[526.528,679.542],[551.428,679.054],[575.399,672.212],[593.173,661.203],[611.591,650.947],[629.851,640.515],[646.536,628.197],[663.478,616.197],[683.975,608.41],[701.96,597.637],[716.683,583],[737.577,575.686],[753.016,561.898],[773.274,553.83],[791.525,543.375],[807.998,530.821],[829.14,523.787],[843.359,508.552],[864.943,502.05],[882.389,490.634],[898.685,477.859],[917.192,467.699],[937.432,459.601],[931.048,448.296],[908.951,445.511],[886.854,446.988],[864.757,449.486],[842.669,447.676],[820.571,446.737],[798.475,448.917],[776.378,445.274],[754.289,445.127],[732.183,447.462],[710.095,449.072],[687.998,445.341],[665.901,445.865],[643.804,446.154],[621.707,449.08],[599.61,446.759],[577.513,447.572],[555.416,446.279],[533.318,447.964],[511.221,447.801],[489.124,447.55],[467.018,444.573],[444.921,444.713],[422.824,449.811],[400.727,448.326],[378.621,445.947],[356.524,444.72],[334.427,444.196],[312.33,444.262],[290.233,449.035],[268.127,448.533],[246.03,446.612],[223.924,444.838],[201.827,448.407],[179.721,444.794],[157.465,445.296]]}],"i":{"x":0.88,"y":0.17},"o":{"x":0.69,"y":0.25}},{"t":30.6,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[135.509,593.414],[132.503,597.245],[153.928,607.169],[172.391,621.714],[190.668,636.561],[206.434,655.318],[228.054,664.939],[243.634,683.988],[264.086,695.444],[283.628,708.312],[301.48,723.812],[319.086,739.683],[336.602,755.72],[357.161,766.999],[374.925,782.636],[391.301,800.447],[411.25,812.701],[429.244,827.986],[448.874,840.728],[466.17,857.106],[487.135,867.762],[504.545,883.973],[526.528,896.589],[551.428,895.945],[575.399,886.918],[593.173,872.392],[611.591,858.861],[629.851,845.096],[646.536,828.845],[663.478,813.012],[683.975,802.738],[701.96,788.524],[716.683,769.212],[737.577,759.561],[753.016,741.37],[773.274,730.724],[791.525,716.93],[807.998,700.367],[829.14,691.086],[843.359,670.984],[864.943,662.405],[882.389,647.344],[898.685,630.488],[917.192,617.083],[937.432,606.399],[931.048,591.483],[908.951,587.808],[886.854,589.757],[864.757,593.053],[842.669,590.664],[820.571,589.426],[798.475,592.302],[776.378,587.496],[754.289,587.301],[732.183,590.382],[710.095,592.507],[687.998,587.584],[665.901,588.276],[643.804,588.656],[621.707,592.517],[599.61,589.455],[577.513,590.528],[555.416,588.822],[533.318,591.045],[511.221,590.83],[489.124,590.498],[467.018,586.57],[444.921,586.755],[422.824,593.482],[400.727,591.522],[378.621,588.383],[356.524,586.764],[334.427,586.073],[312.33,586.16],[290.233,592.458],[268.127,591.795],[246.03,589.26],[223.924,586.921],[201.827,591.629],[179.721,586.862],[157.465,587.525]]}],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.737,0.863,0.992]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":12.6,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":16.2,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":19.2,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":22.8,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":24.6,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":27,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":28.2,"s":[0,0],"i":{"x":0.88,"y":0.17},"o":{"x":0.69,"y":0.25}},{"t":30,"s":[0,0],"i":{"x":0.88,"y":0.17},"o":{"x":0.69,"y":0.25}},{"t":30.6,"s":[0,0],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"o":{"a":0,"k":100},"p":{"a":1,"k":[{"t":0,"s":[-603.861,-759.482],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[-603.861,-508.046],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":12.6,"s":[-530.363,-508.046],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":16.2,"s":[-578.404,-631.493],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":19.2,"s":[-557.029,-537.667],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":22.8,"s":[-534.968,-487.612],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":24.6,"s":[-534.968,-435.732],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":27,"s":[-534.968,-504.346],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":28.2,"s":[-534.968,-521.508],"i":{"x":0.88,"y":0.17},"o":{"x":0.69,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":30,"s":[-534.968,-561.869],"i":{"x":0.88,"y":0.17},"o":{"x":0.69,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":30.6,"s":[-534.968,-741.331],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"tr","nm":"Transform","a":{"a":1,"k":[{"t":0,"s":[0,5.274],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":12.6,"s":[0,5.274],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":16.2,"s":[0,5.274],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":19.2,"s":[0,-107.33],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":22.8,"s":[0,-96.847],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":24.6,"s":[0,-85.982],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":27,"s":[0,-52.34],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":28.2,"s":[0,5.274],"i":{"x":0.88,"y":0.17},"o":{"x":0.69,"y":0.25}},{"t":30,"s":[0,-51.22],"i":{"x":0.88,"y":0.17},"o":{"x":0.69,"y":0.25}},{"t":30.6,"s":[0,-151.755],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"o":{"a":1,"k":[{"t":0,"s":[100],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":22.8,"s":[100],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":27,"s":[100],"i":{"x":0.88,"y":0.17},"o":{"x":0.69,"y":0.25}},{"t":30,"s":[100],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"p":{"a":1,"k":[{"t":0,"s":[0.832,-25.463],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":8.4,"s":[0.832,-33.732],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":12.6,"s":[0.731,-33.732],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":16.2,"s":[0.797,-28.198],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":19.2,"s":[-0.179,-56.508],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":22.8,"s":[0.737,-56.508],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":24.6,"s":[0.737,-56.346],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":27,"s":[0.737,-46.1],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":28.2,"s":[0.539,-33.568],"i":{"x":0.88,"y":0.17},"o":{"x":0.69,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":30,"s":[0.055,-43.893],"i":{"x":0.88,"y":0.17},"o":{"x":0.69,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":30.6,"s":[-0.115,-57.943],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"r":{"a":0,"k":0},"s":{"a":0,"k":[21.405,21.405]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"gr","hd":false,"nm":"์„ผํ„ฐ Group","bm":0,"it":[{"ty":"gr","hd":false,"nm":"์„ผํ„ฐ_์„  Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"์„ผํ„ฐ_์„ ","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[1045.339,1159.138],[1049.132,1153.825],[1024.249,1138.913],[1003.928,1119.238],[983.905,1099.264],[967.726,1075.254],[942.544,1060.652],[926.654,1036.353],[903.288,1019.854],[881.3,1001.916],[861.927,981.262],[842.903,960.229],[824.048,939.016],[800.493,922.716],[781.249,901.922],[764.132,878.891],[741.545,861.583],[721.942,841.149],[699.834,823.331],[681.299,801.769],[657.135,786.099],[638.43,764.715],[615.054,748.196],[586.349,748.915],[559.889,761.659],[539.378,782.183],[517.879,801.679],[496.629,821.424],[477.805,843.716],[458.571,865.578],[433.888,881.728],[413.057,901.922],[397.237,927.37],[371.945,942.881],[355.027,967.17],[330.703,983.699],[309.474,1003.464],[290.959,1026.086],[265.287,1041.207],[250.246,1067.474],[223.886,1081.875],[203.884,1102.939],[185.658,1125.87],[164.029,1145.245],[168.521,1167.238],[196.807,1161.385],[225.093,1165.67],[253.37,1163.393],[281.656,1159.557],[309.933,1162.344],[338.209,1163.782],[366.495,1160.436],[394.772,1166.039],[423.058,1166.259],[451.335,1162.673],[479.621,1160.197],[507.908,1165.929],[536.184,1165.12],[564.47,1164.671],[592.757,1160.187],[621.043,1163.752],[649.32,1162.504],[677.606,1164.481],[705.892,1161.894],[734.179,1162.154],[762.465,1162.534],[790.751,1167.108],[819.028,1166.898],[847.314,1159.058],[875.601,1161.345],[903.887,1164.99],[932.174,1166.878],[960.47,1167.687],[988.756,1167.587],[1016.823,1160.266]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[1045.339,1101.741],[1049.132,1096.691],[1024.249,1082.518],[1003.928,1063.817],[983.905,1044.831],[967.726,1022.01],[942.544,1008.132],[926.654,985.036],[903.288,969.354],[881.3,952.304],[861.927,932.673],[842.903,912.681],[824.048,892.518],[800.493,877.026],[781.249,857.262],[764.132,835.371],[741.545,818.92],[721.942,799.498],[699.834,782.563],[681.299,762.068],[657.135,747.173],[638.43,726.849],[615.054,711.148],[586.349,711.831],[559.889,723.944],[539.378,743.452],[517.879,761.982],[496.629,780.75],[477.805,801.938],[458.571,822.717],[433.888,838.068],[413.057,857.262],[397.237,881.45],[371.945,896.192],[355.027,919.279],[330.703,934.989],[309.474,953.776],[290.959,975.277],[265.287,989.649],[250.246,1014.616],[223.886,1028.304],[203.884,1048.325],[185.658,1070.12],[164.029,1088.536],[168.521,1109.44],[196.807,1103.877],[225.093,1107.949],[253.37,1105.785],[281.656,1102.14],[309.933,1104.788],[338.209,1106.155],[366.495,1102.975],[394.772,1108.301],[423.058,1108.509],[451.335,1105.101],[479.621,1102.747],[507.908,1108.196],[536.184,1107.427],[564.47,1107],[592.757,1102.738],[621.043,1106.127],[649.32,1104.94],[677.606,1106.82],[705.892,1104.361],[734.179,1104.608],[762.465,1104.969],[790.751,1109.316],[819.028,1109.117],[847.314,1101.665],[875.601,1103.839],[903.887,1107.304],[932.174,1109.098],[960.47,1109.867],[988.756,1109.772],[1016.823,1102.814]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":12.6,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[918.108,1101.741],[921.44,1096.691],[899.585,1082.518],[881.737,1063.817],[864.152,1044.831],[849.942,1022.01],[827.825,1008.132],[813.869,985.036],[793.347,969.354],[774.035,952.304],[757.02,932.673],[740.311,912.681],[723.752,892.518],[703.063,877.026],[686.162,857.262],[671.128,835.371],[651.29,818.92],[634.073,799.498],[614.655,782.563],[598.376,762.068],[577.153,747.173],[560.725,726.849],[540.195,711.148],[514.983,711.831],[491.744,723.944],[473.729,743.452],[454.846,761.982],[436.183,780.75],[419.65,801.938],[402.757,822.717],[381.078,838.068],[362.783,857.262],[348.889,881.45],[326.675,896.192],[311.816,919.279],[290.453,934.989],[271.807,953.776],[255.545,975.277],[232.999,989.649],[219.788,1014.616],[196.636,1028.304],[179.069,1048.325],[163.061,1070.12],[144.065,1088.536],[148.01,1109.44],[172.853,1103.877],[197.697,1107.949],[222.532,1105.785],[247.375,1102.14],[272.21,1104.788],[297.045,1106.155],[321.888,1102.975],[346.723,1108.301],[371.567,1108.509],[396.402,1105.101],[421.245,1102.747],[446.089,1108.196],[470.924,1107.427],[495.767,1107],[520.611,1102.738],[545.455,1106.127],[570.289,1104.94],[595.133,1106.82],[619.977,1104.361],[644.82,1104.608],[669.664,1104.969],[694.507,1109.316],[719.342,1109.117],[744.186,1101.665],[769.029,1103.839],[793.873,1107.304],[818.717,1109.098],[843.569,1109.867],[868.412,1109.772],[893.063,1102.814]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":16.2,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[1001.271,1101.741],[1004.904,1096.691],[981.07,1082.518],[961.606,1063.817],[942.428,1044.831],[926.93,1022.01],[902.81,1008.132],[887.59,985.036],[865.209,969.354],[844.147,952.304],[825.591,932.673],[807.369,912.681],[789.31,892.518],[766.747,877.026],[748.315,857.262],[731.919,835.371],[710.284,818.92],[691.507,799.498],[670.331,782.563],[652.578,762.068],[629.432,747.173],[611.516,726.849],[589.126,711.148],[561.631,711.831],[536.286,723.944],[516.64,743.452],[496.047,761.982],[475.693,780.75],[457.662,801.938],[439.239,822.717],[415.597,838.068],[395.644,857.262],[380.491,881.45],[356.265,896.192],[340.061,919.279],[316.762,934.989],[296.427,953.776],[278.693,975.277],[254.104,989.649],[239.696,1014.616],[214.448,1028.304],[195.289,1048.325],[177.831,1070.12],[157.114,1088.536],[161.416,1109.44],[188.51,1103.877],[215.604,1107.949],[242.689,1105.785],[269.783,1102.14],[296.867,1104.788],[323.951,1106.155],[351.045,1102.975],[378.13,1108.301],[405.224,1108.509],[432.308,1105.101],[459.402,1102.747],[486.496,1108.196],[513.58,1107.427],[540.674,1107],[567.768,1102.738],[594.862,1106.127],[621.947,1104.94],[649.04,1106.82],[676.134,1104.361],[703.229,1104.608],[730.322,1104.969],[757.416,1109.316],[784.501,1109.117],[811.595,1101.665],[838.689,1103.839],[865.783,1107.304],[892.876,1109.098],[919.98,1109.867],[947.074,1109.772],[973.958,1102.814]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":22.8,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[926.079,1101.741],[929.439,1096.691],[907.395,1082.518],[889.392,1063.817],[871.654,1044.831],[857.321,1022.01],[835.011,1008.132],[820.934,985.036],[800.234,969.354],[780.754,952.304],[763.592,932.673],[746.738,912.681],[730.035,892.518],[709.167,877.026],[692.119,857.262],[676.954,835.371],[656.944,818.92],[639.577,799.498],[619.991,782.563],[603.571,762.068],[582.164,747.173],[565.593,726.849],[544.884,711.148],[519.454,711.831],[496.013,723.944],[477.842,743.452],[458.795,761.982],[439.97,780.75],[423.293,801.938],[406.254,822.717],[384.387,838.068],[365.932,857.262],[351.917,881.45],[329.511,896.192],[314.523,919.279],[292.974,934.989],[274.166,953.776],[257.764,975.277],[235.021,989.649],[221.696,1014.616],[198.343,1028.304],[180.623,1048.325],[164.477,1070.12],[145.315,1088.536],[149.295,1109.44],[174.354,1103.877],[199.413,1107.949],[224.464,1105.785],[249.523,1102.14],[274.573,1104.788],[299.624,1106.155],[324.683,1102.975],[349.733,1108.301],[374.793,1108.509],[399.843,1105.101],[424.902,1102.747],[449.962,1108.196],[475.012,1107.427],[500.071,1107],[525.13,1102.738],[550.19,1106.127],[575.24,1104.94],[600.299,1106.82],[625.359,1104.361],[650.418,1104.608],[675.477,1104.969],[700.537,1109.316],[725.587,1109.117],[750.646,1101.665],[775.706,1103.839],[800.765,1107.304],[825.824,1109.098],[850.892,1109.867],[875.951,1109.772],[900.816,1102.814]]}],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]}},{"ty":"st","hd":false,"bm":0,"c":{"a":0,"k":[0.537,0.702,0.871]},"lc":2,"lj":2,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":12.46}},{"ty":"tr","nm":"Transform","a":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":12.6,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":16.2,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":22.8,"s":[0,0],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"o":{"a":0,"k":100},"p":{"a":1,"k":[{"t":0,"s":[-606.581,-957.942],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[-606.581,-910.507],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":12.6,"s":[-532.752,-910.507],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":16.2,"s":[-581.009,-910.507],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":22.8,"s":[-537.377,-910.507],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"gr","hd":false,"nm":"์„ผํ„ฐ_๋ฉด Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"์„ผํ„ฐ_๋ฉด","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[1045.339,1159.138],[1049.132,1153.825],[1024.249,1138.913],[1003.928,1119.238],[983.905,1099.264],[967.726,1075.254],[942.544,1060.652],[926.654,1036.353],[903.288,1019.854],[881.3,1001.916],[861.927,981.262],[842.903,960.229],[824.048,939.016],[800.493,922.716],[781.249,901.922],[764.132,878.891],[741.545,861.583],[721.942,841.149],[699.834,823.331],[681.299,801.769],[657.135,786.099],[638.43,764.715],[615.054,748.196],[586.349,748.915],[559.889,761.659],[539.378,782.183],[517.879,801.679],[496.629,821.424],[477.805,843.716],[458.571,865.578],[433.888,881.728],[413.057,901.922],[397.237,927.37],[371.945,942.881],[355.027,967.17],[330.703,983.699],[309.474,1003.464],[290.959,1026.086],[265.287,1041.207],[250.246,1067.474],[223.886,1081.875],[203.884,1102.939],[185.658,1125.87],[164.029,1145.245],[168.521,1167.238],[196.807,1161.385],[225.093,1165.67],[253.37,1163.393],[281.656,1159.557],[309.933,1162.344],[338.209,1163.782],[366.495,1160.436],[394.772,1166.039],[423.058,1166.259],[451.335,1162.673],[479.621,1160.197],[507.908,1165.929],[536.184,1165.12],[564.47,1164.671],[592.757,1160.187],[621.043,1163.752],[649.32,1162.504],[677.606,1164.481],[705.892,1161.894],[734.179,1162.154],[762.465,1162.534],[790.751,1167.108],[819.028,1166.898],[847.314,1159.058],[875.601,1161.345],[903.887,1164.99],[932.174,1166.878],[960.47,1167.687],[988.756,1167.587],[1016.823,1160.266]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[1045.339,1101.741],[1049.132,1096.691],[1024.249,1082.518],[1003.928,1063.817],[983.905,1044.831],[967.726,1022.01],[942.544,1008.132],[926.654,985.036],[903.288,969.354],[881.3,952.304],[861.927,932.673],[842.903,912.681],[824.048,892.518],[800.493,877.026],[781.249,857.262],[764.132,835.371],[741.545,818.92],[721.942,799.498],[699.834,782.563],[681.299,762.068],[657.135,747.173],[638.43,726.849],[615.054,711.148],[586.349,711.831],[559.889,723.944],[539.378,743.452],[517.879,761.982],[496.629,780.75],[477.805,801.938],[458.571,822.717],[433.888,838.068],[413.057,857.262],[397.237,881.45],[371.945,896.192],[355.027,919.279],[330.703,934.989],[309.474,953.776],[290.959,975.277],[265.287,989.649],[250.246,1014.616],[223.886,1028.304],[203.884,1048.325],[185.658,1070.12],[164.029,1088.536],[168.521,1109.44],[196.807,1103.877],[225.093,1107.949],[253.37,1105.785],[281.656,1102.14],[309.933,1104.788],[338.209,1106.155],[366.495,1102.975],[394.772,1108.301],[423.058,1108.509],[451.335,1105.101],[479.621,1102.747],[507.908,1108.196],[536.184,1107.427],[564.47,1107],[592.757,1102.738],[621.043,1106.127],[649.32,1104.94],[677.606,1106.82],[705.892,1104.361],[734.179,1104.608],[762.465,1104.969],[790.751,1109.316],[819.028,1109.117],[847.314,1101.665],[875.601,1103.839],[903.887,1107.304],[932.174,1109.098],[960.47,1109.867],[988.756,1109.772],[1016.823,1102.814]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":12.6,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[918.108,1101.741],[921.44,1096.691],[899.585,1082.518],[881.737,1063.817],[864.152,1044.831],[849.942,1022.01],[827.825,1008.132],[813.869,985.036],[793.347,969.354],[774.035,952.304],[757.02,932.673],[740.311,912.681],[723.752,892.518],[703.063,877.026],[686.162,857.262],[671.128,835.371],[651.29,818.92],[634.073,799.498],[614.655,782.563],[598.376,762.068],[577.153,747.173],[560.725,726.849],[540.195,711.148],[514.983,711.831],[491.744,723.944],[473.729,743.452],[454.846,761.982],[436.183,780.75],[419.65,801.938],[402.757,822.717],[381.078,838.068],[362.783,857.262],[348.889,881.45],[326.675,896.192],[311.816,919.279],[290.453,934.989],[271.807,953.776],[255.545,975.277],[232.999,989.649],[219.788,1014.616],[196.636,1028.304],[179.069,1048.325],[163.061,1070.12],[144.065,1088.536],[148.01,1109.44],[172.853,1103.877],[197.697,1107.949],[222.532,1105.785],[247.375,1102.14],[272.21,1104.788],[297.045,1106.155],[321.888,1102.975],[346.723,1108.301],[371.567,1108.509],[396.402,1105.101],[421.245,1102.747],[446.089,1108.196],[470.924,1107.427],[495.767,1107],[520.611,1102.738],[545.455,1106.127],[570.289,1104.94],[595.133,1106.82],[619.977,1104.361],[644.82,1104.608],[669.664,1104.969],[694.507,1109.316],[719.342,1109.117],[744.186,1101.665],[769.029,1103.839],[793.873,1107.304],[818.717,1109.098],[843.569,1109.867],[868.412,1109.772],[893.063,1102.814]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":16.2,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[1001.271,1101.741],[1004.904,1096.691],[981.07,1082.518],[961.606,1063.817],[942.428,1044.831],[926.93,1022.01],[902.81,1008.132],[887.59,985.036],[865.209,969.354],[844.147,952.304],[825.591,932.673],[807.369,912.681],[789.31,892.518],[766.747,877.026],[748.315,857.262],[731.919,835.371],[710.284,818.92],[691.507,799.498],[670.331,782.563],[652.578,762.068],[629.432,747.173],[611.516,726.849],[589.126,711.148],[561.631,711.831],[536.286,723.944],[516.64,743.452],[496.047,761.982],[475.693,780.75],[457.662,801.938],[439.239,822.717],[415.597,838.068],[395.644,857.262],[380.491,881.45],[356.265,896.192],[340.061,919.279],[316.762,934.989],[296.427,953.776],[278.693,975.277],[254.104,989.649],[239.696,1014.616],[214.448,1028.304],[195.289,1048.325],[177.831,1070.12],[157.114,1088.536],[161.416,1109.44],[188.51,1103.877],[215.604,1107.949],[242.689,1105.785],[269.783,1102.14],[296.867,1104.788],[323.951,1106.155],[351.045,1102.975],[378.13,1108.301],[405.224,1108.509],[432.308,1105.101],[459.402,1102.747],[486.496,1108.196],[513.58,1107.427],[540.674,1107],[567.768,1102.738],[594.862,1106.127],[621.947,1104.94],[649.04,1106.82],[676.134,1104.361],[703.229,1104.608],[730.322,1104.969],[757.416,1109.316],[784.501,1109.117],[811.595,1101.665],[838.689,1103.839],[865.783,1107.304],[892.876,1109.098],[919.98,1109.867],[947.074,1109.772],[973.958,1102.814]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":22.8,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[926.079,1101.741],[929.439,1096.691],[907.395,1082.518],[889.392,1063.817],[871.654,1044.831],[857.321,1022.01],[835.011,1008.132],[820.934,985.036],[800.234,969.354],[780.754,952.304],[763.592,932.673],[746.738,912.681],[730.035,892.518],[709.167,877.026],[692.119,857.262],[676.954,835.371],[656.944,818.92],[639.577,799.498],[619.991,782.563],[603.571,762.068],[582.164,747.173],[565.593,726.849],[544.884,711.148],[519.454,711.831],[496.013,723.944],[477.842,743.452],[458.795,761.982],[439.97,780.75],[423.293,801.938],[406.254,822.717],[384.387,838.068],[365.932,857.262],[351.917,881.45],[329.511,896.192],[314.523,919.279],[292.974,934.989],[274.166,953.776],[257.764,975.277],[235.021,989.649],[221.696,1014.616],[198.343,1028.304],[180.623,1048.325],[164.477,1070.12],[145.315,1088.536],[149.295,1109.44],[174.354,1103.877],[199.413,1107.949],[224.464,1105.785],[249.523,1102.14],[274.573,1104.788],[299.624,1106.155],[324.683,1102.975],[349.733,1108.301],[374.793,1108.509],[399.843,1105.101],[424.902,1102.747],[449.962,1108.196],[475.012,1107.427],[500.071,1107],[525.13,1102.738],[550.19,1106.127],[575.24,1104.94],[600.299,1106.82],[625.359,1104.361],[650.418,1104.608],[675.477,1104.969],[700.537,1109.316],[725.587,1109.117],[750.646,1101.665],[775.706,1103.839],[800.765,1107.304],[825.824,1109.098],[850.892,1109.867],[875.951,1109.772],[900.816,1102.814]]}],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.737,0.863,0.992]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":12.6,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":16.2,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":22.8,"s":[0,0],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"o":{"a":0,"k":100},"p":{"a":1,"k":[{"t":0,"s":[-606.581,-957.942],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[-606.581,-910.507],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":12.6,"s":[-532.752,-910.507],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":16.2,"s":[-581.009,-910.507],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":22.8,"s":[-537.377,-910.507],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"tr","nm":"Transform","a":{"a":1,"k":[{"t":0,"s":[0,13.331],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[0,13.331],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":12.6,"s":[0,13.331],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":16.2,"s":[0,13.331],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":22.8,"s":[0,13.331],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"o":{"a":0,"k":100},"p":{"a":1,"k":[{"t":0,"s":[1.414,18.743],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":8.4,"s":[1.414,17.956],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":12.6,"s":[1.242,17.956],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":16.2,"s":[1.355,17.956],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":22.8,"s":[1.253,17.956],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"r":{"a":0,"k":0},"s":{"a":0,"k":[21.405,21.405]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"gr","hd":false,"nm":"์™ผ์ชฝ Group","bm":0,"it":[{"ty":"gr","hd":false,"nm":"์™ผ์ชฝ_์„  Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"์™ผ์ชฝ_์„ ","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[564.62,859.995],[538.739,841.908],[510.083,833.539],[487.506,815.272],[461.156,803.137],[437.501,786.618],[412.858,771.707],[388.953,755.587],[366.855,736.511],[342.172,721.68],[315.672,709.795],[292.466,692.536],[264.359,683.278],[242.201,664.302],[219.155,646.764],[193.174,634.04],[168.341,619.419],[145.215,606.865],[145.215,632.472],[141.851,660.667],[139.665,688.861],[144.297,717.056],[145.684,745.26],[148.219,773.454],[144.197,801.649],[145.434,829.853],[143.029,858.048],[142.57,886.252],[145.454,914.447],[143.348,942.641],[143.219,970.835],[146.812,999.04],[141.801,1027.244],[144.217,1055.449],[140.843,1083.653],[140.334,1111.858],[140.134,1140.062],[159.957,1151.637],[186.826,1138.993],[209.313,1120.297],[232.749,1102.919],[256.204,1085.551],[280.748,1069.671],[304.293,1052.432],[324.335,1030.41],[347.462,1012.583],[371.945,996.633],[398.674,983.739],[422.669,967.11],[441.703,943.7],[466.177,927.73],[491.878,913.408],[515.523,896.309],[535.944,874.607]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[564.62,817.411],[538.739,800.219],[510.083,792.264],[487.506,774.902],[461.156,763.368],[437.501,747.667],[412.858,733.494],[388.953,718.173],[366.855,700.041],[342.172,685.944],[315.672,674.648],[292.466,658.244],[264.359,649.444],[242.201,631.408],[219.155,614.738],[193.174,602.645],[168.341,588.747],[145.215,576.814],[145.215,601.154],[141.851,627.953],[139.665,654.751],[144.297,681.549],[145.684,708.357],[148.219,735.155],[144.197,761.954],[145.434,788.762],[143.029,815.56],[142.57,842.368],[145.454,869.166],[143.348,895.964],[143.219,922.763],[146.812,949.57],[141.801,976.378],[144.217,1003.186],[140.843,1029.994],[140.334,1056.802],[140.134,1083.61],[159.957,1094.612],[186.826,1082.594],[209.313,1064.823],[232.749,1048.306],[256.204,1031.798],[280.748,1016.704],[304.293,1000.319],[324.335,979.388],[347.462,962.443],[371.945,947.283],[398.674,935.027],[422.669,919.222],[441.703,896.971],[466.177,881.792],[491.878,868.179],[515.523,851.927],[535.944,831.299]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":12.6,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[495.899,817.411],[473.168,800.219],[448,792.264],[428.171,774.902],[405.028,763.368],[384.252,747.667],[362.608,733.494],[341.613,718.173],[322.204,700.041],[300.525,685.944],[277.251,674.648],[256.869,658.244],[232.183,649.444],[212.722,631.408],[192.481,614.738],[169.662,602.645],[147.852,588.747],[127.54,576.814],[127.54,601.154],[124.586,627.953],[122.666,654.751],[126.734,681.549],[127.952,708.357],[130.179,735.155],[126.646,761.954],[127.733,788.762],[125.621,815.56],[125.217,842.368],[127.751,869.166],[125.901,895.964],[125.787,922.763],[128.943,949.57],[124.542,976.378],[126.664,1003.186],[123.701,1029.994],[123.254,1056.802],[123.078,1083.61],[140.488,1094.612],[164.087,1082.594],[183.837,1064.823],[204.421,1048.306],[225.021,1031.798],[246.578,1016.704],[267.257,1000.319],[284.86,979.388],[305.171,962.443],[326.675,947.283],[350.151,935.027],[371.225,919.222],[387.942,896.971],[409.437,881.792],[432.01,868.179],[452.778,851.927],[470.713,831.299]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":16.2,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[540.818,817.411],[516.028,800.219],[488.58,792.264],[466.955,774.902],[441.715,763.368],[419.057,747.667],[395.453,733.494],[372.556,718.173],[351.39,700.041],[327.747,685.944],[302.364,674.648],[280.136,658.244],[253.215,649.444],[231.991,631.408],[209.916,614.738],[185.03,602.645],[161.244,588.747],[139.093,576.814],[139.093,601.154],[135.871,627.953],[133.778,654.751],[138.214,681.549],[139.542,708.357],[141.971,735.155],[138.118,761.954],[139.303,788.762],[136.999,815.56],[136.56,842.368],[139.323,869.166],[137.305,895.964],[137.181,922.763],[140.623,949.57],[135.823,976.378],[138.137,1003.186],[134.906,1029.994],[134.418,1056.802],[134.227,1083.61],[153.214,1094.612],[178.95,1082.594],[200.489,1064.823],[222.937,1048.306],[245.404,1031.798],[268.913,1016.704],[291.465,1000.319],[310.663,979.388],[332.814,962.443],[356.265,947.283],[381.868,935.027],[404.851,919.222],[423.082,896.971],[446.524,881.792],[471.142,868.179],[493.791,851.927],[513.351,831.299]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":22.8,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[500.204,817.411],[477.276,800.219],[451.889,792.264],[431.888,774.902],[408.544,763.368],[387.587,747.667],[365.756,733.494],[344.578,718.173],[325.001,700.041],[303.134,685.944],[279.658,674.648],[259.099,658.244],[234.199,649.444],[214.569,631.408],[194.152,614.738],[171.135,602.645],[149.135,588.747],[128.648,576.814],[128.648,601.154],[125.668,627.953],[123.731,654.751],[127.834,681.549],[129.063,708.357],[131.309,735.155],[127.746,761.954],[128.842,788.762],[126.711,815.56],[126.304,842.368],[128.86,869.166],[126.994,895.964],[126.879,922.763],[130.062,949.57],[125.624,976.378],[127.763,1003.186],[124.775,1029.994],[124.324,1056.802],[124.147,1083.61],[141.708,1094.612],[165.511,1082.594],[185.433,1064.823],[206.195,1048.306],[226.975,1031.798],[248.718,1016.704],[269.577,1000.319],[287.333,979.388],[307.82,962.443],[329.511,947.283],[353.191,935.027],[374.448,919.222],[391.31,896.971],[412.992,881.792],[435.761,868.179],[456.708,851.927],[474.8,831.299]]}],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]}},{"ty":"st","hd":false,"bm":0,"c":{"a":0,"k":[0.537,0.702,0.871]},"lc":2,"lj":2,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":11.13}},{"ty":"tr","nm":"Transform","a":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":12.6,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":16.2,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":22.8,"s":[0,0],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"o":{"a":0,"k":100},"p":{"a":1,"k":[{"t":0,"s":[-352.143,-879.251],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[-352.143,-835.713],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":12.6,"s":[-309.283,-835.713],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":16.2,"s":[-337.298,-835.713],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":22.8,"s":[-311.968,-835.713],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"gr","hd":false,"nm":"์™ผ์ชฝ_๋ฉด Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"์™ผ์ชฝ_๋ฉด","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[564.62,859.995],[538.739,841.908],[510.083,833.539],[487.506,815.272],[461.156,803.137],[437.501,786.618],[412.858,771.707],[388.953,755.587],[366.855,736.511],[342.172,721.68],[315.672,709.795],[292.466,692.536],[264.359,683.278],[242.201,664.302],[219.155,646.764],[193.174,634.04],[168.341,619.419],[145.215,606.865],[145.215,632.472],[141.851,660.667],[139.665,688.861],[144.297,717.056],[145.684,745.26],[148.219,773.454],[144.197,801.649],[145.434,829.853],[143.029,858.048],[142.57,886.252],[145.454,914.447],[143.348,942.641],[143.219,970.835],[146.812,999.04],[141.801,1027.244],[144.217,1055.449],[140.843,1083.653],[140.334,1111.858],[140.134,1140.062],[159.957,1151.637],[186.826,1138.993],[209.313,1120.297],[232.749,1102.919],[256.204,1085.551],[280.748,1069.671],[304.293,1052.432],[324.335,1030.41],[347.462,1012.583],[371.945,996.633],[398.674,983.739],[422.669,967.11],[441.703,943.7],[466.177,927.73],[491.878,913.408],[515.523,896.309],[535.944,874.607]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[564.62,817.411],[538.739,800.219],[510.083,792.264],[487.506,774.902],[461.156,763.368],[437.501,747.667],[412.858,733.494],[388.953,718.173],[366.855,700.041],[342.172,685.944],[315.672,674.648],[292.466,658.244],[264.359,649.444],[242.201,631.408],[219.155,614.738],[193.174,602.645],[168.341,588.747],[145.215,576.814],[145.215,601.154],[141.851,627.953],[139.665,654.751],[144.297,681.549],[145.684,708.357],[148.219,735.155],[144.197,761.954],[145.434,788.762],[143.029,815.56],[142.57,842.368],[145.454,869.166],[143.348,895.964],[143.219,922.763],[146.812,949.57],[141.801,976.378],[144.217,1003.186],[140.843,1029.994],[140.334,1056.802],[140.134,1083.61],[159.957,1094.612],[186.826,1082.594],[209.313,1064.823],[232.749,1048.306],[256.204,1031.798],[280.748,1016.704],[304.293,1000.319],[324.335,979.388],[347.462,962.443],[371.945,947.283],[398.674,935.027],[422.669,919.222],[441.703,896.971],[466.177,881.792],[491.878,868.179],[515.523,851.927],[535.944,831.299]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":12.6,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[495.899,817.411],[473.168,800.219],[448,792.264],[428.171,774.902],[405.028,763.368],[384.252,747.667],[362.608,733.494],[341.613,718.173],[322.204,700.041],[300.525,685.944],[277.251,674.648],[256.869,658.244],[232.183,649.444],[212.722,631.408],[192.481,614.738],[169.662,602.645],[147.852,588.747],[127.54,576.814],[127.54,601.154],[124.586,627.953],[122.666,654.751],[126.734,681.549],[127.952,708.357],[130.179,735.155],[126.646,761.954],[127.733,788.762],[125.621,815.56],[125.217,842.368],[127.751,869.166],[125.901,895.964],[125.787,922.763],[128.943,949.57],[124.542,976.378],[126.664,1003.186],[123.701,1029.994],[123.254,1056.802],[123.078,1083.61],[140.488,1094.612],[164.087,1082.594],[183.837,1064.823],[204.421,1048.306],[225.021,1031.798],[246.578,1016.704],[267.257,1000.319],[284.86,979.388],[305.171,962.443],[326.675,947.283],[350.151,935.027],[371.225,919.222],[387.942,896.971],[409.437,881.792],[432.01,868.179],[452.778,851.927],[470.713,831.299]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":16.2,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[540.818,817.411],[516.028,800.219],[488.58,792.264],[466.955,774.902],[441.715,763.368],[419.057,747.667],[395.453,733.494],[372.556,718.173],[351.39,700.041],[327.747,685.944],[302.364,674.648],[280.136,658.244],[253.215,649.444],[231.991,631.408],[209.916,614.738],[185.03,602.645],[161.244,588.747],[139.093,576.814],[139.093,601.154],[135.871,627.953],[133.778,654.751],[138.214,681.549],[139.542,708.357],[141.971,735.155],[138.118,761.954],[139.303,788.762],[136.999,815.56],[136.56,842.368],[139.323,869.166],[137.305,895.964],[137.181,922.763],[140.623,949.57],[135.823,976.378],[138.137,1003.186],[134.906,1029.994],[134.418,1056.802],[134.227,1083.61],[153.214,1094.612],[178.95,1082.594],[200.489,1064.823],[222.937,1048.306],[245.404,1031.798],[268.913,1016.704],[291.465,1000.319],[310.663,979.388],[332.814,962.443],[356.265,947.283],[381.868,935.027],[404.851,919.222],[423.082,896.971],[446.524,881.792],[471.142,868.179],[493.791,851.927],[513.351,831.299]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":22.8,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[500.204,817.411],[477.276,800.219],[451.889,792.264],[431.888,774.902],[408.544,763.368],[387.587,747.667],[365.756,733.494],[344.578,718.173],[325.001,700.041],[303.134,685.944],[279.658,674.648],[259.099,658.244],[234.199,649.444],[214.569,631.408],[194.152,614.738],[171.135,602.645],[149.135,588.747],[128.648,576.814],[128.648,601.154],[125.668,627.953],[123.731,654.751],[127.834,681.549],[129.063,708.357],[131.309,735.155],[127.746,761.954],[128.842,788.762],[126.711,815.56],[126.304,842.368],[128.86,869.166],[126.994,895.964],[126.879,922.763],[130.062,949.57],[125.624,976.378],[127.763,1003.186],[124.775,1029.994],[124.324,1056.802],[124.147,1083.61],[141.708,1094.612],[165.511,1082.594],[185.433,1064.823],[206.195,1048.306],[226.975,1031.798],[248.718,1016.704],[269.577,1000.319],[287.333,979.388],[307.82,962.443],[329.511,947.283],[353.191,935.027],[374.448,919.222],[391.31,896.971],[412.992,881.792],[435.761,868.179],[456.708,851.927],[474.8,831.299]]}],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.737,0.863,0.992]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":12.6,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":16.2,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":22.8,"s":[0,0],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"o":{"a":0,"k":100},"p":{"a":1,"k":[{"t":0,"s":[-352.143,-879.251],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[-352.143,-835.713],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":12.6,"s":[-309.283,-835.713],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":16.2,"s":[-337.298,-835.713],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":22.8,"s":[-311.968,-835.713],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"tr","nm":"Transform","a":{"a":1,"k":[{"t":0,"s":[0,8.281],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[0,8.281],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":12.6,"s":[0,8.281],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":16.2,"s":[0,8.281],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":22.8,"s":[0,8.281],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"o":{"a":0,"k":100},"p":{"a":1,"k":[{"t":0,"s":[-53.049,0.818],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":8.4,"s":[-53.049,0.865],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":12.6,"s":[-46.593,0.865],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":16.2,"s":[-50.813,0.865],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":22.8,"s":[-46.997,0.865],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"r":{"a":0,"k":0},"s":{"a":0,"k":[21.405,21.405]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"gr","hd":false,"nm":"์˜ค๋ฅธ์ชฝ Group","bm":0,"it":[{"ty":"gr","hd":false,"nm":"์˜ค๋ฅธ์ชฝ_์„  Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"์˜ค๋ฅธ์ชฝ_์„ ","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[635.705,859.945],[662.904,845.613],[688.895,832.899],[710.573,813.164],[738.041,802.867],[762.206,787.167],[785.172,769.519],[812.131,758.383],[836.734,743.412],[858.333,723.527],[881.999,707.018],[907.79,693.995],[931.774,677.985],[957.046,664.102],[981.051,648.133],[1006.543,634.6],[1031.006,619.389],[1057.586,600.533],[1052.635,632.802],[1059.792,661.336],[1054.412,689.87],[1051.348,718.414],[1059.173,746.948],[1054.112,775.482],[1057.187,804.016],[1052.346,832.56],[1056.588,861.094],[1055.151,889.628],[1057.606,918.162],[1059.991,946.696],[1052.495,975.24],[1057.127,1003.774],[1056.717,1032.318],[1058.873,1060.852],[1058.654,1089.396],[1058.145,1117.94],[1052.176,1146.484],[1042.664,1156.381],[1020.756,1139.143],[997.57,1123.653],[974.324,1108.242],[950.06,1094.21],[927.961,1077.231],[908.169,1057.097],[885.203,1041.307],[863.923,1023.209],[841.655,1006.461],[816.912,993.097],[796.63,973.622],[773.903,957.512],[750.069,942.901],[727.581,926.441],[706.202,908.464],[683.036,892.944],[662.844,873.179]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[635.705,817.364],[662.904,803.741],[688.895,791.657],[710.573,772.899],[738.041,763.112],[762.206,748.189],[785.172,731.415],[812.131,720.831],[836.734,706.601],[858.333,687.701],[881.999,672.009],[907.79,659.63],[931.774,644.413],[957.046,631.218],[981.051,616.039],[1006.543,603.176],[1031.006,588.719],[1057.586,570.796],[1052.635,601.467],[1059.792,628.588],[1054.412,655.71],[1051.348,682.84],[1059.173,709.961],[1054.112,737.082],[1057.187,764.203],[1052.346,791.334],[1056.588,818.455],[1055.151,845.576],[1057.606,872.697],[1059.991,899.818],[1052.495,926.949],[1057.127,954.07],[1056.717,981.201],[1058.873,1008.322],[1058.654,1035.452],[1058.145,1062.583],[1052.176,1089.714],[1042.664,1099.121],[1020.756,1082.736],[997.57,1068.013],[974.324,1053.365],[950.06,1040.028],[927.961,1023.89],[908.169,1004.753],[885.203,989.744],[863.923,972.543],[841.655,956.624],[816.912,943.922],[796.63,925.411],[773.903,910.099],[750.069,896.211],[727.581,880.567],[706.202,863.48],[683.036,848.728],[662.844,829.942]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":12.6,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[558.332,817.364],[582.22,803.741],[605.048,791.657],[624.088,772.899],[648.213,763.112],[669.436,748.189],[689.607,731.415],[713.285,720.831],[734.893,706.601],[753.864,687.701],[774.648,672.009],[797.301,659.63],[818.366,644.413],[840.562,631.218],[861.645,616.039],[884.034,603.176],[905.52,588.719],[928.865,570.796],[924.517,601.467],[930.802,628.588],[926.077,655.71],[923.386,682.84],[930.258,709.961],[925.814,737.082],[928.514,764.203],[924.262,791.334],[927.988,818.455],[926.726,845.576],[928.882,872.697],[930.977,899.818],[924.394,926.949],[928.461,954.07],[928.102,981.201],[929.995,1008.322],[929.803,1035.452],[929.356,1062.583],[924.113,1089.714],[915.759,1099.121],[896.517,1082.736],[876.153,1068.013],[855.736,1053.365],[834.426,1040.028],[815.017,1023.89],[797.634,1004.753],[777.463,989.744],[758.773,972.543],[739.215,956.624],[717.484,943.922],[699.671,925.411],[679.71,910.099],[658.776,896.211],[639.026,880.567],[620.248,863.48],[599.902,848.728],[582.168,829.942]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":16.2,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[608.906,817.364],[634.958,803.741],[659.853,791.657],[680.618,772.899],[706.928,763.112],[730.074,748.189],[752.072,731.415],[777.894,720.831],[801.461,706.601],[822.149,687.701],[844.817,672.009],[869.521,659.63],[892.494,644.413],[916.701,631.218],[939.693,616.039],[964.11,603.176],[987.543,588.719],[1013.002,570.796],[1008.26,601.467],[1015.115,628.588],[1009.962,655.71],[1007.027,682.84],[1014.522,709.961],[1009.675,737.082],[1012.619,764.203],[1007.983,791.334],[1012.046,818.455],[1010.669,845.576],[1013.021,872.697],[1015.306,899.818],[1008.126,926.949],[1012.562,954.07],[1012.17,981.201],[1014.235,1008.322],[1014.025,1035.452],[1013.537,1062.583],[1007.82,1089.714],[998.709,1099.121],[977.724,1082.736],[955.516,1068.013],[933.25,1053.365],[910.009,1040.028],[888.842,1023.89],[869.884,1004.753],[847.886,989.744],[827.503,972.543],[806.174,956.624],[782.474,943.922],[763.047,925.411],[741.278,910.099],[718.448,896.211],[696.909,880.567],[676.431,863.48],[654.241,848.728],[634.901,829.942]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":22.8,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[563.179,817.364],[587.275,803.741],[610.3,791.657],[629.506,772.899],[653.84,763.112],[675.247,748.189],[695.594,731.415],[719.477,720.831],[741.273,706.601],[760.408,687.701],[781.374,672.009],[804.222,659.63],[825.47,644.413],[847.859,631.218],[869.125,616.039],[891.708,603.176],[913.381,588.719],[936.928,570.796],[932.543,601.467],[938.883,628.588],[934.117,655.71],[931.402,682.84],[938.334,709.961],[933.851,737.082],[936.575,764.203],[932.286,791.334],[936.044,818.455],[934.771,845.576],[936.946,872.697],[939.059,899.818],[932.419,926.949],[936.522,954.07],[936.159,981.201],[938.069,1008.322],[937.875,1035.452],[937.424,1062.583],[932.136,1089.714],[923.709,1099.121],[904.3,1082.736],[883.759,1068.013],[863.165,1053.365],[841.67,1040.028],[822.093,1023.89],[804.558,1004.753],[784.212,989.744],[765.36,972.543],[745.633,956.624],[723.712,943.922],[705.745,925.411],[685.611,910.099],[664.495,896.211],[644.573,880.567],[625.633,863.48],[605.11,848.728],[587.222,829.942]]}],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]}},{"ty":"st","hd":false,"bm":0,"c":{"a":0,"k":[0.537,0.702,0.871]},"lc":2,"lj":2,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":11.13}},{"ty":"tr","nm":"Transform","a":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":12.6,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":16.2,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":22.8,"s":[0,0],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"o":{"a":0,"k":100},"p":{"a":1,"k":[{"t":0,"s":[-847.848,-878.457],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[-847.848,-834.959],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":12.6,"s":[-744.655,-834.959],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":16.2,"s":[-812.106,-834.959],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":22.8,"s":[-751.119,-834.959],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"gr","hd":false,"nm":"์˜ค๋ฅธ์ชฝ_๋ฉด Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"์˜ค๋ฅธ์ชฝ_๋ฉด","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[635.705,859.945],[662.904,845.613],[688.895,832.899],[710.573,813.164],[738.041,802.867],[762.206,787.167],[785.172,769.519],[812.131,758.383],[836.734,743.412],[858.333,723.527],[881.999,707.018],[907.79,693.995],[931.774,677.985],[957.046,664.102],[981.051,648.133],[1006.543,634.6],[1031.006,619.389],[1057.586,600.533],[1052.635,632.802],[1059.792,661.336],[1054.412,689.87],[1051.348,718.414],[1059.173,746.948],[1054.112,775.482],[1057.187,804.016],[1052.346,832.56],[1056.588,861.094],[1055.151,889.628],[1057.606,918.162],[1059.991,946.696],[1052.495,975.24],[1057.127,1003.774],[1056.717,1032.318],[1058.873,1060.852],[1058.654,1089.396],[1058.145,1117.94],[1052.176,1146.484],[1042.664,1156.381],[1020.756,1139.143],[997.57,1123.653],[974.324,1108.242],[950.06,1094.21],[927.961,1077.231],[908.169,1057.097],[885.203,1041.307],[863.923,1023.209],[841.655,1006.461],[816.912,993.097],[796.63,973.622],[773.903,957.512],[750.069,942.901],[727.581,926.441],[706.202,908.464],[683.036,892.944],[662.844,873.179]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[635.705,817.364],[662.904,803.741],[688.895,791.657],[710.573,772.899],[738.041,763.112],[762.206,748.189],[785.172,731.415],[812.131,720.831],[836.734,706.601],[858.333,687.701],[881.999,672.009],[907.79,659.63],[931.774,644.413],[957.046,631.218],[981.051,616.039],[1006.543,603.176],[1031.006,588.719],[1057.586,570.796],[1052.635,601.467],[1059.792,628.588],[1054.412,655.71],[1051.348,682.84],[1059.173,709.961],[1054.112,737.082],[1057.187,764.203],[1052.346,791.334],[1056.588,818.455],[1055.151,845.576],[1057.606,872.697],[1059.991,899.818],[1052.495,926.949],[1057.127,954.07],[1056.717,981.201],[1058.873,1008.322],[1058.654,1035.452],[1058.145,1062.583],[1052.176,1089.714],[1042.664,1099.121],[1020.756,1082.736],[997.57,1068.013],[974.324,1053.365],[950.06,1040.028],[927.961,1023.89],[908.169,1004.753],[885.203,989.744],[863.923,972.543],[841.655,956.624],[816.912,943.922],[796.63,925.411],[773.903,910.099],[750.069,896.211],[727.581,880.567],[706.202,863.48],[683.036,848.728],[662.844,829.942]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":12.6,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[558.332,817.364],[582.22,803.741],[605.048,791.657],[624.088,772.899],[648.213,763.112],[669.436,748.189],[689.607,731.415],[713.285,720.831],[734.893,706.601],[753.864,687.701],[774.648,672.009],[797.301,659.63],[818.366,644.413],[840.562,631.218],[861.645,616.039],[884.034,603.176],[905.52,588.719],[928.865,570.796],[924.517,601.467],[930.802,628.588],[926.077,655.71],[923.386,682.84],[930.258,709.961],[925.814,737.082],[928.514,764.203],[924.262,791.334],[927.988,818.455],[926.726,845.576],[928.882,872.697],[930.977,899.818],[924.394,926.949],[928.461,954.07],[928.102,981.201],[929.995,1008.322],[929.803,1035.452],[929.356,1062.583],[924.113,1089.714],[915.759,1099.121],[896.517,1082.736],[876.153,1068.013],[855.736,1053.365],[834.426,1040.028],[815.017,1023.89],[797.634,1004.753],[777.463,989.744],[758.773,972.543],[739.215,956.624],[717.484,943.922],[699.671,925.411],[679.71,910.099],[658.776,896.211],[639.026,880.567],[620.248,863.48],[599.902,848.728],[582.168,829.942]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":16.2,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[608.906,817.364],[634.958,803.741],[659.853,791.657],[680.618,772.899],[706.928,763.112],[730.074,748.189],[752.072,731.415],[777.894,720.831],[801.461,706.601],[822.149,687.701],[844.817,672.009],[869.521,659.63],[892.494,644.413],[916.701,631.218],[939.693,616.039],[964.11,603.176],[987.543,588.719],[1013.002,570.796],[1008.26,601.467],[1015.115,628.588],[1009.962,655.71],[1007.027,682.84],[1014.522,709.961],[1009.675,737.082],[1012.619,764.203],[1007.983,791.334],[1012.046,818.455],[1010.669,845.576],[1013.021,872.697],[1015.306,899.818],[1008.126,926.949],[1012.562,954.07],[1012.17,981.201],[1014.235,1008.322],[1014.025,1035.452],[1013.537,1062.583],[1007.82,1089.714],[998.709,1099.121],[977.724,1082.736],[955.516,1068.013],[933.25,1053.365],[910.009,1040.028],[888.842,1023.89],[869.884,1004.753],[847.886,989.744],[827.503,972.543],[806.174,956.624],[782.474,943.922],[763.047,925.411],[741.278,910.099],[718.448,896.211],[696.909,880.567],[676.431,863.48],[654.241,848.728],[634.901,829.942]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":22.8,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[563.179,817.364],[587.275,803.741],[610.3,791.657],[629.506,772.899],[653.84,763.112],[675.247,748.189],[695.594,731.415],[719.477,720.831],[741.273,706.601],[760.408,687.701],[781.374,672.009],[804.222,659.63],[825.47,644.413],[847.859,631.218],[869.125,616.039],[891.708,603.176],[913.381,588.719],[936.928,570.796],[932.543,601.467],[938.883,628.588],[934.117,655.71],[931.402,682.84],[938.334,709.961],[933.851,737.082],[936.575,764.203],[932.286,791.334],[936.044,818.455],[934.771,845.576],[936.946,872.697],[939.059,899.818],[932.419,926.949],[936.522,954.07],[936.159,981.201],[938.069,1008.322],[937.875,1035.452],[937.424,1062.583],[932.136,1089.714],[923.709,1099.121],[904.3,1082.736],[883.759,1068.013],[863.165,1053.365],[841.67,1040.028],[822.093,1023.89],[804.558,1004.753],[784.212,989.744],[765.36,972.543],[745.633,956.624],[723.712,943.922],[705.745,925.411],[685.611,910.099],[664.495,896.211],[644.573,880.567],[625.633,863.48],[605.11,848.728],[587.222,829.942]]}],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.737,0.863,0.992]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":12.6,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":16.2,"s":[0,0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":22.8,"s":[0,0],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"o":{"a":0,"k":100},"p":{"a":1,"k":[{"t":0,"s":[-847.848,-878.457],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[-847.848,-834.958],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":12.6,"s":[-744.655,-834.958],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":16.2,"s":[-812.106,-834.958],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":22.8,"s":[-751.119,-834.958],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"gr","hd":false,"nm":"์ข…์ด_half Group","bm":0,"it":[{"ty":"gr","hd":false,"nm":"์ข…์ด_half Group","bm":0,"it":[{"ty":"gr","hd":false,"nm":"Path 1 Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"Path 1","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[178.726,189.879],[178.692,178.952],[178.851,165.753],[176.987,152.554],[177.7,139.355],[178.929,126.156],[179.148,112.957],[179.03,99.759],[177.146,86.3],[192.092,86.928],[206.778,86.688],[221.465,87.427],[236.152,84.589],[250.82,85.678],[264.631,85.678],[280.196,86.23],[294.883,86.23],[309.569,84.7],[324.256,87.981],[338.807,86.709],[337.618,99.761],[339.184,114.46],[339.184,128.074],[338.809,141.349],[339.184,152.559],[338.57,165.758],[338.809,177.732],[338.65,191.157]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[178.726,180.477],[178.692,170.09],[178.851,157.545],[176.987,145],[177.7,132.455],[178.929,119.909],[179.148,107.364],[179.03,94.819],[177.146,82.027],[192.092,82.623],[206.778,82.396],[221.465,83.098],[236.152,80.4],[250.82,81.435],[264.631,81.435],[280.196,81.96],[294.883,81.96],[309.569,80.506],[324.256,83.625],[338.807,82.415],[337.618,94.821],[339.184,108.792],[339.184,121.732],[338.809,134.35],[339.184,145.005],[338.57,157.55],[338.809,168.931],[338.65,181.691]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":12.6,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[156.973,180.477],[156.943,170.09],[157.083,157.545],[155.446,145],[156.072,132.455],[157.151,119.909],[157.344,107.364],[157.24,94.819],[155.585,82.027],[168.712,82.623],[181.611,82.396],[194.51,83.098],[207.409,80.4],[220.293,81.435],[232.422,81.435],[246.093,81.96],[258.992,81.96],[271.891,80.506],[284.79,83.625],[297.57,82.415],[296.526,94.821],[297.901,108.792],[297.901,121.732],[297.572,134.35],[297.901,145.005],[297.362,157.55],[297.572,168.931],[297.432,181.691]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":16.2,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[171.192,180.477],[171.159,170.09],[171.311,157.545],[169.526,145],[170.209,132.455],[171.386,119.909],[171.596,107.364],[171.483,94.819],[169.678,82.027],[183.994,82.623],[198.061,82.396],[212.129,83.098],[226.196,80.4],[240.247,81.435],[253.475,81.435],[268.384,81.96],[282.452,81.96],[296.519,80.506],[310.587,83.625],[324.524,82.415],[323.385,94.821],[324.886,108.792],[324.886,121.732],[324.526,134.35],[324.886,145.005],[324.297,157.55],[324.526,168.931],[324.374,181.691]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":22.8,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[139.412,158.907],[139.386,149.762],[139.51,138.716],[138.056,127.67],[138.612,116.624],[139.571,105.579],[139.741,94.533],[139.649,83.487],[138.18,72.223],[149.838,72.749],[161.294,72.548],[172.75,73.166],[184.206,70.791],[195.648,71.702],[206.421,71.702],[218.562,72.164],[230.018,72.164],[241.474,70.884],[252.93,73.63],[264.28,72.565],[263.353,83.489],[264.575,95.79],[264.575,107.184],[264.282,118.293],[264.575,127.675],[264.096,138.72],[264.282,148.741],[264.158,159.976]]}],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[1,1,1]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":1,"k":[{"t":0,"s":[0.05,39.754],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[0.05,37.786],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":12.6,"s":[0.044,37.786],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":16.2,"s":[0.048,37.786],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":22.8,"s":[0.039,33.27],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"o":{"a":0,"k":100},"p":{"a":1,"k":[{"t":0,"s":[0.05,39.754],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[0.05,37.786],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":12.6,"s":[0.044,37.786],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":16.2,"s":[0.048,37.786],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":22.8,"s":[0.039,33.27],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"tr","nm":"Transform","a":{"a":1,"k":[{"t":0,"s":[240.247,129.182],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[240.247,122.354],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":12.6,"s":[208.835,122.354],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":16.2,"s":[229.367,122.354],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":22.8,"s":[183.476,106.693],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"o":{"a":0,"k":100},"p":{"a":1,"k":[{"t":0,"s":[-17.839,1.227],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[-17.839,0.736],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":12.6,"s":[-17.839,0.736],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":16.2,"s":[-17.839,0.736],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":22.8,"s":[-17.839,-0.391],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"tr","nm":"Transform","a":{"a":1,"k":[{"t":0,"s":[-17.839,6.411],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[-17.839,6.276],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":12.6,"s":[-17.839,6.276],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":16.2,"s":[-17.839,6.276],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":17.4,"s":[-17.839,5.966],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":22.8,"s":[-17.839,5.966],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"o":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":17.4,"s":[0],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":22.8,"s":[100],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"p":{"a":1,"k":[{"t":0,"s":[-333.819,-65.91],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":8.4,"s":[-333.819,-61.748],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":12.6,"s":[-303.869,-61.748],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":16.2,"s":[-323.445,-61.748],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":17.4,"s":[-323.445,29.045],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":22.8,"s":[-305.745,29.045],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"r":{"a":0,"k":0},"s":{"a":0,"k":[491.888,491.888]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"gr","hd":false,"nm":"๋’ท๋ฉดbg Group","bm":0,"it":[{"ty":"sh","hd":false,"nm":"๋’ท๋ฉดbg","d":1,"ks":{"a":1,"k":[{"t":0,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[91.974,-56.459],[91.974,56.459],[-91.974,56.459],[-91.974,-56.459]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[91.974,-53.664],[91.974,53.664],[-91.974,53.664],[-91.974,-53.664]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":12.6,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[80.779,-53.664],[80.779,53.664],[-80.779,53.664],[-80.779,-53.664]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":16.2,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[88.096,-53.664],[88.096,53.664],[-88.096,53.664],[-88.096,-53.664]]}],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":22.8,"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[81.481,-53.664],[81.481,53.664],[-81.481,53.664],[-81.481,-53.664]]}],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.537,0.702,0.871]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":1,"k":[{"t":0,"s":[-250.023,8.152],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":8.4,"s":[-250.023,7.748],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":12.6,"s":[-219.592,7.748],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":16.2,"s":[-239.483,7.748],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":22.8,"s":[-221.499,7.748],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"r":{"a":0,"k":0},"s":{"a":0,"k":[491.888,491.888]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"tr","nm":"Transform","a":{"a":1,"k":[{"t":0,"s":[37.148,35.924],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":8.4,"s":[37.148,35.924],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":12.6,"s":[37.148,35.924],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":16.2,"s":[37.148,35.924],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":22.8,"s":[37.148,35.924],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"o":{"a":0,"k":100},"p":{"a":1,"k":[{"t":0,"s":[61.01,6.565],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":8.4,"s":[61.01,6.62],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":12.6,"s":[54.552,6.62],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":16.2,"s":[58.774,6.62],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25},"ti":[0,0],"to":[0,0]},{"t":22.8,"s":[54.957,6.62],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"r":{"a":0,"k":0},"s":{"a":0,"k":[21.405,21.405]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]},{"ddd":0,"ind":18,"ty":4,"nm":"์ข…์ด_์ „์ฒด","hd":true,"sr":1,"ks":{"a":{"a":0,"k":[0,2.728]},"o":{"a":0,"k":100},"p":{"a":0,"k":[187.5,337.03]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":37,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":22,"ty":0,"nm":"to","parent":18,"hd":true,"sr":1,"ks":{"a":{"a":0,"k":[38.5,28.5]},"o":{"a":0,"k":100},"p":{"a":0,"k":[-34,-89.302]},"r":{"a":0,"k":0},"s":{"a":0,"k":[55.541,55.541]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":37,"st":0,"bm":0,"h":57,"refId":"el-159-_-Uo","w":77},{"ddd":0,"ind":23,"ty":4,"nm":"์ข…์ด_์ „์ฒด","hd":true,"sr":1,"ks":{"a":{"a":0,"k":[0,2.728]},"o":{"a":0,"k":100},"p":{"a":0,"k":[187.5,337.03]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":37,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":true,"nm":"์ข…์ด_์ „์ฒด Group","bm":0,"it":[{"ty":"gr","hd":true,"nm":"Path 1 Group","bm":0,"it":[{"ty":"sh","hd":true,"nm":"Path 1","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[339.825,284.126],[324.871,284.404],[310.156,284.126],[295.442,283.591],[280.727,283.203],[266.03,285.436],[251.314,286.009],[237.23,286.009],[221.882,285.325],[207.168,284.255],[192.453,285.105],[178.433,284.237],[177.3,271.686],[178.433,258.47],[179.367,245.255],[177.759,232.039],[177.759,220.928],[177.938,205.608],[178.435,192.393],[179.031,179.177],[179.19,165.962],[177.323,152.746],[178.037,139.531],[179.268,126.315],[179.488,113.1],[179.369,99.884],[177.482,86.409],[192.456,87.037],[207.17,86.798],[221.885,87.537],[236.599,84.696],[251.296,85.786],[265.132,85.786],[280.727,86.338],[295.442,86.338],[310.156,84.807],[324.871,88.092],[339.449,86.818],[338.258,99.887],[339.827,114.604],[339.827,128.236],[339.451,141.527],[339.827,152.751],[339.211,165.967],[339.451,177.956],[339.292,192.398],[339.133,205.613],[339.769,218.829],[338.916,232.044],[338.24,245.26],[339.83,260.004],[339.83,284.131]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[1,1,1]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[258.565,185.352]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,9.931]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]},{"ddd":0,"ind":26,"ty":4,"nm":"์ข…์ด_์ „์ฒด","hd":true,"sr":1,"ks":{"a":{"a":0,"k":[0,2.728]},"o":{"a":0,"k":100},"p":{"a":0,"k":[187.5,337.03]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":37,"st":0,"bm":0,"shapes":[]},{"ddd":0,"ind":27,"ty":4,"nm":"๋’ท๋ฉด","hd":true,"sr":1,"ks":{"a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[35.002,80.037]},"r":{"a":0,"k":0},"s":{"a":0,"k":[25.311,25.311]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":37,"st":0,"bm":0,"shapes":[{"ty":"sh","hd":true,"nm":"๋’ท๋ฉด","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[1058.32,594.37],[1057.48,622.15],[1057.34,649.5],[1058.3,676.85],[1057.04,704.21],[1059.07,731.56],[1056.29,758.92],[1057.99,786.27],[1057.43,813.63],[1055.89,840.99],[1059.2,868.34],[1057.45,895.69],[1057.22,923.05],[1058.97,950.4],[1056.85,977.76],[1058.41,1005.12],[1056.57,1032.48],[1059.58,1059.84],[1057.82,1087.2],[1058.23,1114.57],[1046.38,1143.91],[1019.81,1158.06],[991.87,1157.56],[963.95,1159.39],[936.02,1158.26],[908.09,1160.52],[880.16,1158.63],[852.23,1160.65],[824.31,1161.22],[796.38,1161.03],[768.44,1158.15],[740.51,1157.24],[712.58,1160.4],[684.65,1158.68],[656.72,1160.43],[628.79,1161.27],[600.86,1157.92],[572.92,1161.33],[544.99,1161.39],[517.06,1160.13],[489.13,1161.39],[461.19,1158.47],[433.26,1161.08],[405.33,1158.52],[377.4,1159.73],[349.46,1160.71],[321.53,1161.44],[293.59,1157.4],[265.66,1161.14],[237.72,1159.04],[209.78,1159.17],[181.84,1160.67],[153.67,1145.14],[142.48,1114.57],[144.38,1087.21],[146,1059.86],[145.06,1032.51],[142.89,1005.15],[142.66,977.8],[146.25,950.44],[142.31,923.09],[142.52,895.73],[145.53,868.37],[142.74,841.02],[143.73,813.67],[145.36,786.31],[144.83,758.96],[142.47,731.6],[145.87,704.24],[142.63,676.88],[146,649.52],[145.79,622.16],[144.5,595],[166.34,578.64],[189.51,563.89],[213.91,551],[235.73,534.22],[258.06,518.2],[281.66,504.11],[304.11,488.29],[326.82,472.84],[350.12,458.29],[373.46,443.82],[395.37,427.17],[417.69,411.13],[441.72,397.68],[464.47,382.29],[487.66,367.58],[511.03,353.12],[534.44,338.72],[556.65,322.51],[585.3,310.61],[616.16,311.53],[644.51,323.23],[668.37,336.94],[691.71,351.43],[713.81,367.79],[737.31,382.03],[760.76,396.36],[783,412.51],[805.26,428.62],[829.47,441.79],[852.31,457.04],[874.08,473.9],[897.34,488.5],[919.09,505.39],[942.45,519.86],[965.45,534.87],[987.69,551.03],[1011.76,564.42],[1034.79,579.38]]}}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.537,0.702,0.871]},"r":1,"o":{"a":0,"k":100}}]},{"ddd":0,"ind":28,"ty":4,"nm":"Screen","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[163.5,90]},"o":{"a":0,"k":100},"p":{"a":0,"k":[163.5,90]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":37,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Screen Group","bm":0,"it":[{"ty":"rc","hd":false,"nm":"Screen","d":1,"p":{"a":0,"k":[163.5,90]},"r":{"a":0,"k":0},"s":{"a":0,"k":[327,180]}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[0.142,0.142,0.142]},"r":1,"o":{"a":0,"k":0}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}],"meta":{"g":"@phase-software/lottie-exporter 0.7.0"},"nm":"","op":36,"v":"5.6.0","w":327} \ No newline at end of file diff --git a/data/src/main/java/com/yapp/data/local/di/MediaModule.kt b/core/media/src/main/java/com/yapp/media/di/MediaModule.kt similarity index 64% rename from data/src/main/java/com/yapp/data/local/di/MediaModule.kt rename to core/media/src/main/java/com/yapp/media/di/MediaModule.kt index 1b9e9168..ff75332b 100644 --- a/data/src/main/java/com/yapp/data/local/di/MediaModule.kt +++ b/core/media/src/main/java/com/yapp/media/di/MediaModule.kt @@ -1,9 +1,8 @@ -package com.yapp.data.local.di +package com.yapp.media.di import android.content.ContentResolver import android.content.Context -import com.yapp.data.local.datasource.ImageLocalDataSource -import com.yapp.data.local.datasource.ImageLocalDataSourceImpl +import com.yapp.media.storage.ImageSaver import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -23,7 +22,9 @@ object MediaModule { @Provides @Singleton - fun provideImageLocalDataSource(contentResolver: ContentResolver): ImageLocalDataSource { - return ImageLocalDataSourceImpl(contentResolver) + fun provideImageSaver( + contentResolver: ContentResolver, + ): ImageSaver { + return ImageSaver(contentResolver) } } diff --git a/data/src/main/java/com/yapp/data/local/datasource/ImageLocalDataSourceImpl.kt b/core/media/src/main/java/com/yapp/media/storage/ImageSaver.kt similarity index 76% rename from data/src/main/java/com/yapp/data/local/datasource/ImageLocalDataSourceImpl.kt rename to core/media/src/main/java/com/yapp/media/storage/ImageSaver.kt index 6b1a324f..0c1d98d8 100644 --- a/data/src/main/java/com/yapp/data/local/datasource/ImageLocalDataSourceImpl.kt +++ b/core/media/src/main/java/com/yapp/media/storage/ImageSaver.kt @@ -1,4 +1,4 @@ -package com.yapp.data.local.datasource +package com.yapp.media.storage import android.content.ContentResolver import android.content.ContentValues @@ -9,11 +9,11 @@ import android.util.Log import java.io.IOException import javax.inject.Inject -class ImageLocalDataSourceImpl @Inject constructor( +class ImageSaver @Inject constructor( private val contentResolver: ContentResolver, -) : ImageLocalDataSource { +) { - override suspend fun saveImage(byteArray: ByteArray, fileName: String): Boolean { + fun saveImage(byteArray: ByteArray, fileName: String): Boolean { return try { val bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size) @@ -32,10 +32,10 @@ class ImageLocalDataSourceImpl @Inject constructor( true } catch (e: SecurityException) { - Log.e("ImageLocalDataSource", "๊ถŒํ•œ ์—†์Œ: ${e.message}") + Log.e("ImageSaver", "๊ถŒํ•œ ์—†์Œ: ${e.message}") false } catch (e: IOException) { - Log.e("ImageLocalDataSource", "ํŒŒ์ผ ์ €์žฅ ์‹คํŒจ: ${e.message}") + Log.e("ImageSaver", "ํŒŒ์ผ ์ €์žฅ ์‹คํŒจ: ${e.message}") false } } diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index b15a325a..beabc390 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -11,7 +11,6 @@ android { } dependencies { - implementation(projects.core.datastore) implementation(projects.core.common) implementation(platform(libs.okhttp.bom)) implementation(libs.okhttp.logging) diff --git a/core/network/src/main/java/com/yapp/network/TokenRefreshService.kt b/core/network/src/main/java/com/yapp/network/TokenRefreshService.kt deleted file mode 100644 index db8def42..00000000 --- a/core/network/src/main/java/com/yapp/network/TokenRefreshService.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.yapp.network - -import com.yapp.network.model.BaseResponse -import com.yapp.network.model.ResponseAuthRefreshDto -import retrofit2.http.Header -import retrofit2.http.POST - -interface TokenRefreshService { - @POST("/$API/$VERSION/$AUTH/$REISSUE") - suspend fun postAuthRefresh( - @Header("refreshToken") refreshToken: String, - ): BaseResponse - - companion object { - const val API = "api" - const val VERSION = "v1" - const val AUTH = "auth" - const val REISSUE = "reissue" - } -} diff --git a/core/network/src/main/java/com/yapp/network/authenticator/AuthenticationIntercept.kt b/core/network/src/main/java/com/yapp/network/authenticator/AuthenticationIntercept.kt deleted file mode 100644 index 6051ebd9..00000000 --- a/core/network/src/main/java/com/yapp/network/authenticator/AuthenticationIntercept.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.yapp.network.authenticator - -import com.yapp.datastore.token.TokenDataStore -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.runBlocking -import okhttp3.Interceptor -import okhttp3.Request -import okhttp3.Response -import javax.inject.Inject - -class AuthenticationIntercept @Inject constructor( - private val datastore: TokenDataStore, -) : Interceptor { - - override fun intercept(chain: Interceptor.Chain): Response { - val originalRequest = chain.request() - val authRequest = originalRequest.addAuthorizationHeader() - return chain.proceed(authRequest) - } - - private fun Request.addAuthorizationHeader(): Request { - val accessToken = runBlocking { datastore.token.first().accessToken } - return this.newBuilder() - .addHeader("Authorization", "Bearer $accessToken") - .build() - } -} diff --git a/core/network/src/main/java/com/yapp/network/authenticator/OrbitAuthenticator.kt b/core/network/src/main/java/com/yapp/network/authenticator/OrbitAuthenticator.kt deleted file mode 100644 index 9183a058..00000000 --- a/core/network/src/main/java/com/yapp/network/authenticator/OrbitAuthenticator.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.yapp.network.authenticator - -import android.content.Context -import com.jakewharton.processphoenix.ProcessPhoenix -import com.yapp.datastore.token.TokenDataStore -import com.yapp.network.TokenRefreshService -import com.yapp.network.model.ResponseAuthRefreshDto -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.runBlocking -import okhttp3.Authenticator -import okhttp3.Request -import okhttp3.Response -import okhttp3.Route -import javax.inject.Inject - -class OrbitAuthenticator @Inject constructor( - private val dataStore: TokenDataStore, - private val tokenRefreshService: TokenRefreshService, - @ApplicationContext private val context: Context, -) : Authenticator { - - override fun authenticate(route: Route?, response: Response): Request? { - if (response.code == CODE_TOKEN_EXPIRED) { - return handleTokenExpiration(response) - } - return null - } - - private fun handleTokenExpiration(response: Response): Request? { - val newTokens = refreshTokens() - return newTokens?.let { - response.request.newBuilder() - .header("Authorization", "Bearer ${it.accessToken}") - .build() - } - } - - private fun refreshTokens(): ResponseAuthRefreshDto? { - return runCatching { - runBlocking { - val refreshToken = dataStore.token.first().refreshToken - tokenRefreshService.postAuthRefresh(refreshToken).data - } - }.onSuccess { newToken -> - runBlocking { - newToken?.let { - dataStore.setAccessToken(it.accessToken) - } - } - }.onFailure { - handleTokenRefreshFailure() - }.getOrNull() - } - - private fun handleTokenRefreshFailure() { - runBlocking { dataStore.setAutoLogin(false) } - ProcessPhoenix.triggerRebirth(context) - } - - companion object { - const val CODE_TOKEN_EXPIRED = 401 - } -} diff --git a/core/network/src/main/java/com/yapp/network/di/NetworkModule.kt b/core/network/src/main/java/com/yapp/network/di/NetworkModule.kt index 3435cb09..25dfb118 100644 --- a/core/network/src/main/java/com/yapp/network/di/NetworkModule.kt +++ b/core/network/src/main/java/com/yapp/network/di/NetworkModule.kt @@ -1,15 +1,11 @@ package com.yapp.network.di import com.yapp.common.buildconfig.BuildConfigFieldProvider -import com.yapp.network.TokenRefreshService -import com.yapp.network.authenticator.AuthenticationIntercept -import com.yapp.network.authenticator.OrbitAuthenticator import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import kotlinx.serialization.json.Json -import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor @@ -22,11 +18,6 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) object NetworkModule { - @Provides - @Singleton - fun provideTokenRefreshService(@NoneAuth retrofit: Retrofit) = - retrofit.create(TokenRefreshService::class.java) - @Provides @Singleton fun provideLoggingInterceptor( @@ -47,23 +38,7 @@ object NetworkModule { @Provides @Singleton - @Auth - fun provideAuthOkHttpClient( - loggingInterceptor: HttpLoggingInterceptor, - authInterceptor: Interceptor, - authenticator: OrbitAuthenticator, - ): OkHttpClient = - OkHttpClient.Builder() - .retryOnConnectionFailure(true) - .addInterceptor(loggingInterceptor) - .addInterceptor(authInterceptor) - .authenticator(authenticator) - .build() - - @Provides - @Singleton - @NoneAuth - fun provideNoneAuthOkHttpClient( + fun provideHttpClient( loggingInterceptor: HttpLoggingInterceptor, ): OkHttpClient = OkHttpClient.Builder() @@ -76,18 +51,8 @@ object NetworkModule { @Provides @Singleton - @Auth - fun provideAuthRetrofit(@Auth okHttpClient: OkHttpClient, buildConfigFieldProvider: BuildConfigFieldProvider): Retrofit = Retrofit.Builder() - .addConverterFactory(Json.asConverterFactory("application/json".toMediaType())) - .baseUrl(buildConfigFieldProvider.get().baseUrl) - .client(okHttpClient) - .build() - - @Provides - @Singleton - @NoneAuth - fun provideNoneAuthRetrofit( - @NoneAuth okHttpClient: OkHttpClient, + fun provideRetrofit( + okHttpClient: OkHttpClient, buildConfigFieldProvider: BuildConfigFieldProvider, json: Json, ): Retrofit = @@ -96,18 +61,4 @@ object NetworkModule { .baseUrl(buildConfigFieldProvider.get().baseUrl) .client(okHttpClient) .build() - - @Provides - @Singleton - @S3 - fun provideS3Retrofit(@NoneAuth okHttpClient: OkHttpClient, buildConfigFieldProvider: BuildConfigFieldProvider): Retrofit = - Retrofit.Builder() - .addConverterFactory(Json.asConverterFactory("application/json".toMediaType())) - .baseUrl(buildConfigFieldProvider.get().baseUrl) - .client(okHttpClient) - .build() - - @Provides - @Singleton - fun provideAuthInterceptor(interceptor: AuthenticationIntercept): Interceptor = interceptor } diff --git a/core/network/src/main/java/com/yapp/network/di/Qualifier.kt b/core/network/src/main/java/com/yapp/network/di/Qualifier.kt deleted file mode 100644 index 68817100..00000000 --- a/core/network/src/main/java/com/yapp/network/di/Qualifier.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.yapp.network.di - -import javax.inject.Qualifier - -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class NoneAuth - -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class Auth - -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class S3 diff --git a/data/src/main/java/com/yapp/data/remote/utils/ApiError.kt b/core/network/src/main/java/com/yapp/network/model/ApiError.kt similarity index 67% rename from data/src/main/java/com/yapp/data/remote/utils/ApiError.kt rename to core/network/src/main/java/com/yapp/network/model/ApiError.kt index 947ef498..6bbcb596 100644 --- a/data/src/main/java/com/yapp/data/remote/utils/ApiError.kt +++ b/core/network/src/main/java/com/yapp/network/model/ApiError.kt @@ -1,4 +1,4 @@ -package com.yapp.data.remote.utils +package com.yapp.network.model data class ApiError( override val message: String, diff --git a/core/network/src/main/java/com/yapp/network/model/ResponseAuthRefreshDto.kt b/core/network/src/main/java/com/yapp/network/model/ResponseAuthRefreshDto.kt deleted file mode 100644 index d45c731a..00000000 --- a/core/network/src/main/java/com/yapp/network/model/ResponseAuthRefreshDto.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.yapp.network.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class ResponseAuthRefreshDto( - @SerialName("accessToken") - val accessToken: String, -) diff --git a/core/network/src/main/java/com/yapp/network/utils/ApiCallUtils.kt b/core/network/src/main/java/com/yapp/network/utils/ApiCallUtils.kt new file mode 100644 index 00000000..e80dc731 --- /dev/null +++ b/core/network/src/main/java/com/yapp/network/utils/ApiCallUtils.kt @@ -0,0 +1,33 @@ +package com.yapp.network.utils + +import com.yapp.network.model.ApiError +import kotlinx.coroutines.CancellationException +import retrofit2.HttpException +import java.io.IOException + +inline fun safeApiCall(action: () -> T): Result { + return try { + Result.success(action()) + } catch (exception: Throwable) { + if (exception is CancellationException) throw exception + + val mappedException = when (exception) { + is HttpException -> mapHttpException(exception) + is IOException -> ApiError("๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ ๋ฐœ์ƒ") + else -> ApiError("์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜ ๋ฐœ์ƒ") + } + + Result.failure(mappedException) + } +} + +fun mapHttpException(exception: HttpException): ApiError { + return when (exception.code()) { + 400 -> ApiError("์ž˜๋ชป๋œ ์š”์ฒญ") + 401 -> ApiError("์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค") + 403 -> ApiError("๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค") + 404 -> ApiError("์š”์ฒญํ•œ ๋ฆฌ์†Œ์Šค๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค") + in 500..599 -> ApiError("์„œ๋ฒ„ ์˜ค๋ฅ˜") + else -> ApiError("์•Œ ์ˆ˜ ์—†๋Š” ์„œ๋ฒ„ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.") + } +} diff --git a/core/security/build.gradle.kts b/core/security/build.gradle.kts deleted file mode 100644 index f727009c..00000000 --- a/core/security/build.gradle.kts +++ /dev/null @@ -1,14 +0,0 @@ -import com.yapp.convention.setNamespace - -plugins { - id("orbit.android.library") - id("orbit.android.hilt") -} - -android { - setNamespace("core.security") -} - -dependencies { - implementation(projects.core.common) -} diff --git a/core/security/src/main/AndroidManifest.xml b/core/security/src/main/AndroidManifest.xml deleted file mode 100644 index 8bdb7e14..00000000 --- a/core/security/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/core/security/src/main/java/com/yapp/security/CryptoManagerImpl.kt b/core/security/src/main/java/com/yapp/security/CryptoManagerImpl.kt deleted file mode 100644 index c21d2810..00000000 --- a/core/security/src/main/java/com/yapp/security/CryptoManagerImpl.kt +++ /dev/null @@ -1,88 +0,0 @@ -package com.yapp.security - -import android.security.keystore.KeyGenParameterSpec -import android.security.keystore.KeyProperties.BLOCK_MODE_GCM -import android.security.keystore.KeyProperties.ENCRYPTION_PADDING_NONE -import android.security.keystore.KeyProperties.KEY_ALGORITHM_AES -import android.security.keystore.KeyProperties.PURPOSE_DECRYPT -import android.security.keystore.KeyProperties.PURPOSE_ENCRYPT -import com.yapp.common.security.CryptoManager -import java.security.KeyStore -import javax.crypto.Cipher -import javax.crypto.KeyGenerator -import javax.crypto.SecretKey -import javax.crypto.spec.GCMParameterSpec -import javax.inject.Inject - -class CryptoManagerImpl @Inject constructor() : CryptoManager { - - private val provider = "AndroidKeyStore" - private val charset = Charsets.UTF_8 - - private val cipher: Cipher by lazy { Cipher.getInstance("AES/GCM/NoPadding") } - private val keyStore: KeyStore by lazy { KeyStore.getInstance(provider).apply { load(null) } } - private val keyGenerator: KeyGenerator by lazy { KeyGenerator.getInstance(KEY_ALGORITHM_AES, provider) } - - /** - * ๋ฐ์ดํ„ฐ ์•”ํ˜ธํ™”. - * @param keyAlias ํ‚ค ๋ณ„์นญ - * @param text ์•”ํ˜ธํ™”ํ•  ํ…์ŠคํŠธ - * @return ์•”ํ˜ธํ™”๋œ ๋ฐ์ดํ„ฐ + ์ดˆ๊ธฐํ™” ๋ฒกํ„ฐ(IV) - */ - override fun encryptData(keyAlias: String, text: String): Pair { - val secretKey = getOrCreateSecretKey(keyAlias) - cipher.init(Cipher.ENCRYPT_MODE, secretKey) - return cipher.doFinal(text.toByteArray(charset)) to cipher.iv - } - - /** - * ๋ฐ์ดํ„ฐ๋ฅผ ๋ณตํ˜ธํ™”. - * @param keyAlias ํ‚ค ๋ณ„์นญ - * @param encryptedData ์•”ํ˜ธํ™”๋œ ๋ฐ์ดํ„ฐ - * @param iv ์ดˆ๊ธฐํ™” ๋ฒกํ„ฐ(IV) - * @return ๋ณตํ˜ธํ™”๋œ ๋ฐ์ดํ„ฐ - */ - override fun decryptData(keyAlias: String, encryptedData: ByteArray, iv: ByteArray): ByteArray { - val secretKey = getSecretKey(keyAlias) - cipher.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(128, iv)) - return cipher.doFinal(encryptedData) - } - - /** - * ํ‚ค๊ฐ€ ์—†์œผ๋ฉด ์ƒ์„ฑํ•˜๊ณ , ์ด๋ฏธ ์กด์žฌํ•˜๋ฉด ๊ฐ€์ ธ์˜ด. - * @param keyAlias ํ‚ค ๋ณ„์นญ - * @return SecretKey - */ - private fun getOrCreateSecretKey(keyAlias: String): SecretKey = - keyStore.getSecretKeyOrNull(keyAlias) ?: generateSecretKey(keyAlias) - - /** - * ์ƒˆ๋กœ์šด SecretKey๋ฅผ ์ƒ์„ฑ. - * @param keyAlias ํ‚ค ๋ณ„์นญ - * @return SecretKey - */ - private fun generateSecretKey(keyAlias: String): SecretKey { - val parameterSpec = KeyGenParameterSpec.Builder(keyAlias, PURPOSE_ENCRYPT or PURPOSE_DECRYPT) - .apply { - setBlockModes(BLOCK_MODE_GCM) - setEncryptionPaddings(ENCRYPTION_PADDING_NONE) - }.build() - keyGenerator.init(parameterSpec) - return keyGenerator.generateKey() - } - - /** - * KeyStore์—์„œ SecretKey ๊ฐ€์ ธ์˜ค๊ธฐ. - * @param keyAlias ํ‚ค ๋ณ„์นญ - * @return SecretKey - */ - private fun getSecretKey(keyAlias: String): SecretKey = - keyStore.getSecretKeyOrNull(keyAlias) - ?: throw IllegalStateException("SecretKey for alias $keyAlias does not exist") - - /** - * ํ‚ค ์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด null ๋ฐ˜ํ™˜. - */ - private fun KeyStore.getSecretKeyOrNull(keyAlias: String): SecretKey? = - (getEntry(keyAlias, null) as? KeyStore.SecretKeyEntry)?.secretKey -} diff --git a/core/security/src/main/java/com/yapp/security/di/SecurityModule.kt b/core/security/src/main/java/com/yapp/security/di/SecurityModule.kt deleted file mode 100644 index 3e8a6c99..00000000 --- a/core/security/src/main/java/com/yapp/security/di/SecurityModule.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.yapp.security.di - -import com.yapp.common.security.CryptoManager -import com.yapp.security.CryptoManagerImpl -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -abstract class SecurityModule { - @Singleton - @Binds - abstract fun bindsCryptoManager(cryptoManagerImpl: CryptoManagerImpl): CryptoManager -} diff --git a/core/ui/src/main/java/com/yapp/ui/base/BaseViewModel.kt b/core/ui/src/main/java/com/yapp/ui/base/BaseViewModel.kt deleted file mode 100644 index 52bf9a30..00000000 --- a/core/ui/src/main/java/com/yapp/ui/base/BaseViewModel.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.yapp.ui.base - -import androidx.lifecycle.ViewModel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.catch -import org.orbitmvi.orbit.ContainerHost -import org.orbitmvi.orbit.syntax.simple.intent -import org.orbitmvi.orbit.syntax.simple.postSideEffect -import org.orbitmvi.orbit.syntax.simple.reduce -import org.orbitmvi.orbit.viewmodel.container - -abstract class BaseViewModel( - initialState: UI_STATE, -) : ViewModel(), ContainerHost { - - override val container = container(initialState) - val currentState: UI_STATE - get() = container.stateFlow.value - - /** - * UI ์ƒํƒœ ์—…๋ฐ์ดํŠธ - * @param reducer ํ˜„์žฌ ์ƒํƒœ๋ฅผ ์ˆ˜์ •ํ•˜๋Š” ๋žŒ๋‹ค์‹ - */ - protected fun updateState(reducer: UI_STATE.() -> UI_STATE) = intent { - reduce { reducer(state) } - } - - /** - * ๋‹จ์ผ ๋ถ€์ˆ˜ ํšจ๊ณผ ์ „๋‹ฌ - * @param effect ์ „๋‹ฌํ•  ๋ถ€์ˆ˜ ํšจ๊ณผ - */ - protected fun emitSideEffect(effect: SIDE_EFFECT) = intent { - postSideEffect(effect) - } - - /** - * ์—ฌ๋Ÿฌ ๋ถ€์ˆ˜ ํšจ๊ณผ ์ „๋‹ฌ - * @param effects ์ „๋‹ฌํ•  ๋ถ€์ˆ˜ ํšจ๊ณผ ๋ฆฌ์ŠคํŠธ - */ - protected fun emitSideEffects(vararg effects: SIDE_EFFECT) = intent { - effects.forEach { postSideEffect(it) } - } - - /** - * Flow ๊ตฌ๋…ํ•˜๊ณ  ์ƒํƒœ ์—…๋ฐ์ดํŠธ or ๋ถ€์ˆ˜ ํšจ๊ณผ ์ฒ˜๋ฆฌ - * @param flow ๊ตฌ๋…ํ•  Flow - * @param onEach ๊ฐ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ๋กœ์ง - * @param onError ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋กœ์ง - */ - protected fun collectFlow( - flow: Flow, - onEach: (T) -> Unit, - onError: ((Throwable) -> Unit)? = null, - ) = intent { - flow.catch { onError?.invoke(it) } - .collect { onEach(it) } - } - - /** - * ๋น„๋™๊ธฐ ์ž‘์—… ์ˆ˜ํ–‰ํ•˜๊ณ  ์ƒํƒœ ์—…๋ฐ์ดํŠธ or ๋ถ€์ˆ˜ ํšจ๊ณผ ์ฒ˜๋ฆฌ - * @param block ์‹คํ–‰ํ•  suspend ๋ธ”๋ก - * @param onError ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋กœ์ง (์˜ต์…˜) - */ - protected fun launchWithErrorHandler( - block: suspend () -> Unit, - onError: ((Throwable) -> Unit)? = null, - ) = intent { - kotlin.runCatching { - block() - }.onFailure { onError?.invoke(it) } - } -} diff --git a/core/ui/src/main/java/com/yapp/ui/component/bottomsheet/BottomSheetContent.kt b/core/ui/src/main/java/com/yapp/ui/component/bottomsheet/BottomSheetContent.kt new file mode 100644 index 00000000..23dba597 --- /dev/null +++ b/core/ui/src/main/java/com/yapp/ui/component/bottomsheet/BottomSheetContent.kt @@ -0,0 +1,6 @@ +package com.yapp.ui.component.bottomsheet + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.runtime.Composable + +typealias BottomSheetContent = @Composable BoxScope.() -> Unit diff --git a/core/ui/src/main/java/com/yapp/ui/component/OrbitBottomSheet.kt b/core/ui/src/main/java/com/yapp/ui/component/bottomsheet/OrbitBottomSheetLayout.kt similarity index 69% rename from core/ui/src/main/java/com/yapp/ui/component/OrbitBottomSheet.kt rename to core/ui/src/main/java/com/yapp/ui/component/bottomsheet/OrbitBottomSheetLayout.kt index baf7e50b..ff048441 100644 --- a/core/ui/src/main/java/com/yapp/ui/component/OrbitBottomSheet.kt +++ b/core/ui/src/main/java/com/yapp/ui/component/bottomsheet/OrbitBottomSheetLayout.kt @@ -1,22 +1,19 @@ -package com.yapp.ui.component +package com.yapp.ui.component.bottomsheet import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +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.shape.RoundedCornerShape +import androidx.compose.material.ModalBottomSheetLayout import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.SheetState import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope -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.geometry.Rect @@ -33,42 +30,31 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable -fun OrbitBottomSheet( +fun OrbitBottomSheetLayout( modifier: Modifier = Modifier, - sheetState: SheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = true, - ), - isSheetOpen: Boolean, - onDismissRequest: () -> Unit = {}, + sheetState: OrbitBottomSheetState, shape: Shape = RoundedCornerShape(topStart = 30.dp, topEnd = 30.dp), containerColor: Color = OrbitTheme.colors.gray_800, strokeColor: Color = OrbitTheme.colors.gray_700, strokeThickness: Dp = 1.dp, content: @Composable () -> Unit, ) { - val scope = rememberCoroutineScope() - if (isSheetOpen) { - ModalBottomSheet( - modifier = modifier, - sheetState = sheetState, - shape = shape, - onDismissRequest = { - scope.launch { - sheetState.hide() - onDismissRequest() - } - }, - containerColor = containerColor, - dragHandle = null, - ) { + ModalBottomSheetLayout( + modifier = modifier.navigationBarsPadding(), + sheetState = sheetState.state, + sheetShape = shape, + sheetBackgroundColor = containerColor, + sheetContent = { Box { - content() + sheetState.content?.invoke(this) BottomSheetTopRoundedStroke( strokeColor = strokeColor, strokeThickness = strokeThickness, ) } - } + }, + ) { + content() } } @@ -139,43 +125,31 @@ fun BottomSheetTopRoundedStroke( @Preview @Composable fun OrbitBottomSheetPreview() { - var isSheetOpen by rememberSaveable { mutableStateOf(true) } - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val sheetState = rememberOrbitBottomSheetState() val scope = rememberCoroutineScope() OrbitTheme { - Button( - onClick = { - scope.launch { - sheetState.show() - }.invokeOnCompletion { - if (!isSheetOpen) { - isSheetOpen = true - } - } - }, - ) { - Text("Toggle Bottom Sheet") - } - - OrbitBottomSheet( - isSheetOpen = isSheetOpen, + OrbitBottomSheetLayout( sheetState = sheetState, - onDismissRequest = { isSheetOpen = !isSheetOpen }, content = { Box( modifier = Modifier - .fillMaxWidth() - .height(600.dp), + .fillMaxSize() + .background(color = OrbitTheme.colors.white), contentAlignment = Alignment.Center, ) { Button( onClick = { scope.launch { - sheetState.hide() - }.invokeOnCompletion { - if (isSheetOpen) { - isSheetOpen = false + sheetState.show { + Box( + modifier = Modifier + .fillMaxWidth() + .height(500.dp), + contentAlignment = Alignment.Center, + ) { + Text("This is a bottom sheet content") + } } } }, diff --git a/core/ui/src/main/java/com/yapp/ui/component/bottomsheet/OrbitBottomSheetState.kt b/core/ui/src/main/java/com/yapp/ui/component/bottomsheet/OrbitBottomSheetState.kt new file mode 100644 index 00000000..b558e1d8 --- /dev/null +++ b/core/ui/src/main/java/com/yapp/ui/component/bottomsheet/OrbitBottomSheetState.kt @@ -0,0 +1,49 @@ +package com.yapp.ui.component.bottomsheet + +import androidx.compose.material.ModalBottomSheetState +import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.material.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember + +@Composable +fun rememberOrbitBottomSheetState(): OrbitBottomSheetState { + val contentState = remember { mutableStateOf(null) } + + val bottomSheetState = rememberModalBottomSheetState( + initialValue = ModalBottomSheetValue.Hidden, + confirmValueChange = { value -> + if (value == ModalBottomSheetValue.Hidden) { + contentState.value = null + } + true + }, + skipHalfExpanded = true, + ) + + return remember(contentState, bottomSheetState) { + OrbitBottomSheetState( + state = bottomSheetState, + contentState = contentState, + setContent = { contentState.value = it }, + ) + } +} + +class OrbitBottomSheetState( + val state: ModalBottomSheetState, + val contentState: State, + private val setContent: (BottomSheetContent?) -> Unit, +) { + val content: BottomSheetContent? + get() = contentState.value + + suspend fun show(sheetContent: BottomSheetContent) { + setContent(sheetContent) + state.show() + } + + suspend fun hide() = state.hide() +} diff --git a/core/ui/src/main/java/com/yapp/ui/component/button/OrbitButton.kt b/core/ui/src/main/java/com/yapp/ui/component/button/OrbitButton.kt index b1c97a1d..ddfa218a 100644 --- a/core/ui/src/main/java/com/yapp/ui/component/button/OrbitButton.kt +++ b/core/ui/src/main/java/com/yapp/ui/component/button/OrbitButton.kt @@ -33,6 +33,7 @@ fun OrbitButton( modifier: Modifier = Modifier, onClick: () -> Unit, enabled: Boolean = false, + useFillMaxWidth: Boolean = true, debounceTime: Long = 500L, height: Dp = 54.dp, containerColor: Color = OrbitTheme.colors.main, @@ -77,7 +78,9 @@ fun OrbitButton( ), interactionSource = interactionSource, modifier = modifier - .fillMaxWidth() + .then( + if (useFillMaxWidth) Modifier.fillMaxWidth() else Modifier, + ) .padding(padding) .height(height - padding * 2), ) { diff --git a/core/ui/src/main/java/com/yapp/ui/component/navigation/NavigationBarScrim.kt b/core/ui/src/main/java/com/yapp/ui/component/navigation/NavigationBarScrim.kt new file mode 100644 index 00000000..c0bcb5a7 --- /dev/null +++ b/core/ui/src/main/java/com/yapp/ui/component/navigation/NavigationBarScrim.kt @@ -0,0 +1,26 @@ +package com.yapp.ui.component.navigation + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.windowInsetsBottomHeight +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.zIndex + +@Composable +fun BoxScope.NavigationBarScrim() { + Box( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .windowInsetsBottomHeight(WindowInsets.navigationBars) + .background(Color.Black) + .zIndex(1f), + ) +} diff --git a/core/ui/src/main/java/com/yapp/ui/component/timepicker/OrbitPicker.kt b/core/ui/src/main/java/com/yapp/ui/component/timepicker/OrbitPicker.kt index ee48f71d..6b379cb3 100644 --- a/core/ui/src/main/java/com/yapp/ui/component/timepicker/OrbitPicker.kt +++ b/core/ui/src/main/java/com/yapp/ui/component/timepicker/OrbitPicker.kt @@ -14,8 +14,11 @@ import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Surface import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview @@ -23,16 +26,22 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.yapp.designsystem.theme.OrbitTheme import kotlinx.coroutines.launch -import java.util.Locale +import java.time.LocalTime + +enum class TimePeriod(val displayName: String) { + AM("์˜ค์ „"), + PM("์˜คํ›„"), + ; + + override fun toString(): String = displayName +} @Composable fun OrbitPicker( modifier: Modifier = Modifier, itemSpacing: Dp = 2.dp, - initialAmPm: String = "์˜ค์ „", - initialHour: String = "1", - initialMinute: String = "00", - onValueChange: (String, Int, Int) -> Unit, + initialTime: LocalTime = LocalTime.now(), + onValueChange: (LocalTime) -> Unit, ) { Surface( modifier = modifier @@ -46,23 +55,24 @@ fun OrbitPicker( .wrapContentSize() .background(OrbitTheme.colors.gray_900), ) { - val amPmItems = remember { listOf("์˜คํ›„", "์˜ค์ „") } - val hourItems = remember { (1..12).map { it.toString() } } - val minuteItems = remember { (0..59).map { String.format(Locale.ROOT, "%02d", it) } } + val amPmItems = remember { TimePeriod.entries.toList().map { it.displayName } } + val hourItems = remember { (1..12).toList() } + val minuteItems = remember { (0..59).toList() } val amPmPickerState = rememberPickerState( - selectedItem = amPmItems.indexOf(initialAmPm).toString(), - startIndex = amPmItems.indexOf(initialAmPm), + initialIndex = if (initialTime.hour < 12) 0 else 1, + items = amPmItems, ) val hourPickerState = rememberPickerState( - selectedItem = hourItems.indexOf(initialHour).toString(), - startIndex = hourItems.indexOf(initialHour), + initialIndex = hourItems.indexOf(if (initialTime.hour % 12 == 0) 12 else initialTime.hour % 12), + items = hourItems, ) val minutePickerState = rememberPickerState( - selectedItem = minuteItems.indexOf(initialMinute).toString(), - startIndex = minuteItems.indexOf(initialMinute), + initialIndex = minuteItems.indexOf(initialTime.minute), + items = minuteItems, ) + var previousHour by remember { mutableIntStateOf(initialTime.hour) } val scope = rememberCoroutineScope() Box(modifier = Modifier.fillMaxWidth()) { @@ -71,7 +81,7 @@ fun OrbitPicker( .fillMaxWidth() .align(Alignment.Center) .padding(horizontal = 20.dp) - .height(50.dp) + .height(45.dp) .background(OrbitTheme.colors.gray_700, shape = RoundedCornerShape(12.dp)), ) @@ -86,7 +96,7 @@ fun OrbitPicker( items = amPmItems, visibleItemsCount = 3, itemSpacing = itemSpacing, - textStyle = OrbitTheme.typography.title2Medium, + textStyle = OrbitTheme.typography.heading1SemiBold, modifier = Modifier.weight(1f), textModifier = Modifier.padding(8.dp), infiniteScroll = false, @@ -105,7 +115,7 @@ fun OrbitPicker( items = hourItems, visibleItemsCount = 5, itemSpacing = itemSpacing, - textStyle = OrbitTheme.typography.title2Medium, + textStyle = OrbitTheme.typography.heading1SemiBold, modifier = Modifier.weight(1f), textModifier = Modifier.padding(8.dp), infiniteScroll = true, @@ -116,12 +126,17 @@ fun OrbitPicker( minutePickerState, onValueChange, ) - }, - onScrollCompleted = { scope.launch { + val currentHour = hourPickerState.selectedItem val currentIndex = amPmPickerState.lazyListState.firstVisibleItemIndex % amPmItems.size val nextIndex = (currentIndex + 1) % amPmItems.size - amPmPickerState.lazyListState.animateScrollToItem(nextIndex) + + if ((currentHour == 12 && previousHour == 11) || + (currentHour == 11 && previousHour == 12) + ) { + amPmPickerState.lazyListState.animateScrollToItem(nextIndex) + } + previousHour = currentHour } }, ) @@ -131,10 +146,11 @@ fun OrbitPicker( items = minuteItems, visibleItemsCount = 5, itemSpacing = itemSpacing, - textStyle = OrbitTheme.typography.title2Medium, + textStyle = OrbitTheme.typography.heading1SemiBold, modifier = Modifier.weight(1f), textModifier = Modifier.padding(8.dp), infiniteScroll = true, + itemFormatter = { it.toString().padStart(2, '0') }, onValueChange = { onPickerValueChange( amPmPickerState, @@ -151,21 +167,32 @@ fun OrbitPicker( } private fun onPickerValueChange( - amPmState: PickerState, - hourState: PickerState, - minuteState: PickerState, - onValueChange: (String, Int, Int) -> Unit, + amPmState: PickerState, + hourState: PickerState, + minuteState: PickerState, + onValueChange: (LocalTime) -> Unit, ) { val amPm = amPmState.selectedItem - val hour = hourState.selectedItem.toIntOrNull() ?: 0 - val minute = minuteState.selectedItem.toIntOrNull() ?: 0 - onValueChange(amPm, hour, minute) + val hour = hourState.selectedItem + val minute = minuteState.selectedItem + + val adjustedHour = if (amPm == TimePeriod.AM.displayName && hour == 12) { + 0 + } else if (amPm == TimePeriod.PM.displayName && hour != 12) { + hour + 12 + } else { + hour + } + + val newTime = LocalTime.of(adjustedHour, minute) + + onValueChange(newTime) } @Preview(showBackground = true) @Composable fun OrbitPickerPreview() { - OrbitPicker { amPm, hour, minute -> - Log.d("OrbitPicker", "selectedAmPm: $amPm, selectedHour: $hour, selectedMinute: $minute") + OrbitPicker() { newTime -> + Log.d("OrbitPicker", "selectedTime: $newTime") } } diff --git a/core/ui/src/main/java/com/yapp/ui/component/timepicker/OrbitPickerItem.kt b/core/ui/src/main/java/com/yapp/ui/component/timepicker/OrbitPickerItem.kt index 76ba98d8..729421a2 100644 --- a/core/ui/src/main/java/com/yapp/ui/component/timepicker/OrbitPickerItem.kt +++ b/core/ui/src/main/java/com/yapp/ui/component/timepicker/OrbitPickerItem.kt @@ -10,8 +10,11 @@ import androidx.compose.foundation.lazy.LazyColumn 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.mutableIntStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -30,17 +33,17 @@ import kotlinx.coroutines.flow.map import kotlin.math.abs @Composable -fun OrbitPickerItem( +fun OrbitPickerItem( modifier: Modifier = Modifier, - items: List, - state: PickerState = rememberPickerState(), + items: List, + state: PickerState = rememberPickerState(items = items), visibleItemsCount: Int, textModifier: Modifier = Modifier, + itemFormatter: (T) -> String = { it.toString() }, infiniteScroll: Boolean = true, textStyle: TextStyle, itemSpacing: Dp, - onValueChange: (String) -> Unit, - onScrollCompleted: () -> Unit = {}, + onValueChange: (T) -> Unit, ) { val visibleItemsMiddle = visibleItemsCount / 2 val listScrollCount = if (infiniteScroll) Int.MAX_VALUE else items.size + visibleItemsMiddle * 2 @@ -48,31 +51,28 @@ fun OrbitPickerItem( val listState = state.lazyListState val flingBehavior = rememberSnapFlingBehavior(lazyListState = listState) - val itemHeightPixels = remember { mutableIntStateOf(0) } - val itemHeightDp = with(LocalDensity.current) { itemHeightPixels.intValue.toDp() } + var itemHeightPixels by remember { mutableIntStateOf(0) } + val itemHeightDp = with(LocalDensity.current) { itemHeightPixels.toDp() } - LaunchedEffect(key1 = state.startIndex) { - val safeStartIndex = state.startIndex.takeIf { it >= 0 } ?: 0 + LaunchedEffect(state.initialIndex) { + val safeStartIndex = state.initialIndex val listStartIndex = if (infiniteScroll) { - calculateStartIndex(infiniteScroll, items.size, listScrollMiddle, visibleItemsMiddle, safeStartIndex) + getStartIndexForInfiniteScroll(itemHeightPixels, listScrollMiddle, visibleItemsMiddle, safeStartIndex) } else { safeStartIndex } - listState.scrollToItem(listStartIndex, 0) if (!infiniteScroll) { - val selectedItem = items.getOrNull(safeStartIndex) ?: "" - if (selectedItem != state.selectedItem) { - state.selectedItem = selectedItem - onValueChange(selectedItem) + val selectedItem = items.getOrNull(listStartIndex) ?: items.first() + if (listStartIndex != state.selectedIndex.value) { + state.updateSelectedIndex(listStartIndex) } + onValueChange(selectedItem) } } LaunchedEffect(listState) { - var previousAdjustedIndex = -1 - snapshotFlow { listState.layoutInfo } .map { layoutInfo -> val centerOffset = layoutInfo.viewportStartOffset + @@ -82,30 +82,20 @@ fun OrbitPickerItem( abs(itemCenter - centerOffset) }?.index } - .distinctUntilChanged() - .collect { centerIndex -> - if (centerIndex != null) { - val adjustedIndex = if (infiniteScroll) { - centerIndex % items.size - } else { - centerIndex - visibleItemsMiddle - }.coerceIn(0, items.size - 1) - - val newValue = items[adjustedIndex] - + .map { centerIndex -> + centerIndex?.let { index -> if (infiniteScroll) { - val lastIndex = items.size - 1 - if ((previousAdjustedIndex == 0 && adjustedIndex == lastIndex) || - (previousAdjustedIndex == lastIndex && adjustedIndex == 0) - ) { - onScrollCompleted() - } - } - if (newValue != state.selectedItem) { - state.selectedItem = newValue - onValueChange(newValue) + index % items.size + } else { + (index - visibleItemsMiddle).coerceIn(0, items.size - 1) } - previousAdjustedIndex = adjustedIndex + } + } + .distinctUntilChanged() + .collect { adjustedIndex -> + if (adjustedIndex != null && adjustedIndex != state.selectedIndex.value) { + state.updateSelectedIndex(adjustedIndex) + onValueChange(items[adjustedIndex]) } } } @@ -122,8 +112,9 @@ fun OrbitPickerItem( .height(totalItemHeight * visibleItemsCount) .pointerInput(Unit) { detectVerticalDragGestures { change, _ -> change.consume() } }, ) { - items(listScrollCount) { index -> - val layoutInfo = listState.layoutInfo + items(listScrollCount, key = { index -> index }) { index -> + val layoutInfo by remember { derivedStateOf { listState.layoutInfo } } + val viewportCenterOffset = layoutInfo.viewportStartOffset + (layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset) / 2 @@ -141,15 +132,22 @@ fun OrbitPickerItem( val scaleY = 1f - (0.2f * (distanceFromCenter / maxDistance)).coerceIn(0f, 0.4f) + val item = getItemForIndex( + index = index, + items = items, + infiniteScroll = infiniteScroll, + visibleItemsMiddle = visibleItemsMiddle, + ) + Text( - text = getItemForIndex(index, items, infiniteScroll, visibleItemsMiddle), + text = item?.let { itemFormatter(it) } ?: "", maxLines = 1, style = textStyle, color = OrbitTheme.colors.white.copy(alpha = alpha), modifier = Modifier .padding(vertical = itemSpacing / 2) .graphicsLayer(scaleY = scaleY) - .onSizeChanged { size -> itemHeightPixels.intValue = size.height } + .onSizeChanged { size -> itemHeightPixels = size.height } .then(textModifier), ) } @@ -157,37 +155,31 @@ fun OrbitPickerItem( } } -/** - * ๋ฌดํ•œ ์Šคํฌ๋กค๊ณผ ์ดˆ๊ธฐ ์‹œ์ž‘ ์ธ๋ฑ์Šค๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฆฌ์ŠคํŠธ์˜ ์‹œ์ž‘ ์ธ๋ฑ์Šค๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. - */ -private fun calculateStartIndex( - infiniteScroll: Boolean, +private fun getStartIndexForInfiniteScroll( itemSize: Int, listScrollMiddle: Int, visibleItemsMiddle: Int, startIndex: Int, ): Int { - return if (infiniteScroll) { - listScrollMiddle - listScrollMiddle % itemSize - visibleItemsMiddle + startIndex - } else { - startIndex + visibleItemsMiddle + if (itemSize == 0) { + return listScrollMiddle - visibleItemsMiddle + startIndex } + + return listScrollMiddle - listScrollMiddle % itemSize - visibleItemsMiddle + startIndex } -/** - * ์ฃผ์–ด์ง„ ์ธ๋ฑ์Šค์— ํ•ด๋‹นํ•˜๋Š” ํ•ญ๋ชฉ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. - * ๋ฌดํ•œ ์Šคํฌ๋กค๊ณผ ๋ณด์ด๋Š” ํ•ญ๋ชฉ์˜ ๊ฐœ์ˆ˜๋ฅผ ๊ณ ๋ คํ•ฉ๋‹ˆ๋‹ค. - */ -private fun getItemForIndex( +private fun getItemForIndex( index: Int, - items: List, + items: List, infiniteScroll: Boolean, visibleItemsMiddle: Int, -): String { +): T? { + require(items.isNotEmpty()) { "Items list cannot be empty." } + return if (!infiniteScroll) { - items.getOrNull(index - visibleItemsMiddle) ?: "" + items.getOrNull(index - visibleItemsMiddle) } else { - items.getOrNull(index % items.size) ?: "" + items.getOrNull(index % items.size) } } @@ -197,7 +189,10 @@ fun OrbitPickerItemPreview() { OrbitTheme { OrbitPickerItem( items = (0..100).map { it.toString() }, - state = rememberPickerState(), + state = rememberPickerState( + initialIndex = 50, + items = (0..100).map { it.toString() }, + ), visibleItemsCount = 5, textStyle = TextStyle.Default, itemSpacing = 8.dp, diff --git a/core/ui/src/main/java/com/yapp/ui/component/timepicker/OrbitYearMonthPicker.kt b/core/ui/src/main/java/com/yapp/ui/component/timepicker/OrbitYearMonthPicker.kt index 7d90d39b..012623e5 100644 --- a/core/ui/src/main/java/com/yapp/ui/component/timepicker/OrbitYearMonthPicker.kt +++ b/core/ui/src/main/java/com/yapp/ui/component/timepicker/OrbitYearMonthPicker.kt @@ -37,23 +37,29 @@ fun OrbitYearMonthPicker( ) { val screenWidth = LocalConfiguration.current.screenWidthDp.dp + val lunarItems = remember { listOf("์–‘๋ ฅ", "์Œ๋ ฅ") } + val yearItems = remember { (1900..2024).map { it.toString() } } + val monthItems = remember { (1..12).map { it.toString() } } + + val startIndexYear = yearItems.indexOf(initialYear).coerceAtLeast(0) + val startIndexMonth = monthItems.indexOf(initialMonth).coerceAtLeast(0) + val lunarState = remember { mutableStateOf(initialLunar) } val yearState = remember { mutableIntStateOf(initialYear.toInt()) } val monthState = remember { mutableIntStateOf(initialMonth.toInt()) } - - val maxDay = getMaxDaysInMonth(yearState.intValue, monthState.intValue) - val dayItems = (1..maxDay).map { it.toString() } - - val startIndexYear = (1900..2024).map { it.toString() }.indexOf(initialYear).takeIf { it >= 0 } ?: 0 - val startIndexMonth = (1..12).map { it.toString() }.indexOf(initialMonth).takeIf { it >= 0 } ?: 0 - val startIndexDay = dayItems.indexOf(initialDay).takeIf { it >= 0 } ?: 0 - val dayState = remember { mutableIntStateOf(initialDay.toInt()) } - val yearPickerState = rememberPickerState(startIndex = startIndexYear) - val monthPickerState = rememberPickerState(startIndex = startIndexMonth) - val dayPickerState = rememberPickerState(startIndex = startIndexDay) + val yearPickerState = rememberPickerState(initialIndex = startIndexYear, items = yearItems) + val monthPickerState = rememberPickerState(initialIndex = startIndexMonth, items = monthItems) + // dayItems๋Š” year/month ๋ณ€๊ฒฝ ์‹œ๋งˆ๋‹ค ๋™๊ธฐํ™” + val dayItems = remember(yearState.intValue, monthState.intValue) { + (1..getMaxDaysInMonth(yearState.intValue, monthState.intValue)).map { it.toString() } + } + val startIndexDay = dayItems.indexOf(initialDay).coerceAtLeast(0) + val dayPickerState = rememberPickerState(initialIndex = startIndexDay, items = dayItems) + + // ์ผ ์ˆ˜ ๋„˜์–ด๊ฐ€๋Š” ๊ฒฝ์šฐ ์กฐ์ • LaunchedEffect(yearState.intValue, monthState.intValue) { val newMaxDay = getMaxDaysInMonth(yearState.intValue, monthState.intValue) if (dayState.intValue > newMaxDay) { @@ -61,25 +67,18 @@ fun OrbitYearMonthPicker( } } + // ๋ณ€๊ฒฝ ์ฝœ๋ฐฑ LaunchedEffect(lunarState.value, yearState.intValue, monthState.intValue, dayState.intValue) { onValueChange(lunarState.value, yearState.intValue, monthState.intValue, dayState.intValue) } - Surface( - modifier = modifier.fillMaxWidth(), - ) { + Surface(modifier = modifier.fillMaxWidth()) { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Bottom, modifier = Modifier.background(OrbitTheme.colors.gray_900), ) { - val lunarItems = listOf("์–‘๋ ฅ", "์Œ๋ ฅ") - val yearItems = (1900..2024).map { it.toString() } - val monthItems = (1..12).map { it.toString() } - - Box( - modifier = Modifier.fillMaxWidth(), - ) { + Box(modifier = Modifier.fillMaxWidth()) { Box( modifier = Modifier .fillMaxWidth() @@ -90,7 +89,9 @@ fun OrbitYearMonthPicker( ) Row( - modifier = Modifier.fillMaxWidth().padding(horizontal = screenWidth * 0.1f), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = screenWidth * 0.1f), verticalAlignment = Alignment.CenterVertically, ) { OrbitPickerItem( @@ -142,9 +143,6 @@ fun OrbitYearMonthPicker( } } -/** - * ํŠน์ • ์—ฐ๋„์™€ ์›”์— ๋”ฐ๋ฅธ ์ตœ๋Œ€ ์ผ ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜. - */ private fun getMaxDaysInMonth(year: Int, month: Int): Int { return when (month) { 1, 3, 5, 7, 8, 10, 12 -> 31 @@ -154,9 +152,6 @@ private fun getMaxDaysInMonth(year: Int, month: Int): Int { } } -/** - * ์œค๋…„ ๊ณ„์‚ฐ - */ private fun isLeapYear(year: Int): Boolean { return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) } diff --git a/core/ui/src/main/java/com/yapp/ui/component/timepicker/PickerState.kt b/core/ui/src/main/java/com/yapp/ui/component/timepicker/PickerState.kt index 120e3398..2e8b9793 100644 --- a/core/ui/src/main/java/com/yapp/ui/component/timepicker/PickerState.kt +++ b/core/ui/src/main/java/com/yapp/ui/component/timepicker/PickerState.kt @@ -4,16 +4,29 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow -class PickerState( +class PickerState( val lazyListState: LazyListState, - var selectedItem: String, - var startIndex: Int, -) + val initialIndex: Int, + private val items: List, +) { + private val _selectedIndex = MutableStateFlow(initialIndex) + val selectedIndex: StateFlow + get() = _selectedIndex + + val selectedItem: T + get() = items.getOrElse(_selectedIndex.value) { items.first() } + + fun updateSelectedIndex(newIndex: Int) { + _selectedIndex.value = newIndex.coerceIn(0, items.size - 1) + } +} @Composable -fun rememberPickerState( +fun rememberPickerState( lazyListState: LazyListState = rememberLazyListState(), - selectedItem: String = "", - startIndex: Int = 0, -): PickerState = remember { PickerState(lazyListState, selectedItem, startIndex) } + initialIndex: Int = 0, + items: List, +): PickerState = remember { PickerState(lazyListState, initialIndex, items) } diff --git a/data/build.gradle.kts b/data/build.gradle.kts index f2c52f14..354e0002 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -10,20 +10,16 @@ android { } dependencies { - implementation(projects.domain) implementation(projects.core.network) + implementation(projects.core.database) implementation(projects.core.datastore) + + implementation(projects.domain) implementation(projects.core.media) implementation(projects.core.remoteconfig) - ksp(libs.androidx.room.compiler) - implementation(libs.androidx.room.ktx) - implementation(libs.androidx.room.runtime) - implementation(libs.androidx.room.paging) - implementation(libs.kotlinx.serialization.json) implementation(libs.retrofit.core) implementation(libs.retrofit.kotlin.serialization) implementation(libs.okhttp.logging) - implementation(libs.androidx.datastore) } diff --git a/data/src/main/java/com/yapp/data/remote/di/RepositoryModule.kt b/data/src/main/java/com/yapp/data/di/RepositoryModule.kt similarity index 67% rename from data/src/main/java/com/yapp/data/remote/di/RepositoryModule.kt rename to data/src/main/java/com/yapp/data/di/RepositoryModule.kt index ed92d2db..8e3cc519 100644 --- a/data/src/main/java/com/yapp/data/remote/di/RepositoryModule.kt +++ b/data/src/main/java/com/yapp/data/di/RepositoryModule.kt @@ -1,11 +1,11 @@ -package com.yapp.data.remote.di +package com.yapp.data.di -import com.yapp.data.remote.repositoryimpl.DummyRepositoryImpl -import com.yapp.data.remote.repositoryimpl.FortuneRepositoryImpl -import com.yapp.data.remote.repositoryimpl.RemoteConfigRepositoryImpl -import com.yapp.data.remote.repositoryimpl.SignUpRepositoryImpl -import com.yapp.data.remote.repositoryimpl.UserInfoRepositoryImpl -import com.yapp.domain.repository.DummyRepository +import com.yapp.data.repositoryimpl.AlarmRepositoryImpl +import com.yapp.data.repositoryimpl.FortuneRepositoryImpl +import com.yapp.data.repositoryimpl.RemoteConfigRepositoryImpl +import com.yapp.data.repositoryimpl.SignUpRepositoryImpl +import com.yapp.data.repositoryimpl.UserInfoRepositoryImpl +import com.yapp.domain.repository.AlarmRepository import com.yapp.domain.repository.FortuneRepository import com.yapp.domain.repository.RemoteConfigRepository import com.yapp.domain.repository.SignUpRepository @@ -21,15 +21,15 @@ import javax.inject.Singleton abstract class RepositoryModule { @Binds @Singleton - abstract fun bindsDummyRepository( - dummyRepository: DummyRepositoryImpl, - ): DummyRepository + abstract fun bindsAlarmRepository( + alarmRepository: AlarmRepositoryImpl, + ): AlarmRepository @Binds @Singleton - abstract fun bindsSignUpRepository( - signUpRepository: SignUpRepositoryImpl, - ): SignUpRepository + abstract fun bindsFortuneRepository( + fortuneRepository: FortuneRepositoryImpl, + ): FortuneRepository @Binds @Singleton @@ -39,9 +39,9 @@ abstract class RepositoryModule { @Binds @Singleton - abstract fun bindsFortuneRepository( - fortuneRepository: FortuneRepositoryImpl, - ): FortuneRepository + abstract fun bindsSignUpRepository( + signUpRepository: SignUpRepositoryImpl, + ): SignUpRepository @Binds @Singleton diff --git a/data/src/main/java/com/yapp/data/local/datasource/AlarmLocalDataSource.kt b/data/src/main/java/com/yapp/data/local/datasource/AlarmLocalDataSource.kt index f41b0caa..8ff592d2 100644 --- a/data/src/main/java/com/yapp/data/local/datasource/AlarmLocalDataSource.kt +++ b/data/src/main/java/com/yapp/data/local/datasource/AlarmLocalDataSource.kt @@ -1,14 +1,12 @@ package com.yapp.data.local.datasource -import com.yapp.data.local.AlarmEntity +import com.yapp.database.AlarmEntity import com.yapp.domain.model.Alarm import kotlinx.coroutines.flow.Flow interface AlarmLocalDataSource { fun getAllAlarms(): Flow> - fun getPagedAlarms(limit: Int, offset: Int): Flow> - fun getAlarmsByTime(hour: Int, minute: Int, isAm: Boolean): Flow> - fun getAlarmCount(): Flow + fun getAlarmsByTime(hour: Int, minute: Int): Flow> suspend fun insertAlarm(alarm: AlarmEntity): Long suspend fun updateAlarm(alarm: AlarmEntity): Int suspend fun updateAlarmActive(id: Long, active: Boolean): Int diff --git a/data/src/main/java/com/yapp/data/local/datasource/AlarmLocalDataSourceImpl.kt b/data/src/main/java/com/yapp/data/local/datasource/AlarmLocalDataSourceImpl.kt index d84acfa8..03fecd55 100644 --- a/data/src/main/java/com/yapp/data/local/datasource/AlarmLocalDataSourceImpl.kt +++ b/data/src/main/java/com/yapp/data/local/datasource/AlarmLocalDataSourceImpl.kt @@ -1,8 +1,8 @@ package com.yapp.data.local.datasource -import com.yapp.data.local.AlarmDao -import com.yapp.data.local.AlarmEntity -import com.yapp.data.local.toDomain +import com.yapp.database.AlarmDao +import com.yapp.database.AlarmEntity +import com.yapp.database.toDomain import com.yapp.domain.model.Alarm import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -16,24 +16,12 @@ class AlarmLocalDataSourceImpl @Inject constructor( .map { alarmEntities -> alarmEntities.map { it.toDomain() } } } - override fun getPagedAlarms( - limit: Int, - offset: Int, - ): Flow> { - return alarmDao.getPagedAlarms(limit, offset) - .map { alarmEntities -> alarmEntities.map { it.toDomain() } } - } - - override fun getAlarmsByTime(hour: Int, minute: Int, isAm: Boolean): Flow> { - return alarmDao.getAlarmsByTime(hour, minute, isAm).map { alarmEntities -> + override fun getAlarmsByTime(hour: Int, minute: Int): Flow> { + return alarmDao.getAlarmsByTime(hour, minute).map { alarmEntities -> alarmEntities.map { it.toDomain() } } } - override fun getAlarmCount(): Flow { - return alarmDao.getAlarmCount() - } - override suspend fun insertAlarm(alarm: AlarmEntity): Long { return alarmDao.insertAlarm(alarm) } diff --git a/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSource.kt b/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSource.kt new file mode 100644 index 00000000..4e519ddb --- /dev/null +++ b/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSource.kt @@ -0,0 +1,27 @@ +package com.yapp.data.local.datasource + +import com.yapp.domain.model.FortuneCreateStatus +import kotlinx.coroutines.flow.Flow + +interface FortuneLocalDataSource { + val fortuneIdFlow: Flow + val fortuneDateEpochFlow: Flow + val fortuneImageIdFlow: Flow + val fortuneScoreFlow: Flow + val hasUnseenFortuneFlow: Flow + val shouldShowFortuneToolTipFlow: Flow + val isFirstAlarmDismissedTodayFlow: Flow + + val fortuneCreateStatusFlow: Flow + + suspend fun markFortuneCreating() + suspend fun markFortuneCreated(fortuneId: Long) + suspend fun markFortuneFailed() + suspend fun markFortuneSeen() + suspend fun markFortuneTooltipShown() + suspend fun saveFortuneImageId(imageResId: Int) + suspend fun saveFortuneScore(score: Int) + suspend fun markFirstAlarmDismissedToday() + + suspend fun clearFortuneData() +} diff --git a/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSourceImpl.kt b/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSourceImpl.kt new file mode 100644 index 00000000..b8ab799f --- /dev/null +++ b/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSourceImpl.kt @@ -0,0 +1,73 @@ +package com.yapp.data.local.datasource + +import com.yapp.datastore.UserPreferences +import com.yapp.domain.model.FortuneCreateStatus +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import java.time.LocalDate +import javax.inject.Inject + +class FortuneLocalDataSourceImpl @Inject constructor( + private val userPreferences: UserPreferences, +) : FortuneLocalDataSource { + + override val fortuneIdFlow = userPreferences.fortuneIdFlow + override val fortuneDateEpochFlow = userPreferences.fortuneDateEpochFlow + override val fortuneImageIdFlow = userPreferences.fortuneImageIdFlow + override val fortuneScoreFlow = userPreferences.fortuneScoreFlow + override val hasUnseenFortuneFlow = userPreferences.hasUnseenFortuneFlow + override val shouldShowFortuneToolTipFlow = userPreferences.shouldShowFortuneToolTipFlow + override val isFirstAlarmDismissedTodayFlow = userPreferences.isFirstAlarmDismissedTodayFlow + + override val fortuneCreateStatusFlow = combine( + userPreferences.fortuneIdFlow, + userPreferences.fortuneDateEpochFlow, + userPreferences.isFortuneCreatingFlow, + userPreferences.isFortuneFailedFlow, + ) { fortuneId, fortuneDate, isCreating, isFailed -> + when { + isFailed -> FortuneCreateStatus.Failure + isCreating -> FortuneCreateStatus.Creating + fortuneId != null && fortuneDate == todayEpoch() -> FortuneCreateStatus.Success(fortuneId) + else -> FortuneCreateStatus.Idle + } + }.distinctUntilChanged() + + private fun todayEpoch(): Long = LocalDate.now().toEpochDay() + + override suspend fun markFortuneCreating() { + userPreferences.markFortuneCreating() + } + + override suspend fun markFortuneCreated(fortuneId: Long) { + userPreferences.markFortuneCreated(fortuneId) + } + + override suspend fun markFortuneFailed() { + userPreferences.markFortuneFailed() + } + + override suspend fun markFortuneSeen() { + userPreferences.markFortuneSeen() + } + + override suspend fun markFortuneTooltipShown() { + userPreferences.markFortuneTooltipShown() + } + + override suspend fun saveFortuneImageId(imageResId: Int) { + userPreferences.saveFortuneImageId(imageResId) + } + + override suspend fun saveFortuneScore(score: Int) { + userPreferences.saveFortuneScore(score) + } + + override suspend fun markFirstAlarmDismissedToday() { + userPreferences.markFirstAlarmDismissedToday() + } + + override suspend fun clearFortuneData() { + userPreferences.clearFortuneData() + } +} diff --git a/data/src/main/java/com/yapp/data/local/datasource/ImageLocalDataSource.kt b/data/src/main/java/com/yapp/data/local/datasource/ImageLocalDataSource.kt deleted file mode 100644 index 607ee87a..00000000 --- a/data/src/main/java/com/yapp/data/local/datasource/ImageLocalDataSource.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.yapp.data.local.datasource - -interface ImageLocalDataSource { - suspend fun saveImage(byteArray: ByteArray, fileName: String = "fortune_${System.currentTimeMillis()}.png"): Boolean -} diff --git a/data/src/main/java/com/yapp/data/local/datasource/UserLocalDataSource.kt b/data/src/main/java/com/yapp/data/local/datasource/UserLocalDataSource.kt new file mode 100644 index 00000000..3ad851df --- /dev/null +++ b/data/src/main/java/com/yapp/data/local/datasource/UserLocalDataSource.kt @@ -0,0 +1,18 @@ +package com.yapp.data.local.datasource + +import kotlinx.coroutines.flow.Flow + +interface UserLocalDataSource { + val userIdFlow: Flow + val userNameFlow: Flow + val onboardingCompletedFlow: Flow + val updateNoticeDontShowVersionFlow: Flow + val updateNoticeLastShownDateEpochFlow: Flow + + suspend fun saveUserId(userId: Long) + suspend fun saveUserName(userName: String) + suspend fun setOnboardingCompleted() + suspend fun markUpdateNoticeDontShow(version: String) + suspend fun markUpdateNoticeShownToday() + suspend fun clearUserData() +} diff --git a/data/src/main/java/com/yapp/data/local/datasource/UserLocalDataSourceImpl.kt b/data/src/main/java/com/yapp/data/local/datasource/UserLocalDataSourceImpl.kt new file mode 100644 index 00000000..187a7a59 --- /dev/null +++ b/data/src/main/java/com/yapp/data/local/datasource/UserLocalDataSourceImpl.kt @@ -0,0 +1,40 @@ +package com.yapp.data.local.datasource + +import com.yapp.datastore.UserPreferences +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class UserLocalDataSourceImpl @Inject constructor( + private val userPreferences: UserPreferences, +) : UserLocalDataSource { + + override val userIdFlow: Flow = userPreferences.userIdFlow + override val userNameFlow: Flow = userPreferences.userNameFlow + override val onboardingCompletedFlow: Flow = userPreferences.onboardingCompletedFlow + override val updateNoticeDontShowVersionFlow: Flow = userPreferences.updateNoticeDontShowVersionFlow + override val updateNoticeLastShownDateEpochFlow: Flow = userPreferences.updateNoticeLastShownDateEpochFlow + + override suspend fun saveUserId(userId: Long) { + userPreferences.saveUserId(userId) + } + + override suspend fun saveUserName(userName: String) { + userPreferences.saveUserName(userName) + } + + override suspend fun setOnboardingCompleted() { + userPreferences.setOnboardingCompleted() + } + + override suspend fun markUpdateNoticeDontShow(version: String) { + userPreferences.markUpdateNoticeDontShow(version) + } + + override suspend fun markUpdateNoticeShownToday() { + userPreferences.markUpdateNoticeShownToday() + } + + override suspend fun clearUserData() { + userPreferences.clearUserData() + } +} diff --git a/data/src/main/java/com/yapp/data/local/di/DataSourceModule.kt b/data/src/main/java/com/yapp/data/local/di/DataSourceModule.kt index eb567b3e..4a1ad9b8 100644 --- a/data/src/main/java/com/yapp/data/local/di/DataSourceModule.kt +++ b/data/src/main/java/com/yapp/data/local/di/DataSourceModule.kt @@ -2,6 +2,10 @@ package com.yapp.data.local.di import com.yapp.data.local.datasource.AlarmLocalDataSource import com.yapp.data.local.datasource.AlarmLocalDataSourceImpl +import com.yapp.data.local.datasource.FortuneLocalDataSource +import com.yapp.data.local.datasource.FortuneLocalDataSourceImpl +import com.yapp.data.local.datasource.UserLocalDataSource +import com.yapp.data.local.datasource.UserLocalDataSourceImpl import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -16,4 +20,16 @@ abstract class DataSourceModule { abstract fun bindsAlarmDataSource( alarmLocalDataSource: AlarmLocalDataSourceImpl, ): AlarmLocalDataSource + + @Binds + @Singleton + abstract fun bindsFortuneDataSource( + fortuneLocalDataSource: FortuneLocalDataSourceImpl, + ): FortuneLocalDataSource + + @Binds + @Singleton + abstract fun bindsUserDataSource( + userLocalDataSource: UserLocalDataSourceImpl, + ): UserLocalDataSource } diff --git a/data/src/main/java/com/yapp/data/local/di/RepositoryModule.kt b/data/src/main/java/com/yapp/data/local/di/RepositoryModule.kt deleted file mode 100644 index 3d7235c6..00000000 --- a/data/src/main/java/com/yapp/data/local/di/RepositoryModule.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.yapp.data.local.di - -import com.yapp.data.local.repositoryimpl.AlarmRepositoryImpl -import com.yapp.data.local.repositoryimpl.ImageRepositoryImpl -import com.yapp.domain.repository.AlarmRepository -import com.yapp.domain.repository.ImageRepository -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -abstract class RepositoryModule { - @Binds - @Singleton - abstract fun bindsAlarmRepository( - alarmRepository: AlarmRepositoryImpl, - ): AlarmRepository - - @Binds - @Singleton - abstract fun bindsImageRepository( - imageRepository: ImageRepositoryImpl, - ): ImageRepository -} diff --git a/data/src/main/java/com/yapp/data/local/repositoryimpl/ImageRepositoryImpl.kt b/data/src/main/java/com/yapp/data/local/repositoryimpl/ImageRepositoryImpl.kt deleted file mode 100644 index 86cba2cc..00000000 --- a/data/src/main/java/com/yapp/data/local/repositoryimpl/ImageRepositoryImpl.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.yapp.data.local.repositoryimpl - -import com.yapp.data.local.datasource.ImageLocalDataSource -import com.yapp.domain.repository.ImageRepository -import javax.inject.Inject - -class ImageRepositoryImpl @Inject constructor( - private val imageLocalDataSource: ImageLocalDataSource, -) : ImageRepository { - - override suspend fun saveImage(byteArray: ByteArray): Boolean { - return imageLocalDataSource.saveImage(byteArray) - } -} diff --git a/data/src/main/java/com/yapp/data/remote/datasource/DummyDataSource.kt b/data/src/main/java/com/yapp/data/remote/datasource/DummyDataSource.kt deleted file mode 100644 index 2d12449b..00000000 --- a/data/src/main/java/com/yapp/data/remote/datasource/DummyDataSource.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.yapp.data.remote.datasource - -import com.yapp.data.remote.dto.request.RequestDummyDto -import com.yapp.data.remote.dto.response.ResponseDummyDto -import com.yapp.network.model.BaseResponse - -interface DummyDataSource { - suspend fun fetchDummy(): BaseResponse - suspend fun saveDummy(requestDummyDto: RequestDummyDto): BaseResponse -} diff --git a/data/src/main/java/com/yapp/data/remote/datasource/DummyDataSourceImpl.kt b/data/src/main/java/com/yapp/data/remote/datasource/DummyDataSourceImpl.kt deleted file mode 100644 index 102bbdeb..00000000 --- a/data/src/main/java/com/yapp/data/remote/datasource/DummyDataSourceImpl.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.yapp.data.remote.datasource - -import com.yapp.data.remote.dto.request.RequestDummyDto -import com.yapp.data.remote.dto.response.ResponseDummyDto -import com.yapp.data.remote.service.DummyService -import com.yapp.network.model.BaseResponse -import javax.inject.Inject - -class DummyDataSourceImpl @Inject constructor( - private val dummyService: DummyService, -) : DummyDataSource { - override suspend fun fetchDummy(): BaseResponse = dummyService.fetchDummy() - override suspend fun saveDummy(requestDummyDto: RequestDummyDto): BaseResponse = dummyService.saveDummy(requestDummyDto) -} diff --git a/data/src/main/java/com/yapp/data/remote/datasource/FortuneDataSourceImpl.kt b/data/src/main/java/com/yapp/data/remote/datasource/FortuneDataSourceImpl.kt index f549500d..d8dc2dd7 100644 --- a/data/src/main/java/com/yapp/data/remote/datasource/FortuneDataSourceImpl.kt +++ b/data/src/main/java/com/yapp/data/remote/datasource/FortuneDataSourceImpl.kt @@ -2,7 +2,7 @@ package com.yapp.data.remote.datasource import com.yapp.data.remote.dto.response.FortuneResponse import com.yapp.data.remote.service.ApiService -import com.yapp.data.remote.utils.safeApiCall +import com.yapp.network.utils.safeApiCall import javax.inject.Inject class FortuneDataSourceImpl @Inject constructor( diff --git a/data/src/main/java/com/yapp/data/remote/datasource/SignUpDataSourceImpl.kt b/data/src/main/java/com/yapp/data/remote/datasource/SignUpDataSourceImpl.kt index 2acfa4ff..d6023c3d 100644 --- a/data/src/main/java/com/yapp/data/remote/datasource/SignUpDataSourceImpl.kt +++ b/data/src/main/java/com/yapp/data/remote/datasource/SignUpDataSourceImpl.kt @@ -3,8 +3,8 @@ package com.yapp.data.remote.datasource import android.util.Log import com.yapp.data.remote.dto.request.SignUpRequest import com.yapp.data.remote.service.ApiService -import com.yapp.data.remote.utils.ApiError -import com.yapp.data.remote.utils.safeApiCall +import com.yapp.network.model.ApiError +import com.yapp.network.utils.safeApiCall import javax.inject.Inject class SignUpDataSourceImpl @Inject constructor( diff --git a/data/src/main/java/com/yapp/data/remote/datasource/UserInfoDataSourceImpl.kt b/data/src/main/java/com/yapp/data/remote/datasource/UserInfoDataSourceImpl.kt index 3c6cb580..d81e9189 100644 --- a/data/src/main/java/com/yapp/data/remote/datasource/UserInfoDataSourceImpl.kt +++ b/data/src/main/java/com/yapp/data/remote/datasource/UserInfoDataSourceImpl.kt @@ -3,7 +3,7 @@ package com.yapp.data.remote.datasource import com.yapp.data.remote.dto.request.UpdateUserInfoRequest import com.yapp.data.remote.dto.response.UserResponse import com.yapp.data.remote.service.ApiService -import com.yapp.data.remote.utils.safeApiCall +import com.yapp.network.utils.safeApiCall import javax.inject.Inject class UserInfoDataSourceImpl @Inject constructor( diff --git a/data/src/main/java/com/yapp/data/remote/di/DataSourceModule.kt b/data/src/main/java/com/yapp/data/remote/di/DataSourceModule.kt index e7f06d23..f30ecb5d 100644 --- a/data/src/main/java/com/yapp/data/remote/di/DataSourceModule.kt +++ b/data/src/main/java/com/yapp/data/remote/di/DataSourceModule.kt @@ -1,7 +1,5 @@ package com.yapp.data.remote.di -import com.yapp.data.remote.datasource.DummyDataSource -import com.yapp.data.remote.datasource.DummyDataSourceImpl import com.yapp.data.remote.datasource.FortuneDataSource import com.yapp.data.remote.datasource.FortuneDataSourceImpl import com.yapp.data.remote.datasource.SignUpDataSource @@ -17,11 +15,6 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) abstract class DataSourceModule { - @Binds - @Singleton - abstract fun bindsDummyDataSource( - dummyDataSource: DummyDataSourceImpl, - ): DummyDataSource @Binds @Singleton diff --git a/data/src/main/java/com/yapp/data/remote/di/ServiceModule.kt b/data/src/main/java/com/yapp/data/remote/di/ServiceModule.kt index 8e5f451a..be0e97f5 100644 --- a/data/src/main/java/com/yapp/data/remote/di/ServiceModule.kt +++ b/data/src/main/java/com/yapp/data/remote/di/ServiceModule.kt @@ -1,8 +1,6 @@ package com.yapp.data.remote.di import com.yapp.data.remote.service.ApiService -import com.yapp.data.remote.service.DummyService -import com.yapp.network.di.NoneAuth import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -15,11 +13,6 @@ import javax.inject.Singleton object ServiceModule { @Provides @Singleton - fun providesDummyService(@NoneAuth retrofit: Retrofit): DummyService = - retrofit.create(DummyService::class.java) - - @Provides - @Singleton - fun providesSignUpService(@NoneAuth retrofit: Retrofit): ApiService = + fun providesApiService(retrofit: Retrofit): ApiService = retrofit.create(ApiService::class.java) } diff --git a/data/src/main/java/com/yapp/data/remote/dto/request/RequestDummyDto.kt b/data/src/main/java/com/yapp/data/remote/dto/request/RequestDummyDto.kt deleted file mode 100644 index ff249f68..00000000 --- a/data/src/main/java/com/yapp/data/remote/dto/request/RequestDummyDto.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.yapp.data.remote.dto.request - -import com.yapp.domain.model.Dummy -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class RequestDummyDto( - @SerialName("id") val id: Int, - @SerialName("name") val name: String, -) -fun Dummy.toData() = RequestDummyDto( - id = id, - name = name, -) diff --git a/data/src/main/java/com/yapp/data/remote/dto/response/FortuneResponse.kt b/data/src/main/java/com/yapp/data/remote/dto/response/FortuneResponse.kt index 925cc1c3..49709723 100644 --- a/data/src/main/java/com/yapp/data/remote/dto/response/FortuneResponse.kt +++ b/data/src/main/java/com/yapp/data/remote/dto/response/FortuneResponse.kt @@ -1,7 +1,7 @@ package com.yapp.data.remote.dto.response -import com.yapp.domain.model.fortune.Fortune -import com.yapp.domain.model.fortune.FortuneDetailModel +import com.yapp.domain.model.Fortune +import com.yapp.domain.model.FortuneDetailModel import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/data/src/main/java/com/yapp/data/remote/dto/response/ResponseDummyDto.kt b/data/src/main/java/com/yapp/data/remote/dto/response/ResponseDummyDto.kt deleted file mode 100644 index 9fcfc078..00000000 --- a/data/src/main/java/com/yapp/data/remote/dto/response/ResponseDummyDto.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.yapp.data.remote.dto.response - -import com.yapp.domain.model.Dummy -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class ResponseDummyDto( - @SerialName("id") val id: Int, - @SerialName("name") val name: String, -) -fun ResponseDummyDto.toDomain() = Dummy( - id = id, - name = name, -) diff --git a/data/src/main/java/com/yapp/data/remote/repositoryimpl/DummyRepositoryImpl.kt b/data/src/main/java/com/yapp/data/remote/repositoryimpl/DummyRepositoryImpl.kt deleted file mode 100644 index c581348a..00000000 --- a/data/src/main/java/com/yapp/data/remote/repositoryimpl/DummyRepositoryImpl.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.yapp.data.remote.repositoryimpl - -import com.yapp.data.remote.datasource.DummyDataSource -import com.yapp.data.remote.dto.request.toData -import com.yapp.data.remote.dto.response.toDomain -import com.yapp.data.remote.utils.ApiError -import com.yapp.data.remote.utils.safeApiCall -import com.yapp.domain.model.Dummy -import com.yapp.domain.repository.DummyRepository -import javax.inject.Inject - -class DummyRepositoryImpl @Inject constructor( - private val dummyDataSource: DummyDataSource, -) : DummyRepository { - - override suspend fun fetchDummy(): Result = safeApiCall { - dummyDataSource.fetchDummy().data?.toDomain() - ?: return Result.failure(ApiError("No data found")) - } - - override suspend fun saveDummy(dummy: Dummy): Result = safeApiCall { - dummyDataSource.saveDummy(dummy.toData()).data - ?: return Result.failure(ApiError("Save operation failed")) - } -} diff --git a/data/src/main/java/com/yapp/data/remote/repositoryimpl/FortuneRepositoryImpl.kt b/data/src/main/java/com/yapp/data/remote/repositoryimpl/FortuneRepositoryImpl.kt deleted file mode 100644 index b41e225a..00000000 --- a/data/src/main/java/com/yapp/data/remote/repositoryimpl/FortuneRepositoryImpl.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.yapp.data.remote.repositoryimpl - -import com.yapp.data.remote.datasource.FortuneDataSource -import com.yapp.data.remote.dto.response.toDomain -import com.yapp.domain.model.fortune.Fortune -import com.yapp.domain.repository.FortuneRepository -import javax.inject.Inject - -class FortuneRepositoryImpl @Inject constructor( - private val fortuneDataSource: FortuneDataSource, -) : FortuneRepository { - override suspend fun postFortune(userId: Long): Result { - return fortuneDataSource.postFortune(userId) - .mapCatching { fortuneResponse -> - fortuneResponse.toDomain() - } - } - override suspend fun getFortune(fortuneId: Long): Result { - return fortuneDataSource.getFortune(fortuneId) - .mapCatching { fortuneResponse -> - fortuneResponse.toDomain() - } - } -} diff --git a/data/src/main/java/com/yapp/data/remote/repositoryimpl/UserInfoRepositoryImpl.kt b/data/src/main/java/com/yapp/data/remote/repositoryimpl/UserInfoRepositoryImpl.kt deleted file mode 100644 index c4720bb6..00000000 --- a/data/src/main/java/com/yapp/data/remote/repositoryimpl/UserInfoRepositoryImpl.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.yapp.data.remote.repositoryimpl - -import com.yapp.data.remote.datasource.UserInfoDataSource -import com.yapp.data.remote.dto.request.UpdateUserInfoRequest.Companion.toUpdateRequest -import com.yapp.data.remote.dto.response.toDomain -import com.yapp.domain.model.EditUser -import com.yapp.domain.model.User -import com.yapp.domain.repository.UserInfoRepository -import javax.inject.Inject - -class UserInfoRepositoryImpl @Inject constructor( - private val userInfoDataSource: UserInfoDataSource, -) : UserInfoRepository { - override suspend fun getUserInfo(userId: Long): Result { - return userInfoDataSource.getUserInfo(userId) - .mapCatching { userResponse -> - userResponse.toDomain() - } - } - - override suspend fun updateUserInfo(userId: Long, editUser: EditUser): Result { - val request = editUser.toUpdateRequest() - return userInfoDataSource.updateUserInfo(userId, request) - .mapCatching { - if (it) { - Unit - } else { - throw Exception("Failed to update user info") - } - } - } -} diff --git a/data/src/main/java/com/yapp/data/remote/service/DummyService.kt b/data/src/main/java/com/yapp/data/remote/service/DummyService.kt deleted file mode 100644 index 066c49f3..00000000 --- a/data/src/main/java/com/yapp/data/remote/service/DummyService.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.yapp.data.remote.service - -import com.yapp.data.remote.dto.request.RequestDummyDto -import com.yapp.data.remote.dto.response.ResponseDummyDto -import com.yapp.network.model.BaseResponse - -interface DummyService { - suspend fun fetchDummy(): BaseResponse - suspend fun saveDummy(requestDummyDto: RequestDummyDto): BaseResponse -} diff --git a/data/src/main/java/com/yapp/data/remote/utils/ApiCallUtils.kt b/data/src/main/java/com/yapp/data/remote/utils/ApiCallUtils.kt deleted file mode 100644 index b1efd325..00000000 --- a/data/src/main/java/com/yapp/data/remote/utils/ApiCallUtils.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.yapp.data.remote.utils - -import retrofit2.HttpException -import java.io.IOException - -internal inline fun safeApiCall(action: () -> T): Result = - runCatching(action).recoverCatching { exception -> - when (exception) { - is HttpException -> throw mapHttpException(exception) - is IOException -> throw ApiError("๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ ๋ฐœ์ƒ") - else -> throw exception - } - } - -private fun mapHttpException(exception: HttpException): ApiError { - return when (exception.code()) { - 400 -> ApiError("์ž˜๋ชป๋œ ์š”์ฒญ") - 401 -> ApiError("์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค") - 403 -> ApiError("๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค") - 404 -> ApiError("์š”์ฒญํ•œ ๋ฆฌ์†Œ์Šค๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค") - in 500..599 -> ApiError("์„œ๋ฒ„ ์˜ค๋ฅ˜") - else -> ApiError("์•Œ ์ˆ˜ ์—†๋Š” ์„œ๋ฒ„ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.") - } -} diff --git a/data/src/main/java/com/yapp/data/local/repositoryimpl/AlarmRepositoryImpl.kt b/data/src/main/java/com/yapp/data/repositoryimpl/AlarmRepositoryImpl.kt similarity index 86% rename from data/src/main/java/com/yapp/data/local/repositoryimpl/AlarmRepositoryImpl.kt rename to data/src/main/java/com/yapp/data/repositoryimpl/AlarmRepositoryImpl.kt index 6f2a3109..50385b4f 100644 --- a/data/src/main/java/com/yapp/data/local/repositoryimpl/AlarmRepositoryImpl.kt +++ b/data/src/main/java/com/yapp/data/repositoryimpl/AlarmRepositoryImpl.kt @@ -1,8 +1,8 @@ -package com.yapp.data.local.repositoryimpl +package com.yapp.data.repositoryimpl import android.net.Uri import com.yapp.data.local.datasource.AlarmLocalDataSource -import com.yapp.data.local.toEntity +import com.yapp.database.toEntity import com.yapp.domain.model.Alarm import com.yapp.domain.model.AlarmSound import com.yapp.domain.repository.AlarmRepository @@ -45,14 +45,8 @@ class AlarmRepositoryImpl @Inject constructor( override fun getAllAlarms(): Flow> = alarmLocalDataSource.getAllAlarms() - override fun getPagedAlarms(limit: Int, offset: Int): Flow> = - alarmLocalDataSource.getPagedAlarms(limit, offset) - - override fun getAlarmsByTime(hour: Int, minute: Int, isAm: Boolean): Flow> = - alarmLocalDataSource.getAlarmsByTime(hour, minute, isAm) - - override fun getAlarmCount(): Flow = - alarmLocalDataSource.getAlarmCount() + override fun getAlarmsByTime(hour: Int, minute: Int): Flow> = + alarmLocalDataSource.getAlarmsByTime(hour, minute) override suspend fun insertAlarm(alarm: Alarm): Result = runCatching { val alarmId = alarmLocalDataSource.insertAlarm(alarm.toEntity()) diff --git a/data/src/main/java/com/yapp/data/repositoryimpl/FortuneRepositoryImpl.kt b/data/src/main/java/com/yapp/data/repositoryimpl/FortuneRepositoryImpl.kt new file mode 100644 index 00000000..1c761ba6 --- /dev/null +++ b/data/src/main/java/com/yapp/data/repositoryimpl/FortuneRepositoryImpl.kt @@ -0,0 +1,47 @@ +package com.yapp.data.repositoryimpl + +import com.yapp.data.local.datasource.FortuneLocalDataSource +import com.yapp.data.remote.datasource.FortuneDataSource +import com.yapp.data.remote.dto.response.toDomain +import com.yapp.domain.model.Fortune +import com.yapp.domain.model.FortuneCreateStatus +import com.yapp.domain.repository.FortuneRepository +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class FortuneRepositoryImpl @Inject constructor( + private val fortuneLocalDataSource: FortuneLocalDataSource, + private val fortuneRemoteDataSource: FortuneDataSource, +) : FortuneRepository { + + override val fortuneIdFlow: Flow = fortuneLocalDataSource.fortuneIdFlow + override val fortuneDateEpochFlow: Flow = fortuneLocalDataSource.fortuneDateEpochFlow + override val fortuneImageIdFlow: Flow = fortuneLocalDataSource.fortuneImageIdFlow + override val fortuneScoreFlow: Flow = fortuneLocalDataSource.fortuneScoreFlow + override val hasUnseenFortuneFlow: Flow = fortuneLocalDataSource.hasUnseenFortuneFlow + override val shouldShowFortuneToolTipFlow: Flow = fortuneLocalDataSource.shouldShowFortuneToolTipFlow + override val isFirstAlarmDismissedTodayFlow: Flow = fortuneLocalDataSource.isFirstAlarmDismissedTodayFlow + + override val fortuneCreateStatusFlow: Flow = fortuneLocalDataSource.fortuneCreateStatusFlow + + override suspend fun markFortuneAsCreating() = fortuneLocalDataSource.markFortuneCreating() + override suspend fun markFortuneAsCreated(fortuneId: Long) = fortuneLocalDataSource.markFortuneCreated(fortuneId) + override suspend fun markFortuneAsFailed() = fortuneLocalDataSource.markFortuneFailed() + override suspend fun markFortuneSeen() = fortuneLocalDataSource.markFortuneSeen() + override suspend fun markFortuneTooltipShown() = fortuneLocalDataSource.markFortuneTooltipShown() + override suspend fun saveFortuneImageId(imageResId: Int) = fortuneLocalDataSource.saveFortuneImageId(imageResId) + override suspend fun saveFortuneScore(score: Int) = fortuneLocalDataSource.saveFortuneScore(score) + override suspend fun markFirstAlarmDismissedToday() = fortuneLocalDataSource.markFirstAlarmDismissedToday() + + override suspend fun clearFortuneData() = fortuneLocalDataSource.clearFortuneData() + + override suspend fun postFortune(userId: Long): Result { + return fortuneRemoteDataSource.postFortune(userId) + .mapCatching { it.toDomain() } + } + + override suspend fun getFortune(fortuneId: Long): Result { + return fortuneRemoteDataSource.getFortune(fortuneId) + .mapCatching { it.toDomain() } + } +} diff --git a/data/src/main/java/com/yapp/data/remote/repositoryimpl/RemoteConfigRepositoryImpl.kt b/data/src/main/java/com/yapp/data/repositoryimpl/RemoteConfigRepositoryImpl.kt similarity index 92% rename from data/src/main/java/com/yapp/data/remote/repositoryimpl/RemoteConfigRepositoryImpl.kt rename to data/src/main/java/com/yapp/data/repositoryimpl/RemoteConfigRepositoryImpl.kt index e45ae5a1..46a14431 100644 --- a/data/src/main/java/com/yapp/data/remote/repositoryimpl/RemoteConfigRepositoryImpl.kt +++ b/data/src/main/java/com/yapp/data/repositoryimpl/RemoteConfigRepositoryImpl.kt @@ -1,4 +1,4 @@ -package com.yapp.data.remote.repositoryimpl +package com.yapp.data.repositoryimpl import com.yapp.domain.model.MissionType import com.yapp.domain.repository.RemoteConfigRepository diff --git a/data/src/main/java/com/yapp/data/remote/repositoryimpl/SignUpRepositoryImpl.kt b/data/src/main/java/com/yapp/data/repositoryimpl/SignUpRepositoryImpl.kt similarity index 94% rename from data/src/main/java/com/yapp/data/remote/repositoryimpl/SignUpRepositoryImpl.kt rename to data/src/main/java/com/yapp/data/repositoryimpl/SignUpRepositoryImpl.kt index 5977c5ae..2c593a9c 100644 --- a/data/src/main/java/com/yapp/data/remote/repositoryimpl/SignUpRepositoryImpl.kt +++ b/data/src/main/java/com/yapp/data/repositoryimpl/SignUpRepositoryImpl.kt @@ -1,4 +1,4 @@ -package com.yapp.data.remote.repositoryimpl +package com.yapp.data.repositoryimpl import android.util.Log import com.yapp.data.remote.datasource.SignUpDataSource diff --git a/data/src/main/java/com/yapp/data/repositoryimpl/UserInfoRepositoryImpl.kt b/data/src/main/java/com/yapp/data/repositoryimpl/UserInfoRepositoryImpl.kt new file mode 100644 index 00000000..818e232d --- /dev/null +++ b/data/src/main/java/com/yapp/data/repositoryimpl/UserInfoRepositoryImpl.kt @@ -0,0 +1,48 @@ +package com.yapp.data.repositoryimpl + +import com.yapp.data.local.datasource.UserLocalDataSource +import com.yapp.data.remote.datasource.UserInfoDataSource +import com.yapp.data.remote.dto.request.UpdateUserInfoRequest.Companion.toUpdateRequest +import com.yapp.data.remote.dto.response.toDomain +import com.yapp.domain.model.EditUser +import com.yapp.domain.model.User +import com.yapp.domain.repository.UserInfoRepository +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class UserInfoRepositoryImpl @Inject constructor( + private val userLocalDataSource: UserLocalDataSource, + private val userInfoDataSource: UserInfoDataSource, +) : UserInfoRepository { + override val userIdFlow: Flow = userLocalDataSource.userIdFlow + override val userNameFlow: Flow = userLocalDataSource.userNameFlow + override val onboardingCompletedFlow: Flow = userLocalDataSource.onboardingCompletedFlow + override val updateNoticeDontShowVersionFlow: Flow = userLocalDataSource.updateNoticeDontShowVersionFlow + override val updateNoticeLastShownDateEpochFlow: Flow = userLocalDataSource.updateNoticeLastShownDateEpochFlow + + override suspend fun saveUserId(userId: Long) = userLocalDataSource.saveUserId(userId) + override suspend fun saveUserName(userName: String) = userLocalDataSource.saveUserName(userName) + override suspend fun setOnboardingCompleted() = userLocalDataSource.setOnboardingCompleted() + override suspend fun markUpdateNoticeDontShow(version: String) = userLocalDataSource.markUpdateNoticeDontShow(version) + override suspend fun markUpdateNoticeShownToday() = userLocalDataSource.markUpdateNoticeShownToday() + override suspend fun clearUserData() = userLocalDataSource.clearUserData() + + override suspend fun getUserInfo(userId: Long): Result { + return userInfoDataSource.getUserInfo(userId) + .mapCatching { userResponse -> + userResponse.toDomain() + } + } + + override suspend fun updateUserInfo(userId: Long, editUser: EditUser): Result { + val request = editUser.toUpdateRequest() + return userInfoDataSource.updateUserInfo(userId, request) + .mapCatching { + if (it) { + Unit + } else { + throw Exception("Failed to update user info") + } + } + } +} diff --git a/data/src/test/kotlin/com/yapp/data/FortuneDataSourceImplTest.kt b/data/src/test/kotlin/com/yapp/data/FortuneDataSourceImplTest.kt new file mode 100644 index 00000000..b7d9d0f4 --- /dev/null +++ b/data/src/test/kotlin/com/yapp/data/FortuneDataSourceImplTest.kt @@ -0,0 +1,83 @@ +package com.yapp.data + +import com.yapp.data.remote.datasource.FortuneDataSourceImpl +import com.yapp.data.remote.dto.response.FortuneResponse +import com.yapp.data.remote.service.ApiService +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class FortuneDataSourceImplTest { + + private lateinit var dataSource: FortuneDataSourceImpl + private val apiService: ApiService = mockk() + + @Before + fun setup() { + dataSource = FortuneDataSourceImpl(apiService) + } + + @Test + fun `์šด์„ธ ๋“ฑ๋ก์— ์„ฑ๊ณตํ•˜๋ฉด ์„ฑ๊ณต Result๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค`() = runTest { + // Given + val userId = 1L + val mockResponse = mockk() + coEvery { apiService.postFortune(userId) } returns mockResponse + + // When + val result = dataSource.postFortune(userId) + + // Then + assertTrue(result.isSuccess) + assertEquals(mockResponse, result.getOrNull()) + coVerify { apiService.postFortune(userId) } + } + + @Test + fun `์šด์„ธ ๋“ฑ๋ก ์ค‘ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์‹คํŒจ Result๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค`() = runTest { + // Given + val userId = 1L + coEvery { apiService.postFortune(userId) } throws RuntimeException("Network Error") + + // When + val result = dataSource.postFortune(userId) + + // Then + assertTrue(result.isFailure) + coVerify { apiService.postFortune(userId) } + } + + @Test + fun `์šด์„ธ ์กฐํšŒ์— ์„ฑ๊ณตํ•˜๋ฉด ์„ฑ๊ณต Result๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค`() = runTest { + // Given + val fortuneId = 10L + val mockResponse = mockk() + coEvery { apiService.getFortune(fortuneId) } returns mockResponse + + // When + val result = dataSource.getFortune(fortuneId) + + // Then + assertTrue(result.isSuccess) + assertEquals(mockResponse, result.getOrNull()) + coVerify { apiService.getFortune(fortuneId) } + } + + @Test + fun `์šด์„ธ ์กฐํšŒ ์ค‘ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์‹คํŒจ Result๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค`() = runTest { + // Given + val fortuneId = 10L + coEvery { apiService.getFortune(fortuneId) } throws RuntimeException("Network Error") + + // When + val result = dataSource.getFortune(fortuneId) + + // Then + assertTrue(result.isFailure) + coVerify { apiService.getFortune(fortuneId) } + } +} diff --git a/data/src/test/kotlin/com/yapp/data/FortuneMapperTest.kt b/data/src/test/kotlin/com/yapp/data/FortuneMapperTest.kt new file mode 100644 index 00000000..6c3313b7 --- /dev/null +++ b/data/src/test/kotlin/com/yapp/data/FortuneMapperTest.kt @@ -0,0 +1,57 @@ +package com.yapp.data + +import com.yapp.data.remote.dto.response.FortuneDetail +import com.yapp.data.remote.dto.response.FortuneResponse +import com.yapp.data.remote.dto.response.toDomain +import org.junit.Assert.assertEquals +import org.junit.Test + +class FortuneMapperTest { + + @Test + fun `FortuneResponse๋ฅผ ๋„๋ฉ”์ธ ๋ชจ๋ธ๋กœ ๋งคํ•‘ํ•˜๋ฉด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋ณ€ํ™˜๋œ๋‹ค`() { + val response = dummyFortuneResponse() + val domain = response.toDomain() + + assertEquals(response.id, domain.id) + assertEquals(response.dailyFortune, domain.dailyFortuneTitle) + assertEquals(response.dailyFortuneDescription, domain.dailyFortuneDescription) + assertEquals(response.avgFortuneScore, domain.avgFortuneScore) + assertEquals(response.studyCareerFortune.toDomain(), domain.studyCareerFortune) + assertEquals(response.luckyFood, domain.luckyFood) + } + + @Test + fun `FortuneDetail์„ ๋„๋ฉ”์ธ ๋ชจ๋ธ๋กœ ๋งคํ•‘ํ•˜๋ฉด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋ณ€ํ™˜๋œ๋‹ค`() { + val detail = FortuneDetail(score = 85, title = "Success", description = "Great things happen") + val domain = detail.toDomain() + + assertEquals(85, domain.score) + assertEquals("Success", domain.title) + assertEquals("Great things happen", domain.description) + } + + private fun dummyFortuneResponse() = FortuneResponse( + id = 123, + dailyFortune = "Today is your lucky day", + dailyFortuneDescription = "You'll find success in your endeavors.", + avgFortuneScore = 88, + studyCareerFortune = dummyDetail(), + wealthFortune = dummyDetail(), + healthFortune = dummyDetail(), + loveFortune = dummyDetail(), + luckyOutfitTop = "T-shirt", + luckyOutfitBottom = "Shorts", + luckyOutfitShoes = "Sneakers", + luckyOutfitAccessory = "Bracelet", + unluckyColor = "Gray", + luckyColor = "Yellow", + luckyFood = "Sushi" + ) + + private fun dummyDetail() = FortuneDetail( + score = 90, + title = "High Energy", + description = "You will feel energetic all day." + ) +} diff --git a/data/src/test/kotlin/com/yapp/data/FortuneRepositoryImplTest.kt b/data/src/test/kotlin/com/yapp/data/FortuneRepositoryImplTest.kt new file mode 100644 index 00000000..7abaf7b3 --- /dev/null +++ b/data/src/test/kotlin/com/yapp/data/FortuneRepositoryImplTest.kt @@ -0,0 +1,69 @@ +package com.yapp.data + +import com.yapp.data.local.datasource.FortuneLocalDataSource +import com.yapp.data.remote.datasource.FortuneDataSource +import com.yapp.data.remote.dto.response.FortuneDetail +import com.yapp.data.remote.dto.response.FortuneResponse +import com.yapp.data.remote.dto.response.toDomain +import com.yapp.data.repositoryimpl.FortuneRepositoryImpl +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Test + +class FortuneRepositoryImplTest { + + private val remoteDataSource = mockk() + private val localDataSource = mockk(relaxed = true) + + private val repository = FortuneRepositoryImpl( + fortuneRemoteDataSource = remoteDataSource, + fortuneLocalDataSource = localDataSource, + ) + + @Test + fun `์šด์„ธ ์š”์ฒญ์— ์„ฑ๊ณตํ•˜๋ฉด ๋„๋ฉ”์ธ ๋ชจ๋ธ๋กœ ๋ฐ˜ํ™˜๋œ๋‹ค`() = runTest { + val response = dummyFortuneResponse() + coEvery { remoteDataSource.postFortune(1L) } returns Result.success(response) + + val result = repository.postFortune(1L) + + assert(result.isSuccess) + assertEquals(response.toDomain(), result.getOrNull()) + } + + @Test + fun `์šด์„ธ ์ƒ์„ธ ์กฐํšŒ์— ์‹คํŒจํ•˜๋ฉด ์‹คํŒจ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค`() = runTest { + val exception = RuntimeException("Not found") + coEvery { remoteDataSource.getFortune(2L) } returns Result.failure(exception) + + val result = repository.getFortune(2L) + + assert(result.isFailure) + } + + private fun dummyFortuneResponse() = FortuneResponse( + id = 1L, + dailyFortune = "Good luck", + dailyFortuneDescription = "You will be lucky today", + avgFortuneScore = 90, + studyCareerFortune = dummyDetail(), + wealthFortune = dummyDetail(), + healthFortune = dummyDetail(), + loveFortune = dummyDetail(), + luckyOutfitTop = "Hoodie", + luckyOutfitBottom = "Jeans", + luckyOutfitShoes = "Sneakers", + luckyOutfitAccessory = "Watch", + unluckyColor = "Black", + luckyColor = "White", + luckyFood = "Pizza", + ) + + private fun dummyDetail() = FortuneDetail( + score = 100, + title = "Title", + description = "Description" + ) +} diff --git a/domain/src/main/java/com/yapp/domain/MissionMode.kt b/domain/src/main/java/com/yapp/domain/MissionMode.kt new file mode 100644 index 00000000..b047c7c8 --- /dev/null +++ b/domain/src/main/java/com/yapp/domain/MissionMode.kt @@ -0,0 +1,13 @@ +package com.yapp.domain + +enum class MissionMode { + REAL, + PREVIEW, + ; + + companion object { + fun fromRaw(raw: String?): MissionMode { + return raw?.let { entries.find { it.name == raw } } ?: REAL + } + } +} diff --git a/domain/src/main/java/com/yapp/domain/model/Alarm.kt b/domain/src/main/java/com/yapp/domain/model/Alarm.kt index f04d4148..14079633 100644 --- a/domain/src/main/java/com/yapp/domain/model/Alarm.kt +++ b/domain/src/main/java/com/yapp/domain/model/Alarm.kt @@ -12,8 +12,6 @@ import kotlinx.serialization.json.Json data class Alarm( val id: Long = 0, - val isAm: Boolean = true, - val hour: Int = 6, val minute: Int = 0, val second: Int = 0, @@ -35,6 +33,9 @@ data class Alarm( val soundVolume: Int = 70, val isAlarmActive: Boolean = true, + + val missionType: MissionType = MissionType.TAP, + val missionCount: Int = 10, ) : Parcelable { companion object { @@ -62,14 +63,7 @@ fun Alarm.copyFrom(source: Alarm): Alarm { } fun Alarm.toTimeString(): String { - val displayHour = if (isAm && hour == 12) { - 0 // ์˜ค์ „ 12์‹œ๋Š” 0์œผ๋กœ ํ‘œ์‹œ - } else if (!isAm && hour != 12) { - hour + 12 // ์˜คํ›„ 1์‹œ~11์‹œ์—๋Š” 12๋ฅผ ๋”ํ•จ - } else { - hour // ์˜ค์ „ 1์‹œ~11์‹œ ๋ฐ ์˜คํ›„ 12์‹œ๋Š” ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ - } - val formattedHour = displayHour.toString().padStart(2, '0') + val formattedHour = hour.toString().padStart(2, '0') val formattedMinute = minute.toString().padStart(2, '0') return "$formattedHour:$formattedMinute" diff --git a/domain/src/main/java/com/yapp/domain/model/AlarmDay.kt b/domain/src/main/java/com/yapp/domain/model/AlarmDay.kt index beaead69..7f349ed5 100644 --- a/domain/src/main/java/com/yapp/domain/model/AlarmDay.kt +++ b/domain/src/main/java/com/yapp/domain/model/AlarmDay.kt @@ -1,5 +1,7 @@ package com.yapp.domain.model +import java.time.DayOfWeek + enum class AlarmDay(val bitValue: Int) { SUN(0b0000001), // 1 MON(0b0000010), // 2 @@ -11,8 +13,13 @@ enum class AlarmDay(val bitValue: Int) { ; } -fun AlarmDay.toDayOfWeek(): java.time.DayOfWeek { - return java.time.DayOfWeek.of(((this.ordinal + 6) % 7) + 1) +fun AlarmDay.toDayOfWeek(): DayOfWeek { + return DayOfWeek.of(((this.ordinal + 6) % 7) + 1) +} + +fun DayOfWeek.toAlarmDay(): AlarmDay { + val index = (this.value % 7) + return AlarmDay.entries[index] } fun Set.toRepeatDays(): Int { diff --git a/domain/src/main/java/com/yapp/domain/model/Dummy.kt b/domain/src/main/java/com/yapp/domain/model/Dummy.kt deleted file mode 100644 index 9a450d6f..00000000 --- a/domain/src/main/java/com/yapp/domain/model/Dummy.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.yapp.domain.model - -data class Dummy( - val id: Int, - val name: String, -) diff --git a/domain/src/main/java/com/yapp/domain/model/fortune/Fortune.kt b/domain/src/main/java/com/yapp/domain/model/Fortune.kt similarity index 94% rename from domain/src/main/java/com/yapp/domain/model/fortune/Fortune.kt rename to domain/src/main/java/com/yapp/domain/model/Fortune.kt index 0a15638f..0af841cc 100644 --- a/domain/src/main/java/com/yapp/domain/model/fortune/Fortune.kt +++ b/domain/src/main/java/com/yapp/domain/model/Fortune.kt @@ -1,4 +1,4 @@ -package com.yapp.domain.model.fortune +package com.yapp.domain.model data class Fortune( val id: Long, diff --git a/domain/src/main/java/com/yapp/domain/model/FortuneCreateStatus.kt b/domain/src/main/java/com/yapp/domain/model/FortuneCreateStatus.kt new file mode 100644 index 00000000..27ae9ad0 --- /dev/null +++ b/domain/src/main/java/com/yapp/domain/model/FortuneCreateStatus.kt @@ -0,0 +1,8 @@ +package com.yapp.domain.model + +sealed class FortuneCreateStatus { + data object Idle : FortuneCreateStatus() + data object Creating : FortuneCreateStatus() + data class Success(val fortuneId: Long) : FortuneCreateStatus() + data object Failure : FortuneCreateStatus() +} diff --git a/domain/src/main/java/com/yapp/domain/model/MissionMode.kt b/domain/src/main/java/com/yapp/domain/model/MissionMode.kt new file mode 100644 index 00000000..16009fc2 --- /dev/null +++ b/domain/src/main/java/com/yapp/domain/model/MissionMode.kt @@ -0,0 +1,13 @@ +package com.yapp.domain.model + +enum class MissionMode { + REAL, + PREVIEW, + ; + + companion object { + fun fromRaw(raw: String?): MissionMode { + return raw?.let { entries.find { it.name == raw } } ?: REAL + } + } +} diff --git a/domain/src/main/java/com/yapp/domain/model/MissionType.kt b/domain/src/main/java/com/yapp/domain/model/MissionType.kt index 45388ee6..bcfdff3c 100644 --- a/domain/src/main/java/com/yapp/domain/model/MissionType.kt +++ b/domain/src/main/java/com/yapp/domain/model/MissionType.kt @@ -1,17 +1,21 @@ package com.yapp.domain.model -sealed class MissionType { - data object Shake : MissionType() - data object Click : MissionType() +enum class MissionType(val value: Int) { + NONE(0), + TAP(1), + SHAKE(2), + ; companion object { + fun fromInt(value: Int): MissionType { + return MissionType.entries.find { it.value == value } ?: NONE + } + fun fromRemoteValue(value: String): MissionType { return when (value) { - "tap_mission" -> Click - "shake_mission" -> Shake - else -> { - Click - } + "tap_mission" -> TAP + "shake_mission" -> SHAKE + else -> NONE } } } diff --git a/domain/src/main/java/com/yapp/domain/repository/AlarmRepository.kt b/domain/src/main/java/com/yapp/domain/repository/AlarmRepository.kt index d3a7ae83..60123473 100644 --- a/domain/src/main/java/com/yapp/domain/repository/AlarmRepository.kt +++ b/domain/src/main/java/com/yapp/domain/repository/AlarmRepository.kt @@ -13,9 +13,7 @@ interface AlarmRepository { fun updateAlarmVolume(volume: Int) fun releaseSoundPlayer() fun getAllAlarms(): Flow> - fun getPagedAlarms(limit: Int, offset: Int): Flow> - fun getAlarmsByTime(hour: Int, minute: Int, isAm: Boolean): Flow> - fun getAlarmCount(): Flow + fun getAlarmsByTime(hour: Int, minute: Int): Flow> suspend fun insertAlarm(alarm: Alarm): Result suspend fun updateAlarm(alarm: Alarm): Result suspend fun updateAlarmActive(id: Long, active: Boolean): Result diff --git a/domain/src/main/java/com/yapp/domain/repository/DummyRepository.kt b/domain/src/main/java/com/yapp/domain/repository/DummyRepository.kt deleted file mode 100644 index c64fe8b0..00000000 --- a/domain/src/main/java/com/yapp/domain/repository/DummyRepository.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.yapp.domain.repository - -import com.yapp.domain.model.Dummy - -interface DummyRepository { - suspend fun fetchDummy(): Result - suspend fun saveDummy(dummy: Dummy): Result -} diff --git a/domain/src/main/java/com/yapp/domain/repository/FortuneRepository.kt b/domain/src/main/java/com/yapp/domain/repository/FortuneRepository.kt index efbabfc1..372fd5fe 100644 --- a/domain/src/main/java/com/yapp/domain/repository/FortuneRepository.kt +++ b/domain/src/main/java/com/yapp/domain/repository/FortuneRepository.kt @@ -1,8 +1,31 @@ package com.yapp.domain.repository -import com.yapp.domain.model.fortune.Fortune +import com.yapp.domain.model.Fortune +import com.yapp.domain.model.FortuneCreateStatus +import kotlinx.coroutines.flow.Flow interface FortuneRepository { + val fortuneIdFlow: Flow + val fortuneDateEpochFlow: Flow + val fortuneImageIdFlow: Flow + val fortuneScoreFlow: Flow + val hasUnseenFortuneFlow: Flow + val shouldShowFortuneToolTipFlow: Flow + val isFirstAlarmDismissedTodayFlow: Flow + + val fortuneCreateStatusFlow: Flow + + suspend fun markFortuneAsCreating() + suspend fun markFortuneAsCreated(fortuneId: Long) + suspend fun markFortuneAsFailed() + suspend fun markFortuneSeen() + suspend fun markFortuneTooltipShown() + suspend fun saveFortuneImageId(imageResId: Int) + suspend fun saveFortuneScore(score: Int) + suspend fun markFirstAlarmDismissedToday() + + suspend fun clearFortuneData() + suspend fun postFortune(userId: Long): Result suspend fun getFortune(fortuneId: Long): Result } diff --git a/domain/src/main/java/com/yapp/domain/repository/ImageRepository.kt b/domain/src/main/java/com/yapp/domain/repository/ImageRepository.kt deleted file mode 100644 index 9abddf8a..00000000 --- a/domain/src/main/java/com/yapp/domain/repository/ImageRepository.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.yapp.domain.repository - -interface ImageRepository { - suspend fun saveImage(byteArray: ByteArray): Boolean -} diff --git a/domain/src/main/java/com/yapp/domain/repository/UserInfoRepository.kt b/domain/src/main/java/com/yapp/domain/repository/UserInfoRepository.kt index 34a6580a..a9df412e 100644 --- a/domain/src/main/java/com/yapp/domain/repository/UserInfoRepository.kt +++ b/domain/src/main/java/com/yapp/domain/repository/UserInfoRepository.kt @@ -2,8 +2,22 @@ package com.yapp.domain.repository import com.yapp.domain.model.EditUser import com.yapp.domain.model.User +import kotlinx.coroutines.flow.Flow interface UserInfoRepository { + val userIdFlow: Flow + val userNameFlow: Flow + val onboardingCompletedFlow: Flow + val updateNoticeDontShowVersionFlow: Flow + val updateNoticeLastShownDateEpochFlow: Flow + + suspend fun saveUserId(userId: Long) + suspend fun saveUserName(userName: String) + suspend fun setOnboardingCompleted() + suspend fun markUpdateNoticeDontShow(version: String) + suspend fun markUpdateNoticeShownToday() + suspend fun clearUserData() + suspend fun getUserInfo(userId: Long): Result suspend fun updateUserInfo(userId: Long, editUser: EditUser): Result } diff --git a/domain/src/main/java/com/yapp/domain/scheduler/AlarmScheduler.kt b/domain/src/main/java/com/yapp/domain/scheduler/AlarmScheduler.kt new file mode 100644 index 00000000..1656ac30 --- /dev/null +++ b/domain/src/main/java/com/yapp/domain/scheduler/AlarmScheduler.kt @@ -0,0 +1,8 @@ +package com.yapp.domain.scheduler + +import com.yapp.domain.model.Alarm + +interface AlarmScheduler { + fun scheduleAlarm(alarm: Alarm) + fun unScheduleAlarm(alarm: Alarm) +} diff --git a/domain/src/main/java/com/yapp/domain/usecase/AlarmUseCase.kt b/domain/src/main/java/com/yapp/domain/usecase/AlarmUseCase.kt index 2411beeb..86720460 100644 --- a/domain/src/main/java/com/yapp/domain/usecase/AlarmUseCase.kt +++ b/domain/src/main/java/com/yapp/domain/usecase/AlarmUseCase.kt @@ -4,11 +4,13 @@ import android.net.Uri import com.yapp.domain.model.Alarm import com.yapp.domain.model.AlarmSound import com.yapp.domain.repository.AlarmRepository +import com.yapp.domain.scheduler.AlarmScheduler import kotlinx.coroutines.flow.Flow import javax.inject.Inject class AlarmUseCase @Inject constructor( private val alarmRepository: AlarmRepository, + private val alarmScheduler: AlarmScheduler, ) { suspend fun getAlarmSounds(): Result> = alarmRepository.getAlarmSounds() fun initializeSoundPlayer(uri: Uri) = alarmRepository.initializeSoundPlayer(uri) @@ -17,12 +19,13 @@ class AlarmUseCase @Inject constructor( fun updateAlarmVolume(volume: Int) = alarmRepository.updateAlarmVolume(volume) fun releaseSoundPlayer() = alarmRepository.releaseSoundPlayer() fun getAllAlarms(): Flow> = alarmRepository.getAllAlarms() - fun getPagedAlarms(limit: Int, offset: Int): Flow> = alarmRepository.getPagedAlarms(limit, offset) - fun getAlarmsByTime(hour: Int, minute: Int, isAm: Boolean): Flow> = alarmRepository.getAlarmsByTime(hour, minute, isAm) - fun getAlarmCount(): Flow = alarmRepository.getAlarmCount() + fun getAlarmsByTime(hour: Int, minute: Int): Flow> = alarmRepository.getAlarmsByTime(hour, minute) suspend fun insertAlarm(alarm: Alarm): Result = alarmRepository.insertAlarm(alarm) suspend fun updateAlarm(alarm: Alarm): Result = alarmRepository.updateAlarm(alarm) suspend fun updateAlarmActive(id: Long, active: Boolean): Result = alarmRepository.updateAlarmActive(id, active) suspend fun getAlarm(id: Long): Result = alarmRepository.getAlarm(id) suspend fun deleteAlarm(id: Long): Result = alarmRepository.deleteAlarm(id) + + fun scheduleAlarm(alarm: Alarm) = alarmScheduler.scheduleAlarm(alarm) + fun unScheduleAlarm(alarm: Alarm) = alarmScheduler.unScheduleAlarm(alarm) } diff --git a/domain/src/main/java/com/yapp/domain/usecase/DummyUseCase.kt b/domain/src/main/java/com/yapp/domain/usecase/DummyUseCase.kt deleted file mode 100644 index a3584da6..00000000 --- a/domain/src/main/java/com/yapp/domain/usecase/DummyUseCase.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.yapp.domain.usecase - -import com.yapp.domain.model.Dummy -import com.yapp.domain.repository.DummyRepository -import javax.inject.Inject - -class DummyUseCase @Inject constructor( - private val dummyRepository: DummyRepository, -) { - suspend fun fetch(): Result = dummyRepository.fetchDummy() - suspend fun save(dummy: Dummy): Result = dummyRepository.saveDummy(dummy) -} diff --git a/feature/alarm-interaction/build.gradle.kts b/feature/alarm-interaction/build.gradle.kts index efc6eeec..22e53709 100644 --- a/feature/alarm-interaction/build.gradle.kts +++ b/feature/alarm-interaction/build.gradle.kts @@ -15,7 +15,6 @@ dependencies { implementation(projects.core.alarm) implementation(projects.core.media) implementation(projects.domain) - implementation(projects.core.datastore) implementation(libs.orbit.core) implementation(libs.orbit.compose) implementation(libs.orbit.viewmodel) diff --git a/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/AlarmInteractionActivity.kt b/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/AlarmInteractionActivity.kt index 5860eb6d..e95a93b2 100644 --- a/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/AlarmInteractionActivity.kt +++ b/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/AlarmInteractionActivity.kt @@ -7,14 +7,11 @@ import android.content.IntentFilter import android.content.pm.ActivityInfo import android.os.Build import android.os.Bundle -import android.util.Log 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.Box import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.material3.Surface import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Modifier import androidx.core.util.Consumer @@ -23,8 +20,8 @@ import com.yapp.alarm.AlarmConstants import com.yapp.alarm.receivers.AlarmInteractionActivityReceiver import com.yapp.common.navigation.rememberOrbitNavigator import com.yapp.common.navigation.route.AlarmInteractionBaseRoute -import com.yapp.designsystem.theme.OrbitTheme import com.yapp.domain.model.Alarm +import com.yapp.ui.component.navigation.NavigationBarScrim import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -48,35 +45,23 @@ class AlarmInteractionActivity : ComponentActivity() { registerAlarmInteractionActivityCloseReceiver() - enableEdgeToEdge( - statusBarStyle = SystemBarStyle.light( - android.graphics.Color.TRANSPARENT, - android.graphics.Color.TRANSPARENT, - ), - navigationBarStyle = SystemBarStyle.light( - android.graphics.Color.BLACK, - android.graphics.Color.BLACK, - ), - ) + enableEdgeToEdge() setContent { val navigator = rememberOrbitNavigator() - Surface( - color = OrbitTheme.colors.gray_900, - modifier = Modifier - .fillMaxSize() - .navigationBarsPadding(), - ) { + Box { NavHost( + modifier = Modifier.navigationBarsPadding(), navController = navigator.navController, startDestination = AlarmInteractionBaseRoute, - modifier = Modifier.navigationBarsPadding(), ) { alarmInteractionNavGraph( navigator = navigator, alarm = alarm, ) } + + NavigationBarScrim() } DisposableEffect(this, navigator.navController) { @@ -87,7 +72,6 @@ class AlarmInteractionActivity : ComponentActivity() { @Suppress("DEPRECATION") newIntent.getParcelableExtra(AlarmConstants.EXTRA_ALARM) } - Log.d("AlarmInteractionActivity", "New Intent: $newIntent") newAlarm?.let { alarm -> navigator.navigateToAlarmAction(alarm = alarm) } diff --git a/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/AlarmInteractionNavGraph.kt b/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/AlarmInteractionNavGraph.kt index bc8d0035..eb025a86 100644 --- a/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/AlarmInteractionNavGraph.kt +++ b/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/AlarmInteractionNavGraph.kt @@ -48,14 +48,6 @@ fun NavGraphBuilder.alarmInteractionNavGraph( } }, ) - } ?: run { - navigator.navigateToHome( - navOptions { - popUpTo(AlarmInteractionBaseRoute) { - inclusive = true - } - }, - ) } } } @@ -71,9 +63,7 @@ fun NavGraphBuilder.alarmInteractionNavGraph( composable( typeMap = mapOf(typeOf() to AlarmArgType), ) { - AlarmSnoozeTimerRoute( - navigator = navigator, - ) + AlarmSnoozeTimerRoute() } } } diff --git a/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/action/AlarmActionContract.kt b/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/action/AlarmActionContract.kt index 470f89c3..9eee8bad 100644 --- a/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/action/AlarmActionContract.kt +++ b/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/action/AlarmActionContract.kt @@ -1,7 +1,6 @@ package com.yapp.alarm.interaction.action import com.yapp.domain.model.Alarm -import com.yapp.ui.base.SideEffect import com.yapp.ui.base.UiState class AlarmActionContract { @@ -15,7 +14,7 @@ class AlarmActionContract { val snoozeEnabled: Boolean = true, val snoozeInterval: Int = 5, val snoozeCount: Int = 5, - val isFirstMission: Boolean? = null, + val shouldShowMissionStart: Boolean? = null, ) : UiState sealed class Action { diff --git a/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/action/AlarmActionScreen.kt b/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/action/AlarmActionScreen.kt index 4db7ed7b..8ff13cb9 100644 --- a/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/action/AlarmActionScreen.kt +++ b/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/action/AlarmActionScreen.kt @@ -17,7 +17,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.Surface 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 @@ -36,6 +35,7 @@ import com.yapp.ui.component.button.OrbitButton import com.yapp.ui.component.lottie.LottieAnimation import com.yapp.ui.utils.heightForScreenPercentage import feature.alarm.interaction.R +import org.orbitmvi.orbit.compose.collectSideEffect import java.util.Locale @Composable @@ -44,30 +44,33 @@ internal fun AlarmActionRoute( navigator: OrbitNavigator, ) { val state by viewModel.container.stateFlow.collectAsStateWithLifecycle() - val sideEffect = viewModel.container.sideEffectFlow - LaunchedEffect(sideEffect) { - sideEffect.collect { action -> - when (action) { - is AlarmActionContract.SideEffect.NavigateToAlarmSnooze -> { - navigator.navigateToAlarmSnoozeTimer(action.alarm) - } - } - } + viewModel.collectSideEffect { + handleSideEffect(it, navigator) } AlarmActionScreen( - stateProvider = { state }, - eventDispatcher = viewModel::processAction, + state = state, + processAction = viewModel::processAction, ) } +private fun handleSideEffect( + sideEffect: AlarmActionContract.SideEffect, + navigator: OrbitNavigator, +) { + when (sideEffect) { + is AlarmActionContract.SideEffect.NavigateToAlarmSnooze -> { + navigator.navigateToAlarmSnoozeTimer(sideEffect.alarm) + } + } +} + @Composable internal fun AlarmActionScreen( - stateProvider: () -> AlarmActionContract.State, - eventDispatcher: (AlarmActionContract.Action) -> Unit, + state: AlarmActionContract.State, + processAction: (AlarmActionContract.Action) -> Unit, ) { - val state = stateProvider() val context = LocalContext.current if (state.initialLoading) { @@ -81,10 +84,10 @@ internal fun AlarmActionScreen( snoozeEnabled = state.snoozeEnabled, snoozeInterval = state.snoozeInterval, snoozeCount = state.snoozeCount, - isFirstMission = state.isFirstMission, - onSnoozeClick = { eventDispatcher(AlarmActionContract.Action.Snooze) }, + isFirstMission = state.shouldShowMissionStart, + onSnoozeClick = { processAction(AlarmActionContract.Action.Snooze) }, onDismissClick = { - eventDispatcher(AlarmActionContract.Action.Dismiss) + processAction(AlarmActionContract.Action.Dismiss) (context as? androidx.activity.ComponentActivity)?.finish() }, ) @@ -121,74 +124,72 @@ private fun AlarmActionContent( onSnoozeClick: () -> Unit, onDismissClick: () -> Unit, ) { - Box(modifier = Modifier.statusBarsPadding()) { - Column( - modifier = Modifier - .fillMaxSize() - .background( - color = Color(0xFF496381), - ), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Spacer( - modifier = Modifier.heightForScreenPercentage( - 0.17f, - ), - ) + Column( + modifier = Modifier + .fillMaxSize() + .background( + color = Color(0xFF496381), + ), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer( + modifier = Modifier.heightForScreenPercentage( + 0.17f, + ), + ) - AlarmTime( - isAm = isAm, - hour = hour, - minute = minute, - todayDate = todayDate, - ) + AlarmTime( + isAm = isAm, + hour = hour, + minute = minute, + todayDate = todayDate, + ) - Spacer(modifier = Modifier.height(102.dp)) + Spacer(modifier = Modifier.height(102.dp)) - Icon( - painter = painterResource(id = core.designsystem.R.drawable.ic_alarm_action_character), - tint = Color(0xFF07203E), - contentDescription = "Alarm Action Character", - ) + Icon( + painter = painterResource(id = core.designsystem.R.drawable.ic_alarm_action_character), + tint = Color(0xFF07203E), + contentDescription = "Alarm Action Character", + ) - Spacer(modifier = Modifier.height(56.dp)) + Spacer(modifier = Modifier.height(56.dp)) - if (snoozeEnabled && snoozeCount != 0) { - AlarmSnoozeButton( - snoozeInterval = snoozeInterval, - snoozeCount = snoozeCount, - onSnoozeClick = onSnoozeClick, - ) - } else { - Spacer(modifier = Modifier.height(54.dp)) - } + if (snoozeEnabled && snoozeCount != 0) { + AlarmSnoozeButton( + snoozeInterval = snoozeInterval, + snoozeCount = snoozeCount, + onSnoozeClick = onSnoozeClick, + ) + } else { + Spacer(modifier = Modifier.height(54.dp)) + } - Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.weight(1f)) - if (isFirstMission != null) { - OrbitButton( - label = if (isFirstMission) { - stringResource(id = R.string.alarm_off_mission_start_btn) - } else { - stringResource(id = R.string.alarm_off_btn) - }, - enabled = true, - modifier = Modifier - .padding( - start = 40.dp, - end = 40.dp, - bottom = 48.dp, - ) - .height(62.dp), - onClick = onDismissClick, - ) - } else { - Spacer(modifier = Modifier.height(62.dp)) - } + if (isFirstMission != null) { + OrbitButton( + label = if (isFirstMission) { + stringResource(id = R.string.alarm_off_mission_start_btn) + } else { + stringResource(id = R.string.alarm_off_btn) + }, + enabled = true, + modifier = Modifier + .padding( + start = 40.dp, + end = 40.dp, + bottom = 48.dp, + ) + .height(62.dp), + onClick = onDismissClick, + ) + } else { + Spacer(modifier = Modifier.height(62.dp)) } - - AdsBanner() } + + AdsBanner(modifier = Modifier.statusBarsPadding()) } @Composable @@ -298,18 +299,16 @@ private fun AlarmSnoozeButton( internal fun AlarmActionScreenPreview() { OrbitTheme { AlarmActionScreen( - stateProvider = { - AlarmActionContract.State( - initialLoading = false, - isAm = true, - hour = 10, - minute = 30, - todayDate = "10์›” 10์ผ ์›”์š”์ผ", - snoozeInterval = 5, - snoozeCount = -1, - ) - }, - eventDispatcher = {}, + state = AlarmActionContract.State( + initialLoading = false, + isAm = true, + hour = 10, + minute = 30, + todayDate = "10์›” 10์ผ ์›”์š”์ผ", + snoozeInterval = 5, + snoozeCount = -1, + ), + processAction = {}, ) } } diff --git a/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/action/AlarmActionViewModel.kt b/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/action/AlarmActionViewModel.kt index 57e6cd8c..49182b15 100644 --- a/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/action/AlarmActionViewModel.kt +++ b/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/action/AlarmActionViewModel.kt @@ -2,20 +2,21 @@ package com.yapp.alarm.interaction.action import android.app.Application import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.viewModelScope +import androidx.lifecycle.ViewModel import com.yapp.alarm.pendingIntent.interaction.createAlarmDismissIntent import com.yapp.alarm.pendingIntent.interaction.createAlarmSnoozeIntent -import com.yapp.datastore.UserPreferences import com.yapp.domain.model.Alarm -import com.yapp.ui.base.BaseViewModel +import com.yapp.domain.model.MissionType import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.syntax.simple.intent +import org.orbitmvi.orbit.syntax.simple.postSideEffect +import org.orbitmvi.orbit.syntax.simple.reduce +import org.orbitmvi.orbit.viewmodel.container import java.time.LocalDate import java.time.LocalTime -import java.time.format.DateTimeFormatter import java.time.format.TextStyle import java.util.Locale import javax.inject.Inject @@ -23,81 +24,76 @@ import javax.inject.Inject @HiltViewModel class AlarmActionViewModel @Inject constructor( private val app: Application, - private val userPreferences: UserPreferences, savedStateHandle: SavedStateHandle, -) : BaseViewModel( - AlarmActionContract.State(), -) { +) : ViewModel(), ContainerHost { + + override val container: Container = container( + initialState = AlarmActionContract.State(), + ) { + fetchShouldShowMissionStart() + initializeAlarmState() + startClock() + } + private val alarmJson: String? = savedStateHandle.get("alarm") private val alarm: Alarm? = alarmJson?.let { Alarm.fromJson(it) } - init { - fetchIsFirstMission() - updateState { - copy( + fun processAction(action: AlarmActionContract.Action) { + when (action) { + is AlarmActionContract.Action.Snooze -> snooze() + is AlarmActionContract.Action.Dismiss -> dismiss() + } + } + + private fun initializeAlarmState() = intent { + reduce { + state.copy( snoozeEnabled = alarm?.isSnoozeEnabled ?: false, snoozeCount = alarm?.snoozeCount ?: 5, snoozeInterval = alarm?.snoozeInterval ?: 5, ) } - - startClock() } - private fun fetchIsFirstMission() { - viewModelScope.launch { - val fortuneDate = userPreferences.fortuneDateFlow.firstOrNull() - val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE) - val isFirstMission = fortuneDate != todayDate - - updateState { - copy(isFirstMission = isFirstMission) - } + private fun fetchShouldShowMissionStart() = intent { + reduce { + state.copy(shouldShowMissionStart = (alarm?.missionType ?: MissionType.NONE) != MissionType.NONE) } } - private fun startClock() { - viewModelScope.launch { - while (isActive) { - val now = LocalTime.now() - val today = LocalDate.now() - val dayOfWeek = today.dayOfWeek.getDisplayName(TextStyle.FULL, Locale.KOREAN) - - updateState { - copy( - isAm = now.hour < 12, - hour = if (now.hour % 12 == 0) 12 else now.hour % 12, - minute = now.minute, - todayDate = "${today.monthValue}์›” ${today.dayOfMonth}์ผ $dayOfWeek", - initialLoading = false, - ) - } + private fun startClock() = intent { + while (true) { + val now = LocalTime.now() + val today = LocalDate.now() + val dayOfWeek = today.dayOfWeek.getDisplayName(TextStyle.FULL, Locale.KOREAN) - delay(1000L) + reduce { + state.copy( + isAm = now.hour < 12, + hour = if (now.hour % 12 == 0) 12 else now.hour % 12, + minute = now.minute, + todayDate = "${today.monthValue}์›” ${today.dayOfMonth}์ผ $dayOfWeek", + initialLoading = false, + ) } - } - } - fun processAction(action: AlarmActionContract.Action) { - when (action) { - is AlarmActionContract.Action.Snooze -> snooze() - is AlarmActionContract.Action.Dismiss -> dismiss() + delay(1000L) } } - private fun snooze() { + private fun snooze() = intent { sendAlarmSnoozeEventToAlarmReceiver() - updateState { - copy( - snoozeCount = if (currentState.snoozeCount == -1) { - currentState.snoozeCount + reduce { + state.copy( + snoozeCount = if (state.snoozeCount == -1) { + state.snoozeCount } else { - currentState.snoozeCount - 1 + state.snoozeCount - 1 }, ) } alarm?.let { - emitSideEffect(AlarmActionContract.SideEffect.NavigateToAlarmSnooze(it)) + postSideEffect(AlarmActionContract.SideEffect.NavigateToAlarmSnooze(it)) } } @@ -116,10 +112,12 @@ class AlarmActionViewModel @Inject constructor( } private fun sendAlarmDismissEventToAlarmReceiver() { - alarm?.id?.let { id -> + alarm?.let { alarm -> val alarmDismissIntent = createAlarmDismissIntent( context = app, - notificationId = id, + notificationId = alarm.id, + missionType = alarm.missionType.value, + missionCount = alarm.missionCount, ) app.sendBroadcast(alarmDismissIntent) } diff --git a/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/snooze/AlarmSnoozeTimerScreen.kt b/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/snooze/AlarmSnoozeTimerScreen.kt index e914f506..da09090f 100644 --- a/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/snooze/AlarmSnoozeTimerScreen.kt +++ b/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/snooze/AlarmSnoozeTimerScreen.kt @@ -19,7 +19,6 @@ import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -42,7 +41,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.yapp.common.navigation.OrbitNavigator import com.yapp.designsystem.theme.OrbitTheme import com.yapp.ui.component.lottie.LottieAnimation import com.yapp.ui.utils.heightForScreenPercentage @@ -51,27 +49,20 @@ import feature.alarm.interaction.R @Composable internal fun AlarmSnoozeTimerRoute( viewModel: AlarmSnoozeTimerViewModel = hiltViewModel(), - navigator: OrbitNavigator, ) { val state by viewModel.container.stateFlow.collectAsStateWithLifecycle() - val sideEffect = viewModel.container.sideEffectFlow - - LaunchedEffect(sideEffect) { - sideEffect.collect { } - } AlarmSnoozeTimerScreen( - stateProvider = { state }, - eventDispatcher = viewModel::processAction, + state = state, + processAction = viewModel::processAction, ) } @Composable internal fun AlarmSnoozeTimerScreen( - stateProvider: () -> AlarmSnoozeTimerContract.State, - eventDispatcher: (AlarmSnoozeTimerContract.Action) -> Unit, + state: AlarmSnoozeTimerContract.State, + processAction: (AlarmSnoozeTimerContract.Action) -> Unit, ) { - val state = stateProvider() val context = LocalContext.current if (state.initialLoading) { @@ -82,7 +73,7 @@ internal fun AlarmSnoozeTimerScreen( totalSeconds = state.totalSeconds, isFirstMission = state.isFirstMission, onDismissClick = { - eventDispatcher(AlarmSnoozeTimerContract.Action.Dismiss) + processAction(AlarmSnoozeTimerContract.Action.Dismiss) (context as? ComponentActivity)?.finish() }, ) @@ -304,8 +295,8 @@ private fun AlarmOffButton( internal fun PreviewAlarmSnoozeTimerScreen() { OrbitTheme { AlarmSnoozeTimerScreen( - stateProvider = { AlarmSnoozeTimerContract.State() }, - eventDispatcher = {}, + state = AlarmSnoozeTimerContract.State(), + processAction = {}, ) } } diff --git a/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/snooze/AlarmSnoozeTimerViewModel.kt b/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/snooze/AlarmSnoozeTimerViewModel.kt index 6076cf8c..05b6c798 100644 --- a/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/snooze/AlarmSnoozeTimerViewModel.kt +++ b/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/snooze/AlarmSnoozeTimerViewModel.kt @@ -2,20 +2,21 @@ package com.yapp.alarm.interaction.snooze import android.app.Application import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.viewModelScope +import androidx.lifecycle.ViewModel import com.yapp.alarm.pendingIntent.interaction.createAlarmDismissIntent -import com.yapp.datastore.UserPreferences import com.yapp.domain.model.Alarm -import com.yapp.ui.base.BaseViewModel +import com.yapp.domain.repository.FortuneRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.syntax.simple.intent +import org.orbitmvi.orbit.syntax.simple.reduce +import org.orbitmvi.orbit.viewmodel.container import java.time.LocalDate import java.time.LocalDateTime import java.time.ZoneId -import java.time.format.DateTimeFormatter import javax.inject.Inject import kotlin.math.max @@ -23,67 +24,64 @@ import kotlin.math.max class AlarmSnoozeTimerViewModel @Inject constructor( private val app: Application, savedStateHandle: SavedStateHandle, - private val userPreferences: UserPreferences, -) : BaseViewModel( - AlarmSnoozeTimerContract.State(), -) { - private val alarmJson: String? = savedStateHandle.get("alarm") - private val alarm: Alarm? = alarmJson?.let { Alarm.fromJson(it) } + private val fortuneRepository: FortuneRepository, +) : ViewModel(), ContainerHost { - init { + override val container: Container = container( + initialState = AlarmSnoozeTimerContract.State(), + ) { fetchIsFirstMission() startClock() } - private fun fetchIsFirstMission() { - viewModelScope.launch { - val fortuneDate = userPreferences.fortuneDateFlow.firstOrNull() - val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE) - val isFirstMission = fortuneDate != todayDate + private val alarmJson: String? = savedStateHandle.get("alarm") + private val alarm: Alarm? = alarmJson?.let { Alarm.fromJson(it) } - updateState { - copy(isFirstMission = isFirstMission) - } + fun processAction(action: AlarmSnoozeTimerContract.Action) { + when (action) { + is AlarmSnoozeTimerContract.Action.Dismiss -> dismiss() } } - private fun startClock() { - viewModelScope.launch { - val nowMillis = System.currentTimeMillis() - val nextSnoozeTimeMillis = alarm?.let { getNextSnoozeAlarmTimeMillis(it.snoozeInterval) } ?: nowMillis - val remainingMillis = max(0, nextSnoozeTimeMillis - nowMillis) - val remainingSeconds = (remainingMillis / 1000).toInt() - - updateState { - copy( - remainingSeconds = remainingSeconds, - totalSeconds = remainingSeconds, - alarmTimeStamp = nextSnoozeTimeMillis / 1000, - initialLoading = true, - ) - }.join() + private fun fetchIsFirstMission() = intent { + val fortuneDate = fortuneRepository.fortuneDateEpochFlow.firstOrNull() + val todayDate = LocalDate.now().toEpochDay() + val isFirstMission = fortuneDate != todayDate - while (isActive) { - val currentTime = System.currentTimeMillis() / 1000 - val remaining = max(0, currentState.alarmTimeStamp - currentTime) + reduce { + state.copy(isFirstMission = isFirstMission) + } + } - updateState { - copy( - remainingSeconds = remaining.toInt(), - initialLoading = false, - ) - } + private fun startClock() = intent { + val nowMillis = System.currentTimeMillis() + val nextSnoozeTimeMillis = alarm?.let { getNextSnoozeAlarmTimeMillis(it.snoozeInterval) } ?: nowMillis + val remainingMillis = max(0, nextSnoozeTimeMillis - nowMillis) + val remainingSeconds = (remainingMillis / 1000).toInt() + + reduce { + state.copy( + remainingSeconds = remainingSeconds, + totalSeconds = remainingSeconds, + alarmTimeStamp = nextSnoozeTimeMillis / 1000, + initialLoading = true, + ) + } - if (remaining.toInt() == 0) break + while (true) { + val currentTime = System.currentTimeMillis() / 1000 + val remaining = max(0, state.alarmTimeStamp - currentTime) - delay(1000L) + reduce { + state.copy( + remainingSeconds = remaining.toInt(), + initialLoading = false, + ) } - } - } - fun processAction(action: AlarmSnoozeTimerContract.Action) { - when (action) { - is AlarmSnoozeTimerContract.Action.Dismiss -> dismiss() + if (remaining.toInt() == 0) break + + delay(1000L) } } @@ -92,10 +90,12 @@ class AlarmSnoozeTimerViewModel @Inject constructor( } private fun sendAlarmDismissEventToAlarmReceiver() { - alarm?.id?.let { id -> + alarm?.let { alarm -> val alarmDismissIntent = createAlarmDismissIntent( context = app, - notificationId = id, + notificationId = alarm.id, + missionType = alarm.missionType.value, + missionCount = alarm.missionCount, ) app.sendBroadcast(alarmDismissIntent) } diff --git a/feature/fortune/build.gradle.kts b/feature/fortune/build.gradle.kts index ac291d36..543510e8 100644 --- a/feature/fortune/build.gradle.kts +++ b/feature/fortune/build.gradle.kts @@ -12,11 +12,14 @@ dependencies { implementation(projects.core.ui) implementation(projects.core.common) implementation(projects.core.analytics) - implementation(projects.core.datastore) + implementation(projects.core.alarm) implementation(libs.orbit.core) implementation(libs.orbit.compose) implementation(libs.orbit.viewmodel) implementation(libs.coil.compose) + implementation(libs.androidx.work.runtime) + testImplementation(libs.androidx.work.testing) + androidTestImplementation(libs.androidx.work.testing) implementation(projects.domain) implementation(projects.core.media) } diff --git a/feature/fortune/src/main/java/com/yapp/fortune/FortuneNavGraph.kt b/feature/fortune/src/main/java/com/yapp/fortune/FortuneNavGraph.kt index 5f1b5133..0a7b62ce 100644 --- a/feature/fortune/src/main/java/com/yapp/fortune/FortuneNavGraph.kt +++ b/feature/fortune/src/main/java/com/yapp/fortune/FortuneNavGraph.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable +import androidx.navigation.navDeepLink import androidx.navigation.navOptions import androidx.navigation.navigation import com.yapp.common.navigation.OrbitNavigator @@ -19,7 +20,11 @@ fun NavGraphBuilder.fortuneNavGraph( snackBarHostState: SnackbarHostState, ) { navigation(startDestination = FortuneDestination.Fortune) { - composable { backStackEntry -> + composable( + deepLinks = listOf( + navDeepLink { uriPattern = "orbitapp://fortune" }, + ), + ) { backStackEntry -> val viewModel = backStackEntry.sharedHiltViewModel(navigator.navController) val coroutineScope = rememberCoroutineScope() diff --git a/feature/fortune/src/main/java/com/yapp/fortune/FortuneRewardScreen.kt b/feature/fortune/src/main/java/com/yapp/fortune/FortuneRewardScreen.kt index 868464e8..778a3726 100644 --- a/feature/fortune/src/main/java/com/yapp/fortune/FortuneRewardScreen.kt +++ b/feature/fortune/src/main/java/com/yapp/fortune/FortuneRewardScreen.kt @@ -55,15 +55,15 @@ fun FortuneRewardRoute( FortuneRewardScreen( state = state, - onCloseClick = { viewModel.onAction(FortuneContract.Action.NavigateToHome) }, - onCompleteClick = { viewModel.onAction(FortuneContract.Action.NavigateToHome) }, + onCloseClick = { viewModel.processAction(FortuneContract.Action.NavigateToHome) }, + onCompleteClick = { viewModel.processAction(FortuneContract.Action.NavigateToHome) }, onSaveImage = { analyticsHelper.logEvent( AnalyticsEvent( type = "fortune_talisman_save", ), ) - viewModel.onAction(FortuneContract.Action.SaveImage(it)) + viewModel.processAction(FortuneContract.Action.SaveImage(it)) }, ) } diff --git a/feature/fortune/src/main/java/com/yapp/fortune/FortuneScreen.kt b/feature/fortune/src/main/java/com/yapp/fortune/FortuneScreen.kt index 0b7357bc..1eb97abc 100644 --- a/feature/fortune/src/main/java/com/yapp/fortune/FortuneScreen.kt +++ b/feature/fortune/src/main/java/com/yapp/fortune/FortuneScreen.kt @@ -3,11 +3,13 @@ package com.yapp.fortune import androidx.activity.compose.BackHandler 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.height import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable @@ -15,6 +17,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -34,6 +37,7 @@ import com.yapp.fortune.component.FortuneTopAppBar import com.yapp.fortune.component.SlidingIndicator import com.yapp.fortune.page.FortunePager import com.yapp.ui.component.lottie.LottieAnimation +import kotlinx.coroutines.delay import java.math.BigDecimal import java.math.RoundingMode @@ -105,15 +109,15 @@ fun FortuneRoute( } if (state.currentStep != pagerState.currentPage) { - viewModel.onAction(FortuneContract.Action.UpdateStep(pagerState.currentPage)) + viewModel.processAction(FortuneContract.Action.UpdateStep(pagerState.currentPage)) } } FortuneScreen( state = state, pagerState = pagerState, - onNextStep = { viewModel.onAction(FortuneContract.Action.NextStep) }, - onNavigateToHome = { viewModel.onAction(FortuneContract.Action.NavigateToHome) }, + onNextStep = { viewModel.processAction(FortuneContract.Action.NextStep) }, + onNavigateToHome = { viewModel.processAction(FortuneContract.Action.NavigateToHome) }, onCloseClick = { analyticsHelper.logEvent( AnalyticsEvent( @@ -123,7 +127,7 @@ fun FortuneRoute( ), ), ) - viewModel.onAction(FortuneContract.Action.NavigateToHome) + viewModel.processAction(FortuneContract.Action.NavigateToHome) }, ) } @@ -184,21 +188,49 @@ fun FortuneScreen( @Composable fun FortuneLoadingScreen() { + var isDelivering by remember { mutableStateOf(false) } + + LaunchedEffect(Unit) { + while (true) { + delay(2000) + isDelivering = !isDelivering + } + } + Box( modifier = Modifier .fillMaxSize() .background(OrbitTheme.colors.gray_900.copy(alpha = 0.7f)), contentAlignment = Alignment.Center, ) { - LottieAnimation( - modifier = Modifier - .size(70.dp), - resId = core.designsystem.R.raw.star_loading, - ) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + val imageRes = if (isDelivering) { + core.designsystem.R.drawable.ic_fortune_delivering_speech_bubble + } else { + core.designsystem.R.drawable.ic_fortune_waiting_speech_bubble + } + Image( + painter = painterResource(id = imageRes), + contentDescription = null, + ) + + LottieAnimation( + modifier = Modifier + .width(375.dp) + .height(267.dp), + resId = core.designsystem.R.raw.fortune_loading, + ) + } } } @Composable @Preview -fun FortuneRoutePreview() { +private fun FortuneLoadingScreenPreview() { + OrbitTheme { + FortuneLoadingScreen() + } } diff --git a/feature/fortune/src/main/java/com/yapp/fortune/FortuneViewModel.kt b/feature/fortune/src/main/java/com/yapp/fortune/FortuneViewModel.kt index b4890bfb..4a83a561 100644 --- a/feature/fortune/src/main/java/com/yapp/fortune/FortuneViewModel.kt +++ b/feature/fortune/src/main/java/com/yapp/fortune/FortuneViewModel.kt @@ -3,108 +3,136 @@ package com.yapp.fortune import android.app.Application import android.util.Log import androidx.annotation.DrawableRes -import androidx.lifecycle.viewModelScope -import com.yapp.datastore.UserPreferences +import androidx.lifecycle.ViewModel +import com.yapp.domain.model.FortuneCreateStatus import com.yapp.domain.repository.FortuneRepository -import com.yapp.domain.repository.ImageRepository import com.yapp.fortune.page.toFortunePages import com.yapp.media.decoder.ImageUtils -import com.yapp.ui.base.BaseViewModel +import com.yapp.media.storage.ImageSaver import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.launch +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.syntax.simple.intent import org.orbitmvi.orbit.syntax.simple.postSideEffect import org.orbitmvi.orbit.syntax.simple.reduce -import java.time.LocalDate -import java.time.format.DateTimeFormatter +import org.orbitmvi.orbit.viewmodel.container import javax.inject.Inject @HiltViewModel class FortuneViewModel @Inject constructor( private val application: Application, private val fortuneRepository: FortuneRepository, - private val imageRepository: ImageRepository, - private val userPreferences: UserPreferences, -) : BaseViewModel( - FortuneContract.State(), -) { - - init { - viewModelScope.launch { - val fortuneId = userPreferences.fortuneIdFlow.firstOrNull() - val firstDismissedAlarmId = userPreferences.firstDismissedAlarmIdFlow.firstOrNull() - val fortuneDate = userPreferences.fortuneDateFlow.firstOrNull() - fortuneId?.let { getFortune(it, firstDismissedAlarmId, fortuneDate) } + private val imageSaver: ImageSaver, +) : ViewModel(), ContainerHost { + + override val container: Container = container( + initialState = FortuneContract.State(), + ) { + observeFortune() + } + + fun processAction(action: FortuneContract.Action) { + when (action) { + is FortuneContract.Action.NextStep -> { + moveToNextStep() + } + is FortuneContract.Action.UpdateStep -> { + updateStep(action.step) + } + is FortuneContract.Action.NavigateToHome -> { + navigateToHome() + } + is FortuneContract.Action.SaveImage -> { + saveImage(action.resId) + } } } - private fun getFortune(fortuneId: Long, firstDismissedAlarmId: Long?, fortuneDate: String?) = intent { - updateState { copy(isLoading = true) } + + private fun observeFortune() = intent { + fortuneRepository.fortuneCreateStatusFlow.collect { status -> + when (status) { + is FortuneCreateStatus.Creating -> { + reduce { state.copy(isLoading = true) } + } + + is FortuneCreateStatus.Success -> { + fetchAndUpdateFortune( + fortuneId = status.fortuneId, + isFirstAlarmDismissedToday = fortuneRepository.isFirstAlarmDismissedTodayFlow.first(), + ) + } + + is FortuneCreateStatus.Failure, FortuneCreateStatus.Idle -> { + postSideEffect(FortuneContract.SideEffect.NavigateToHome) + } + } + } + } + + private fun fetchAndUpdateFortune( + fortuneId: Long, + isFirstAlarmDismissedToday: Boolean, + ) = intent { + reduce { state.copy(isLoading = true) } fortuneRepository.getFortune(fortuneId).onSuccess { fortune -> - val savedImageId = userPreferences.fortuneImageIdFlow.firstOrNull() + val savedImageId = fortuneRepository.fortuneImageIdFlow.firstOrNull() val imageId = savedImageId ?: getRandomImage() val formattedTitle = fortune.dailyFortuneTitle.replace(",", ",\n").trim() - val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE) - val hasReward = (fortuneDate == todayDate) && (firstDismissedAlarmId != null) - updateState { - copy( + + fortuneRepository.markFortuneSeen() + + reduce { + state.copy( isLoading = false, dailyFortuneTitle = formattedTitle, dailyFortuneDescription = fortune.dailyFortuneDescription, avgFortuneScore = fortune.avgFortuneScore, fortunePages = fortune.toFortunePages(), fortuneImageId = imageId, - hasReward = hasReward, + hasReward = isFirstAlarmDismissedToday, ) } }.onFailure { error -> Log.e("FortuneViewModel", "์šด์„ธ ๋ฐ์ดํ„ฐ ์š”์ฒญ ์‹คํŒจ: ${error.message}") - updateState { copy(isLoading = false) } + reduce { state.copy(isLoading = false) } } } - fun saveFortuneImageIdIfNeeded(imageId: Int) = viewModelScope.launch { - val savedImageId = userPreferences.fortuneImageIdFlow.firstOrNull() + fun saveFortuneImageIdIfNeeded(imageId: Int) = intent { + val savedImageId = fortuneRepository.fortuneImageIdFlow.firstOrNull() if (savedImageId == null || savedImageId != imageId) { - userPreferences.saveFortuneImageId(imageId) + fortuneRepository.saveFortuneImageId(imageId) } } - fun onAction(action: FortuneContract.Action) = intent { - when (action) { - is FortuneContract.Action.NextStep -> { - if (state.hasReward) { - postSideEffect(FortuneContract.SideEffect.NavigateToFortuneReward) - } else { - reduce { state.copy(currentStep = (state.currentStep + 1).coerceAtMost(5)) } - } - } - is FortuneContract.Action.UpdateStep -> { - reduce { state.copy(currentStep = action.step) } - } - is FortuneContract.Action.NavigateToHome -> { - navigateToHome() - } - is FortuneContract.Action.SaveImage -> { - saveImage(action.resId) - } + private fun moveToNextStep() = intent { + if (state.hasReward) { + postSideEffect(FortuneContract.SideEffect.NavigateToFortuneReward) + } else { + reduce { state.copy(currentStep = (state.currentStep + 1).coerceAtMost(5)) } } } - private fun navigateToHome() { - emitSideEffect(FortuneContract.SideEffect.NavigateToHome) + private fun updateStep(step: Int) = intent { + reduce { state.copy(currentStep = step) } + } + + private fun navigateToHome() = intent { + postSideEffect(FortuneContract.SideEffect.NavigateToHome) } - private fun saveImage(@DrawableRes resId: Int) = viewModelScope.launch { + private fun saveImage(@DrawableRes resId: Int) = intent { val bitmap = ImageUtils.getBitmapFromResource(application, resId) val byteArray = ImageUtils.bitmapToByteArray(bitmap) - val isSuccess = imageRepository.saveImage(byteArray) + val isSuccess = imageSaver.saveImage(byteArray, "fortune_${System.currentTimeMillis()}.png") if (isSuccess) { - emitSideEffect( + postSideEffect( FortuneContract.SideEffect.ShowSnackBar( message = "์•จ๋ฒ”์— ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", iconRes = core.designsystem.R.drawable.ic_check_green, diff --git a/feature/fortune/src/main/java/com/yapp/fortune/di/SchedulerModule.kt b/feature/fortune/src/main/java/com/yapp/fortune/di/SchedulerModule.kt new file mode 100644 index 00000000..47fbd0b0 --- /dev/null +++ b/feature/fortune/src/main/java/com/yapp/fortune/di/SchedulerModule.kt @@ -0,0 +1,19 @@ +package com.yapp.fortune.di + +import com.yapp.alarm.scheduler.PostFortuneTaskScheduler +import com.yapp.fortune.scheduler.WorkManagerPostFortuneTaskScheduler +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class SchedulerModule { + @Binds + @Singleton + abstract fun bindsPostFortuneTaskScheduler( + postFortuneTaskScheduler: WorkManagerPostFortuneTaskScheduler, + ): PostFortuneTaskScheduler +} diff --git a/feature/fortune/src/main/java/com/yapp/fortune/page/FortunePageData.kt b/feature/fortune/src/main/java/com/yapp/fortune/page/FortunePageData.kt index e7070276..4ae64ca9 100644 --- a/feature/fortune/src/main/java/com/yapp/fortune/page/FortunePageData.kt +++ b/feature/fortune/src/main/java/com/yapp/fortune/page/FortunePageData.kt @@ -1,6 +1,6 @@ package com.yapp.fortune.page -import com.yapp.domain.model.fortune.Fortune +import com.yapp.domain.model.Fortune data class FortunePageData( val title: String, diff --git a/feature/fortune/src/main/java/com/yapp/fortune/page/FortunePager.kt b/feature/fortune/src/main/java/com/yapp/fortune/page/FortunePager.kt index 08b7cfc5..aa1dc78b 100644 --- a/feature/fortune/src/main/java/com/yapp/fortune/page/FortunePager.kt +++ b/feature/fortune/src/main/java/com/yapp/fortune/page/FortunePager.kt @@ -51,11 +51,13 @@ fun FortunePager( val index = (page - 1).coerceIn(0, state.fortunePages.lastIndex) FortunePageLayout(state.fortunePages[index]) } + 5 -> FortuneCompletePage( hasReward = state.hasReward, onCompleteClick = onNextStep, onNavigateToHome = onNavigateToHome, ) + else -> {} } } diff --git a/feature/fortune/src/main/java/com/yapp/fortune/scheduler/WorkManagerPostFortuneTaskScheduler.kt b/feature/fortune/src/main/java/com/yapp/fortune/scheduler/WorkManagerPostFortuneTaskScheduler.kt new file mode 100644 index 00000000..36e49c4f --- /dev/null +++ b/feature/fortune/src/main/java/com/yapp/fortune/scheduler/WorkManagerPostFortuneTaskScheduler.kt @@ -0,0 +1,32 @@ +package com.yapp.fortune.scheduler + +import android.content.Context +import androidx.work.BackoffPolicy +import androidx.work.Constraints +import androidx.work.ExistingWorkPolicy +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import com.yapp.alarm.scheduler.PostFortuneTaskScheduler +import com.yapp.fortune.worker.PostFortuneWorker +import dagger.hilt.android.qualifiers.ApplicationContext +import java.time.LocalDate +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class WorkManagerPostFortuneTaskScheduler @Inject constructor( + @ApplicationContext private val context: Context, +) : PostFortuneTaskScheduler { + override fun enqueueOnceForToday() { + val name = "post_fortune_${LocalDate.now()}" + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + val req = OneTimeWorkRequestBuilder() + .setConstraints(constraints) + .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 15, TimeUnit.SECONDS) + .build() + WorkManager.getInstance(context) + .enqueueUniqueWork(name, ExistingWorkPolicy.KEEP, req) + } +} diff --git a/feature/fortune/src/main/java/com/yapp/fortune/worker/PostFortuneWorker.kt b/feature/fortune/src/main/java/com/yapp/fortune/worker/PostFortuneWorker.kt new file mode 100644 index 00000000..2dd14f72 --- /dev/null +++ b/feature/fortune/src/main/java/com/yapp/fortune/worker/PostFortuneWorker.kt @@ -0,0 +1,63 @@ +package com.yapp.fortune.worker + +import android.content.Context +import androidx.hilt.work.HiltWorker +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.yapp.domain.model.FortuneCreateStatus +import com.yapp.domain.repository.FortuneRepository +import com.yapp.domain.repository.UserInfoRepository +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull + +@HiltWorker +class PostFortuneWorker @AssistedInject constructor( + @Assisted appContext: Context, + @Assisted params: WorkerParameters, + private val fortuneRepository: FortuneRepository, + private val userInfoRepository: UserInfoRepository, +) : CoroutineWorker(appContext, params) { + + override suspend fun doWork(): Result { + when (fortuneRepository.fortuneCreateStatusFlow.first()) { + is FortuneCreateStatus.Creating, + is FortuneCreateStatus.Success, + -> { + return Result.success() + } + FortuneCreateStatus.Failure, + FortuneCreateStatus.Idle, + -> { + val userId = userInfoRepository.userIdFlow.firstOrNull() + ?: run { + // ์‚ฌ์šฉ์ž ์—†์œผ๋ฉด ์‹คํŒจ ์ƒํƒœ ํ‘œ์‹œ ํ›„ ์‹คํŒจ ๋ฐ˜ํ™˜ + fortuneRepository.markFortuneAsFailed() + return Result.failure() + } + + return try { + fortuneRepository.markFortuneAsCreating() + + val result = fortuneRepository.postFortune(userId) + result.fold( + onSuccess = { fortune -> + fortuneRepository.markFortuneAsCreated(fortune.id) + fortuneRepository.saveFortuneScore(fortune.avgFortuneScore) + Result.success() + }, + onFailure = { + fortuneRepository.markFortuneAsFailed() + // WM ๋ฐฑ์˜คํ”„ ๊ทœ์น™์— ๋”ฐ๋ผ ์žฌ์‹œ๋„ + Result.retry() + }, + ) + } catch (_: Throwable) { + fortuneRepository.markFortuneAsFailed() + Result.retry() + } + } + } + } +} diff --git a/feature/home/build.gradle.kts b/feature/home/build.gradle.kts index b8a4b12d..0b90300c 100644 --- a/feature/home/build.gradle.kts +++ b/feature/home/build.gradle.kts @@ -12,13 +12,12 @@ dependencies { implementation(projects.core.ui) implementation(projects.core.common) implementation(projects.core.analytics) - implementation(projects.core.alarm) implementation(projects.core.media) - implementation(projects.core.datastore) implementation(projects.domain) implementation(libs.orbit.core) implementation(libs.orbit.compose) implementation(libs.orbit.viewmodel) implementation(libs.androidx.material.android) implementation(libs.androidx.annotation) + implementation(libs.coil.compose) } diff --git a/feature/home/src/main/AndroidManifest.xml b/feature/home/src/main/AndroidManifest.xml index 8bdb7e14..1c6dcf4e 100644 --- a/feature/home/src/main/AndroidManifest.xml +++ b/feature/home/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ - + diff --git a/feature/home/src/main/java/com/yapp/alarm/addedit/AlarmAddEditScreen.kt b/feature/home/src/main/java/com/yapp/alarm/addedit/AlarmAddEditScreen.kt deleted file mode 100644 index d479e3f6..00000000 --- a/feature/home/src/main/java/com/yapp/alarm/addedit/AlarmAddEditScreen.kt +++ /dev/null @@ -1,699 +0,0 @@ -package com.yapp.alarm.addedit - -import android.net.Uri -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsPressedAsState -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ripple.RippleAlpha -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.LocalRippleConfiguration -import androidx.compose.material3.RippleConfiguration -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.SnackbarResult -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.material3.ripple -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -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.yapp.alarm.component.AlarmCheckItem -import com.yapp.alarm.component.AlarmDayButton -import com.yapp.alarm.component.bottomsheet.AlarmSnoozeBottomSheet -import com.yapp.alarm.component.bottomsheet.AlarmSoundBottomSheet -import com.yapp.alarm.getLabelStringRes -import com.yapp.common.navigation.OrbitNavigator -import com.yapp.designsystem.theme.OrbitTheme -import com.yapp.domain.model.AlarmDay -import com.yapp.domain.model.AlarmSound -import com.yapp.home.ADD_ALARM_RESULT_KEY -import com.yapp.home.DELETE_ALARM_RESULT_KEY -import com.yapp.home.UPDATE_ALARM_RESULT_KEY -import com.yapp.ui.component.button.OrbitButton -import com.yapp.ui.component.dialog.OrbitDialog -import com.yapp.ui.component.lottie.LottieAnimation -import com.yapp.ui.component.snackbar.showCustomSnackBar -import com.yapp.ui.component.switch.OrbitSwitch -import com.yapp.ui.component.timepicker.OrbitPicker -import feature.home.R -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch - -@Composable -fun AlarmAddEditRoute( - viewModel: AlarmAddEditViewModel = hiltViewModel(), - navigator: OrbitNavigator, - snackBarHostState: SnackbarHostState, -) { - val state by viewModel.container.stateFlow.collectAsStateWithLifecycle() - val sideEffect = viewModel.container.sideEffectFlow - - val coroutineScope = rememberCoroutineScope() - - LaunchedEffect(sideEffect) { - sideEffect.collectLatest { effect -> - when (effect) { - is AlarmAddEditContract.SideEffect.NavigateBack -> { - navigator.navigateBack() - } - is AlarmAddEditContract.SideEffect.SaveAlarm -> { - navigator.navController.previousBackStackEntry - ?.savedStateHandle - ?.set(ADD_ALARM_RESULT_KEY, effect.id) - navigator.navController.popBackStack() - } - is AlarmAddEditContract.SideEffect.UpdateAlarm -> { - navigator.navController.previousBackStackEntry - ?.savedStateHandle - ?.set(UPDATE_ALARM_RESULT_KEY, effect.id) - navigator.navigateBack() - } - is AlarmAddEditContract.SideEffect.DeleteAlarm -> { - navigator.navController.previousBackStackEntry - ?.savedStateHandle - ?.set(DELETE_ALARM_RESULT_KEY, effect.id) - navigator.navigateBack() - } - is AlarmAddEditContract.SideEffect.ShowSnackBar -> { - val result = showCustomSnackBar( - scope = coroutineScope, - snackBarHostState = snackBarHostState, - message = effect.message, - actionLabel = effect.label, - iconRes = effect.iconRes, - bottomPadding = effect.bottomPadding, - durationMillis = effect.durationMillis, - ) - - when (result) { - SnackbarResult.ActionPerformed -> effect.onAction() - SnackbarResult.Dismissed -> effect.onDismiss() - } - } - } - } - } - - AlarmAddEditScreen( - stateProvider = { state }, - eventDispatcher = viewModel::processAction, - ) -} - -@Composable -fun AlarmAddEditScreen( - stateProvider: () -> AlarmAddEditContract.State, - eventDispatcher: (AlarmAddEditContract.Action) -> Unit, -) { - val state = stateProvider() - - if (state.initialLoading) { - AlarmAddEditLoadingScreen() - } else { - AlarmAddEditContent( - state = state, - eventDispatcher = eventDispatcher, - ) - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AlarmAddEditContent( - state: AlarmAddEditContract.State, - eventDispatcher: (AlarmAddEditContract.Action) -> Unit, -) { - BackHandler { - eventDispatcher(AlarmAddEditContract.Action.CheckUnsavedChangesBeforeExit) - } - - val snoozeState = state.snoozeState - val snoozeBottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - val soundBottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - val scope = rememberCoroutineScope() - - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - AlarmAddEditTopBar( - mode = state.mode, - title = state.timeState.alarmMessage, - onBack = { eventDispatcher(AlarmAddEditContract.Action.CheckUnsavedChangesBeforeExit) }, - onDelete = { eventDispatcher(AlarmAddEditContract.Action.ShowDeleteDialog) }, - ) - Box( - modifier = Modifier.weight(1f), - contentAlignment = Alignment.Center, - ) { - OrbitPicker( - initialAmPm = state.timeState.initialAmPm, - initialHour = state.timeState.initialHour, - initialMinute = state.timeState.initialMinute, - ) { amPm, hour, minute -> - eventDispatcher(AlarmAddEditContract.Action.SetAlarmTime(amPm, hour, minute)) - } - } - AlarmAddEditSettingsSection( - modifier = Modifier.padding(horizontal = 20.dp), - state = state, - processAction = eventDispatcher, - ) - Spacer(modifier = Modifier.height(24.dp)) - OrbitButton( - label = stringResource(R.string.alarm_add_edit_save), - onClick = { eventDispatcher(AlarmAddEditContract.Action.SaveAlarm) }, - enabled = true, - modifier = Modifier - .padding( - start = 20.dp, - end = 20.dp, - bottom = 12.dp, - ), - ) - } - - AlarmSnoozeBottomSheet( - snoozeEnabled = snoozeState.isSnoozeEnabled, - snoozeIntervalIndex = snoozeState.snoozeIntervalIndex, - snoozeCountIndex = snoozeState.snoozeCountIndex, - snoozeIntervals = snoozeState.snoozeIntervals, - snoozeCounts = snoozeState.snoozeCounts, - onSnoozeToggle = { eventDispatcher(AlarmAddEditContract.Action.ToggleSnoozeOption) }, - onIntervalSelected = { index -> - eventDispatcher( - AlarmAddEditContract.Action.SetSnoozeInterval( - index, - ), - ) - }, - onCountSelected = { index -> - eventDispatcher( - AlarmAddEditContract.Action.SetSnoozeRepeatCount( - index, - ), - ) - }, - onComplete = { - scope.launch { - snoozeBottomSheetState.hide() - }.invokeOnCompletion { - eventDispatcher(AlarmAddEditContract.Action.ToggleBottomSheet(AlarmAddEditContract.BottomSheetType.SnoozeSetting)) - } - }, - isSheetOpen = state.bottomSheetState == AlarmAddEditContract.BottomSheetType.SnoozeSetting, - onDismiss = { - scope.launch { - snoozeBottomSheetState.hide() - }.invokeOnCompletion { - eventDispatcher(AlarmAddEditContract.Action.ToggleBottomSheet(AlarmAddEditContract.BottomSheetType.SnoozeSetting)) - } - }, - ) - - AlarmSoundBottomSheet( - vibrationEnabled = state.soundState.isVibrationEnabled, - soundEnabled = state.soundState.isSoundEnabled, - soundVolume = state.soundState.soundVolume, - soundIndex = state.soundState.soundIndex, - sounds = state.soundState.sounds, - onVibrationToggle = { eventDispatcher(AlarmAddEditContract.Action.ToggleVibrationOption) }, - onSoundToggle = { eventDispatcher(AlarmAddEditContract.Action.ToggleSoundOption) }, - onVolumeChanged = { eventDispatcher(AlarmAddEditContract.Action.AdjustSoundVolume(it)) }, - onSoundSelected = { eventDispatcher(AlarmAddEditContract.Action.SelectAlarmSound(it)) }, - onComplete = { - scope.launch { - soundBottomSheetState.hide() - }.invokeOnCompletion { - eventDispatcher(AlarmAddEditContract.Action.ToggleBottomSheet(AlarmAddEditContract.BottomSheetType.SoundSetting)) - } - }, - isSheetOpen = state.bottomSheetState == AlarmAddEditContract.BottomSheetType.SoundSetting, - onDismiss = { - scope.launch { - soundBottomSheetState.hide() - }.invokeOnCompletion { - eventDispatcher(AlarmAddEditContract.Action.ToggleBottomSheet(AlarmAddEditContract.BottomSheetType.SoundSetting)) - } - }, - ) - - if (state.isDeleteDialogVisible) { - OrbitDialog( - title = stringResource(id = R.string.alarm_delete_dialog_title), - message = stringResource(id = R.string.alarm_delete_dialog_message), - confirmText = stringResource(id = R.string.alarm_delete_dialog_btn_delete), - cancelText = stringResource(id = R.string.alarm_delete_dialog_btn_cancel), - onConfirm = { - eventDispatcher(AlarmAddEditContract.Action.DeleteAlarm) - }, - onCancel = { - eventDispatcher(AlarmAddEditContract.Action.HideDeleteDialog) - }, - ) - } - - if (state.isUnsavedChangesDialogVisible) { - OrbitDialog( - title = stringResource(id = R.string.alarm_unsaved_changes_dialog_title), - message = stringResource(id = R.string.alarm_unsaved_changes_dialog_message), - confirmText = stringResource(id = R.string.alarm_unsaved_changes_dialog_btn_discard), - cancelText = stringResource(id = R.string.alarm_unsaved_changes_dialog_btn_cancel), - onConfirm = { - eventDispatcher(AlarmAddEditContract.Action.NavigateBack) - }, - onCancel = { - eventDispatcher(AlarmAddEditContract.Action.HideUnsavedChangesDialog) - }, - ) - } -} - -@Composable -private fun AlarmAddEditLoadingScreen() { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - LottieAnimation( - modifier = Modifier - .size(70.dp) - .align(Alignment.Center), - resId = core.designsystem.R.raw.star_loading, - ) - } -} - -@Composable -private fun AlarmAddEditTopBar( - mode: AlarmAddEditContract.EditMode = AlarmAddEditContract.EditMode.ADD, - title: String, - onBack: () -> Unit, - onDelete: () -> Unit, -) { - Box( - modifier = Modifier - .fillMaxWidth() - .statusBarsPadding() - .height(56.dp), - contentAlignment = Alignment.Center, - ) { - Icon( - painter = painterResource(id = core.designsystem.R.drawable.ic_back), - contentDescription = "Back", - tint = OrbitTheme.colors.white, - modifier = Modifier - .clickable(onClick = onBack) - .padding(start = 20.dp) - .align(Alignment.CenterStart), - ) - - Text( - title, - style = OrbitTheme.typography.body1SemiBold, - color = OrbitTheme.colors.white, - ) - - if (mode == AlarmAddEditContract.EditMode.EDIT) { - DeleteAlarmButton( - modifier = Modifier - .align(Alignment.CenterEnd) - .padding(end = 20.dp), - ) { - onDelete() - } - } - } -} - -@Composable -private fun DeleteAlarmButton( - modifier: Modifier = Modifier, - onDelete: () -> Unit, -) { - val interactionSource = remember { MutableInteractionSource() } - val isPressed = interactionSource.collectIsPressedAsState().value - - Surface( - onClick = onDelete, - modifier = modifier, - shape = RoundedCornerShape(8.dp), - interactionSource = interactionSource, - color = if (isPressed) OrbitTheme.colors.gray_800 else Color.Transparent, - ) { - Text( - text = stringResource(id = R.string.alarm_add_edit_delete), - style = OrbitTheme.typography.body1Medium, - color = OrbitTheme.colors.alert, - modifier = Modifier - .padding( - horizontal = 8.dp, - vertical = 4.dp, - ), - ) - } -} - -@Composable -private fun AlarmAddEditSettingsSection( - modifier: Modifier = Modifier, - state: AlarmAddEditContract.State, - processAction: (AlarmAddEditContract.Action) -> Unit, -) { - Column( - modifier = modifier - .fillMaxWidth() - .background( - color = OrbitTheme.colors.gray_800, - shape = RoundedCornerShape(12.dp), - ) - .clip( - shape = RoundedCornerShape(12.dp), - ), - ) { - AlarmAddEditSelectDaysSection( - state = state.daySelectionState, - processAction = processAction, - ) - AlarmAddEditDisableHolidaySwitch( - state = state.holidayState, - processAction = processAction, - ) - Spacer( - modifier = Modifier - .fillMaxWidth() - .height(1.dp) - .padding(horizontal = 20.dp) - .background(OrbitTheme.colors.gray_700), - ) - - AlarmAddEditSettingItem( - label = stringResource(id = R.string.alarm_add_edit_alarm_snooze), - description = if (state.snoozeState.isSnoozeEnabled) { - stringResource( - id = R.string.alarm_add_edit_alarm_selected_option, - state.snoozeState.snoozeIntervals[state.snoozeState.snoozeIntervalIndex], - state.snoozeState.snoozeCounts[state.snoozeState.snoozeCountIndex], - ) - } else { - stringResource(id = R.string.alarm_add_edit_alarm_selected_option_none) - }, - onClick = { - processAction( - AlarmAddEditContract.Action.ToggleBottomSheet( - AlarmAddEditContract.BottomSheetType.SnoozeSetting, - ), - ) - }, - ) - Spacer( - modifier = Modifier - .fillMaxWidth() - .height(1.dp) - .padding(horizontal = 20.dp) - .background(OrbitTheme.colors.gray_700), - ) - AlarmAddEditSettingItem( - label = stringResource(id = R.string.alarm_add_edit_sound), - description = when { - state.soundState.isSoundEnabled && state.soundState.isVibrationEnabled -> { - "${stringResource(id = R.string.alarm_add_edit_vibration)}, ${ - state.soundState.sounds.getOrElse(state.soundState.soundIndex) { - AlarmSound("", Uri.EMPTY) - }.title - }" - } - - state.soundState.isSoundEnabled -> state.soundState.sounds.getOrElse(state.soundState.soundIndex) { - AlarmSound( - "", - Uri.EMPTY, - ) - }.title - - state.soundState.isVibrationEnabled -> stringResource(id = R.string.alarm_add_edit_vibration) - else -> stringResource(id = R.string.alarm_add_edit_alarm_selected_option_none) - }, - onClick = { - processAction( - AlarmAddEditContract.Action.ToggleBottomSheet( - AlarmAddEditContract.BottomSheetType.SoundSetting, - ), - ) - }, - ) - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun AlarmAddEditSettingItem( - label: String, - description: String, - onClick: () -> Unit, -) { - val interactionSource = remember { MutableInteractionSource() } - - CompositionLocalProvider( - LocalRippleConfiguration provides RippleConfiguration( - rippleAlpha = RippleAlpha( - pressedAlpha = 1f, - focusedAlpha = 1f, - hoveredAlpha = 1f, - draggedAlpha = 1f, - ), - ), - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable( - interactionSource = interactionSource, - indication = ripple( - color = OrbitTheme.colors.gray_700, - ), - ) { - onClick() - } - .padding( - horizontal = 20.dp, - vertical = 14.dp, - ), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - label, - modifier = Modifier.width(80.dp), - style = OrbitTheme.typography.body1SemiBold, - color = OrbitTheme.colors.white, - ) - Text( - description, - modifier = Modifier.weight(1f), - style = OrbitTheme.typography.body2Regular, - color = OrbitTheme.colors.gray_50, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - textAlign = TextAlign.End, - ) - Icon( - painter = painterResource(id = core.designsystem.R.drawable.ic_arrow_right), - contentDescription = "Arrow", - tint = OrbitTheme.colors.gray_300, - ) - } - } -} - -@Composable -private fun AlarmAddEditSelectDaysSection( - state: AlarmAddEditContract.AlarmDaySelectionState, - processAction: (AlarmAddEditContract.Action) -> Unit, -) { - val configuration = LocalConfiguration.current - val screenWidthDp = configuration.screenWidthDp.dp - - Column( - modifier = Modifier.padding(horizontal = 20.dp, vertical = 16.dp), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringResource(id = R.string.alarm_add_edit_repeat), - style = OrbitTheme.typography.body1SemiBold, - color = OrbitTheme.colors.white, - ) - - Spacer(modifier = Modifier.weight(1f)) - - AlarmCheckItem( - label = stringResource(id = R.string.alarm_add_edit_weekdays), - isPressed = state.isWeekdaysChecked, - onClick = { - processAction(AlarmAddEditContract.Action.ToggleWeekdaysSelection) - }, - ) - Spacer(modifier = Modifier.width(2.dp)) - AlarmCheckItem( - label = stringResource(id = R.string.alarm_add_edit_weekends), - isPressed = state.isWeekendsChecked, - onClick = { - processAction(AlarmAddEditContract.Action.ToggleWeekendsSelection) - }, - ) - } - - Spacer(modifier = Modifier.height(12.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - state.days.forEach { day -> - AlarmDayButton( - modifier = Modifier.size( - if (screenWidthDp > 360.dp) 36.dp else 34.dp, - ), - label = stringResource(id = day.getLabelStringRes()), - isPressed = state.selectedDays.contains(day), - onClick = { - processAction(AlarmAddEditContract.Action.ToggleSpecificDaySelection(day)) - }, - ) - } - } - } -} - -@Composable -private fun AlarmAddEditDisableHolidaySwitch( - state: AlarmAddEditContract.AlarmHolidayState, - processAction: (AlarmAddEditContract.Action) -> Unit, -) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding( - start = 20.dp, - end = 20.dp, - bottom = 16.dp, - ), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - painter = painterResource(id = core.designsystem.R.drawable.ic_holiday), - contentDescription = "Holiday", - tint = OrbitTheme.colors.gray_400, - modifier = Modifier.padding(end = 4.dp), - ) - Text( - text = stringResource(id = R.string.alarm_add_edit_disable_holiday), - style = OrbitTheme.typography.label1Medium, - color = OrbitTheme.colors.gray_400, - ) - - Spacer(modifier = Modifier.weight(1f)) - - OrbitSwitch( - isChecked = state.isDisableHolidayChecked, - isEnabled = state.isDisableHolidayEnabled, - onClick = { - processAction(AlarmAddEditContract.Action.ToggleHolidaySkipOption) - }, - ) - } -} - -@Preview -@Composable -fun AlarmAddEditSettingsSectionPreview() { - AlarmAddEditSettingsSection( - state = AlarmAddEditContract.State( - timeState = AlarmAddEditContract.AlarmTimeState( - currentAmPm = "AM", - currentHour = 9, - currentMinute = 30, - ), - daySelectionState = AlarmAddEditContract.AlarmDaySelectionState( - isWeekdaysChecked = true, - isWeekendsChecked = false, - selectedDays = setOf(AlarmDay.MON, AlarmDay.TUE), - days = AlarmDay.entries.toSet(), - ), - holidayState = AlarmAddEditContract.AlarmHolidayState( - isDisableHolidayChecked = false, - ), - ), - processAction = { }, - ) -} - -@Preview -@Composable -fun AlarmAddEditSettingItemPreview() { - AlarmAddEditSettingItem( - label = "์•Œ๋žŒ ๋ฏธ๋ฃจ๊ธฐ", - description = "5๋ถ„, ๋ฌดํ•œ", - onClick = { }, - ) -} - -@Preview -@Composable -fun AlarmAddEditScreenPreview() { - AlarmAddEditScreen( - stateProvider = { - AlarmAddEditContract.State( - timeState = AlarmAddEditContract.AlarmTimeState( - currentAmPm = "AM", - currentHour = 9, - currentMinute = 30, - ), - daySelectionState = AlarmAddEditContract.AlarmDaySelectionState( - isWeekdaysChecked = true, - isWeekendsChecked = false, - selectedDays = setOf(AlarmDay.MON, AlarmDay.TUE), - days = AlarmDay.entries.toSet(), - ), - holidayState = AlarmAddEditContract.AlarmHolidayState( - isDisableHolidayChecked = false, - ), - ) - }, - eventDispatcher = { }, - ) -} diff --git a/feature/home/src/main/java/com/yapp/alarm/addedit/AlarmAddEditViewModel.kt b/feature/home/src/main/java/com/yapp/alarm/addedit/AlarmAddEditViewModel.kt deleted file mode 100644 index 08713ebd..00000000 --- a/feature/home/src/main/java/com/yapp/alarm/addedit/AlarmAddEditViewModel.kt +++ /dev/null @@ -1,554 +0,0 @@ -package com.yapp.alarm.addedit - -import android.util.Log -import androidx.compose.ui.unit.dp -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.viewModelScope -import com.yapp.alarm.AlarmHelper -import com.yapp.analytics.AnalyticsEvent -import com.yapp.analytics.AnalyticsHelper -import com.yapp.common.util.ResourceProvider -import com.yapp.domain.model.Alarm -import com.yapp.domain.model.AlarmDay -import com.yapp.domain.model.AlarmSound -import com.yapp.domain.model.copyFrom -import com.yapp.domain.model.toAlarmDayNames -import com.yapp.domain.model.toAlarmDays -import com.yapp.domain.model.toDayOfWeek -import com.yapp.domain.usecase.AlarmUseCase -import com.yapp.media.haptic.HapticFeedbackManager -import com.yapp.media.haptic.HapticType -import com.yapp.ui.base.BaseViewModel -import dagger.hilt.android.lifecycle.HiltViewModel -import feature.home.R -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import java.time.LocalTime -import javax.inject.Inject - -@HiltViewModel -class AlarmAddEditViewModel @Inject constructor( - private val analyticsHelper: AnalyticsHelper, - private val alarmUseCase: AlarmUseCase, - private val resourceProvider: ResourceProvider, - private val hapticFeedbackManager: HapticFeedbackManager, - private val alarmHelper: AlarmHelper, - savedStateHandle: SavedStateHandle, -) : BaseViewModel( - initialState = AlarmAddEditContract.State(), -) { - private val alarmId: Long = savedStateHandle.get("alarmId") ?: -1 - - init { - updateState { copy(mode = if (alarmId == -1L) AlarmAddEditContract.EditMode.ADD else AlarmAddEditContract.EditMode.EDIT) } - initializeAlarmScreen() - } - - private fun initializeAlarmScreen() = viewModelScope.launch { - alarmUseCase.getAlarmSounds().onSuccess { sounds -> - if (alarmId == -1L) { - setupNewAlarmScreen(sounds) - } else { - loadExistingAlarm(sounds) - } - }.onFailure { - Log.e("AlarmAddEditViewModel", "Failed to load alarm sounds", it) - } - } - - private fun setupNewAlarmScreen(sounds: List) { - val defaultSoundIndex = sounds.indexOfFirst { it.title == "Homecoming" }.takeIf { it >= 0 } ?: 0 - val defaultSound = sounds[defaultSoundIndex] - - alarmUseCase.initializeSoundPlayer(defaultSound.uri) - - val now = LocalTime.now() - val initialAmPm = if (now.hour < 12) "์˜ค์ „" else "์˜คํ›„" - val initialHour = if (now.hour == 0 || now.hour == 12) 12 else now.hour % 12 - val initialMinute = now.minute - - updateState { - copy( - initialLoading = false, - timeState = timeState.copy( - initialAmPm = initialAmPm, - initialHour = "$initialHour", - initialMinute = initialMinute.toString().padStart(2, '0'), - currentAmPm = initialAmPm, - currentHour = initialHour, - currentMinute = initialMinute, - alarmMessage = getAlarmMessage(initialAmPm, initialHour, initialMinute, emptySet()), - ), - soundState = soundState.copy(sounds = sounds, soundIndex = defaultSoundIndex), - ) - } - } - - private suspend fun loadExistingAlarm(sounds: List) { - alarmUseCase.getAlarm(alarmId).onSuccess { alarm -> - val repeatDays = alarm.repeatDays.toAlarmDays() - val isAM = alarm.isAm - val hour = alarm.hour - val selectedSoundIndex = sounds.indexOfFirst { it.uri.toString() == alarm.soundUri } - val selectedSound = sounds.getOrNull(selectedSoundIndex) ?: sounds.first() - - alarmUseCase.initializeSoundPlayer(selectedSound.uri) - - updateState { - copy( - initialLoading = false, - timeState = timeState.copy( - initialAmPm = if (isAM) "์˜ค์ „" else "์˜คํ›„", - initialHour = "$hour", - initialMinute = alarm.minute.toString().padStart(2, '0'), - currentAmPm = if (isAM) "์˜ค์ „" else "์˜คํ›„", - currentHour = hour, - currentMinute = alarm.minute, - alarmMessage = getAlarmMessage(if (isAM) "์˜ค์ „" else "์˜คํ›„", hour, alarm.minute, repeatDays), - ), - daySelectionState = setupDaySelectionState(repeatDays), - holidayState = holidayState.copy( - isDisableHolidayEnabled = repeatDays.isNotEmpty(), - isDisableHolidayChecked = alarm.isHolidayAlarmOff, - ), - snoozeState = setupSnoozeState(alarm), - soundState = soundState.copy( - isVibrationEnabled = alarm.isVibrationEnabled, - isSoundEnabled = alarm.isSoundEnabled, - soundVolume = alarm.soundVolume, - sounds = sounds, - soundIndex = selectedSoundIndex, - ), - ) - } - } - } - - private fun setupDaySelectionState(repeatDays: Set) = currentState.daySelectionState.copy( - selectedDays = repeatDays, - isWeekdaysChecked = repeatDays.containsAll(setOf(AlarmDay.MON, AlarmDay.TUE, AlarmDay.WED, AlarmDay.THU, AlarmDay.FRI)), - isWeekendsChecked = repeatDays.containsAll(setOf(AlarmDay.SAT, AlarmDay.SUN)), - ) - - private fun setupSnoozeState(alarm: Alarm) = currentState.snoozeState.copy( - isSnoozeEnabled = alarm.isSnoozeEnabled, - snoozeIntervalIndex = findSnoozeIndex(alarm.snoozeInterval, currentState.snoozeState.snoozeIntervals), - snoozeCountIndex = findSnoozeIndex(alarm.snoozeCount, currentState.snoozeState.snoozeCounts), - ) - - private fun findSnoozeIndex(value: Int, list: List): Int { - return list.indexOfFirst { - it == "๋ฌดํ•œ" && value == -1 || it.filter { char -> char.isDigit() }.toIntOrNull() == value - }.takeIf { it >= 0 } ?: 0 - } - - override fun onCleared() { - super.onCleared() - alarmUseCase.releaseSoundPlayer() - } - - fun processAction(action: AlarmAddEditContract.Action) { - when (action) { - is AlarmAddEditContract.Action.CheckUnsavedChangesBeforeExit -> checkUnsavedChangesBeforeExit() - is AlarmAddEditContract.Action.NavigateBack -> navigateBack() - is AlarmAddEditContract.Action.SaveAlarm -> saveAlarm() - is AlarmAddEditContract.Action.ShowDeleteDialog -> showDeleteDialog() - is AlarmAddEditContract.Action.HideDeleteDialog -> hideDeleteDialog() - is AlarmAddEditContract.Action.ShowUnsavedChangesDialog -> showUnsavedChangesDialog() - is AlarmAddEditContract.Action.HideUnsavedChangesDialog -> hideUnsavedChangesDialog() - is AlarmAddEditContract.Action.DeleteAlarm -> deleteAlarm() - is AlarmAddEditContract.Action.SetAlarmTime -> setAlarmTime(action.amPm, action.hour, action.minute) - is AlarmAddEditContract.Action.ToggleWeekdaysSelection -> toggleWeekdaysSelection() - is AlarmAddEditContract.Action.ToggleWeekendsSelection -> toggleWeekendsSelection() - is AlarmAddEditContract.Action.ToggleSpecificDaySelection -> toggleSpecificDaySelection(action.day) - is AlarmAddEditContract.Action.ToggleHolidaySkipOption -> toggleHolidaySkipOption() - is AlarmAddEditContract.Action.ToggleSnoozeOption -> toggleSnoozeOption() - is AlarmAddEditContract.Action.SetSnoozeInterval -> setSnoozeInterval(action.index) - is AlarmAddEditContract.Action.SetSnoozeRepeatCount -> setSnoozeRepeatCount(action.index) - is AlarmAddEditContract.Action.ToggleVibrationOption -> toggleVibrationOption() - is AlarmAddEditContract.Action.ToggleSoundOption -> toggleSoundOption() - is AlarmAddEditContract.Action.AdjustSoundVolume -> adjustSoundVolume(action.volume) - is AlarmAddEditContract.Action.SelectAlarmSound -> selectAlarmSound(action.index) - is AlarmAddEditContract.Action.ToggleBottomSheet -> toggleBottomSheet(action.sheetType) - } - } - - private fun checkUnsavedChangesBeforeExit() { - if (currentState.mode == AlarmAddEditContract.EditMode.ADD) { - navigateBack() - } else { - val updatedAlarm = currentState.toAlarm() - viewModelScope.launch { - alarmUseCase.getAlarm(alarmId).onSuccess { existingAlarm -> - if (updatedAlarm.copy(id = alarmId) != existingAlarm) { - showUnsavedChangesDialog() - } else { - emitSideEffect(AlarmAddEditContract.SideEffect.NavigateBack) - } - } - } - } - } - - private fun navigateBack() { - emitSideEffect(AlarmAddEditContract.SideEffect.NavigateBack) - } - - private fun saveAlarm() { - val newAlarm = currentState.toAlarm() - - viewModelScope.launch { - when (currentState.mode) { - AlarmAddEditContract.EditMode.EDIT -> updateExistingAlarm(newAlarm) - AlarmAddEditContract.EditMode.ADD -> checkAndCreateAlarm(newAlarm) - } - } - } - - private suspend fun updateExistingAlarm(alarm: Alarm) { - val updatedAlarm = alarm.copy(id = alarmId) - - alarmUseCase.getAlarm(alarmId).onSuccess { oldAlarm -> - alarmHelper.unScheduleAlarm(oldAlarm) - } - - alarmUseCase.updateAlarm(updatedAlarm) - .onSuccess { - alarmHelper.scheduleAlarm(updatedAlarm) - emitSideEffect(AlarmAddEditContract.SideEffect.UpdateAlarm(it.id)) - } - .onFailure { - Log.e("AlarmAddEditViewModel", "Failed to update alarm", it) - } - } - - private suspend fun checkAndCreateAlarm(newAlarm: Alarm) { - val timeMatchedAlarms = alarmUseCase.getAlarmsByTime(newAlarm.hour, newAlarm.minute, newAlarm.isAm) - .first() - - when { - timeMatchedAlarms.any { it.copy(id = 0) == newAlarm.copy(id = 0) } -> { - showAlarmAlreadySetWarning() - } - - timeMatchedAlarms.isNotEmpty() -> { - val existingAlarm = timeMatchedAlarms.first() - val updatedAlarm = existingAlarm.copyFrom(newAlarm).copy(id = existingAlarm.id) - updateExistingAlarm(updatedAlarm) - } - - else -> { - createNewAlarm(newAlarm) - } - } - } - - private fun showAlarmAlreadySetWarning() { - emitSideEffect( - AlarmAddEditContract.SideEffect.ShowSnackBar( - message = resourceProvider.getString(R.string.alarm_already_set), - iconRes = resourceProvider.getDrawable(core.designsystem.R.drawable.ic_alert), - bottomPadding = 78.dp, - onDismiss = { }, - onAction = { }, - ), - ) - } - - private suspend fun createNewAlarm(alarm: Alarm) { - alarmUseCase.insertAlarm(alarm) - .onSuccess { - analyticsHelper.logEvent( - AnalyticsEvent( - type = "alarm_create", - properties = mapOf( - AnalyticsEvent.AlarmPropertiesKeys.ALARM_ID to "${it.id}", - AnalyticsEvent.AlarmPropertiesKeys.REPEAT_DAYS to it.repeatDays.toAlarmDayNames(), - AnalyticsEvent.AlarmPropertiesKeys.SNOOZE_OPTION to listOf(it.snoozeInterval, it.snoozeCount), - ), - ), - ) - alarmHelper.scheduleAlarm(it) - emitSideEffect(AlarmAddEditContract.SideEffect.SaveAlarm(it.id)) - } - .onFailure { - Log.e("AlarmAddEditViewModel", "Failed to insert alarm", it) - } - } - - private fun setAlarmTime(amPm: String, hour: Int, minute: Int) { - val newTimeState = currentState.timeState.copy( - currentAmPm = amPm, - currentHour = hour, - currentMinute = minute, - alarmMessage = getAlarmMessage(amPm, hour, minute, currentState.daySelectionState.selectedDays), - ) - - hapticFeedbackManager.performHapticFeedback(HapticType.LIGHT_TICK) - - updateState { - copy(timeState = newTimeState) - } - } - - private fun showDeleteDialog() { - updateState { copy(isDeleteDialogVisible = true) } - } - - private fun hideDeleteDialog() { - updateState { copy(isDeleteDialogVisible = false) } - } - - private fun showUnsavedChangesDialog() { - updateState { copy(isUnsavedChangesDialogVisible = true) } - } - - private fun hideUnsavedChangesDialog() { - updateState { copy(isUnsavedChangesDialogVisible = false) } - } - - private fun deleteAlarm() { - emitSideEffect(AlarmAddEditContract.SideEffect.DeleteAlarm(alarmId)) - } - - private fun toggleWeekdaysSelection() { - val weekdays = setOf(AlarmDay.MON, AlarmDay.TUE, AlarmDay.WED, AlarmDay.THU, AlarmDay.FRI) - val isChecked = !currentState.daySelectionState.isWeekdaysChecked - val updatedDays = if (isChecked) { - currentState.daySelectionState.selectedDays + weekdays - } else { - currentState.daySelectionState.selectedDays - weekdays - } - val newDayState = currentState.daySelectionState.copy( - isWeekdaysChecked = isChecked, - selectedDays = updatedDays, - ) - updateState { - copy( - timeState = timeState.copy( - alarmMessage = getAlarmMessage(timeState.currentAmPm, timeState.currentHour, timeState.currentMinute, newDayState.selectedDays), - ), - daySelectionState = newDayState, - holidayState = holidayState.copy( - isDisableHolidayEnabled = newDayState.selectedDays.isNotEmpty(), - isDisableHolidayChecked = if (newDayState.selectedDays.isEmpty()) false else holidayState.isDisableHolidayChecked, - ), - ) - } - } - - private fun toggleWeekendsSelection() { - val weekends = setOf(AlarmDay.SAT, AlarmDay.SUN) - val isChecked = !currentState.daySelectionState.isWeekendsChecked - val updatedDays = if (isChecked) { - currentState.daySelectionState.selectedDays + weekends - } else { - currentState.daySelectionState.selectedDays - weekends - } - val newDayState = currentState.daySelectionState.copy( - isWeekendsChecked = isChecked, - selectedDays = updatedDays, - ) - updateState { - copy( - timeState = timeState.copy( - alarmMessage = getAlarmMessage(timeState.currentAmPm, timeState.currentHour, timeState.currentMinute, newDayState.selectedDays), - ), - daySelectionState = newDayState, - holidayState = holidayState.copy( - isDisableHolidayEnabled = newDayState.selectedDays.isNotEmpty(), - isDisableHolidayChecked = if (newDayState.selectedDays.isEmpty()) false else holidayState.isDisableHolidayChecked, - ), - ) - } - } - - private fun toggleSpecificDaySelection(day: AlarmDay) { - val updatedDays = if (day in currentState.daySelectionState.selectedDays) { - currentState.daySelectionState.selectedDays - day - } else { - currentState.daySelectionState.selectedDays + day - } - val weekdays = setOf(AlarmDay.MON, AlarmDay.TUE, AlarmDay.WED, AlarmDay.THU, AlarmDay.FRI) - val weekends = setOf(AlarmDay.SAT, AlarmDay.SUN) - - val newDayState = currentState.daySelectionState.copy( - selectedDays = updatedDays, - isWeekdaysChecked = updatedDays.containsAll(weekdays), - isWeekendsChecked = updatedDays.containsAll(weekends), - ) - updateState { - copy( - timeState = timeState.copy( - alarmMessage = getAlarmMessage(timeState.currentAmPm, timeState.currentHour, timeState.currentMinute, newDayState.selectedDays), - ), - daySelectionState = newDayState, - holidayState = holidayState.copy( - isDisableHolidayEnabled = newDayState.selectedDays.isNotEmpty(), - isDisableHolidayChecked = if (newDayState.selectedDays.isEmpty()) false else holidayState.isDisableHolidayChecked, - ), - ) - } - } - - private fun toggleHolidaySkipOption() { - val newHolidayState = currentState.holidayState.copy( - isDisableHolidayChecked = !currentState.holidayState.isDisableHolidayChecked, - ) - - updateState { - copy(holidayState = newHolidayState) - } - - if (newHolidayState.isDisableHolidayChecked) { - emitSideEffect( - AlarmAddEditContract.SideEffect.ShowSnackBar( - message = resourceProvider.getString(R.string.alarm_disabled_warning), - label = resourceProvider.getString(R.string.alarm_delete_dialog_btn_cancel), - iconRes = resourceProvider.getDrawable(core.designsystem.R.drawable.ic_check_green), - bottomPadding = 78.dp, - onDismiss = { }, - onAction = { - updateState { - copy(holidayState = holidayState.copy(isDisableHolidayChecked = false)) - } - }, - ), - ) - } - } - - private fun toggleSnoozeOption() { - val newSnoozeState = currentState.snoozeState.copy( - isSnoozeEnabled = !currentState.snoozeState.isSnoozeEnabled, - ) - updateState { - copy(snoozeState = newSnoozeState) - } - } - - private fun setSnoozeInterval(index: Int) { - val newSnoozeState = currentState.snoozeState.copy(snoozeIntervalIndex = index) - updateState { - copy(snoozeState = newSnoozeState) - } - } - - private fun setSnoozeRepeatCount(index: Int) { - val newSnoozeState = currentState.snoozeState.copy(snoozeCountIndex = index) - updateState { - copy(snoozeState = newSnoozeState) - } - } - - private fun toggleVibrationOption() { - val newSoundState = currentState.soundState.copy(isVibrationEnabled = !currentState.soundState.isVibrationEnabled) - - if (newSoundState.isVibrationEnabled) { - hapticFeedbackManager.performHapticFeedback(HapticType.SUCCESS) - } - updateState { - copy(soundState = newSoundState) - } - } - - private fun toggleSoundOption() { - val newSoundState = currentState.soundState.copy(isSoundEnabled = !currentState.soundState.isSoundEnabled) - if (!newSoundState.isSoundEnabled) { - alarmUseCase.stopAlarmSound() - } - updateState { - copy(soundState = newSoundState) - } - } - - private fun adjustSoundVolume(volume: Int) { - val newSoundState = currentState.soundState.copy(soundVolume = volume) - alarmUseCase.updateAlarmVolume(volume) - updateState { - copy(soundState = newSoundState) - } - } - - private fun selectAlarmSound(index: Int) { - val newSoundState = currentState.soundState.copy(soundIndex = index) - updateState { - copy(soundState = newSoundState) - } - - val selectedSound = currentState.soundState.sounds[index] - alarmUseCase.initializeSoundPlayer(selectedSound.uri) - alarmUseCase.playAlarmSound(currentState.soundState.soundVolume) - } - - private fun toggleBottomSheet(sheetType: AlarmAddEditContract.BottomSheetType) { - val newBottomSheetState = if (currentState.bottomSheetState == sheetType) { - if (currentState.bottomSheetState == AlarmAddEditContract.BottomSheetType.SoundSetting) { - alarmUseCase.stopAlarmSound() - } - null - } else { - sheetType - } - updateState { - copy(bottomSheetState = newBottomSheetState) - } - } - - private fun getAlarmMessage(amPm: String, hour: Int, minute: Int, selectedDays: Set): String { - val now = java.time.LocalDateTime.now() - val alarmHour = convertTo24HourFormat(amPm, hour) - val alarmTimeToday = now.toLocalDate().atTime(alarmHour, minute) - val nextAlarmDateTime = calculateNextAlarmDateTime(now, alarmTimeToday, selectedDays) - val duration = java.time.Duration.between(now, nextAlarmDateTime) - val totalMinutes = duration.toMinutes() - val days = totalMinutes / (24 * 60) - val hours = (totalMinutes % (24 * 60)) / 60 - val minutes = totalMinutes % 60 - - return when { - days > 0 -> "${days}์ผ ${hours}์‹œ๊ฐ„ ํ›„์— ์šธ๋ ค์š”" - hours > 0 -> "${hours}์‹œ๊ฐ„ ${minutes}๋ถ„ ํ›„์— ์šธ๋ ค์š”" - minutes == 0L -> "๊ณง ์šธ๋ ค์š”" - else -> "${minutes}๋ถ„ ํ›„์— ์šธ๋ ค์š”" - } - } - - private fun convertTo24HourFormat(amPm: String, hour: Int): Int = when { - amPm == "์˜คํ›„" && hour != 12 -> hour + 12 - amPm == "์˜ค์ „" && hour == 12 -> 0 - else -> hour - } - - private fun calculateNextAlarmDateTime( - now: java.time.LocalDateTime, - alarmTimeToday: java.time.LocalDateTime, - selectedDays: Set, - ): java.time.LocalDateTime { - if (selectedDays.isEmpty()) { - return if (alarmTimeToday.isBefore(now)) { - alarmTimeToday.plusDays(1) - } else { - alarmTimeToday - } - } - - val currentDayOfWeek = now.dayOfWeek.value - val selectedDaysOfWeek = selectedDays.map { it.toDayOfWeek().value }.sorted() - - if (selectedDaysOfWeek.contains(currentDayOfWeek) && now.toLocalTime().isBefore(alarmTimeToday.toLocalTime())) { - return alarmTimeToday - } - - val nextDay = selectedDaysOfWeek.firstOrNull { it > currentDayOfWeek } - ?: selectedDaysOfWeek.first() - val daysToAdd = if (nextDay > currentDayOfWeek) { - nextDay - currentDayOfWeek - } else { - 7 - (currentDayOfWeek - nextDay) - } - - val nextAlarmDate = now.toLocalDate().plusDays(daysToAdd.toLong()) - return nextAlarmDate.atTime(alarmTimeToday.toLocalTime()) - } -} diff --git a/feature/home/src/main/java/com/yapp/alarm/component/bottomsheet/AlarmSnoozeBottomSheet.kt b/feature/home/src/main/java/com/yapp/alarm/component/bottomsheet/AlarmSnoozeBottomSheet.kt deleted file mode 100644 index 8e2b9109..00000000 --- a/feature/home/src/main/java/com/yapp/alarm/component/bottomsheet/AlarmSnoozeBottomSheet.kt +++ /dev/null @@ -1,294 +0,0 @@ -package com.yapp.alarm.component.bottomsheet - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.yapp.designsystem.theme.OrbitTheme -import com.yapp.ui.component.OrbitBottomSheet -import com.yapp.ui.component.button.OrbitButton -import com.yapp.ui.component.radiobutton.OrbitRadioButton -import com.yapp.ui.component.switch.OrbitSwitch -import feature.home.R -import kotlinx.coroutines.launch - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -internal fun AlarmSnoozeBottomSheet( - snoozeEnabled: Boolean, - snoozeIntervalIndex: Int, - snoozeIntervals: List, - onIntervalSelected: (Int) -> Unit, - snoozeCountIndex: Int, - snoozeCounts: List, - onSnoozeToggle: () -> Unit, - onCountSelected: (Int) -> Unit, - onComplete: () -> Unit, - isSheetOpen: Boolean, - onDismiss: () -> Unit, -) { - val scope = rememberCoroutineScope() - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - - OrbitBottomSheet( - isSheetOpen = isSheetOpen, - sheetState = sheetState, - onDismissRequest = { - scope.launch { - sheetState.hide() - }.invokeOnCompletion { onDismiss() } - }, - ) { - BottomSheetContent( - isSnoozeEnabled = snoozeEnabled, - snoozeIntervalIndex = snoozeIntervalIndex, - snoozeIntervals = snoozeIntervals, - onIntervalSelected = onIntervalSelected, - snoozeCountIndex = snoozeCountIndex, - snoozeCounts = snoozeCounts, - onSnoozeToggle = onSnoozeToggle, - onCountSelected = onCountSelected, - onComplete = { - scope.launch { - sheetState.hide() - }.invokeOnCompletion { onComplete() } - }, - ) - } -} - -@Composable -private fun BottomSheetContent( - isSnoozeEnabled: Boolean, - snoozeIntervalIndex: Int, - snoozeIntervals: List, - onIntervalSelected: (Int) -> Unit, - snoozeCountIndex: Int, - snoozeCounts: List, - onSnoozeToggle: () -> Unit, - onCountSelected: (Int) -> Unit, - onComplete: () -> Unit, -) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = 24.dp, - vertical = 12.dp, - ), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Spacer(modifier = Modifier.height(6.dp)) - VibrationSection(isSnoozeEnabled, onSnoozeToggle) - Spacer(modifier = Modifier.height(20.dp)) - SelectorSection( - title = stringResource(id = R.string.alarm_add_edit_interval), - selectedIndex = snoozeIntervalIndex, - items = snoozeIntervals, - enabled = isSnoozeEnabled, - onItemSelected = onIntervalSelected, - ) - Spacer(modifier = Modifier.height(32.dp)) - SelectorSection( - title = stringResource(id = R.string.alarm_add_edit_repeat_count), - selectedIndex = snoozeCountIndex, - items = snoozeCounts, - enabled = isSnoozeEnabled, - onItemSelected = onCountSelected, - ) - Spacer(modifier = Modifier.height(20.dp)) - if (isSnoozeEnabled) { - AlarmSnoozeMessage( - interval = snoozeIntervals[snoozeIntervalIndex], - count = snoozeCounts[snoozeCountIndex], - ) - } - Spacer(modifier = Modifier.height(40.dp)) - OrbitButton( - label = stringResource(id = R.string.alarm_add_edit_complete), - enabled = true, - containerColor = OrbitTheme.colors.gray_600, - contentColor = OrbitTheme.colors.white, - pressedContainerColor = OrbitTheme.colors.gray_500, - pressedContentColor = OrbitTheme.colors.white.copy(alpha = 0.7f), - onClick = onComplete, - ) - } -} - -@Composable -private fun VibrationSection(snoozeEnabled: Boolean, onSnoozeToggle: () -> Unit) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringResource(id = R.string.alarm_add_edit_alarm_snooze), - style = OrbitTheme.typography.heading2SemiBold, - color = OrbitTheme.colors.white, - ) - Spacer(modifier = Modifier.weight(1f)) - OrbitSwitch( - isChecked = snoozeEnabled, - isEnabled = true, - onClick = onSnoozeToggle, - ) - } -} - -@Composable -private fun SelectorSection( - title: String, - selectedIndex: Int, - items: List, - enabled: Boolean, - onItemSelected: (Int) -> Unit, -) { - Column { - Text( - text = title, - style = OrbitTheme.typography.headline2Medium, - color = OrbitTheme.colors.gray_50, - ) - Spacer(modifier = Modifier.height(16.dp)) - SelectorItems( - items = items, - selectedIndex = selectedIndex, - enabled = enabled, - onItemSelected = onItemSelected, - ) - } -} - -@Composable -private fun SelectorItems( - items: List, - selectedIndex: Int, - enabled: Boolean, - onItemSelected: (Int) -> Unit, -) { - Box { - Column { - Spacer(modifier = Modifier.height(7.dp)) - Spacer( - modifier = Modifier - .fillMaxWidth() - .height(6.dp) - .padding(horizontal = 6.dp) - .background( - if (enabled) { - OrbitTheme.colors.gray_600 - } else { - OrbitTheme.colors.gray_700 - }, - ), - ) - } - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 6.dp), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - items.forEachIndexed { index, item -> - Column(horizontalAlignment = getAlignment(index, items.size)) { - OrbitRadioButton( - selected = index == selectedIndex, - enabled = enabled, - onClick = { if (enabled) onItemSelected(index) }, - ) - Spacer(modifier = Modifier.height(12.dp)) - Text( - text = item, - style = OrbitTheme.typography.body1Medium, - color = OrbitTheme.colors.gray_50, - ) - } - } - } - } -} - -private fun getAlignment(index: Int, size: Int): Alignment.Horizontal = - when (index) { - 0 -> Alignment.Start - size - 1 -> Alignment.End - else -> Alignment.CenterHorizontally - } - -@Composable -private fun AlarmSnoozeMessage(interval: String, count: String) { - val formattedCount = if (count == stringResource(id = R.string.alarm_add_edit_repeat_count_infinite)) "${count}๋ฒˆ" else count - - Box( - modifier = Modifier - .background( - color = OrbitTheme.colors.gray_700, - shape = RoundedCornerShape(8.dp), - ) - .padding(horizontal = 12.dp, vertical = 6.dp), - contentAlignment = Alignment.Center, - ) { - Text( - text = stringResource(id = R.string.alarm_add_edit_alarm_snooze_description, interval, formattedCount), - style = OrbitTheme.typography.label1Medium, - color = OrbitTheme.colors.main, - ) - } -} - -@Preview -@Composable -private fun AlarmSnoozeBottomSheetPreview() { - var isSnoozeEnabled by remember { mutableStateOf(true) } - var snoozeIntervalIndex by remember { mutableIntStateOf(2) } - var snoozeCountIndex by remember { mutableIntStateOf(1) } - var isSheetOpen by remember { mutableStateOf(true) } - - OrbitTheme { - AlarmSnoozeBottomSheet( - snoozeEnabled = isSnoozeEnabled, - snoozeIntervalIndex = snoozeIntervalIndex, - snoozeCountIndex = snoozeCountIndex, - snoozeIntervals = listOf(1, 3, 5, 10, 15).map { - stringResource(id = R.string.alarm_add_edit_interval_minute, it) - }, - snoozeCounts = listOf( - stringResource(id = R.string.alarm_add_edit_repeat_count_times, 1), - stringResource(id = R.string.alarm_add_edit_repeat_count_times, 3), - stringResource(id = R.string.alarm_add_edit_repeat_count_times, 5), - stringResource(id = R.string.alarm_add_edit_repeat_count_times, 10), - stringResource(id = R.string.alarm_add_edit_repeat_count_infinite), - ), - onSnoozeToggle = { isSnoozeEnabled = !isSnoozeEnabled }, - onIntervalSelected = { index -> snoozeIntervalIndex = index }, - onCountSelected = { index -> snoozeCountIndex = index }, - onComplete = { isSheetOpen = false }, - isSheetOpen = isSheetOpen, - onDismiss = { isSheetOpen = false }, - ) - } -} diff --git a/feature/home/src/main/java/com/yapp/home/HomeContract.kt b/feature/home/src/main/java/com/yapp/home/HomeContract.kt index ee3f5dd1..e4d15d19 100644 --- a/feature/home/src/main/java/com/yapp/home/HomeContract.kt +++ b/feature/home/src/main/java/com/yapp/home/HomeContract.kt @@ -3,7 +3,6 @@ package com.yapp.home import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.yapp.domain.model.Alarm -import com.yapp.ui.base.SideEffect import com.yapp.ui.base.UiState sealed class HomeContract { @@ -19,6 +18,7 @@ sealed class HomeContract { val isDeleteDialogVisible: Boolean = false, val isNoActivatedAlarmDialogVisible: Boolean = false, val isNoDailyFortuneDialogVisible: Boolean = false, + val isUpdateNoticeVisible: Boolean = false, val hasNewFortune: Boolean = false, val isToolTipVisible: Boolean = false, val pendingAlarmToggle: Pair? = null, @@ -59,6 +59,8 @@ sealed class HomeContract { data object ShowNoDailyFortuneDialog : Action() data object HideNoDailyFortuneDialog : Action() data object HideToolTip : Action() + data object OnClickDontShowAgain : Action() + data object HideUpdateNotice : Action() data object RollbackPendingAlarmToggle : Action() data object ConfirmDeletion : Action() data class DeleteSingleAlarm(val alarmId: Long) : Action() diff --git a/feature/home/src/main/java/com/yapp/home/HomeNavGraph.kt b/feature/home/src/main/java/com/yapp/home/HomeNavGraph.kt index b044e9de..c46e3458 100644 --- a/feature/home/src/main/java/com/yapp/home/HomeNavGraph.kt +++ b/feature/home/src/main/java/com/yapp/home/HomeNavGraph.kt @@ -4,10 +4,11 @@ import androidx.compose.material3.SnackbarHostState import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import androidx.navigation.compose.navigation -import com.yapp.alarm.addedit.AlarmAddEditRoute import com.yapp.common.navigation.OrbitNavigator import com.yapp.common.navigation.route.HomeBaseRoute import com.yapp.common.navigation.route.HomeDestination +import com.yapp.home.alarm.addedit.AlarmAddEditRoute +import com.yapp.ui.component.bottomsheet.OrbitBottomSheetState const val ADD_ALARM_RESULT_KEY = "addAlarmResult" const val UPDATE_ALARM_RESULT_KEY = "updateAlarmResult" @@ -15,6 +16,7 @@ const val DELETE_ALARM_RESULT_KEY = "deleteAlarmResult" fun NavGraphBuilder.homeNavGraph( navigator: OrbitNavigator, + bottomSheetState: OrbitBottomSheetState, snackBarHostState: SnackbarHostState, ) { navigation( @@ -30,6 +32,7 @@ fun NavGraphBuilder.homeNavGraph( composable { AlarmAddEditRoute( navigator = navigator, + bottomSheetState = bottomSheetState, snackBarHostState = snackBarHostState, ) } diff --git a/feature/home/src/main/java/com/yapp/home/HomeScreen.kt b/feature/home/src/main/java/com/yapp/home/HomeScreen.kt index b8a4dc46..10d74e52 100644 --- a/feature/home/src/main/java/com/yapp/home/HomeScreen.kt +++ b/feature/home/src/main/java/com/yapp/home/HomeScreen.kt @@ -1,5 +1,6 @@ package com.yapp.home +import android.os.Build import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.Canvas @@ -15,12 +16,16 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn @@ -64,12 +69,13 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.yapp.alarm.component.AlarmListItem -import com.yapp.alarm.component.AlarmListItemMenu import com.yapp.common.navigation.OrbitNavigator import com.yapp.designsystem.theme.OrbitTheme import com.yapp.domain.model.Alarm +import com.yapp.home.alarm.component.AlarmListItem +import com.yapp.home.alarm.component.AlarmListItemMenu import com.yapp.home.component.bottomsheet.AlarmListBottomSheet +import com.yapp.home.component.bottomsheet.UpdateNoticeBottomSheet import com.yapp.ui.component.dialog.OrbitDialog import com.yapp.ui.component.lottie.LottieAnimation import com.yapp.ui.component.snackbar.showCustomSnackBar @@ -77,7 +83,8 @@ import com.yapp.ui.component.tooltip.OrbitToolTip import com.yapp.ui.utils.heightForScreenPercentage import com.yapp.ui.utils.toPx import feature.home.R -import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.CoroutineScope +import org.orbitmvi.orbit.compose.collectSideEffect @Composable fun HomeRoute( @@ -86,7 +93,6 @@ fun HomeRoute( snackBarHostState: SnackbarHostState, ) { val state by viewModel.container.stateFlow.collectAsStateWithLifecycle() - val sideEffect = viewModel.container.sideEffectFlow val coroutineScope = rememberCoroutineScope() @@ -120,70 +126,75 @@ fun HomeRoute( } } - LaunchedEffect(sideEffect) { - sideEffect.collectLatest { effect -> - when (effect) { - is HomeContract.SideEffect.NavigateToAddAlarm -> { - navigator.navigateToAddAlarm() - } + viewModel.collectSideEffect { + handleSideEffect(it, navigator, snackBarHostState, coroutineScope) + } - is HomeContract.SideEffect.NavigateToEditAlarm -> { - navigator.navigateToEditAlarm(effect.alarmId) - } + HomeScreen( + state = state, + processAction = viewModel::processAction, + ) +} - is HomeContract.SideEffect.NavigateToFortune -> { - navigator.navigateToFortune() - } +private suspend fun handleSideEffect( + sideEffect: HomeContract.SideEffect, + navigator: OrbitNavigator, + snackBarHostState: SnackbarHostState, + coroutineScope: CoroutineScope, +) { + when (sideEffect) { + is HomeContract.SideEffect.NavigateToAddAlarm -> { + navigator.navigateToAddAlarm() + } - is HomeContract.SideEffect.NavigateToSetting -> { - navigator.navigateToSetting() - } + is HomeContract.SideEffect.NavigateToEditAlarm -> { + navigator.navigateToEditAlarm(sideEffect.alarmId) + } - is HomeContract.SideEffect.ShowSnackBar -> { - val result = showCustomSnackBar( - scope = coroutineScope, - snackBarHostState = snackBarHostState, - message = effect.message, - actionLabel = effect.label, - iconRes = effect.iconRes, - bottomPadding = effect.bottomPadding, - durationMillis = effect.durationMillis, - ) + is HomeContract.SideEffect.NavigateToFortune -> { + navigator.navigateToFortune() + } - when (result) { - SnackbarResult.ActionPerformed -> effect.onAction() - SnackbarResult.Dismissed -> effect.onDismiss() - } - } + is HomeContract.SideEffect.NavigateToSetting -> { + navigator.navigateToSetting() + } + + is HomeContract.SideEffect.ShowSnackBar -> { + val result = showCustomSnackBar( + scope = coroutineScope, + snackBarHostState = snackBarHostState, + message = sideEffect.message, + actionLabel = sideEffect.label, + iconRes = sideEffect.iconRes, + bottomPadding = sideEffect.bottomPadding, + durationMillis = sideEffect.durationMillis, + ) + + when (result) { + SnackbarResult.ActionPerformed -> sideEffect.onAction() + SnackbarResult.Dismissed -> sideEffect.onDismiss() } } } - - HomeScreen( - stateProvider = { state }, - eventDispatcher = viewModel::processAction, - ) } @Composable fun HomeScreen( - stateProvider: () -> HomeContract.State, - eventDispatcher: (HomeContract.Action) -> Unit, + state: HomeContract.State, + processAction: (HomeContract.Action) -> Unit, ) { - val state = stateProvider() - if (state.initialLoading) { HomeLoadingScreen() } else if (state.alarms.isEmpty()) { HomeAlarmEmptyScreen( onSettingClick = { - eventDispatcher(HomeContract.Action.NavigateToSetting) + processAction(HomeContract.Action.NavigateToSetting) }, onMailClick = { - eventDispatcher(HomeContract.Action.ShowDailyFortune) + processAction(HomeContract.Action.ShowDailyFortune) }, onAddClick = { - eventDispatcher(HomeContract.Action.NavigateToAlarmCreation) + processAction(HomeContract.Action.NavigateToAlarmCreation) }, hasNewFortune = state.hasNewFortune, isTooltipVisible = state.isToolTipVisible, @@ -191,7 +202,7 @@ fun HomeScreen( } else { HomeContent( state = state, - eventDispatcher = eventDispatcher, + processAction = processAction, ) } @@ -202,10 +213,10 @@ fun HomeScreen( confirmText = stringResource(id = R.string.alarm_delete_dialog_btn_delete), cancelText = stringResource(id = R.string.alarm_delete_dialog_btn_cancel), onConfirm = { - eventDispatcher(HomeContract.Action.ConfirmDeletion) + processAction(HomeContract.Action.ConfirmDeletion) }, onCancel = { - eventDispatcher(HomeContract.Action.HideDeleteDialog) + processAction(HomeContract.Action.HideDeleteDialog) }, ) } @@ -217,10 +228,10 @@ fun HomeScreen( confirmText = stringResource(id = R.string.no_active_alarm_dialog_btn_confirm), cancelText = stringResource(id = R.string.no_active_alarm_dialog_btn_cancel), onConfirm = { - eventDispatcher(HomeContract.Action.HideNoActivatedAlarmDialog) + processAction(HomeContract.Action.HideNoActivatedAlarmDialog) }, onCancel = { - eventDispatcher(HomeContract.Action.RollbackPendingAlarmToggle) + processAction(HomeContract.Action.RollbackPendingAlarmToggle) }, ) } @@ -231,7 +242,18 @@ fun HomeScreen( message = stringResource(id = R.string.no_daily_fortune_dialog_message), confirmText = stringResource(id = R.string.no_daily_fortune_dialog_btn_confirm), onConfirm = { - eventDispatcher(HomeContract.Action.HideNoDailyFortuneDialog) + processAction(HomeContract.Action.HideNoDailyFortuneDialog) + }, + ) + } + + if (state.isUpdateNoticeVisible) { + UpdateNoticeBottomSheet( + onDontShowAgain = { + processAction(HomeContract.Action.OnClickDontShowAgain) + }, + onClose = { + processAction(HomeContract.Action.HideUpdateNotice) }, ) } @@ -268,9 +290,11 @@ private fun HomeLoadingScreen() { @Composable private fun HomeContent( state: HomeContract.State, - eventDispatcher: (HomeContract.Action) -> Unit, + processAction: (HomeContract.Action) -> Unit, ) { val screenHeight = LocalConfiguration.current.screenHeightDp.dp + val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + val navBarHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() var sheetHalfExpandHeight by remember { mutableStateOf(0.dp) } val listState = rememberLazyListState() @@ -278,7 +302,7 @@ private fun HomeContent( LaunchedEffect(state.lastAddedAlarmIndex) { state.lastAddedAlarmIndex?.let { index -> listState.scrollToItem(index) - eventDispatcher(HomeContract.Action.ResetLastAddedAlarmIndex) + processAction(HomeContract.Action.ResetLastAddedAlarmIndex) } } @@ -288,7 +312,7 @@ private fun HomeContent( interactionSource = remember { MutableInteractionSource() }, indication = null, ) { - eventDispatcher(HomeContract.Action.HideToolTip) + processAction(HomeContract.Action.HideToolTip) }, ) { if (state.activeItemMenu != null) { @@ -299,7 +323,7 @@ private fun HomeContent( interactionSource = remember { MutableInteractionSource() }, indication = null, ) { - eventDispatcher(HomeContract.Action.HideItemMenu) + processAction(HomeContract.Action.HideItemMenu) } .zIndex(1f), ) @@ -325,47 +349,50 @@ private fun HomeContent( halfExpandedHeight = sheetHalfExpandHeight, listState = listState, onClickAlarm = { alarmId -> - eventDispatcher(HomeContract.Action.EditAlarm(alarmId)) + processAction(HomeContract.Action.EditAlarm(alarmId)) }, onLongPressAlarm = { alarmId, x, y -> - eventDispatcher(HomeContract.Action.ShowItemMenu(alarmId, x, y)) + processAction(HomeContract.Action.ShowItemMenu(alarmId, x, y)) }, onClickAdd = { - eventDispatcher(HomeContract.Action.NavigateToAlarmCreation) + processAction(HomeContract.Action.NavigateToAlarmCreation) }, onClickMore = { if (state.dropdownMenuExpanded || state.sortDropDownMenuExpanded) { - eventDispatcher(HomeContract.Action.HideDropDownMenu) + processAction(HomeContract.Action.HideDropDownMenu) } else { - eventDispatcher(HomeContract.Action.ShowDropDownMenu) + processAction(HomeContract.Action.ShowDropDownMenu) } }, onClickCheckAll = { - eventDispatcher(HomeContract.Action.ToggleAllAlarmSelection) + processAction(HomeContract.Action.ToggleAllAlarmSelection) }, onClickClose = { - eventDispatcher(HomeContract.Action.ToggleMultiSelectionMode) + processAction(HomeContract.Action.ToggleMultiSelectionMode) }, onClickEdit = { - eventDispatcher(HomeContract.Action.ToggleMultiSelectionMode) + processAction(HomeContract.Action.ToggleMultiSelectionMode) }, onClickSort = { - eventDispatcher(HomeContract.Action.ShowSortDropDownMenu) + processAction(HomeContract.Action.ShowSortDropDownMenu) }, onSetSortOrder = { sortOrder -> - eventDispatcher(HomeContract.Action.SetSortOrder(sortOrder)) + processAction(HomeContract.Action.SetSortOrder(sortOrder)) }, onDismissRequest = { - eventDispatcher(HomeContract.Action.HideDropDownMenu) + processAction(HomeContract.Action.HideDropDownMenu) }, onToggleSelect = { alarmId -> - eventDispatcher(HomeContract.Action.ToggleAlarmSelection(alarmId)) + processAction(HomeContract.Action.ToggleAlarmSelection(alarmId)) }, onToggleActive = { alarmId -> - eventDispatcher(HomeContract.Action.ToggleAlarmActivation(alarmId)) + processAction(HomeContract.Action.ToggleAlarmActivation(alarmId)) }, onSwipe = { alarmId -> - eventDispatcher(HomeContract.Action.SwipeToDeleteAlarm(alarmId)) + processAction(HomeContract.Action.SwipeToDeleteAlarm(alarmId)) + }, + onExpanded = { + processAction(HomeContract.Action.HideToolTip) }, ) { Box( @@ -384,7 +411,15 @@ private fun HomeContent( .fillMaxWidth() .layout { measurable, constraints -> val placeable = measurable.measure(constraints) - sheetHalfExpandHeight = screenHeight - placeable.height.toDp() + val contentHeight = placeable.height.toDp() + + val offset = if (Build.VERSION.SDK_INT < 35) { + 0.dp + } else { + statusBarHeight + navBarHeight + } + sheetHalfExpandHeight = screenHeight - contentHeight - offset + layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) } @@ -407,8 +442,8 @@ private fun HomeContent( } HomeTopBar( - onSettingClick = { eventDispatcher(HomeContract.Action.NavigateToSetting) }, - onMailClick = { eventDispatcher(HomeContract.Action.ShowDailyFortune) }, + onSettingClick = { processAction(HomeContract.Action.NavigateToSetting) }, + onMailClick = { processAction(HomeContract.Action.ShowDailyFortune) }, hasNewFortune = state.hasNewFortune, isShowTooltip = state.isToolTipVisible, ) @@ -425,7 +460,7 @@ private fun HomeContent( .padding(bottom = 26.dp), selectedAlarmCount = state.selectedAlarmIds.size, onClick = { - eventDispatcher(HomeContract.Action.ShowDeleteDialog) + processAction(HomeContract.Action.ShowDeleteDialog) }, ) } @@ -438,7 +473,7 @@ private fun HomeContent( activeItemMenuPosition = state.activeItemMenuPosition, selectedAlarmIds = state.selectedAlarmIds, onDelete = { alarmId -> - eventDispatcher(HomeContract.Action.DeleteSingleAlarm(alarmId)) + processAction(HomeContract.Action.DeleteSingleAlarm(alarmId)) }, ) } @@ -529,7 +564,7 @@ private fun HomeTopBar( } @Composable -fun HillWithGradient() { +private fun HillWithGradient() { val hillTopY = (LocalConfiguration.current.screenHeightDp.dp * 0.28f).toPx() Canvas( @@ -565,7 +600,7 @@ fun HillWithGradient() { } @Composable -fun SkyImage() { +private fun SkyImage() { Image( painter = painterResource(id = core.designsystem.R.drawable.ic_main_sky), contentDescription = "IMG_MAIN_SKY", @@ -898,7 +933,6 @@ private fun AlarmWithMenu( swipeable = false, selectable = false, selected = selectedAlarmIds.contains(activeItemMenu.id), - isAm = activeItemMenu.isAm, hour = activeItemMenu.hour, minute = activeItemMenu.minute, isActive = activeItemMenu.isAlarmActive, @@ -926,18 +960,16 @@ private fun AlarmWithMenu( fun HomeScreenPreview() { OrbitTheme { HomeScreen( - stateProvider = { - HomeContract.State() - .copy( - initialLoading = false, - alarms = listOf( - Alarm(), - ), - activeItemMenu = 0L, - activeItemMenuPosition = Pair(0f, 0f), - ) - }, - eventDispatcher = {}, + state = HomeContract.State() + .copy( + initialLoading = false, + alarms = listOf( + Alarm(), + ), + activeItemMenu = 0L, + activeItemMenuPosition = Pair(0f, 0f), + ), + processAction = {}, ) } } diff --git a/feature/home/src/main/java/com/yapp/home/HomeViewModel.kt b/feature/home/src/main/java/com/yapp/home/HomeViewModel.kt index 9c5dccc6..57b74428 100644 --- a/feature/home/src/main/java/com/yapp/home/HomeViewModel.kt +++ b/feature/home/src/main/java/com/yapp/home/HomeViewModel.kt @@ -1,39 +1,55 @@ package com.yapp.home +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities import android.util.Log -import androidx.lifecycle.viewModelScope -import com.yapp.alarm.AlarmHelper +import androidx.lifecycle.ViewModel import com.yapp.common.util.ResourceProvider -import com.yapp.datastore.UserPreferences import com.yapp.domain.model.Alarm -import com.yapp.domain.model.toAlarmDays -import com.yapp.domain.model.toDayOfWeek +import com.yapp.domain.repository.FortuneRepository +import com.yapp.domain.repository.UserInfoRepository import com.yapp.domain.usecase.AlarmUseCase -import com.yapp.ui.base.BaseViewModel +import com.yapp.home.util.AlarmDateTimeFormatter import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import feature.home.R import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.launch +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.syntax.simple.intent +import org.orbitmvi.orbit.syntax.simple.postSideEffect +import org.orbitmvi.orbit.syntax.simple.reduce +import org.orbitmvi.orbit.syntax.simple.repeatOnSubscription +import org.orbitmvi.orbit.viewmodel.container import java.time.LocalDate -import java.time.LocalDateTime -import java.time.LocalTime -import java.time.format.DateTimeFormatter import javax.inject.Inject +import javax.inject.Named @HiltViewModel class HomeViewModel @Inject constructor( private val alarmUseCase: AlarmUseCase, private val resourceProvider: ResourceProvider, - private val alarmHelper: AlarmHelper, - private val userPreferences: UserPreferences, -) : BaseViewModel( - initialState = HomeContract.State(), -) { - init { - loadAllAlarms() - loadDailyFortuneState() - loadUserName() + private val alarmDateTimeFormatter: AlarmDateTimeFormatter, + private val fortuneRepository: FortuneRepository, + private val userInfoRepository: UserInfoRepository, + @Named("appVersion") private val appVersion: String, + @ApplicationContext private val context: Context, +) : ViewModel(), ContainerHost { + + override val container: Container = container( + initialState = HomeContract.State(), + ) { + intent { + repeatOnSubscription { + loadAllAlarms() + loadDailyFortuneState() + loadUserName() + loadUpdateNoticeVisibility() + } + } } fun processAction(action: HomeContract.Action) { @@ -54,6 +70,8 @@ class HomeViewModel @Inject constructor( HomeContract.Action.ShowNoDailyFortuneDialog -> showNoDailyFortuneDialog() HomeContract.Action.HideNoDailyFortuneDialog -> hideNoDailyFortuneDialog() HomeContract.Action.HideToolTip -> hideToolTip() + HomeContract.Action.HideUpdateNotice -> hideUpdateNotice() + HomeContract.Action.OnClickDontShowAgain -> setUpdateNoticeDontShowVersion() HomeContract.Action.RollbackPendingAlarmToggle -> rollbackAlarmActivation() HomeContract.Action.ConfirmDeletion -> confirmDeletion() is HomeContract.Action.DeleteSingleAlarm -> deleteSingleAlarm(action.alarmId) @@ -68,17 +86,17 @@ class HomeViewModel @Inject constructor( } } - fun scrollToAddedAlarm(id: Long) { - val newAlarmIndex = currentState.alarms.indexOfFirst { it.id == id } - if (newAlarmIndex == -1) return + fun scrollToAddedAlarm(id: Long) = intent { + val newAlarmIndex = state.alarms.indexOfFirst { it.id == id } + if (newAlarmIndex == -1) return@intent - updateState { - copy( + reduce { + state.copy( lastAddedAlarmIndex = newAlarmIndex, ) } - emitSideEffect( + postSideEffect( HomeContract.SideEffect.ShowSnackBar( message = resourceProvider.getString(R.string.alarm_added), iconRes = resourceProvider.getDrawable(core.designsystem.R.drawable.ic_check_green), @@ -88,164 +106,160 @@ class HomeViewModel @Inject constructor( ) } - fun scrollToUpdatedAlarm(id: Long) { - val updatedAlarmIndex = currentState.alarms.indexOfFirst { it.id == id } - if (updatedAlarmIndex == -1) return + fun scrollToUpdatedAlarm(id: Long) = intent { + val updatedAlarmIndex = state.alarms.indexOfFirst { it.id == id } + if (updatedAlarmIndex == -1) return@intent - updateState { - copy( + reduce { + state.copy( lastAddedAlarmIndex = updatedAlarmIndex, ) } } - private fun navigateToAlarmCreation() { - emitSideEffect(HomeContract.SideEffect.NavigateToAddAlarm) + private fun navigateToAlarmCreation() = intent { + postSideEffect(HomeContract.SideEffect.NavigateToAddAlarm) } - private fun toggleMultiSelectionMode() { - updateState { - copy( - isSelectionMode = !currentState.isSelectionMode, + private fun toggleMultiSelectionMode() = intent { + reduce { + state.copy( + isSelectionMode = !state.isSelectionMode, selectedAlarmIds = emptySet(), dropdownMenuExpanded = false, ) } } - private fun showDropDownMenu() { - updateState { copy(dropdownMenuExpanded = true) } + private fun showDropDownMenu() = intent { + reduce { state.copy(dropdownMenuExpanded = true) } } - private fun showSortDropDownMenu() { - updateState { - copy( + private fun showSortDropDownMenu() = intent { + reduce { + state.copy( dropdownMenuExpanded = false, sortDropDownMenuExpanded = true, ) } } - private fun hideDropDownMenu() { - updateState { - copy( + private fun hideDropDownMenu() = intent { + reduce { + state.copy( dropdownMenuExpanded = false, sortDropDownMenuExpanded = false, ) } } - private fun toggleAlarmSelection(alarmId: Long) { - updateState { - val updatedSelection = currentState.selectedAlarmIds.toMutableSet().apply { + private fun toggleAlarmSelection(alarmId: Long) = intent { + reduce { + val updatedSelection = state.selectedAlarmIds.toMutableSet().apply { if (contains(alarmId)) remove(alarmId) else add(alarmId) } - copy(selectedAlarmIds = updatedSelection) + state.copy(selectedAlarmIds = updatedSelection) } } - private fun toggleAllAlarmSelection() { - updateState { - val allIds = currentState.alarms.map { it.id }.toSet() - val updatedSelection = if (currentState.selectedAlarmIds == allIds) emptySet() else allIds - copy(selectedAlarmIds = updatedSelection) + private fun toggleAllAlarmSelection() = intent { + reduce { + val allIds = state.alarms.map { it.id }.toSet() + val updatedSelection = if (state.selectedAlarmIds == allIds) emptySet() else allIds + state.copy(selectedAlarmIds = updatedSelection) } } - private fun toggleAlarmActivation(alarmId: Long) { - viewModelScope.launch { - val currentIndex = currentState.alarms.indexOfFirst { it.id == alarmId } - if (currentIndex == -1) return@launch - - val currentAlarm = currentState.alarms[currentIndex] - val previousState = currentAlarm.isAlarmActive // ๊ธฐ์กด ์ƒํƒœ ์ €์žฅ - val updatedAlarm = currentAlarm.copy(isAlarmActive = !currentAlarm.isAlarmActive) - - alarmUseCase.updateAlarmActive(alarmId, updatedAlarm.isAlarmActive).onSuccess { - val updatedAlarms = currentState.alarms.toMutableList() - updatedAlarms[currentIndex] = updatedAlarm - - val hasActivatedAlarm = updatedAlarms.any { it.isAlarmActive } - updateState { - copy( - alarms = updatedAlarms, - isNoActivatedAlarmDialogVisible = !hasActivatedAlarm, - pendingAlarmToggle = if (!hasActivatedAlarm) alarmId to previousState else null, - ) - } - - if (updatedAlarm.isAlarmActive) { - alarmHelper.scheduleAlarm(updatedAlarm) - } else { - alarmHelper.unScheduleAlarm(updatedAlarm) - } - }.onFailure { error -> - Log.e("HomeViewModel", "Failed to update alarm state", error) + private fun toggleAlarmActivation(alarmId: Long) = intent { + val currentIndex = state.alarms.indexOfFirst { it.id == alarmId } + if (currentIndex == -1) return@intent + + val currentAlarm = state.alarms[currentIndex] + val previousState = currentAlarm.isAlarmActive // ๊ธฐ์กด ์ƒํƒœ ์ €์žฅ + val updatedAlarm = currentAlarm.copy(isAlarmActive = !currentAlarm.isAlarmActive) + + alarmUseCase.updateAlarmActive(alarmId, updatedAlarm.isAlarmActive).onSuccess { + val updatedAlarms = state.alarms.toMutableList() + updatedAlarms[currentIndex] = updatedAlarm + + val hasActivatedAlarm = updatedAlarms.any { it.isAlarmActive } + reduce { + state.copy( + alarms = updatedAlarms, + isNoActivatedAlarmDialogVisible = !hasActivatedAlarm, + pendingAlarmToggle = if (!hasActivatedAlarm) alarmId to previousState else null, + ) } + + if (updatedAlarm.isAlarmActive) { + alarmUseCase.scheduleAlarm(updatedAlarm) + } else { + alarmUseCase.unScheduleAlarm(updatedAlarm) + } + }.onFailure { error -> + Log.e("HomeViewModel", "Failed to update alarm state", error) } } - private fun showDeleteDialog() { - updateState { copy(isDeleteDialogVisible = true) } + private fun showDeleteDialog() = intent { + reduce { state.copy(isDeleteDialogVisible = true) } } - private fun hideDeleteDialog() { - updateState { copy(isDeleteDialogVisible = false) } + private fun hideDeleteDialog() = intent { + reduce { state.copy(isDeleteDialogVisible = false) } } - private fun confirmDeletion() { - deleteAlarms(currentState.selectedAlarmIds) - updateState { - copy( + private fun confirmDeletion() = intent { + deleteAlarms(state.selectedAlarmIds) + reduce { + state.copy( selectedAlarmIds = emptySet(), isDeleteDialogVisible = false, ) } } - private fun showNoActivatedAlarmDialog() { - updateState { copy(isNoActivatedAlarmDialogVisible = true) } + private fun showNoActivatedAlarmDialog() = intent { + reduce { state.copy(isNoActivatedAlarmDialogVisible = true) } } - private fun hideNoActivatedAlarmDialog() { - updateState { - copy( + private fun hideNoActivatedAlarmDialog() = intent { + reduce { + state.copy( isNoActivatedAlarmDialogVisible = false, pendingAlarmToggle = null, ) } } - private fun rollbackAlarmActivation() { - val pendingAlarm = currentState.pendingAlarmToggle ?: return + private fun rollbackAlarmActivation() = intent { + val pendingAlarm = state.pendingAlarmToggle ?: return@intent val (alarmId, previousState) = pendingAlarm - viewModelScope.launch { - val currentIndex = currentState.alarms.indexOfFirst { it.id == alarmId } - if (currentIndex == -1) return@launch - - val currentAlarm = currentState.alarms[currentIndex] - val restoredAlarm = currentAlarm.copy(isAlarmActive = previousState) - - alarmUseCase.updateAlarm(restoredAlarm).onSuccess { updatedAlarm -> - val updatedAlarms = currentState.alarms.toMutableList() - updatedAlarms[currentIndex] = updatedAlarm - updateState { - copy( - alarms = updatedAlarms, - pendingAlarmToggle = null, - isNoActivatedAlarmDialogVisible = false, - ) - } - - if (updatedAlarm.isAlarmActive) { - alarmHelper.scheduleAlarm(updatedAlarm) - } else { - alarmHelper.unScheduleAlarm(updatedAlarm) - } - }.onFailure { error -> - Log.e("HomeViewModel", "Failed to rollback alarm state", error) + val currentIndex = state.alarms.indexOfFirst { it.id == alarmId } + if (currentIndex == -1) return@intent + + val currentAlarm = state.alarms[currentIndex] + val restoredAlarm = currentAlarm.copy(isAlarmActive = previousState) + + alarmUseCase.updateAlarm(restoredAlarm).onSuccess { updatedAlarm -> + val updatedAlarms = state.alarms.toMutableList() + updatedAlarms[currentIndex] = updatedAlarm + reduce { + state.copy( + alarms = updatedAlarms, + pendingAlarmToggle = null, + isNoActivatedAlarmDialogVisible = false, + ) } + + if (updatedAlarm.isAlarmActive) { + alarmUseCase.scheduleAlarm(updatedAlarm) + } else { + alarmUseCase.unScheduleAlarm(updatedAlarm) + } + }.onFailure { error -> + Log.e("HomeViewModel", "Failed to rollback alarm state", error) } } @@ -253,24 +267,22 @@ class HomeViewModel @Inject constructor( deleteAlarms(setOf(alarmId)) } - private fun deleteAlarms(alarmIds: Set) { - if (alarmIds.isEmpty()) return + private fun deleteAlarms(alarmIds: Set) = intent { + if (alarmIds.isEmpty()) return@intent - val alarmsToDelete = currentState.alarms + val alarmsToDelete = state.alarms .filter { it.id in alarmIds } - viewModelScope.launch { - alarmsToDelete.forEach { alarm -> - alarmUseCase.deleteAlarm(alarm.id) - alarmHelper.unScheduleAlarm(alarm) - } + alarmsToDelete.forEach { alarm -> + alarmUseCase.deleteAlarm(alarm.id) + alarmUseCase.unScheduleAlarm(alarm) } - if (currentState.activeItemMenu != null) { + if (state.activeItemMenu != null) { hideItemMenu() } - emitSideEffect( + postSideEffect( HomeContract.SideEffect.ShowSnackBar( message = resourceProvider.getString(R.string.alarm_deleted), label = resourceProvider.getString(R.string.alarm_delete_dialog_btn_cancel), @@ -283,198 +295,171 @@ class HomeViewModel @Inject constructor( ) } - private fun restoreDeletedAlarms(alarmsWithIndex: List) { - viewModelScope.launch { - alarmsWithIndex.forEach { alarm -> - alarmUseCase.insertAlarm(alarm) - alarmHelper.scheduleAlarm(alarm) - } + private fun restoreDeletedAlarms(alarmsWithIndex: List) = intent { + alarmsWithIndex.forEach { alarm -> + alarmUseCase.insertAlarm(alarm) + alarmUseCase.scheduleAlarm(alarm) } } - private fun restLastAddedAlarmIndex() { - updateState { copy(lastAddedAlarmIndex = null) } + private fun restLastAddedAlarmIndex() = intent { + reduce { state.copy(lastAddedAlarmIndex = null) } } - private fun loadAllAlarms() { - updateState { copy(initialLoading = true) } + private fun loadAllAlarms() = intent { + reduce { state.copy(initialLoading = true) } - viewModelScope.launch { - alarmUseCase.getAllAlarms().collect { - updateState { - copy( - alarms = it, - initialLoading = false, - ) - } - updateDeliveryTime(it) + alarmUseCase.getAllAlarms().collect { alarms -> + reduce { + state.copy( + alarms = alarms, + initialLoading = false, + ) } + updateDeliveryTime(alarms) } } - private fun editAlarm(alarmId: Long) { - emitSideEffect(HomeContract.SideEffect.NavigateToEditAlarm(alarmId)) + private fun editAlarm(alarmId: Long) = intent { + postSideEffect(HomeContract.SideEffect.NavigateToEditAlarm(alarmId)) } - private fun updateDeliveryTime(alarms: List) { - val earliestAlarm = alarms - .filter { it.isAlarmActive } - .minByOrNull { alarm -> - getNextAlarmDateWithTime(alarm.isAm, alarm.hour, alarm.minute, alarm.repeatDays) - } - - val deliveryTime = earliestAlarm?.let { alarm -> - val alarmDateTime = getNextAlarmDateWithTime(alarm.isAm, alarm.hour, alarm.minute, alarm.repeatDays) - alarmDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm")) - } ?: "NONE" + private fun updateDeliveryTime(alarms: List) = intent { + val deliveryTimeFormats = AlarmDateTimeFormatter.DeliveryTimeFormats( + noAlarm = resourceProvider.getString(R.string.home_fortune_no_alarm), + today = resourceProvider.getString(R.string.home_fortune_delivery_today, "%s"), + tomorrow = resourceProvider.getString(R.string.home_fortune_delivery_tomorrow, "%s"), + thisYear = resourceProvider.getString(R.string.home_fortune_delivery_this_year, "%s"), + otherYear = resourceProvider.getString(R.string.home_fortune_delivery_other_year, "%s"), + ) - updateState { copy(deliveryTime = formatDeliveryTime(deliveryTime)) } + val formattedTime = alarmDateTimeFormatter.getFormattedEarliestUpcomingAlarmDeliveryTime( + alarms = alarms, + formats = deliveryTimeFormats, + ) + reduce { state.copy(deliveryTime = formattedTime) } } - private fun getNextAlarmDateWithTime(isAm: Boolean, hour: Int, minute: Int, repeatDays: Int): LocalDateTime { - val now = LocalDateTime.now() + private fun loadDailyFortune() = intent { + val fortuneDate = fortuneRepository.fortuneDateEpochFlow.firstOrNull() + val todayDate = LocalDate.now().toEpochDay() - val alarmHour = when { - isAm && hour == 12 -> 0 - !isAm && hour != 12 -> hour + 12 - else -> hour - } - val alarmTime = LocalTime.of(alarmHour, minute) - val todayAlarm = LocalDateTime.of(now.toLocalDate(), alarmTime) - - // ๋ฐ˜๋ณต ์š”์ผ์ด ์„ค์ •๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ โ†’ ๋‹จ์ผ ์•Œ๋žŒ - if (repeatDays == 0) { - return if (todayAlarm.isAfter(now)) todayAlarm else todayAlarm.plusDays(1) + if (fortuneDate != todayDate) { + processAction(HomeContract.Action.ShowNoDailyFortuneDialog) + } else { + fortuneRepository.markFortuneTooltipShown() + postSideEffect(HomeContract.SideEffect.NavigateToFortune) } + } - // ๋น„ํŠธ๋งˆ์Šคํฌ ๊ธฐ๋ฐ˜ ๋ฐ˜๋ณต ์š”์ผ ์ถ”์ถœ - val selectedDays = repeatDays.toAlarmDays().map { it.toDayOfWeek() }.sortedBy { it.value } - val currentDayOfWeek = now.dayOfWeek - - // ๊ฐ€์žฅ ๋น ๋ฅธ ๋‹ค์Œ ์•Œ๋žŒ ๋‚ ์งœ ๊ณ„์‚ฐ - val nextDayOffset = selectedDays - .map { (it.value + 7 - currentDayOfWeek.value) % 7 } - .filter { it > 0 || todayAlarm.isAfter(now) } - .minOrNull() ?: (selectedDays.first().value + 7 - currentDayOfWeek.value) - - return todayAlarm.plusDays(nextDayOffset.toLong()) - } - - private fun formatDeliveryTime(deliveryTime: String): String { - return try { - if (deliveryTime == "NONE") return resourceProvider.getString(R.string.home_fortune_no_alarm) - - val inputDateTime = LocalDateTime.parse(deliveryTime, DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm")) - val now = LocalDateTime.now() - val today = now.toLocalDate() - val tomorrow = today.plusDays(1) - - return when { - inputDateTime.toLocalDate() == today -> - resourceProvider.getString(R.string.home_fortune_delivery_today, inputDateTime.format(DateTimeFormatter.ofPattern("a h:mm"))) - inputDateTime.toLocalDate() == tomorrow -> - resourceProvider.getString(R.string.home_fortune_delivery_tomorrow, inputDateTime.format(DateTimeFormatter.ofPattern("a h:mm"))) - inputDateTime.year == now.year -> - resourceProvider.getString( - R.string.home_fortune_delivery_this_year, - inputDateTime.format(DateTimeFormatter.ofPattern("M์›” d์ผ a h:mm")), - ) - else -> - resourceProvider.getString( - R.string.home_fortune_delivery_other_year, - inputDateTime.format(DateTimeFormatter.ofPattern("yy๋…„ M์›” d์ผ a h:mm")), - ) + private fun loadDailyFortuneState() = intent { + val todayDate = LocalDate.now().toEpochDay() + + combine( + fortuneRepository.fortuneDateEpochFlow, + fortuneRepository.fortuneScoreFlow, + fortuneRepository.shouldShowFortuneToolTipFlow, + ) { fortuneDate, fortuneScore, shouldShowTooltip -> + val isTodayFortuneAvailable = fortuneDate == todayDate + val finalFortuneScore = if (isTodayFortuneAvailable) fortuneScore ?: -1 else -1 + + Pair(finalFortuneScore, shouldShowTooltip) + }.collect { (finalFortuneScore, hasNewFortune) -> + reduce { + state.copy( + lastFortuneScore = finalFortuneScore, + hasNewFortune = hasNewFortune, + isToolTipVisible = hasNewFortune, + ) } - } catch (e: Exception) { - resourceProvider.getString(R.string.home_fortune_no_alarm) } } - private fun loadDailyFortune() { - viewModelScope.launch { - val fortuneDate = userPreferences.fortuneDateFlow.firstOrNull() - val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE) + private fun loadUpdateNoticeVisibility() = intent { + if (!isOnlineNow()) { + reduce { state.copy(isUpdateNoticeVisible = false) } + return@intent + } - Log.d("HomeViewModel", "fortuneDate: $fortuneDate, todayDate: $todayDate") + val dontShowVersion = + userInfoRepository.updateNoticeDontShowVersionFlow.firstOrNull() + val lastShownDate = + userInfoRepository.updateNoticeLastShownDateEpochFlow.firstOrNull() - if (fortuneDate != todayDate) { - processAction(HomeContract.Action.ShowNoDailyFortuneDialog) - } else { - userPreferences.markFortuneAsChecked() - emitSideEffect(HomeContract.SideEffect.NavigateToFortune) - } + val today = LocalDate.now().toEpochDay() + + val shouldShow = when { + dontShowVersion != null && dontShowVersion == appVersion -> false + lastShownDate != null && lastShownDate == today -> false + else -> true } + + if (shouldShow) userInfoRepository.markUpdateNoticeShownToday() + + reduce { state.copy(isUpdateNoticeVisible = shouldShow) } } - private fun loadDailyFortuneState() { - viewModelScope.launch { - val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE) - - combine( - userPreferences.fortuneDateFlow, - userPreferences.fortuneScoreFlow, - userPreferences.hasNewFortuneFlow, - ) { fortuneDate, fortuneScore, hasNewFortune -> - val isTodayFortuneAvailable = fortuneDate == todayDate - val finalFortuneScore = if (isTodayFortuneAvailable) fortuneScore ?: -1 else -1 - - Pair(finalFortuneScore, hasNewFortune) - }.collect { (finalFortuneScore, hasNewFortune) -> - updateState { - copy( - lastFortuneScore = finalFortuneScore, - hasNewFortune = hasNewFortune, - isToolTipVisible = hasNewFortune, - ) - } - } - } + private fun setUpdateNoticeDontShowVersion() = intent { + userInfoRepository.markUpdateNoticeDontShow(appVersion) + reduce { state.copy(isUpdateNoticeVisible = false) } } - private fun loadUserName() { - viewModelScope.launch { - userPreferences.userNameFlow.collect { userName -> - updateState { copy(name = userName ?: "") } - } + private fun hideUpdateNotice() = intent { + reduce { state.copy(isUpdateNoticeVisible = false) } + } + + private fun loadUserName() = intent { + userInfoRepository.userNameFlow.first { userName -> + reduce { state.copy(name = userName ?: "") } + true } } - private fun showNoDailyFortuneDialog() { - updateState { copy(isNoDailyFortuneDialogVisible = true) } + private fun showNoDailyFortuneDialog() = intent { + reduce { state.copy(isNoDailyFortuneDialogVisible = true) } } - private fun hideNoDailyFortuneDialog() { - updateState { copy(isNoDailyFortuneDialogVisible = false) } + private fun hideNoDailyFortuneDialog() = intent { + reduce { state.copy(isNoDailyFortuneDialogVisible = false) } } - private fun hideToolTip() { - updateState { copy(isToolTipVisible = false) } + private fun hideToolTip() = intent { + reduce { state.copy(isToolTipVisible = false) } } - private fun navigateToSetting() { - emitSideEffect(HomeContract.SideEffect.NavigateToSetting) + private fun navigateToSetting() = intent { + postSideEffect(HomeContract.SideEffect.NavigateToSetting) } - private fun showItemMenu(alarmId: Long, x: Float, y: Float) { - updateState { - copy( + private fun showItemMenu(alarmId: Long, x: Float, y: Float) = intent { + reduce { + state.copy( activeItemMenu = alarmId, activeItemMenuPosition = x to y, ) } } - private fun hideItemMenu() { - updateState { - copy( + private fun hideItemMenu() = intent { + reduce { + state.copy( activeItemMenu = null, activeItemMenuPosition = null, ) } } - private fun setSortOrder(sortOrder: HomeContract.AlarmSortOrder) { - updateState { copy(sortOrder = sortOrder) } + private fun setSortOrder(sortOrder: HomeContract.AlarmSortOrder) = intent { + reduce { state.copy(sortOrder = sortOrder) } hideDropDownMenu() } + + private fun isOnlineNow(): Boolean { + val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val network = cm.activeNetwork ?: return false + val caps = cm.getNetworkCapabilities(network) ?: return false + + return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && + caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + } } diff --git a/feature/home/src/main/java/com/yapp/alarm/AlarmDayLabel.kt b/feature/home/src/main/java/com/yapp/home/alarm/AlarmDayLabel.kt similarity index 94% rename from feature/home/src/main/java/com/yapp/alarm/AlarmDayLabel.kt rename to feature/home/src/main/java/com/yapp/home/alarm/AlarmDayLabel.kt index ee2e1b86..69acb835 100644 --- a/feature/home/src/main/java/com/yapp/alarm/AlarmDayLabel.kt +++ b/feature/home/src/main/java/com/yapp/home/alarm/AlarmDayLabel.kt @@ -1,4 +1,4 @@ -package com.yapp.alarm +package com.yapp.home.alarm import com.yapp.domain.model.AlarmDay import feature.home.R diff --git a/feature/home/src/main/java/com/yapp/alarm/addedit/AlarmAddEditContract.kt b/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditContract.kt similarity index 63% rename from feature/home/src/main/java/com/yapp/alarm/addedit/AlarmAddEditContract.kt rename to feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditContract.kt index de6da472..0f128d45 100644 --- a/feature/home/src/main/java/com/yapp/alarm/addedit/AlarmAddEditContract.kt +++ b/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditContract.kt @@ -1,12 +1,14 @@ -package com.yapp.alarm.addedit +package com.yapp.home.alarm.addedit import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.yapp.domain.model.Alarm import com.yapp.domain.model.AlarmDay import com.yapp.domain.model.AlarmSound +import com.yapp.domain.model.MissionType import com.yapp.domain.model.toRepeatDays import com.yapp.ui.base.UiState +import java.time.LocalTime sealed class AlarmAddEditContract { @@ -16,6 +18,7 @@ sealed class AlarmAddEditContract { val timeState: AlarmTimeState = AlarmTimeState(), val daySelectionState: AlarmDaySelectionState = AlarmDaySelectionState(), val holidayState: AlarmHolidayState = AlarmHolidayState(), + val missionState: AlarmMissionState = AlarmMissionState(), val snoozeState: AlarmSnoozeState = AlarmSnoozeState(), val soundState: AlarmSoundState = AlarmSoundState(), val bottomSheetState: BottomSheetType? = null, @@ -24,12 +27,8 @@ sealed class AlarmAddEditContract { ) : UiState data class AlarmTimeState( - val initialAmPm: String = "์˜ค์ „", - val initialHour: String = "1", - val initialMinute: String = "00", - val currentAmPm: String = "์˜ค์ „", - val currentHour: Int = 1, - val currentMinute: Int = 0, + val initialTime: LocalTime = LocalTime.of(1, 0), + val currentTime: LocalTime = LocalTime.of(1, 0), val alarmMessage: String = "", ) @@ -45,12 +44,15 @@ sealed class AlarmAddEditContract { val isDisableHolidayChecked: Boolean = false, ) + data class AlarmMissionState( + val missionType: MissionType = MissionType.TAP, + val missionCount: Int = 10, + ) + data class AlarmSnoozeState( val isSnoozeEnabled: Boolean = true, - val snoozeIntervalIndex: Int = 2, - val snoozeCountIndex: Int = 2, - val snoozeIntervals: List = listOf("1๋ถ„", "3๋ถ„", "5๋ถ„", "10๋ถ„", "15๋ถ„"), - val snoozeCounts: List = listOf("1ํšŒ", "3ํšŒ", "5ํšŒ", "10ํšŒ", "๋ฌดํ•œ"), + val snoozeInterval: Int = 5, + val snoozeCount: Int = 5, ) data class AlarmSoundState( @@ -74,22 +76,34 @@ sealed class AlarmAddEditContract { data object ShowUnsavedChangesDialog : Action() data object HideUnsavedChangesDialog : Action() data object DeleteAlarm : Action() - data class SetAlarmTime(val amPm: String, val hour: Int, val minute: Int) : Action() + data class SetAlarmTime(val newTime: LocalTime) : Action() data object ToggleWeekdaysSelection : Action() data object ToggleWeekendsSelection : Action() data class ToggleSpecificDaySelection(val day: AlarmDay) : Action() data object ToggleHolidaySkipOption : Action() - data object ToggleSnoozeOption : Action() - data class SetSnoozeInterval(val index: Int) : Action() - data class SetSnoozeRepeatCount(val index: Int) : Action() - data object ToggleVibrationOption : Action() - data object ToggleSoundOption : Action() - data class AdjustSoundVolume(val volume: Int) : Action() - data class SelectAlarmSound(val index: Int) : Action() - data class ToggleBottomSheet(val sheetType: BottomSheetType) : Action() + data class SaveMissionSetting(val type: MissionType, val count: Int) : Action() + data class SaveSnoozeSetting( + val enabled: Boolean, + val interval: Int, + val count: Int, + ) : Action() + data class SaveSoundSetting( + val vibrationEnabled: Boolean, + val soundEnabled: Boolean, + val soundVolume: Int, + val soundIndex: Int, + ) : Action() + data class ToggleVibrationEnabled(val enabled: Boolean) : Action() + data class ToggleSoundEnabled(val enabled: Boolean) : Action() + data class SetSoundVolume(val volume: Int) : Action() + data class SetSoundIndex(val index: Int) : Action() + data class ShowBottomSheet(val sheetType: BottomSheetType) : Action() + data class NavigateToMissionPreview(val missionType: MissionType, val missionCount: Int) : Action() + data object HideBottomSheet : Action() } sealed class BottomSheetType { + data object MissionSetting : BottomSheetType() data object SnoozeSetting : BottomSheetType() data object SoundSetting : BottomSheetType() } @@ -97,6 +111,17 @@ sealed class AlarmAddEditContract { sealed class SideEffect : com.yapp.ui.base.SideEffect { data object NavigateBack : SideEffect() + data class NavigateToMissionPreview( + val missionType: MissionType, + val missionCount: Int, + ) : SideEffect() + + data class ShowBottomSheet( + val sheetType: BottomSheetType, + ) : SideEffect() + + data object HideBottomSheet : SideEffect() + data class SaveAlarm(val id: Long) : SideEffect() data class UpdateAlarm(val id: Long) : SideEffect() @@ -118,19 +143,15 @@ sealed class AlarmAddEditContract { internal fun AlarmAddEditContract.State.toAlarm(id: Long = 0): Alarm { return Alarm( id = id, - isAm = timeState.currentAmPm == "์˜ค์ „", - hour = timeState.currentHour, - minute = timeState.currentMinute, + hour = timeState.currentTime.hour, + minute = timeState.currentTime.minute, repeatDays = daySelectionState.selectedDays.toRepeatDays(), isHolidayAlarmOff = holidayState.isDisableHolidayChecked, + missionType = missionState.missionType, + missionCount = missionState.missionCount, isSnoozeEnabled = snoozeState.isSnoozeEnabled, - snoozeInterval = snoozeState.snoozeIntervals.getOrNull(snoozeState.snoozeIntervalIndex) - ?.filter { it.isDigit() } - ?.toIntOrNull() - ?: 5, - snoozeCount = snoozeState.snoozeCounts.getOrNull(snoozeState.snoozeCountIndex) - ?.let { if (it == "๋ฌดํ•œ") -1 else it.filter { char -> char.isDigit() }.toIntOrNull() ?: 1 } - ?: 1, + snoozeInterval = snoozeState.snoozeInterval, + snoozeCount = snoozeState.snoozeCount, isVibrationEnabled = soundState.isVibrationEnabled, isSoundEnabled = soundState.isSoundEnabled, soundUri = soundState.sounds.getOrNull(soundState.soundIndex)?.uri.toString(), diff --git a/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditScreen.kt b/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditScreen.kt new file mode 100644 index 00000000..9d726ae3 --- /dev/null +++ b/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditScreen.kt @@ -0,0 +1,815 @@ +package com.yapp.home.alarm.addedit + +import android.net.Uri +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ripple.RippleAlpha +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalRippleConfiguration +import androidx.compose.material3.RippleConfiguration +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +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.yapp.common.navigation.OrbitNavigator +import com.yapp.designsystem.theme.OrbitTheme +import com.yapp.domain.model.AlarmDay +import com.yapp.domain.model.AlarmSound +import com.yapp.domain.model.MissionType +import com.yapp.home.ADD_ALARM_RESULT_KEY +import com.yapp.home.DELETE_ALARM_RESULT_KEY +import com.yapp.home.UPDATE_ALARM_RESULT_KEY +import com.yapp.home.alarm.component.AlarmCheckItem +import com.yapp.home.alarm.component.AlarmDayButton +import com.yapp.home.alarm.component.bottomsheet.AlarmMissionBottomSheet +import com.yapp.home.alarm.component.bottomsheet.AlarmSnoozeBottomSheet +import com.yapp.home.alarm.component.bottomsheet.AlarmSoundBottomSheet +import com.yapp.home.alarm.getLabelStringRes +import com.yapp.ui.component.bottomsheet.OrbitBottomSheetLayout +import com.yapp.ui.component.bottomsheet.OrbitBottomSheetState +import com.yapp.ui.component.bottomsheet.rememberOrbitBottomSheetState +import com.yapp.ui.component.button.OrbitButton +import com.yapp.ui.component.dialog.OrbitDialog +import com.yapp.ui.component.lottie.LottieAnimation +import com.yapp.ui.component.snackbar.showCustomSnackBar +import com.yapp.ui.component.switch.OrbitSwitch +import com.yapp.ui.component.timepicker.OrbitPicker +import feature.home.R +import kotlinx.coroutines.CoroutineScope +import org.orbitmvi.orbit.compose.collectSideEffect +import java.time.LocalTime + +@Composable +fun AlarmAddEditRoute( + viewModel: AlarmAddEditViewModel = hiltViewModel(), + navigator: OrbitNavigator, + bottomSheetState: OrbitBottomSheetState, + snackBarHostState: SnackbarHostState, +) { + val state by viewModel.container.stateFlow.collectAsStateWithLifecycle() + + val coroutineScope = rememberCoroutineScope() + + viewModel.collectSideEffect { sideEffect -> + handleSideEffect( + sideEffect = sideEffect, + navigator = navigator, + bottomSheetState = bottomSheetState, + snackBarHostState = snackBarHostState, + coroutineScope = coroutineScope, + state = state, + processAction = viewModel::processAction, + ) + } + + AlarmAddEditScreen( + state = state, + bottomSheetState = bottomSheetState, + processAction = viewModel::processAction, + ) +} + +private suspend fun handleSideEffect( + sideEffect: AlarmAddEditContract.SideEffect, + navigator: OrbitNavigator, + bottomSheetState: OrbitBottomSheetState, + snackBarHostState: SnackbarHostState, + coroutineScope: CoroutineScope, + state: AlarmAddEditContract.State, + processAction: (AlarmAddEditContract.Action) -> Unit, +) { + when (sideEffect) { + is AlarmAddEditContract.SideEffect.NavigateBack -> { + navigator.navigateBack() + } + + is AlarmAddEditContract.SideEffect.NavigateToMissionPreview -> { + navigator.navigateToMissionPreview( + missionType = sideEffect.missionType.value, + missionCount = sideEffect.missionCount, + ) + } + + is AlarmAddEditContract.SideEffect.ShowBottomSheet -> { + bottomSheetState.show { + when (sideEffect.sheetType) { + AlarmAddEditContract.BottomSheetType.MissionSetting -> { + AlarmMissionBottomSheet( + missionState = state.missionState, + onDismiss = { + processAction(AlarmAddEditContract.Action.HideBottomSheet) + }, + onSaveMission = { missionType, missionCount -> + processAction( + AlarmAddEditContract.Action.SaveMissionSetting( + type = missionType, + count = missionCount, + ), + ) + }, + onPreviewMission = { missionType, missionCount -> + processAction( + AlarmAddEditContract.Action.NavigateToMissionPreview( + missionType = missionType, + missionCount = missionCount, + ), + ) + }, + ) + } + + AlarmAddEditContract.BottomSheetType.SnoozeSetting -> { + AlarmSnoozeBottomSheet( + snoozeState = state.snoozeState, + onDismiss = { + processAction(AlarmAddEditContract.Action.HideBottomSheet) + }, + onComplete = { enabled, interval, count -> + processAction( + AlarmAddEditContract.Action.SaveSnoozeSetting( + enabled = enabled, + interval = interval, + count = count, + ), + ) + processAction(AlarmAddEditContract.Action.HideBottomSheet) + }, + ) + } + + AlarmAddEditContract.BottomSheetType.SoundSetting -> { + AlarmSoundBottomSheet( + soundState = state.soundState, + onVibrationToggle = { enabled -> + processAction(AlarmAddEditContract.Action.ToggleVibrationEnabled(enabled)) + }, + onSoundToggle = { enabled -> + processAction(AlarmAddEditContract.Action.ToggleSoundEnabled(enabled)) + }, + onVolumeChanged = { volume -> + processAction(AlarmAddEditContract.Action.SetSoundVolume(volume)) + }, + onSoundSelected = { index -> + processAction(AlarmAddEditContract.Action.SetSoundIndex(index)) + }, + onDismiss = { + processAction(AlarmAddEditContract.Action.HideBottomSheet) + }, + onComplete = { vibrationEnabled, soundEnabled, soundVolume, soundIndex -> + processAction( + AlarmAddEditContract.Action.SaveSoundSetting( + vibrationEnabled = vibrationEnabled, + soundEnabled = soundEnabled, + soundVolume = soundVolume, + soundIndex = soundIndex, + ), + ) + }, + ) + } + } + } + } + + is AlarmAddEditContract.SideEffect.HideBottomSheet -> { + bottomSheetState.hide() + } + + is AlarmAddEditContract.SideEffect.SaveAlarm -> { + navigator.navController.previousBackStackEntry + ?.savedStateHandle + ?.set(ADD_ALARM_RESULT_KEY, sideEffect.id) + navigator.navController.popBackStack() + } + + is AlarmAddEditContract.SideEffect.UpdateAlarm -> { + navigator.navController.previousBackStackEntry + ?.savedStateHandle + ?.set(UPDATE_ALARM_RESULT_KEY, sideEffect.id) + navigator.navigateBack() + } + + is AlarmAddEditContract.SideEffect.DeleteAlarm -> { + navigator.navController.previousBackStackEntry + ?.savedStateHandle + ?.set(DELETE_ALARM_RESULT_KEY, sideEffect.id) + navigator.navigateBack() + } + + is AlarmAddEditContract.SideEffect.ShowSnackBar -> { + val result = showCustomSnackBar( + scope = coroutineScope, + snackBarHostState = snackBarHostState, + message = sideEffect.message, + actionLabel = sideEffect.label, + iconRes = sideEffect.iconRes, + bottomPadding = sideEffect.bottomPadding, + durationMillis = sideEffect.durationMillis, + ) + + when (result) { + SnackbarResult.ActionPerformed -> sideEffect.onAction() + SnackbarResult.Dismissed -> sideEffect.onDismiss() + } + } + } +} + +@Composable +fun AlarmAddEditScreen( + state: AlarmAddEditContract.State, + bottomSheetState: OrbitBottomSheetState, + processAction: (AlarmAddEditContract.Action) -> Unit, +) { + if (state.initialLoading) { + AlarmAddEditLoadingScreen() + } else { + AlarmAddEditContent( + state = state, + bottomSheetState = bottomSheetState, + processAction = processAction, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AlarmAddEditContent( + state: AlarmAddEditContract.State, + bottomSheetState: OrbitBottomSheetState, + processAction: (AlarmAddEditContract.Action) -> Unit, +) { + BackHandler { + if (bottomSheetState.state.isVisible) { + processAction(AlarmAddEditContract.Action.HideBottomSheet) + } else { + processAction(AlarmAddEditContract.Action.CheckUnsavedChangesBeforeExit) + } + } + + OrbitBottomSheetLayout(sheetState = bottomSheetState) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AlarmAddEditTopBar( + mode = state.mode, + title = state.timeState.alarmMessage, + onBack = { processAction(AlarmAddEditContract.Action.CheckUnsavedChangesBeforeExit) }, + onDelete = { processAction(AlarmAddEditContract.Action.ShowDeleteDialog) }, + ) + Box( + modifier = Modifier.weight(1f), + contentAlignment = Alignment.Center, + ) { + OrbitPicker( + initialTime = state.timeState.initialTime, + ) { newTime -> + processAction(AlarmAddEditContract.Action.SetAlarmTime(newTime)) + } + } + AlarmAddEditSelectDaysSection( + modifier = Modifier.padding(horizontal = 20.dp), + daysSelectionState = state.daySelectionState, + holidayState = state.holidayState, + processAction = processAction, + ) + Spacer(modifier = Modifier.height(12.dp)) + AlarmAddEditSettingsSection( + modifier = Modifier.padding(horizontal = 20.dp), + state = state, + processAction = processAction, + ) + Spacer(modifier = Modifier.height(24.dp)) + OrbitButton( + label = stringResource(R.string.alarm_add_edit_save), + onClick = { processAction(AlarmAddEditContract.Action.SaveAlarm) }, + enabled = true, + modifier = Modifier + .padding( + start = 20.dp, + end = 20.dp, + bottom = 12.dp, + ), + ) + } + } + + if (state.isDeleteDialogVisible) { + OrbitDialog( + title = stringResource(id = R.string.alarm_delete_dialog_title), + message = stringResource(id = R.string.alarm_delete_dialog_message), + confirmText = stringResource(id = R.string.alarm_delete_dialog_btn_delete), + cancelText = stringResource(id = R.string.alarm_delete_dialog_btn_cancel), + onConfirm = { + processAction(AlarmAddEditContract.Action.DeleteAlarm) + }, + onCancel = { + processAction(AlarmAddEditContract.Action.HideDeleteDialog) + }, + ) + } + + if (state.isUnsavedChangesDialogVisible) { + OrbitDialog( + title = stringResource(id = R.string.alarm_unsaved_changes_dialog_title), + message = stringResource(id = R.string.alarm_unsaved_changes_dialog_message), + confirmText = stringResource(id = R.string.alarm_unsaved_changes_dialog_btn_discard), + cancelText = stringResource(id = R.string.alarm_unsaved_changes_dialog_btn_cancel), + onConfirm = { + processAction(AlarmAddEditContract.Action.NavigateBack) + }, + onCancel = { + processAction(AlarmAddEditContract.Action.HideUnsavedChangesDialog) + }, + ) + } +} + +@Composable +private fun AlarmAddEditLoadingScreen() { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + LottieAnimation( + modifier = Modifier + .size(70.dp) + .align(Alignment.Center), + resId = core.designsystem.R.raw.star_loading, + ) + } +} + +@Composable +private fun AlarmAddEditTopBar( + mode: AlarmAddEditContract.EditMode = AlarmAddEditContract.EditMode.ADD, + title: String, + onBack: () -> Unit, + onDelete: () -> Unit, +) { + Box( + modifier = Modifier + .fillMaxWidth() + .statusBarsPadding() + .height(56.dp), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = painterResource(id = core.designsystem.R.drawable.ic_back), + contentDescription = "Back", + tint = OrbitTheme.colors.white, + modifier = Modifier + .clickable(onClick = onBack) + .padding(start = 20.dp) + .align(Alignment.CenterStart), + ) + + Text( + title, + style = OrbitTheme.typography.body1SemiBold, + color = OrbitTheme.colors.white, + ) + + if (mode == AlarmAddEditContract.EditMode.EDIT) { + DeleteAlarmButton( + modifier = Modifier + .align(Alignment.CenterEnd) + .padding(end = 20.dp), + ) { + onDelete() + } + } + } +} + +@Composable +private fun DeleteAlarmButton( + modifier: Modifier = Modifier, + onDelete: () -> Unit, +) { + val interactionSource = remember { MutableInteractionSource() } + val isPressed = interactionSource.collectIsPressedAsState().value + + Surface( + onClick = onDelete, + modifier = modifier, + shape = RoundedCornerShape(8.dp), + interactionSource = interactionSource, + color = if (isPressed) OrbitTheme.colors.gray_800 else Color.Transparent, + ) { + Text( + text = stringResource(id = R.string.alarm_add_edit_delete), + style = OrbitTheme.typography.body1Medium, + color = OrbitTheme.colors.alert, + modifier = Modifier + .padding( + horizontal = 8.dp, + vertical = 4.dp, + ), + ) + } +} + +@Composable +private fun AlarmAddEditSettingsSection( + modifier: Modifier = Modifier, + state: AlarmAddEditContract.State, + processAction: (AlarmAddEditContract.Action) -> Unit, +) { + Column( + modifier = modifier + .fillMaxWidth() + .background( + color = OrbitTheme.colors.gray_800, + shape = RoundedCornerShape(12.dp), + ) + .clip( + shape = RoundedCornerShape(12.dp), + ), + ) { + AlarmAddEditSettingItem( + label = stringResource(id = R.string.alarm_add_edit_mission), + description = when (state.missionState.missionType) { + MissionType.TAP -> { + val missionType = stringResource(id = R.string.alarm_add_edit_selected_mission_tap) + val missionCount = state.missionState.missionCount + stringResource( + id = R.string.alarm_add_edit_selected_mission_with_count, + missionType, + missionCount, + ) + } + MissionType.SHAKE -> { + val missionType = stringResource(id = R.string.alarm_add_edit_selected_mission_shake) + val missionCount = state.missionState.missionCount + stringResource( + id = R.string.alarm_add_edit_selected_mission_with_count, + missionType, + missionCount, + ) + } + else -> stringResource(id = R.string.alarm_add_edit_selected_mission_none) + }, + onClick = { + processAction( + AlarmAddEditContract.Action.ShowBottomSheet( + AlarmAddEditContract.BottomSheetType.MissionSetting, + ), + ) + }, + ) + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(1.dp) + .padding(horizontal = 20.dp) + .background(OrbitTheme.colors.gray_700), + ) + AlarmAddEditSettingItem( + label = stringResource(id = R.string.alarm_add_edit_alarm_snooze), + description = if (state.snoozeState.isSnoozeEnabled) { + val interval = stringResource( + id = R.string.alarm_add_edit_interval_minute, + state.snoozeState.snoozeInterval, + ) + val count = if (state.snoozeState.snoozeCount == -1) { + stringResource(id = R.string.alarm_add_edit_repeat_count_infinite) + } else { + stringResource( + id = R.string.alarm_add_edit_repeat_count_times, + state.snoozeState.snoozeCount, + ) + } + stringResource( + id = R.string.alarm_add_edit_alarm_selected_option, + interval, + count, + ) + } else { + stringResource(id = R.string.alarm_add_edit_alarm_selected_option_none) + }, + onClick = { + processAction( + AlarmAddEditContract.Action.ShowBottomSheet( + AlarmAddEditContract.BottomSheetType.SnoozeSetting, + ), + ) + }, + ) + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(1.dp) + .padding(horizontal = 20.dp) + .background(OrbitTheme.colors.gray_700), + ) + AlarmAddEditSettingItem( + label = stringResource(id = R.string.alarm_add_edit_sound), + description = when { + state.soundState.isSoundEnabled && state.soundState.isVibrationEnabled -> { + "${stringResource(id = R.string.alarm_add_edit_vibration)}, ${ + state.soundState.sounds.getOrElse(state.soundState.soundIndex) { + AlarmSound("", Uri.EMPTY) + }.title + }" + } + + state.soundState.isSoundEnabled -> state.soundState.sounds.getOrElse(state.soundState.soundIndex) { + AlarmSound( + "", + Uri.EMPTY, + ) + }.title + + state.soundState.isVibrationEnabled -> stringResource(id = R.string.alarm_add_edit_vibration) + else -> stringResource(id = R.string.alarm_add_edit_alarm_selected_option_none) + }, + onClick = { + processAction( + AlarmAddEditContract.Action.ShowBottomSheet( + AlarmAddEditContract.BottomSheetType.SoundSetting, + ), + ) + }, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun AlarmAddEditSettingItem( + label: String, + description: String, + onClick: () -> Unit, +) { + val interactionSource = remember { MutableInteractionSource() } + + CompositionLocalProvider( + LocalRippleConfiguration provides RippleConfiguration( + rippleAlpha = RippleAlpha( + pressedAlpha = 1f, + focusedAlpha = 1f, + hoveredAlpha = 1f, + draggedAlpha = 1f, + ), + ), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable( + interactionSource = interactionSource, + indication = ripple( + color = OrbitTheme.colors.gray_700, + ), + ) { + onClick() + } + .padding( + horizontal = 20.dp, + vertical = 14.dp, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + label, + modifier = Modifier.width(80.dp), + style = OrbitTheme.typography.body1SemiBold, + color = OrbitTheme.colors.white, + ) + Text( + description, + modifier = Modifier.weight(1f), + style = OrbitTheme.typography.body2Regular, + color = OrbitTheme.colors.gray_50, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.End, + ) + Icon( + painter = painterResource(id = core.designsystem.R.drawable.ic_arrow_right), + contentDescription = "Arrow", + tint = OrbitTheme.colors.gray_300, + ) + } + } +} + +@Composable +private fun AlarmAddEditSelectDaysSection( + modifier: Modifier = Modifier, + daysSelectionState: AlarmAddEditContract.AlarmDaySelectionState, + holidayState: AlarmAddEditContract.AlarmHolidayState, + processAction: (AlarmAddEditContract.Action) -> Unit, +) { + val configuration = LocalConfiguration.current + val screenWidthDp = configuration.screenWidthDp.dp + + Column( + modifier = modifier + .fillMaxWidth() + .background( + color = OrbitTheme.colors.gray_800, + shape = RoundedCornerShape(12.dp), + ) + .clip( + shape = RoundedCornerShape(12.dp), + ), + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + Column( + modifier = Modifier.padding(horizontal = 20.dp, vertical = 16.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(id = R.string.alarm_add_edit_repeat), + style = OrbitTheme.typography.body1SemiBold, + color = OrbitTheme.colors.white, + ) + + Spacer(modifier = Modifier.weight(1f)) + + AlarmCheckItem( + label = stringResource(id = R.string.alarm_add_edit_weekdays), + isPressed = daysSelectionState.isWeekdaysChecked, + onClick = { + processAction(AlarmAddEditContract.Action.ToggleWeekdaysSelection) + }, + ) + Spacer(modifier = Modifier.width(2.dp)) + AlarmCheckItem( + label = stringResource(id = R.string.alarm_add_edit_weekends), + isPressed = daysSelectionState.isWeekendsChecked, + onClick = { + processAction(AlarmAddEditContract.Action.ToggleWeekendsSelection) + }, + ) + } + + Spacer(modifier = Modifier.height(12.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + daysSelectionState.days.forEach { day -> + AlarmDayButton( + modifier = Modifier.size( + if (screenWidthDp > 360.dp) 36.dp else 34.dp, + ), + label = stringResource(id = day.getLabelStringRes()), + isPressed = daysSelectionState.selectedDays.contains(day), + onClick = { + processAction(AlarmAddEditContract.Action.ToggleSpecificDaySelection(day)) + }, + ) + } + } + + Spacer(modifier = Modifier.height(20.dp)) + + AlarmAddEditDisableHolidaySwitch( + state = holidayState, + processAction = processAction, + ) + } + } +} + +@Composable +private fun AlarmAddEditDisableHolidaySwitch( + modifier: Modifier = Modifier, + state: AlarmAddEditContract.AlarmHolidayState, + processAction: (AlarmAddEditContract.Action) -> Unit, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(id = core.designsystem.R.drawable.ic_holiday), + contentDescription = "Holiday", + tint = OrbitTheme.colors.gray_400, + modifier = Modifier.padding(end = 4.dp), + ) + Text( + text = stringResource(id = R.string.alarm_add_edit_disable_holiday), + style = OrbitTheme.typography.label1Medium, + color = OrbitTheme.colors.gray_400, + ) + + Spacer(modifier = Modifier.weight(1f)) + + OrbitSwitch( + isChecked = state.isDisableHolidayChecked, + isEnabled = state.isDisableHolidayEnabled, + onClick = { + processAction(AlarmAddEditContract.Action.ToggleHolidaySkipOption) + }, + ) + } +} + +@Preview +@Composable +fun AlarmAddEditSettingsSectionPreview() { + AlarmAddEditSettingsSection( + state = AlarmAddEditContract.State( + timeState = AlarmAddEditContract.AlarmTimeState( + currentTime = LocalTime.of(19, 30), + ), + daySelectionState = AlarmAddEditContract.AlarmDaySelectionState( + isWeekdaysChecked = true, + isWeekendsChecked = false, + selectedDays = setOf(AlarmDay.MON, AlarmDay.TUE), + days = AlarmDay.entries.toSet(), + ), + holidayState = AlarmAddEditContract.AlarmHolidayState( + isDisableHolidayChecked = false, + ), + ), + processAction = { }, + ) +} + +@Preview +@Composable +fun AlarmAddEditSettingItemPreview() { + AlarmAddEditSettingItem( + label = "์•Œ๋žŒ ๋ฏธ๋ฃจ๊ธฐ", + description = "5๋ถ„, ๋ฌดํ•œ", + onClick = { }, + ) +} + +@Preview +@Composable +fun AlarmAddEditScreenPreview() { + OrbitTheme { + Box( + modifier = Modifier.background( + color = OrbitTheme.colors.gray_900, + ), + ) { + AlarmAddEditScreen( + state = AlarmAddEditContract.State( + initialLoading = false, + timeState = AlarmAddEditContract.AlarmTimeState( + currentTime = LocalTime.of(19, 30), + ), + daySelectionState = AlarmAddEditContract.AlarmDaySelectionState( + isWeekdaysChecked = true, + isWeekendsChecked = false, + selectedDays = setOf(AlarmDay.MON, AlarmDay.TUE), + days = AlarmDay.entries.toSet(), + ), + holidayState = AlarmAddEditContract.AlarmHolidayState( + isDisableHolidayChecked = false, + ), + ), + bottomSheetState = rememberOrbitBottomSheetState(), + processAction = { }, + ) + } + } +} diff --git a/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditViewModel.kt b/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditViewModel.kt new file mode 100644 index 00000000..b0f8dfb5 --- /dev/null +++ b/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditViewModel.kt @@ -0,0 +1,553 @@ +package com.yapp.home.alarm.addedit + +import android.util.Log +import androidx.compose.ui.unit.dp +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import com.yapp.analytics.AnalyticsEvent +import com.yapp.analytics.AnalyticsHelper +import com.yapp.common.util.ResourceProvider +import com.yapp.domain.model.Alarm +import com.yapp.domain.model.AlarmDay +import com.yapp.domain.model.AlarmSound +import com.yapp.domain.model.MissionType +import com.yapp.domain.model.copyFrom +import com.yapp.domain.model.toAlarmDayNames +import com.yapp.domain.model.toAlarmDays +import com.yapp.domain.model.toRepeatDays +import com.yapp.domain.usecase.AlarmUseCase +import com.yapp.home.util.AlarmDateTimeFormatter +import com.yapp.media.haptic.HapticFeedbackManager +import com.yapp.media.haptic.HapticType +import dagger.hilt.android.lifecycle.HiltViewModel +import feature.home.R +import kotlinx.coroutines.flow.first +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.syntax.simple.intent +import org.orbitmvi.orbit.syntax.simple.postSideEffect +import org.orbitmvi.orbit.syntax.simple.reduce +import org.orbitmvi.orbit.viewmodel.container +import java.time.LocalDateTime +import java.time.LocalTime +import javax.inject.Inject + +@HiltViewModel +class AlarmAddEditViewModel @Inject constructor( + private val analyticsHelper: AnalyticsHelper, + private val alarmUseCase: AlarmUseCase, + private val resourceProvider: ResourceProvider, + private val alarmDateTimeFormatter: AlarmDateTimeFormatter, + private val hapticFeedbackManager: HapticFeedbackManager, + savedStateHandle: SavedStateHandle, +) : ViewModel(), ContainerHost { + + override val container: Container = container(initialState = AlarmAddEditContract.State()) { + intent { + reduce { state.copy(mode = if (alarmId == -1L) AlarmAddEditContract.EditMode.ADD else AlarmAddEditContract.EditMode.EDIT) } + initializeAlarmScreen() + } + } + + private val alarmId: Long = savedStateHandle.get("alarmId") ?: -1 + + private fun initializeAlarmScreen() = intent { + alarmUseCase.getAlarmSounds().onSuccess { sounds -> + if (alarmId == -1L) { + setupNewAlarmScreen(sounds) + } else { + loadExistingAlarm(sounds) + } + }.onFailure { + Log.e("AlarmAddEditViewModel", "Failed to load alarm sounds", it) + } + } + + private fun setupNewAlarmScreen(sounds: List) = intent { + val defaultSoundIndex = sounds.indexOfFirst { it.title == "Homecoming" }.takeIf { it >= 0 } ?: 0 + val defaultSound = sounds[defaultSoundIndex] + + alarmUseCase.initializeSoundPlayer(defaultSound.uri) + + val now = LocalTime.now() + + reduce { + state.copy( + initialLoading = false, + timeState = state.timeState.copy( + initialTime = now, + currentTime = now, + alarmMessage = getAlarmMessage(now, emptySet()), + ), + soundState = state.soundState.copy(sounds = sounds, soundIndex = defaultSoundIndex), + ) + } + } + + private fun loadExistingAlarm(sounds: List) = intent { + alarmUseCase.getAlarm(alarmId).onSuccess { alarm -> + val repeatDays = alarm.repeatDays.toAlarmDays() + val selectedSoundIndex = sounds.indexOfFirst { it.uri.toString() == alarm.soundUri } + val selectedSound = sounds.getOrNull(selectedSoundIndex) ?: sounds.first() + + alarmUseCase.initializeSoundPlayer(selectedSound.uri) + + reduce { + state.copy( + initialLoading = false, + timeState = state.timeState.copy( + initialTime = LocalTime.of(alarm.hour, alarm.minute), + currentTime = LocalTime.of(alarm.hour, alarm.minute), + alarmMessage = getAlarmMessage( + LocalTime.of(alarm.hour, alarm.minute), + repeatDays, + ), + ), + daySelectionState = setupDaySelectionState(repeatDays, state), + holidayState = state.holidayState.copy( + isDisableHolidayEnabled = repeatDays.isNotEmpty(), + isDisableHolidayChecked = alarm.isHolidayAlarmOff, + ), + missionState = setUpMissionState(alarm, state), + snoozeState = setupSnoozeState(alarm, state), + soundState = state.soundState.copy( + isVibrationEnabled = alarm.isVibrationEnabled, + isSoundEnabled = alarm.isSoundEnabled, + soundVolume = alarm.soundVolume, + sounds = sounds, + soundIndex = selectedSoundIndex, + ), + ) + } + } + } + + private fun setupDaySelectionState( + repeatDays: Set, + currentState: AlarmAddEditContract.State, + ): AlarmAddEditContract.AlarmDaySelectionState { + return currentState.daySelectionState.copy( + selectedDays = repeatDays, + isWeekdaysChecked = repeatDays.containsAll(setOf(AlarmDay.MON, AlarmDay.TUE, AlarmDay.WED, AlarmDay.THU, AlarmDay.FRI)), + isWeekendsChecked = repeatDays.containsAll(setOf(AlarmDay.SAT, AlarmDay.SUN)), + ) + } + + private fun setUpMissionState( + alarm: Alarm, + currentState: AlarmAddEditContract.State, + ): AlarmAddEditContract.AlarmMissionState { + return currentState.missionState.copy( + missionType = alarm.missionType, + missionCount = alarm.missionCount, + ) + } + + private fun setupSnoozeState( + alarm: Alarm, + currentState: AlarmAddEditContract.State, + ): AlarmAddEditContract.AlarmSnoozeState { + return currentState.snoozeState.copy( + isSnoozeEnabled = alarm.isSnoozeEnabled, + snoozeInterval = alarm.snoozeInterval, + snoozeCount = alarm.snoozeCount, + ) + } + + override fun onCleared() { + super.onCleared() + alarmUseCase.releaseSoundPlayer() + } + + fun processAction(action: AlarmAddEditContract.Action) { + when (action) { + is AlarmAddEditContract.Action.CheckUnsavedChangesBeforeExit -> checkUnsavedChangesBeforeExit() + is AlarmAddEditContract.Action.NavigateBack -> navigateBack() + is AlarmAddEditContract.Action.SaveAlarm -> saveAlarm() + is AlarmAddEditContract.Action.ShowDeleteDialog -> showDeleteDialog() + is AlarmAddEditContract.Action.HideDeleteDialog -> hideDeleteDialog() + is AlarmAddEditContract.Action.ShowUnsavedChangesDialog -> showUnsavedChangesDialog() + is AlarmAddEditContract.Action.HideUnsavedChangesDialog -> hideUnsavedChangesDialog() + is AlarmAddEditContract.Action.DeleteAlarm -> deleteAlarm() + is AlarmAddEditContract.Action.SetAlarmTime -> setAlarmTime(action.newTime) + is AlarmAddEditContract.Action.ToggleWeekdaysSelection -> toggleWeekdaysSelection() + is AlarmAddEditContract.Action.ToggleWeekendsSelection -> toggleWeekendsSelection() + is AlarmAddEditContract.Action.ToggleSpecificDaySelection -> toggleSpecificDaySelection(action.day) + is AlarmAddEditContract.Action.ToggleHolidaySkipOption -> toggleHolidaySkipOption() + is AlarmAddEditContract.Action.SaveMissionSetting -> saveMissionSetting(action.type, action.count) + is AlarmAddEditContract.Action.SaveSnoozeSetting -> saveSnoozeSetting( + action.enabled, + action.interval, + action.count, + ) + is AlarmAddEditContract.Action.SaveSoundSetting -> saveSoundSetting( + action.vibrationEnabled, + action.soundEnabled, + action.soundVolume, + action.soundIndex, + ) + is AlarmAddEditContract.Action.ToggleVibrationEnabled -> toggleVibrationEnabled(action.enabled) + is AlarmAddEditContract.Action.ToggleSoundEnabled -> toggleSoundEnabled(action.enabled) + is AlarmAddEditContract.Action.SetSoundVolume -> setSoundVolume(action.volume) + is AlarmAddEditContract.Action.SetSoundIndex -> setSoundIndex(action.index) + is AlarmAddEditContract.Action.NavigateToMissionPreview -> navigateToMissionPreview(action.missionType, action.missionCount) + is AlarmAddEditContract.Action.ShowBottomSheet -> showBottomSheet(action.sheetType) + is AlarmAddEditContract.Action.HideBottomSheet -> hideBottomSheet() + } + } + + private fun checkUnsavedChangesBeforeExit() = intent { + if (state.mode == AlarmAddEditContract.EditMode.ADD) { + navigateBack() + } else { + val updatedAlarm = state.toAlarm() + alarmUseCase.getAlarm(alarmId).onSuccess { existingAlarm -> + if (updatedAlarm.copy(id = alarmId) != existingAlarm) { + showUnsavedChangesDialog() + } else { + postSideEffect(AlarmAddEditContract.SideEffect.NavigateBack) + } + } + } + } + + private fun navigateBack() = intent { + postSideEffect(AlarmAddEditContract.SideEffect.NavigateBack) + } + + private fun navigateToMissionPreview( + missionType: MissionType, + missionCount: Int, + ) = intent { + val newTimeState = state.timeState.copy( + initialTime = state.timeState.currentTime, + ) + reduce { + state.copy( + timeState = newTimeState, + ) + } + postSideEffect(AlarmAddEditContract.SideEffect.NavigateToMissionPreview(missionType, missionCount)) + } + + private fun saveAlarm() = intent { + val newAlarm = state.toAlarm() + + when (state.mode) { + AlarmAddEditContract.EditMode.EDIT -> updateExistingAlarm(newAlarm) + AlarmAddEditContract.EditMode.ADD -> checkAndCreateAlarm(newAlarm) + } + } + + private fun updateExistingAlarm(alarm: Alarm) = intent { + val updatedAlarm = alarm.copy(id = alarmId) + + alarmUseCase.getAlarm(alarmId).onSuccess { oldAlarm -> + alarmUseCase.unScheduleAlarm(oldAlarm) + } + + alarmUseCase.updateAlarm(updatedAlarm) + .onSuccess { + alarmUseCase.scheduleAlarm(updatedAlarm) + postSideEffect(AlarmAddEditContract.SideEffect.UpdateAlarm(it.id)) + } + .onFailure { + Log.e("AlarmAddEditViewModel", "Failed to update alarm", it) + } + } + + private suspend fun checkAndCreateAlarm(newAlarm: Alarm) { + val timeMatchedAlarms = alarmUseCase.getAlarmsByTime(newAlarm.hour, newAlarm.minute) + .first() + + when { + timeMatchedAlarms.any { it.copy(id = 0) == newAlarm.copy(id = 0) } -> { + showAlarmAlreadySetWarning() + } + + timeMatchedAlarms.isNotEmpty() -> { + val existingAlarm = timeMatchedAlarms.first() + val updatedAlarm = existingAlarm.copyFrom(newAlarm).copy(id = existingAlarm.id) + updateExistingAlarm(updatedAlarm) + } + + else -> { + createNewAlarm(newAlarm) + } + } + } + + private fun showAlarmAlreadySetWarning() = intent { + postSideEffect( + AlarmAddEditContract.SideEffect.ShowSnackBar( + message = resourceProvider.getString(R.string.alarm_already_set), + iconRes = resourceProvider.getDrawable(core.designsystem.R.drawable.ic_alert), + bottomPadding = 78.dp, + onDismiss = { }, + onAction = { }, + ), + ) + } + + private fun createNewAlarm(alarm: Alarm) = intent { + alarmUseCase.insertAlarm(alarm) + .onSuccess { + analyticsHelper.logEvent( + AnalyticsEvent( + type = "alarm_create", + properties = mapOf( + AnalyticsEvent.AlarmPropertiesKeys.ALARM_ID to "${it.id}", + AnalyticsEvent.AlarmPropertiesKeys.REPEAT_DAYS to it.repeatDays.toAlarmDayNames(), + AnalyticsEvent.AlarmPropertiesKeys.SNOOZE_OPTION to listOf(it.snoozeInterval, it.snoozeCount), + ), + ), + ) + alarmUseCase.scheduleAlarm(it) + postSideEffect(AlarmAddEditContract.SideEffect.SaveAlarm(it.id)) + } + .onFailure { + Log.e("AlarmAddEditViewModel", "Failed to insert alarm", it) + } + } + + private fun setAlarmTime(newTime: LocalTime) = intent { + val newTimeState = state.timeState.copy( + currentTime = newTime, + alarmMessage = getAlarmMessage(newTime, state.daySelectionState.selectedDays), + ) + + hapticFeedbackManager.performHapticFeedback(HapticType.LIGHT_TICK) + + reduce { + state.copy(timeState = newTimeState) + } + } + + private fun showDeleteDialog() = intent { + reduce { state.copy(isDeleteDialogVisible = true) } + } + + private fun hideDeleteDialog() = intent { + reduce { state.copy(isDeleteDialogVisible = false) } + } + + private fun showUnsavedChangesDialog() = intent { + reduce { state.copy(isUnsavedChangesDialogVisible = true) } + } + + private fun hideUnsavedChangesDialog() = intent { + reduce { state.copy(isUnsavedChangesDialogVisible = false) } + } + + private fun deleteAlarm() = intent { + postSideEffect(AlarmAddEditContract.SideEffect.DeleteAlarm(alarmId)) + } + + private fun toggleWeekdaysSelection() = intent { + val weekdays = setOf(AlarmDay.MON, AlarmDay.TUE, AlarmDay.WED, AlarmDay.THU, AlarmDay.FRI) + val isChecked = !state.daySelectionState.isWeekdaysChecked + val updatedDays = if (isChecked) { + state.daySelectionState.selectedDays + weekdays + } else { + state.daySelectionState.selectedDays - weekdays + } + val newDayState = state.daySelectionState.copy( + isWeekdaysChecked = isChecked, + selectedDays = updatedDays, + ) + reduce { + state.copy( + timeState = state.timeState.copy( + alarmMessage = getAlarmMessage(state.timeState.currentTime, newDayState.selectedDays), + ), + daySelectionState = newDayState, + holidayState = state.holidayState.copy( + isDisableHolidayEnabled = newDayState.selectedDays.isNotEmpty(), + isDisableHolidayChecked = if (newDayState.selectedDays.isEmpty()) false else state.holidayState.isDisableHolidayChecked, + ), + ) + } + } + + private fun toggleWeekendsSelection() = intent { + val weekends = setOf(AlarmDay.SAT, AlarmDay.SUN) + val isChecked = !state.daySelectionState.isWeekendsChecked + val updatedDays = if (isChecked) { + state.daySelectionState.selectedDays + weekends + } else { + state.daySelectionState.selectedDays - weekends + } + val newDayState = state.daySelectionState.copy( + isWeekendsChecked = isChecked, + selectedDays = updatedDays, + ) + reduce { + state.copy( + timeState = state.timeState.copy( + alarmMessage = getAlarmMessage(state.timeState.currentTime, newDayState.selectedDays), + ), + daySelectionState = newDayState, + holidayState = state.holidayState.copy( + isDisableHolidayEnabled = newDayState.selectedDays.isNotEmpty(), + isDisableHolidayChecked = if (newDayState.selectedDays.isEmpty()) false else state.holidayState.isDisableHolidayChecked, + ), + ) + } + } + + private fun toggleSpecificDaySelection(day: AlarmDay) = intent { + val updatedDays = if (day in state.daySelectionState.selectedDays) { + state.daySelectionState.selectedDays - day + } else { + state.daySelectionState.selectedDays + day + } + val weekdays = setOf(AlarmDay.MON, AlarmDay.TUE, AlarmDay.WED, AlarmDay.THU, AlarmDay.FRI) + val weekends = setOf(AlarmDay.SAT, AlarmDay.SUN) + + val newDayState = state.daySelectionState.copy( + selectedDays = updatedDays, + isWeekdaysChecked = updatedDays.containsAll(weekdays), + isWeekendsChecked = updatedDays.containsAll(weekends), + ) + reduce { + state.copy( + timeState = state.timeState.copy( + alarmMessage = getAlarmMessage(state.timeState.currentTime, newDayState.selectedDays), + ), + daySelectionState = newDayState, + holidayState = state.holidayState.copy( + isDisableHolidayEnabled = newDayState.selectedDays.isNotEmpty(), + isDisableHolidayChecked = if (newDayState.selectedDays.isEmpty()) false else state.holidayState.isDisableHolidayChecked, + ), + ) + } + } + + private fun toggleHolidaySkipOption() = intent { + val newHolidayState = state.holidayState.copy( + isDisableHolidayChecked = !state.holidayState.isDisableHolidayChecked, + ) + + reduce { + state.copy(holidayState = newHolidayState) + } + + if (newHolidayState.isDisableHolidayChecked) { + postSideEffect( + AlarmAddEditContract.SideEffect.ShowSnackBar( + message = resourceProvider.getString(R.string.alarm_disabled_warning), + label = resourceProvider.getString(R.string.alarm_delete_dialog_btn_cancel), + iconRes = resourceProvider.getDrawable(core.designsystem.R.drawable.ic_check_green), + bottomPadding = 78.dp, + onDismiss = { }, + onAction = { + intent { + reduce { + state.copy( + holidayState = state.holidayState.copy( + isDisableHolidayChecked = false, + ), + ) + } + } + }, + ), + ) + } + } + + private fun saveMissionSetting(type: MissionType, count: Int) = intent { + val newMissionState = state.missionState.copy( + missionType = type, + missionCount = count, + ) + reduce { + state.copy(missionState = newMissionState) + } + } + + private fun saveSnoozeSetting( + isSnoozeEnabled: Boolean, + snoozeInterval: Int, + snoozeCount: Int, + ) = intent { + val newSnoozeState = state.snoozeState.copy( + isSnoozeEnabled = isSnoozeEnabled, + snoozeInterval = snoozeInterval, + snoozeCount = snoozeCount, + ) + + reduce { + state.copy(snoozeState = newSnoozeState) + } + } + + private fun saveSoundSetting( + vibrationEnabled: Boolean, + soundEnabled: Boolean, + soundVolume: Int, + soundIndex: Int, + ) = intent { + val newSoundState = state.soundState.copy( + isVibrationEnabled = vibrationEnabled, + isSoundEnabled = soundEnabled, + soundVolume = soundVolume, + soundIndex = soundIndex, + ) + + reduce { + state.copy(soundState = newSoundState) + } + } + + private fun toggleVibrationEnabled(enabled: Boolean) = intent { + if (enabled) { + hapticFeedbackManager.performHapticFeedback(HapticType.SUCCESS) + } + } + + private fun toggleSoundEnabled(enabled: Boolean) = intent { + if (!enabled) { + alarmUseCase.stopAlarmSound() + } + } + + private fun setSoundVolume(volume: Int) = intent { + alarmUseCase.updateAlarmVolume(volume) + } + + private fun setSoundIndex(index: Int) = intent { + val selectedSound = state.soundState.sounds[index] + alarmUseCase.initializeSoundPlayer(selectedSound.uri) + alarmUseCase.playAlarmSound(state.soundState.soundVolume) + } + + private fun showBottomSheet(sheetType: AlarmAddEditContract.BottomSheetType) = intent { + postSideEffect(AlarmAddEditContract.SideEffect.ShowBottomSheet(sheetType)) + } + + private fun hideBottomSheet() = intent { + postSideEffect(AlarmAddEditContract.SideEffect.HideBottomSheet) + } + + private fun getAlarmMessage(currentTime: LocalTime, selectedDays: Set): String { + val repeatDays = selectedDays.toRepeatDays() + val nextOccurrence = alarmDateTimeFormatter.calculateNextOccurrence( + hour = currentTime.hour, + minute = currentTime.minute, + repeatDays = repeatDays, + now = LocalDateTime.now(), + ) + + return alarmDateTimeFormatter.formatTimeDifference( + baseTime = LocalDateTime.now(), + futureTime = nextOccurrence, + formats = AlarmDateTimeFormatter.TimeDifferenceFormats( + daysHoursMinutesFormat = resourceProvider.getString(R.string.alarm_remaining_time_days_hours), + hoursMinutesFormat = resourceProvider.getString(R.string.alarm_remaining_time_hours_minutes), + minutesFormat = resourceProvider.getString(R.string.alarm_remaining_time_minutes_only), + soonFormat = resourceProvider.getString(R.string.alarm_remaining_time_soon), + ), + ) + } +} diff --git a/feature/home/src/main/java/com/yapp/alarm/component/AlarmCheckItem.kt b/feature/home/src/main/java/com/yapp/home/alarm/component/AlarmCheckItem.kt similarity index 91% rename from feature/home/src/main/java/com/yapp/alarm/component/AlarmCheckItem.kt rename to feature/home/src/main/java/com/yapp/home/alarm/component/AlarmCheckItem.kt index 900d7e96..905f9605 100644 --- a/feature/home/src/main/java/com/yapp/alarm/component/AlarmCheckItem.kt +++ b/feature/home/src/main/java/com/yapp/home/alarm/component/AlarmCheckItem.kt @@ -1,4 +1,4 @@ -package com.yapp.alarm.component +package com.yapp.home.alarm.component import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -13,6 +13,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.yapp.designsystem.theme.OrbitTheme +import core.designsystem.R @Composable internal fun AlarmCheckItem( @@ -30,7 +31,7 @@ internal fun AlarmCheckItem( verticalAlignment = Alignment.CenterVertically, ) { Icon( - painter = painterResource(id = core.designsystem.R.drawable.ic_check), + painter = painterResource(id = R.drawable.ic_check), contentDescription = "Check", tint = if (isPressed) OrbitTheme.colors.main else OrbitTheme.colors.gray_400, ) diff --git a/feature/home/src/main/java/com/yapp/alarm/component/AlarmDayButton.kt b/feature/home/src/main/java/com/yapp/home/alarm/component/AlarmDayButton.kt similarity index 98% rename from feature/home/src/main/java/com/yapp/alarm/component/AlarmDayButton.kt rename to feature/home/src/main/java/com/yapp/home/alarm/component/AlarmDayButton.kt index 4172b075..9e76a68c 100644 --- a/feature/home/src/main/java/com/yapp/alarm/component/AlarmDayButton.kt +++ b/feature/home/src/main/java/com/yapp/home/alarm/component/AlarmDayButton.kt @@ -1,4 +1,4 @@ -package com.yapp.alarm.component +package com.yapp.home.alarm.component import androidx.compose.foundation.background import androidx.compose.foundation.border diff --git a/feature/home/src/main/java/com/yapp/alarm/component/AlarmListItem.kt b/feature/home/src/main/java/com/yapp/home/alarm/component/AlarmListItem.kt similarity index 97% rename from feature/home/src/main/java/com/yapp/alarm/component/AlarmListItem.kt rename to feature/home/src/main/java/com/yapp/home/alarm/component/AlarmListItem.kt index a590bd8e..6e978386 100644 --- a/feature/home/src/main/java/com/yapp/alarm/component/AlarmListItem.kt +++ b/feature/home/src/main/java/com/yapp/home/alarm/component/AlarmListItem.kt @@ -1,4 +1,4 @@ -package com.yapp.alarm.component +package com.yapp.home.alarm.component import android.os.Handler import android.os.Looper @@ -76,7 +76,6 @@ internal fun AlarmListItem( onLongPress: (Long, Float, Float) -> Unit, onToggleSelect: (Long) -> Unit, onSwipe: (Long) -> Unit, - isAm: Boolean, hour: Int, minute: Int, isActive: Boolean, @@ -197,7 +196,6 @@ internal fun AlarmListItem( repeatDays = repeatDays, isActive = isActive, isHolidayAlarmOff = isHolidayAlarmOff, - isAm = isAm, hour = hour, minute = minute, ) @@ -220,7 +218,6 @@ private fun AlarmListItemContent( repeatDays: Int, isActive: Boolean, isHolidayAlarmOff: Boolean, - isAm: Boolean, hour: Int, minute: Int, ) { @@ -230,6 +227,13 @@ private fun AlarmListItemContent( OrbitTheme.colors.gray_500 to OrbitTheme.colors.gray_500 } + val isAm = hour < 12 + val displayHour = when { + hour == 0 -> 12 + hour > 12 -> hour - 12 + else -> hour + } + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { Row(verticalAlignment = Alignment.CenterVertically) { Text( @@ -260,7 +264,7 @@ private fun AlarmListItemContent( Spacer(modifier = Modifier.width(6.dp)) Text( - text = "$hour", + text = "$displayHour", style = OrbitTheme.typography.title2Medium, color = if (isActive) OrbitTheme.colors.white else OrbitTheme.colors.gray_500, ) @@ -292,7 +296,6 @@ private fun Int.toRepeatDaysString(isAm: Boolean, hour: Int, minute: Int): Strin days.size == 7 -> "๋งค์ผ" days.isNotEmpty() -> "๋งค์ฃผ " + days.joinToString(", ") { it.toKoreanString() } else -> getNextAlarmDateWithTime( - isAm = isAm, hour = hour, minute = minute, ) @@ -311,16 +314,10 @@ private fun AlarmDay.toKoreanString(): String { } } -private fun getNextAlarmDateWithTime(isAm: Boolean, hour: Int, minute: Int): String { +private fun getNextAlarmDateWithTime(hour: Int, minute: Int): String { val now = LocalDateTime.now() - val alarmHour = if (isAm) { - if (hour == 12) 0 else hour - } else { - if (hour == 12) 12 else hour + 12 - } - - val alarmTime = LocalTime.of(alarmHour, minute) + val alarmTime = LocalTime.of(hour, minute) val todayAlarm = LocalDateTime.of(now.toLocalDate(), alarmTime) // ์˜ค๋Š˜ ์‹œ๊ฐ„ ์ด๋ฏธ ์ง€๋‚ฌ์œผ๋ฉด ๋‚ด์ผ๋กœ ์„ค์ • @@ -408,7 +405,6 @@ private fun AlarmListItemPreview() { selectable = true, swipeable = false, selected = selected, - isAm = true, hour = 6, minute = 0, isActive = isActive, @@ -436,7 +432,6 @@ private fun AlarmListItemPreview() { selectable = false, selected = false, swipeable = true, - isAm = true, hour = 6, minute = 0, isActive = isActive, @@ -467,7 +462,6 @@ private fun AlarmListItemMenuPreview() { selectable = false, swipeable = false, selected = false, - isAm = true, hour = 6, minute = 0, isActive = true, diff --git a/feature/home/src/main/java/com/yapp/home/alarm/component/SelectorItems.kt b/feature/home/src/main/java/com/yapp/home/alarm/component/SelectorItems.kt new file mode 100644 index 00000000..1b4630f9 --- /dev/null +++ b/feature/home/src/main/java/com/yapp/home/alarm/component/SelectorItems.kt @@ -0,0 +1,74 @@ +package com.yapp.home.alarm.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.yapp.designsystem.theme.OrbitTheme +import com.yapp.ui.component.radiobutton.OrbitRadioButton + +@Composable +internal fun SelectorItems( + items: List, + selectedIndex: Int, + enabled: Boolean, + onItemSelected: (Int) -> Unit, +) { + Box { + Column { + Spacer(modifier = Modifier.height(7.dp)) + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(6.dp) + .padding(horizontal = 6.dp) + .background( + if (enabled) { + OrbitTheme.colors.gray_600 + } else { + OrbitTheme.colors.gray_700 + }, + ), + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 6.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + items.forEachIndexed { index, item -> + Column(horizontalAlignment = getAlignment(index, items.size)) { + OrbitRadioButton( + selected = index == selectedIndex, + enabled = enabled, + onClick = { if (enabled) onItemSelected(index) }, + ) + Spacer(modifier = Modifier.height(12.dp)) + Text( + text = item, + style = OrbitTheme.typography.body1Medium, + color = OrbitTheme.colors.gray_50, + ) + } + } + } + } +} + +private fun getAlignment(index: Int, size: Int): Alignment.Horizontal = + when (index) { + 0 -> Alignment.Start + size - 1 -> Alignment.End + else -> Alignment.CenterHorizontally + } diff --git a/feature/home/src/main/java/com/yapp/home/alarm/component/bottomsheet/AlarmMissionBottomSheet.kt b/feature/home/src/main/java/com/yapp/home/alarm/component/bottomsheet/AlarmMissionBottomSheet.kt new file mode 100644 index 00000000..99656263 --- /dev/null +++ b/feature/home/src/main/java/com/yapp/home/alarm/component/bottomsheet/AlarmMissionBottomSheet.kt @@ -0,0 +1,680 @@ +package com.yapp.home.alarm.component.bottomsheet + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +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.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.listSaver +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.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.yapp.designsystem.theme.OrbitTheme +import com.yapp.domain.model.MissionType +import com.yapp.home.alarm.addedit.AlarmAddEditContract +import com.yapp.home.alarm.component.SelectorItems +import com.yapp.ui.component.button.OrbitButton +import com.yapp.ui.component.lottie.LottieAnimation +import com.yapp.ui.extensions.customClickable +import core.designsystem.R + +enum class AlarmMissionSelectBottomSheetType { + MISSION_SETTING, + MISSION_SELECT, + MISSION_DETAIL, +} + +private val countOptions = listOf(5, 10, 15, 20, 30) + +private fun MissionType.displayData(): Pair = when (this) { + MissionType.SHAKE -> Pair(R.drawable.ic_mission_shake, feature.home.R.string.alarm_add_edit_selected_mission_shake) + MissionType.TAP -> Pair(R.drawable.ic_mission_tap, feature.home.R.string.alarm_add_edit_selected_mission_tap) + else -> throw IllegalStateException("Invalid mission type") +} + +val StepStackSaver: Saver>, out Any> = + listSaver( + save = { state -> state.value.map { it.name } }, + restore = { restored -> mutableStateOf(restored.map { AlarmMissionSelectBottomSheetType.valueOf(it) }) }, + ) + +@Composable +internal fun AlarmMissionBottomSheet( + missionState: AlarmAddEditContract.AlarmMissionState, + onDismiss: () -> Unit, + onSaveMission: (MissionType, Int) -> Unit, + onPreviewMission: (MissionType, Int) -> Unit, +) { + val initialMissionType = missionState.missionType + val initialMissionCount = missionState.missionCount + + var stepStack by rememberSaveable(saver = StepStackSaver) { + mutableStateOf(listOf(AlarmMissionSelectBottomSheetType.MISSION_SETTING)) + } + + var currentSelectedMissionType by rememberSaveable { mutableStateOf(initialMissionType) } + var currentSelectedMissionCount by rememberSaveable { mutableIntStateOf(initialMissionCount) } + + fun push(step: AlarmMissionSelectBottomSheetType) { + stepStack = stepStack + step + } + + fun pop() { + if (stepStack.size > 1) { + stepStack = stepStack.dropLast(1) + } + } + + val currentStep = stepStack.last() + + when (currentStep) { + AlarmMissionSelectBottomSheetType.MISSION_SETTING -> { + if (currentSelectedMissionType == MissionType.NONE) { + MissionAddContent { + push(AlarmMissionSelectBottomSheetType.MISSION_SELECT) + } + } else { + MissionSettingContent( + missionType = currentSelectedMissionType, + missionCount = currentSelectedMissionCount, + onDetail = { push(AlarmMissionSelectBottomSheetType.MISSION_DETAIL) }, + onDelete = { + currentSelectedMissionType = MissionType.NONE + onSaveMission(currentSelectedMissionType, currentSelectedMissionCount) + }, + onChange = { push(AlarmMissionSelectBottomSheetType.MISSION_SELECT) }, + onDone = { + onSaveMission(currentSelectedMissionType, currentSelectedMissionCount) + onDismiss() + }, + ) + } + } + + AlarmMissionSelectBottomSheetType.MISSION_SELECT -> { + MissionSelectContent( + onBack = { pop() }, + onClose = { onDismiss() }, + initialMission = currentSelectedMissionType, + onSelect = { selected -> + currentSelectedMissionType = selected + push(AlarmMissionSelectBottomSheetType.MISSION_DETAIL) + }, + ) + } + + AlarmMissionSelectBottomSheetType.MISSION_DETAIL -> { + MissionDetailContent( + missionType = currentSelectedMissionType, + selectedMissionCount = currentSelectedMissionCount, + onCountChange = { currentSelectedMissionCount = it }, + onBack = { pop() }, + onClose = { onDismiss() }, + onSave = { + onSaveMission(currentSelectedMissionType, currentSelectedMissionCount) + onDismiss() + }, + onPreview = { + onPreviewMission(currentSelectedMissionType, currentSelectedMissionCount) + }, + ) + } + } +} + +@Composable +private fun MissionAddContent( + onNext: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .height(600.dp) + .padding(horizontal = 24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(26.dp)) + + Text( + modifier = Modifier.align(Alignment.Start), + text = stringResource(id = feature.home.R.string.mission_bottom_sheet_title), + style = OrbitTheme.typography.heading2SemiBold, + color = OrbitTheme.colors.white, + ) + + Box( + modifier = Modifier.weight(1f), + contentAlignment = Alignment.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResource(id = feature.home.R.string.mission_add_content_empty_title), + style = OrbitTheme.typography.body1Bold, + color = OrbitTheme.colors.white, + ) + + Spacer(modifier = Modifier.height(6.dp)) + + Text( + text = stringResource(id = feature.home.R.string.mission_add_content_empty_description), + style = OrbitTheme.typography.label2Regular, + color = OrbitTheme.colors.white.copy(alpha = 0.8f), + ) + + Spacer(modifier = Modifier.height(32.dp)) + + AddMissionButton { onNext() } + } + } + } +} + +@Composable +private fun AddMissionButton( + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + Button( + onClick = onClick, + modifier = modifier, + shape = CircleShape, + colors = ButtonDefaults.buttonColors( + containerColor = OrbitTheme.colors.white, + contentColor = OrbitTheme.colors.gray_900, + ), + contentPadding = PaddingValues( + horizontal = 24.dp, + vertical = 12.dp, + ), + ) { + Icon( + painter = painterResource(R.drawable.ic_plus), + tint = Color.Unspecified, + contentDescription = "Add Mission", + ) + + Spacer(modifier = Modifier.width(4.dp)) + + Text( + text = stringResource(id = feature.home.R.string.mission_add_content_btn_add), + style = OrbitTheme.typography.body1SemiBold, + ) + } +} + +@Composable +private fun MissionSettingContent( + missionType: MissionType, + missionCount: Int, + onDetail: () -> Unit, + onDelete: () -> Unit, + onChange: () -> Unit, + onDone: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(start = 12.dp, end = 12.dp, bottom = 12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(26.dp)) + + Text( + modifier = Modifier + .padding(start = 12.dp) + .align(Alignment.Start), + text = stringResource(id = feature.home.R.string.mission_bottom_sheet_title), + style = OrbitTheme.typography.heading2SemiBold, + color = OrbitTheme.colors.white, + ) + + Spacer(modifier = Modifier.height(14.dp)) + + SelectedMissionTypeItem( + missionType = missionType, + missionCount = missionCount, + onDetail = onDetail, + onDelete = onDelete, + ) + + Spacer(modifier = Modifier.height(32.dp)) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + OrbitButton( + label = stringResource(id = feature.home.R.string.mission_setting_content_btn_change), + onClick = onChange, + enabled = true, + containerColor = OrbitTheme.colors.gray_600, + contentColor = OrbitTheme.colors.white, + pressedContainerColor = OrbitTheme.colors.gray_500, + pressedContentColor = OrbitTheme.colors.white.copy(alpha = 0.7f), + modifier = Modifier.weight(1f), + ) + + OrbitButton( + label = stringResource(id = feature.home.R.string.mission_setting_content_btn_done), + onClick = onDone, + enabled = true, + modifier = Modifier.weight(1f), + ) + } + } +} + +@Composable +private fun SelectedMissionTypeItem( + missionType: MissionType, + missionCount: Int, + onDetail: () -> Unit, + onDelete: () -> Unit, +) { + val (iconRes, titleRes) = missionType.displayData() + val title = stringResource(id = titleRes) + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Row( + modifier = Modifier + .weight(1f) + .clip(RoundedCornerShape(12.dp)) + .clickable( + onClick = onDetail, + ) + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(id = iconRes), + contentDescription = title, + modifier = Modifier.size(28.dp), + tint = Color.Unspecified, + ) + + Spacer(modifier = Modifier.width(12.dp)) + + Text( + text = title, + style = OrbitTheme.typography.headline2SemiBold, + color = OrbitTheme.colors.white, + ) + + Spacer(modifier = Modifier.width(8.dp)) + + MissionCountChip(count = missionCount) + } + + Box( + modifier = Modifier + .clip(RoundedCornerShape(12.dp)) + .clickable( + onClick = onDelete, + ) + .padding(12.dp), + ) { + Icon( + painter = painterResource(id = R.drawable.ic_delete), + contentDescription = "Delete", + modifier = Modifier.size(20.dp), + tint = OrbitTheme.colors.gray_400, + ) + } + } +} + +@Composable +private fun MissionCountChip( + count: Int, +) { + Row( + modifier = Modifier + .background( + color = OrbitTheme.colors.main.copy(alpha = 0.1f), + shape = CircleShape, + ) + .padding(start = 5.dp, end = 3.dp, top = 2.dp, bottom = 2.dp), + horizontalArrangement = Arrangement.spacedBy(2.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(id = feature.home.R.string.mission_count_chip_format, count), + style = OrbitTheme.typography.label2Regular, + color = OrbitTheme.colors.main.copy(alpha = 0.9f), + ) + + Icon( + painter = painterResource(id = R.drawable.ic_arrow_right), + contentDescription = "Close", + modifier = Modifier.size(12.dp), + tint = OrbitTheme.colors.main.copy(alpha = 0.9f), + ) + } +} + +@Composable +private fun MissionSelectContent( + onBack: () -> Unit, + onClose: () -> Unit, + initialMission: MissionType, + onSelect: (MissionType) -> Unit, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .height(600.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(14.dp)) + + MissionSelectTopAppBar( + title = stringResource(id = feature.home.R.string.mission_select_content_title), + onBack = onBack, + onClose = onClose, + ) + + Column( + modifier = Modifier.padding(horizontal = 12.dp), + ) { + MissionTypeItem( + missionType = MissionType.SHAKE, + selected = initialMission == MissionType.SHAKE, + onClick = { + onSelect(MissionType.SHAKE) + }, + ) + MissionTypeItem( + missionType = MissionType.TAP, + selected = initialMission == MissionType.TAP, + onClick = { + onSelect(MissionType.TAP) + }, + ) + } + } +} + +@Composable +private fun MissionTypeItem( + missionType: MissionType, + selected: Boolean, + onClick: () -> Unit, +) { + val (iconRes, titleRes) = missionType.displayData() + val title = stringResource(id = titleRes) + + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .clickable( + onClick = onClick, + ) + .padding( + horizontal = 12.dp, + vertical = 16.dp, + ), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(id = iconRes), + contentDescription = title, + modifier = Modifier.size(28.dp), + tint = Color.Unspecified, + ) + + Text( + text = title, + style = OrbitTheme.typography.headline2SemiBold, + color = OrbitTheme.colors.white, + ) + + if (selected) { + Spacer(modifier = Modifier.weight(1f)) + + Row( + horizontalArrangement = Arrangement.spacedBy(2.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier.size(16.dp), + painter = painterResource(id = R.drawable.ic_check), + tint = OrbitTheme.colors.white.copy(alpha = 0.5f), + contentDescription = null, + ) + + Text( + text = stringResource(id = feature.home.R.string.mission_select_content_selected), + style = OrbitTheme.typography.body2Medium, + color = OrbitTheme.colors.white.copy(alpha = 0.4f), + ) + } + } + } +} + +@Composable +private fun MissionDetailContent( + missionType: MissionType, + selectedMissionCount: Int, + onCountChange: (Int) -> Unit, + onBack: () -> Unit, + onClose: () -> Unit, + onSave: () -> Unit, + onPreview: () -> Unit, +) { + val (title, lottieRes) = when (missionType) { + MissionType.SHAKE -> + Pair(stringResource(id = feature.home.R.string.alarm_add_edit_selected_mission_shake), R.raw.mission_shake) + MissionType.TAP -> + Pair(stringResource(id = feature.home.R.string.alarm_add_edit_selected_mission_tap), R.raw.mission_tap) + else -> return + } + val selectedMissionCountIndex = countOptions.indexOf(selectedMissionCount).coerceAtLeast(0) + + Column( + modifier = Modifier + .fillMaxWidth() + .height(600.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(14.dp)) + + MissionSelectTopAppBar( + title = title, + onBack = onBack, + onClose = onClose, + ) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = 20.dp, + vertical = 12.dp, + ), + ) { + Spacer(modifier = Modifier.height(12.dp)) + + Box( + modifier = Modifier + .fillMaxWidth() + .height(200.dp) + .background( + color = OrbitTheme.colors.gray_700, + shape = RoundedCornerShape(16.dp), + ), + contentAlignment = Alignment.Center, + ) { + LottieAnimation( + resId = lottieRes, + scaleXAdjustment = 0.85f, + scaleYAdjustment = 0.85f, + ) + } + + Spacer(modifier = Modifier.height(28.dp)) + + Text( + text = stringResource(id = feature.home.R.string.mission_detail_content_count_title), + style = OrbitTheme.typography.headline2SemiBold, + color = OrbitTheme.colors.gray_50, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = stringResource(id = feature.home.R.string.mission_detail_content_count_level_easy), + style = OrbitTheme.typography.label2SemiBold, + color = OrbitTheme.colors.gray_300, + ) + + Text( + text = stringResource(id = feature.home.R.string.mission_detail_content_count_level_hard), + style = OrbitTheme.typography.label2SemiBold, + color = OrbitTheme.colors.gray_300, + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + SelectorItems( + items = countOptions.map { stringResource(id = feature.home.R.string.mission_count_chip_format, it) }, + selectedIndex = selectedMissionCountIndex, + enabled = true, + onItemSelected = { index -> onCountChange(countOptions[index]) }, + ) + + Spacer(modifier = Modifier.weight(1f)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + OrbitButton( + label = stringResource(id = feature.home.R.string.mission_detail_content_btn_preview), + onClick = onPreview, + useFillMaxWidth = false, + enabled = true, + containerColor = OrbitTheme.colors.gray_600, + contentColor = OrbitTheme.colors.white, + pressedContainerColor = OrbitTheme.colors.gray_500, + pressedContentColor = OrbitTheme.colors.white.copy(alpha = 0.7f), + ) + + OrbitButton( + label = stringResource(id = feature.home.R.string.mission_detail_content_btn_save), + onClick = onSave, + enabled = true, + modifier = Modifier.weight(1f), + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun MissionSelectTopAppBar( + title: String, + onBack: () -> Unit, + onClose: () -> Unit, +) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + .height(48.dp), + ) { + Icon( + painter = painterResource(id = R.drawable.ic_back), + contentDescription = "Back", + tint = OrbitTheme.colors.white, + modifier = Modifier + .customClickable( + rippleEnabled = false, + fadeOnPress = true, + pressedAlpha = 0.5f, + onClick = onBack, + ) + .align(Alignment.CenterStart), + ) + + Text( + text = title, + modifier = Modifier.align(Alignment.Center), + style = OrbitTheme.typography.body1SemiBold, + color = OrbitTheme.colors.white, + ) + + Box( + modifier = Modifier + .size(32.dp) + .clip(CircleShape) + .clickable { onClose() } + .align(Alignment.CenterEnd), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_close), + contentDescription = "Close", + modifier = Modifier.size(24.dp), + tint = OrbitTheme.colors.white, + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Composable +private fun AlarmMissionSelectBottomSheetPreview() { + OrbitTheme { + AlarmMissionBottomSheet( + missionState = AlarmAddEditContract.AlarmMissionState(), + onDismiss = { }, + onSaveMission = { _, _ -> }, + onPreviewMission = { _, _ -> }, + ) + } +} diff --git a/feature/home/src/main/java/com/yapp/home/alarm/component/bottomsheet/AlarmSnoozeBottomSheet.kt b/feature/home/src/main/java/com/yapp/home/alarm/component/bottomsheet/AlarmSnoozeBottomSheet.kt new file mode 100644 index 00000000..ef6643c0 --- /dev/null +++ b/feature/home/src/main/java/com/yapp/home/alarm/component/bottomsheet/AlarmSnoozeBottomSheet.kt @@ -0,0 +1,206 @@ +package com.yapp.home.alarm.component.bottomsheet + +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.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.yapp.designsystem.theme.OrbitTheme +import com.yapp.home.alarm.addedit.AlarmAddEditContract +import com.yapp.home.alarm.component.SelectorItems +import com.yapp.ui.component.button.OrbitButton +import com.yapp.ui.component.switch.OrbitSwitch +import feature.home.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun AlarmSnoozeBottomSheet( + snoozeState: AlarmAddEditContract.AlarmSnoozeState, + onDismiss: () -> Unit = {}, + onComplete: (enabled: Boolean, interval: Int, count: Int) -> Unit, +) { + val snoozeIntervalOptions = listOf(1, 3, 5, 10, 15) + val snoozeCountOptions = listOf(1, 3, 5, 10, -1) + + val snoozeIntervals = snoozeIntervalOptions.map { + stringResource(id = R.string.alarm_add_edit_interval_minute, it) + } + val snoozeCounts = listOf( + stringResource(id = R.string.alarm_add_edit_repeat_count_times, 1), + stringResource(id = R.string.alarm_add_edit_repeat_count_times, 3), + stringResource(id = R.string.alarm_add_edit_repeat_count_times, 5), + stringResource(id = R.string.alarm_add_edit_repeat_count_times, 10), + stringResource(id = R.string.alarm_add_edit_repeat_count_infinite), + ) + + var selectedSnoozeEnabled by remember { mutableStateOf(snoozeState.isSnoozeEnabled) } + var selectedSnoozeIntervalIndex by remember { mutableIntStateOf(snoozeIntervalOptions.indexOf(snoozeState.snoozeInterval)) } + var selectedSnoozeCountIndex by remember { + mutableIntStateOf( + if (snoozeState.snoozeCount == -1) { + snoozeCountOptions.lastIndex + } else { + snoozeCountOptions.indexOf(snoozeState.snoozeCount) + }, + ) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(6.dp)) + VibrationSection(selectedSnoozeEnabled) { + selectedSnoozeEnabled = !selectedSnoozeEnabled + } + Spacer(modifier = Modifier.height(20.dp)) + SelectorSection( + title = stringResource(id = R.string.alarm_add_edit_interval), + selectedIndex = selectedSnoozeIntervalIndex, + items = snoozeIntervals, + enabled = selectedSnoozeEnabled, + onItemSelected = { + selectedSnoozeIntervalIndex = it + }, + ) + Spacer(modifier = Modifier.height(32.dp)) + SelectorSection( + title = stringResource(id = R.string.alarm_add_edit_repeat_count), + selectedIndex = selectedSnoozeCountIndex, + items = snoozeCounts, + enabled = selectedSnoozeEnabled, + onItemSelected = { + selectedSnoozeCountIndex = it + }, + ) + Spacer(modifier = Modifier.height(20.dp)) + if (selectedSnoozeEnabled) { + AlarmSnoozeMessage( + interval = snoozeIntervals[selectedSnoozeIntervalIndex], + count = snoozeCounts[selectedSnoozeCountIndex], + ) + } + Spacer(modifier = Modifier.height(40.dp)) + OrbitButton( + label = stringResource(id = R.string.alarm_add_edit_complete), + enabled = true, + containerColor = OrbitTheme.colors.gray_600, + contentColor = OrbitTheme.colors.white, + pressedContainerColor = OrbitTheme.colors.gray_500, + pressedContentColor = OrbitTheme.colors.white.copy(alpha = 0.7f), + onClick = { + onDismiss() + onComplete( + selectedSnoozeEnabled, + snoozeIntervalOptions[selectedSnoozeIntervalIndex], + snoozeCountOptions[selectedSnoozeCountIndex], + ) + }, + ) + } +} + +@Composable +private fun VibrationSection(snoozeEnabled: Boolean, onSnoozeToggle: () -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(id = R.string.alarm_add_edit_alarm_snooze), + style = OrbitTheme.typography.heading2SemiBold, + color = OrbitTheme.colors.white, + ) + Spacer(modifier = Modifier.weight(1f)) + OrbitSwitch( + isChecked = snoozeEnabled, + isEnabled = true, + onClick = onSnoozeToggle, + ) + } +} + +@Composable +private fun SelectorSection( + title: String, + selectedIndex: Int, + items: List, + enabled: Boolean, + onItemSelected: (Int) -> Unit, +) { + Column { + Text( + text = title, + style = OrbitTheme.typography.headline2Medium, + color = OrbitTheme.colors.gray_50, + ) + Spacer(modifier = Modifier.height(16.dp)) + SelectorItems( + items = items, + selectedIndex = selectedIndex, + enabled = enabled, + onItemSelected = onItemSelected, + ) + } +} + +@Composable +private fun AlarmSnoozeMessage(interval: String, count: String) { + val formattedCount = if (count == stringResource(id = R.string.alarm_add_edit_repeat_count_infinite)) "${count}๋ฒˆ" else count + + Box( + modifier = Modifier + .background( + color = OrbitTheme.colors.gray_700, + shape = RoundedCornerShape(8.dp), + ) + .padding(horizontal = 12.dp, vertical = 6.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = stringResource(id = R.string.alarm_add_edit_alarm_snooze_description, interval, formattedCount), + style = OrbitTheme.typography.label1Medium, + color = OrbitTheme.colors.main, + ) + } +} + +@Preview +@Composable +private fun AlarmSnoozeBottomSheetPreview() { + var isSnoozeEnabled by remember { mutableStateOf(true) } + var snoozeInterval by remember { mutableIntStateOf(5) } + var snoozeCount by remember { mutableIntStateOf(5) } + + OrbitTheme { + AlarmSnoozeBottomSheet( + snoozeState = AlarmAddEditContract.AlarmSnoozeState( + isSnoozeEnabled = isSnoozeEnabled, + snoozeInterval = snoozeInterval, + snoozeCount = snoozeCount, + ), + onComplete = { _, _, _ -> }, + ) + } +} diff --git a/feature/home/src/main/java/com/yapp/alarm/component/bottomsheet/AlarmSoundBottomSheet.kt b/feature/home/src/main/java/com/yapp/home/alarm/component/bottomsheet/AlarmSoundBottomSheet.kt similarity index 66% rename from feature/home/src/main/java/com/yapp/alarm/component/bottomsheet/AlarmSoundBottomSheet.kt rename to feature/home/src/main/java/com/yapp/home/alarm/component/bottomsheet/AlarmSoundBottomSheet.kt index 04049c3f..7b706435 100644 --- a/feature/home/src/main/java/com/yapp/alarm/component/bottomsheet/AlarmSoundBottomSheet.kt +++ b/feature/home/src/main/java/com/yapp/home/alarm/component/bottomsheet/AlarmSoundBottomSheet.kt @@ -1,4 +1,4 @@ -package com.yapp.alarm.component.bottomsheet +package com.yapp.home.alarm.component.bottomsheet import android.net.Uri import androidx.compose.foundation.background @@ -10,21 +10,17 @@ 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.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -34,88 +30,41 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.yapp.designsystem.theme.OrbitTheme import com.yapp.domain.model.AlarmSound -import com.yapp.ui.component.OrbitBottomSheet +import com.yapp.home.alarm.addedit.AlarmAddEditContract import com.yapp.ui.component.button.OrbitButton import com.yapp.ui.component.radiobutton.OrbitRadioButton import com.yapp.ui.component.slider.OrbitSlider import com.yapp.ui.component.switch.OrbitSwitch import feature.home.R -import kotlinx.coroutines.launch -@OptIn(ExperimentalMaterial3Api::class) @Composable internal fun AlarmSoundBottomSheet( - vibrationEnabled: Boolean, - soundEnabled: Boolean, - soundVolume: Int, - soundIndex: Int, - sounds: List, - onVibrationToggle: () -> Unit, - onSoundToggle: () -> Unit, + soundState: AlarmAddEditContract.AlarmSoundState, + onVibrationToggle: (Boolean) -> Unit, + onSoundToggle: (Boolean) -> Unit, onVolumeChanged: (Int) -> Unit, onSoundSelected: (Int) -> Unit, - onComplete: () -> Unit, - isSheetOpen: Boolean, - onDismiss: () -> Unit, + onDismiss: () -> Unit = {}, + onComplete: (vibrationEnabled: Boolean, soundEnabled: Boolean, soundVolume: Int, soundIndex: Int) -> Unit, ) { - val scope = rememberCoroutineScope() - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - - OrbitBottomSheet( - modifier = Modifier.statusBarsPadding(), - isSheetOpen = isSheetOpen, - sheetState = sheetState, - onDismissRequest = { - scope.launch { - sheetState.hide() - }.invokeOnCompletion { onDismiss() } - }, - ) { - BottomSheetContent( - vibrationEnabled = vibrationEnabled, - soundEnabled = soundEnabled, - soundVolume = soundVolume, - soundIndex = soundIndex, - sounds = sounds, - onVibrationToggle = onVibrationToggle, - onSoundToggle = onSoundToggle, - onVolumeChanged = onVolumeChanged, - onSoundSelected = onSoundSelected, - onComplete = { - scope.launch { - sheetState.hide() - }.invokeOnCompletion { onComplete() } - }, - ) - } -} + var selectedVibrationEnabled by remember { mutableStateOf(soundState.isVibrationEnabled) } + var selectedSoundEnabled by remember { mutableStateOf(soundState.isSoundEnabled) } + var selectedSoundVolume by remember { mutableIntStateOf(soundState.soundVolume) } + var selectedSoundIndex by remember { mutableIntStateOf(soundState.soundIndex) } -@Composable -private fun BottomSheetContent( - vibrationEnabled: Boolean, - soundEnabled: Boolean, - soundVolume: Int, - soundIndex: Int, - sounds: List, - onVibrationToggle: () -> Unit, - onSoundToggle: () -> Unit, - onVolumeChanged: (Int) -> Unit, - onSoundSelected: (Int) -> Unit, - onComplete: () -> Unit, -) { Column( modifier = Modifier .fillMaxWidth() - .padding( - horizontal = 24.dp, - vertical = 12.dp, - ), + .padding(horizontal = 24.dp, vertical = 12.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { Spacer(modifier = Modifier.height(6.dp)) VibrationSection( - isVibrationEnabled = vibrationEnabled, - onVibrationToggle = onVibrationToggle, + isVibrationEnabled = selectedVibrationEnabled, + onVibrationToggle = { + selectedVibrationEnabled = !selectedVibrationEnabled + onVibrationToggle(selectedVibrationEnabled) + }, ) Spacer( modifier = Modifier @@ -125,13 +74,22 @@ private fun BottomSheetContent( ) SoundSection( modifier = Modifier.weight(1f), - soundEnabled = soundEnabled, - onSoundToggle = onSoundToggle, - soundVolume = soundVolume, - onVolumeChanged = onVolumeChanged, - soundIndex = soundIndex, - sounds = sounds, - onSoundSelected = { onSoundSelected(it) }, + soundEnabled = selectedSoundEnabled, + onSoundToggle = { + selectedSoundEnabled = !selectedSoundEnabled + onSoundToggle(selectedSoundEnabled) + }, + soundVolume = selectedSoundVolume, + onVolumeChanged = { + selectedSoundVolume = it + onVolumeChanged(it) + }, + soundIndex = selectedSoundIndex, + sounds = soundState.sounds, + onSoundSelected = { + selectedSoundIndex = it + onSoundSelected(it) + }, ) OrbitButton( @@ -141,7 +99,15 @@ private fun BottomSheetContent( contentColor = OrbitTheme.colors.white, pressedContainerColor = OrbitTheme.colors.gray_500, pressedContentColor = OrbitTheme.colors.white.copy(alpha = 0.7f), - onClick = onComplete, + onClick = { + onDismiss() + onComplete( + selectedVibrationEnabled, + selectedSoundEnabled, + selectedSoundVolume, + selectedSoundIndex, + ) + }, ) } } @@ -300,27 +266,17 @@ private fun SoundSelectionItem( @Preview @Composable private fun AlarmSoundBottomSheetPreview() { - var isVibrationEnabled by remember { mutableStateOf(true) } - var isSoundEnabled by remember { mutableStateOf(true) } - var soundVolume by remember { mutableIntStateOf(0) } - var soundIndex by remember { mutableIntStateOf(0) } - val sounds by remember { mutableStateOf((1..20).map { AlarmSound("sound $it", Uri.EMPTY) }) } - var isSheetOpen by remember { mutableStateOf(true) } - OrbitTheme { AlarmSoundBottomSheet( - vibrationEnabled = isVibrationEnabled, - soundEnabled = isSoundEnabled, - soundVolume = soundVolume, - soundIndex = soundIndex, - sounds = sounds, - onVibrationToggle = { isVibrationEnabled = !isVibrationEnabled }, - onSoundToggle = { isSoundEnabled = !isSoundEnabled }, - onVolumeChanged = { soundVolume = it }, - onSoundSelected = { soundIndex = it }, - onComplete = { isSheetOpen = false }, - isSheetOpen = isSheetOpen, - onDismiss = { isSheetOpen = false }, + soundState = AlarmAddEditContract.AlarmSoundState( + sounds = (1..20).map { AlarmSound("sound $it", Uri.EMPTY) }, + ), + onVibrationToggle = {}, + onSoundToggle = {}, + onVolumeChanged = {}, + onSoundSelected = {}, + onComplete = { _, _, _, _ -> + }, ) } } diff --git a/feature/home/src/main/java/com/yapp/home/component/bottomsheet/AlarmListBottomSheet.kt b/feature/home/src/main/java/com/yapp/home/component/bottomsheet/AlarmListBottomSheet.kt index 20b00907..61e759c7 100644 --- a/feature/home/src/main/java/com/yapp/home/component/bottomsheet/AlarmListBottomSheet.kt +++ b/feature/home/src/main/java/com/yapp/home/component/bottomsheet/AlarmListBottomSheet.kt @@ -36,6 +36,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -48,10 +49,10 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import com.yapp.alarm.component.AlarmListItem import com.yapp.designsystem.theme.OrbitTheme import com.yapp.domain.model.Alarm import com.yapp.home.HomeContract +import com.yapp.home.alarm.component.AlarmListItem import com.yapp.home.component.AlarmListDropDownMenu import com.yapp.home.component.AlarmSortDropDownMenu import com.yapp.ui.component.checkbox.OrbitCheckBox @@ -87,23 +88,28 @@ internal fun AlarmListBottomSheet( onToggleSelect: (Long) -> Unit, onToggleActive: (Long) -> Unit, onSwipe: (Long) -> Unit, + onExpanded: () -> Unit, content: @Composable () -> Unit, ) { var expandedType by remember { mutableStateOf(BottomSheetExpandState.HALF_EXPANDED) } - val sheetState = rememberStandardBottomSheetState( - confirmValueChange = { - expandedType = when (it) { - SheetValue.Expanded -> BottomSheetExpandState.EXPANDED - else -> BottomSheetExpandState.HALF_EXPANDED - } - true - }, - ) - + val sheetState = rememberStandardBottomSheetState() val scaffoldState = rememberBottomSheetScaffoldState(bottomSheetState = sheetState) val coroutineScope = rememberCoroutineScope() + LaunchedEffect(Unit) { + snapshotFlow { sheetState.currentValue } + .collect { value -> + expandedType = when (value) { + SheetValue.Expanded -> { + onExpanded() + BottomSheetExpandState.EXPANDED + } + SheetValue.PartiallyExpanded, SheetValue.Hidden -> BottomSheetExpandState.HALF_EXPANDED + } + } + } + val nestedScrollConnection = remember { object : androidx.compose.ui.input.nestedscroll.NestedScrollConnection { override fun onPreScroll( @@ -118,13 +124,6 @@ internal fun AlarmListBottomSheet( } } - LaunchedEffect(sheetState.currentValue) { - expandedType = when (sheetState.currentValue) { - SheetValue.Expanded -> BottomSheetExpandState.EXPANDED - else -> BottomSheetExpandState.HALF_EXPANDED - } - } - BottomSheetScaffold( scaffoldState = scaffoldState, sheetContent = { @@ -253,7 +252,6 @@ internal fun AlarmBottomSheetContent( onClick = onClickAlarm, onLongPress = onLongPressAlarm, onToggleSelect = onToggleSelect, - isAm = alarm.isAm, hour = alarm.hour, minute = alarm.minute, isActive = alarm.isAlarmActive, @@ -325,23 +323,19 @@ private fun AlarmListTopBar( onClick = onClickMore, ) - if (menuExpanded) { - AlarmListDropDownMenu( - expanded = menuExpanded, - onDismissRequest = onDismissRequest, - onClickEdit = onClickEdit, - onClickSort = onClickSort, - ) - } + AlarmListDropDownMenu( + expanded = menuExpanded, + onDismissRequest = onDismissRequest, + onClickEdit = onClickEdit, + onClickSort = onClickSort, + ) - if (sortDropDownMenuExpanded) { - AlarmSortDropDownMenu( - expanded = sortDropDownMenuExpanded, - sortOrder = sortOrder, - onDismissRequest = onDismissRequest, - onSetSortOrder = onSetSortOrder, - ) - } + AlarmSortDropDownMenu( + expanded = sortDropDownMenuExpanded, + sortOrder = sortOrder, + onDismissRequest = onDismissRequest, + onSetSortOrder = onSetSortOrder, + ) } } } diff --git a/feature/home/src/main/java/com/yapp/home/component/bottomsheet/UpdateNoticeBottomSheet.kt b/feature/home/src/main/java/com/yapp/home/component/bottomsheet/UpdateNoticeBottomSheet.kt new file mode 100644 index 00000000..fd38d5c2 --- /dev/null +++ b/feature/home/src/main/java/com/yapp/home/component/bottomsheet/UpdateNoticeBottomSheet.kt @@ -0,0 +1,150 @@ +package com.yapp.home.component.bottomsheet + +import android.content.pm.PackageManager +import android.os.Build +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.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.runtime.remember +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.platform.LocalInspectionMode +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.yapp.designsystem.theme.OrbitTheme +import feature.home.R + +private fun resolveVersionName(ctx: android.content.Context): String { + return runCatching { + val pm = ctx.packageManager + val packageName = ctx.packageName + val info = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + pm.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0)) + } else { + @Suppress("DEPRECATION") + pm.getPackageInfo(packageName, 0) + } + info.versionName ?: "" + }.getOrDefault("") +} + +private fun bannerUrl(versionName: String): String = + "https://www.orbitalarm.net/images/aos/$versionName/update-banner.png" + +@Composable +internal fun UpdateNoticeBottomSheet( + onDontShowAgain: () -> Unit, + onClose: () -> Unit, +) { + val context = LocalContext.current + val isPreview = LocalInspectionMode.current + + val versionName = remember(isPreview) { + if (isPreview) "preview" else resolveVersionName(context) + } + val imageUrl = remember(versionName) { bannerUrl(versionName.ifEmpty { "unknown" }) } + + Box( + modifier = Modifier + .fillMaxSize() + .background(Color(0xFF17191F).copy(alpha = 0.85f)) + .clickable(onClick = onClose), + contentAlignment = Alignment.BottomCenter, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .background( + color = OrbitTheme.colors.gray_900, + shape = RoundedCornerShape(topStart = 30.dp, topEnd = 30.dp), + ) + .clip(RoundedCornerShape(topStart = 30.dp, topEnd = 30.dp)) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + ) { }, + ) { + if (isPreview) { + // ํ”„๋ฆฌ๋ทฐ์šฉ ๋ฐ•์Šค + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .background(color = OrbitTheme.colors.white), + ) + } else { + AsyncImage( + model = imageUrl, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f), + ) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp, bottom = 20.dp, start = 20.dp, end = 20.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .weight(1f) + .clickable(onClick = onDontShowAgain) + .padding(vertical = 14.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = stringResource(id = R.string.update_notice_bottom_sheet_dont_show_again), + style = OrbitTheme.typography.body1SemiBold, + color = OrbitTheme.colors.white, + ) + } + Box( + modifier = Modifier + .weight(1f) + .clickable(onClick = onClose) + .padding(vertical = 14.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = stringResource(id = R.string.update_notice_bottom_sheet_close), + style = OrbitTheme.typography.body1SemiBold, + color = OrbitTheme.colors.white, + ) + } + } + } + } +} + +@Preview +@Composable +private fun UpdateNoticeBottomSheetPreview() { + OrbitTheme { + UpdateNoticeBottomSheet( + onDontShowAgain = {}, + onClose = {}, + ) + } +} diff --git a/feature/home/src/main/java/com/yapp/home/util/AlarmDateTimeFormatter.kt b/feature/home/src/main/java/com/yapp/home/util/AlarmDateTimeFormatter.kt new file mode 100644 index 00000000..448cc811 --- /dev/null +++ b/feature/home/src/main/java/com/yapp/home/util/AlarmDateTimeFormatter.kt @@ -0,0 +1,220 @@ +package com.yapp.home.util + +import android.util.Log +import com.yapp.domain.model.Alarm +import com.yapp.domain.model.toAlarmDays +import com.yapp.domain.model.toDayOfWeek +import java.time.Clock +import java.time.Duration +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.format.DateTimeFormatter +import java.time.format.DateTimeParseException +import java.util.Locale +import javax.inject.Inject + +class AlarmDateTimeFormatter @Inject constructor( + private val clock: Clock, + private val displayLocale: Locale, +) { + + companion object { + private const val NO_ALARM_STRING = "NONE" + private const val DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm" + } + + data class DeliveryTimeFormats( + val noAlarm: String, + val today: String, // ์˜ˆ: "์˜ค๋Š˜ %s" + val tomorrow: String, // ์˜ˆ: "๋‚ด์ผ %s" + val thisYear: String, // ์˜ˆ: "%s" (๋‚ ์งœ์™€ ์‹œ๊ฐ„๋งŒ) + val otherYear: String, // ์˜ˆ: "%s" (๋…„๋„, ๋‚ ์งœ, ์‹œ๊ฐ„) + val todayTimePattern: String = "a h:mm", + val thisYearDatePattern: String = "M์›” d์ผ a h:mm", + val otherYearDatePattern: String = "yy๋…„ M์›” d์ผ a h:mm", + ) + + data class TimeDifferenceFormats( + val daysHoursMinutesFormat: String, // ์˜ˆ: "%1$d์ผ %2$d์‹œ๊ฐ„ %3$d๋ถ„ ํ›„์— ์šธ๋ ค์š”" + val hoursMinutesFormat: String, // ์˜ˆ: "%1$d์‹œ๊ฐ„ %2$d๋ถ„ ํ›„์— ์šธ๋ ค์š”" + val minutesFormat: String, // ์˜ˆ: "%1$d๋ถ„ ํ›„์— ์šธ๋ ค์š”" + val soonFormat: String, // ์˜ˆ: "๊ณง ์šธ๋ ค์š”" + ) + + fun calculateNextOccurrence( + hour: Int, + minute: Int, + repeatDays: Int, + now: LocalDateTime = LocalDateTime.now(clock), + ): LocalDateTime { + val alarmTime = LocalTime.of(hour, minute) + val todayAlarmDateTime = LocalDateTime.of(now.toLocalDate(), alarmTime) + + if (repeatDays == 0) { // ๋‹จ์ผ ์•Œ๋žŒ + return if (todayAlarmDateTime.isAfter(now)) { + todayAlarmDateTime + } else { + todayAlarmDateTime.plusDays(1) + } + } + + val selectedDaysOfWeek = repeatDays.toAlarmDays() + .map { it.toDayOfWeek() } + .sortedBy { it.value } + + require(selectedDaysOfWeek.isNotEmpty()) { + "๋ฐ˜๋ณต ์•Œ๋žŒ์€ ์ตœ์†Œ ํ•˜๋‚˜ ์ด์ƒ์˜ ์š”์ผ์„ ์„ ํƒํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. repeatDays: $repeatDays" + } + + val currentDayOfWeek = now.dayOfWeek + + // ์˜ค๋Š˜ ์•Œ๋žŒ์ด ๊ฐ€๋Šฅํ•œ์ง€ ํ™•์ธ + if (selectedDaysOfWeek.contains(currentDayOfWeek) && todayAlarmDateTime.isAfter(now)) { + return todayAlarmDateTime + } + + for (dayOffset in 1..7) { + val nextPotentialDate = now.toLocalDate().plusDays(dayOffset.toLong()) + val dayOfWeekPotentialDate = nextPotentialDate.dayOfWeek + val potentialAlarmDateTime = nextPotentialDate.atTime(alarmTime) + + if (selectedDaysOfWeek.contains(dayOfWeekPotentialDate)) { + return potentialAlarmDateTime + } + } + + error("๋ฐ˜๋ณต ์•Œ๋žŒ์˜ ๋‹ค์Œ ๋ฐœ์ƒ ์‹œ๊ฐ„์„ ๊ณ„์‚ฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. selectedDaysOfWeek: $selectedDaysOfWeek") + } + + private fun formatDeliveryDateTimeString( + deliveryDateTimeString: String, // "yyyy-MM-dd'T'HH:mm" ํฌ๋งท ๋˜๋Š” "NONE" + formats: DeliveryTimeFormats, + now: LocalDateTime = LocalDateTime.now(clock), + ): String { + return try { + if (deliveryDateTimeString.equals(NO_ALARM_STRING, ignoreCase = true)) { + return formats.noAlarm + } + + val inputFormatter = + DateTimeFormatter.ofPattern(DATE_TIME_FORMAT).withLocale(displayLocale) + val alarmOccurrenceDateTime = LocalDateTime.parse( + deliveryDateTimeString, + inputFormatter, + ) + val today = now.toLocalDate() + val tomorrow = today.plusDays(1) + + when { + // 1. ๋…„๋„๊ฐ€ ํ˜„์žฌ ๋…„๋„์™€ ๋‹ค๋ฅด๋ฉด 'otherYear' ํฌ๋งท ์ ์šฉ + alarmOccurrenceDateTime.year != now.year -> { + val formattedDateTime = alarmOccurrenceDateTime.format( + DateTimeFormatter.ofPattern(formats.otherYearDatePattern) + .withLocale(displayLocale), + ) + String.format(formats.otherYear, formattedDateTime) + } + // 2. (๋…„๋„๊ฐ€ ๊ฐ™๊ณ ) ๋‚ ์งœ๊ฐ€ ์˜ค๋Š˜์ด๋ฉด 'today' ํฌ๋งท ์ ์šฉ + alarmOccurrenceDateTime.toLocalDate() == today -> { + val formattedTime = alarmOccurrenceDateTime.format( + DateTimeFormatter.ofPattern(formats.todayTimePattern) + .withLocale(displayLocale), + ) + String.format(formats.today, formattedTime) + } + // 3. (๋…„๋„๊ฐ€ ๊ฐ™๊ณ ) ๋‚ ์งœ๊ฐ€ ๋‚ด์ผ์ด๋ฉด 'tomorrow' ํฌ๋งท ์ ์šฉ + alarmOccurrenceDateTime.toLocalDate() == tomorrow -> { + val formattedTime = alarmOccurrenceDateTime.format( // ๋‚ด์ผ๋„ ์‹œ๊ฐ„ ํฌ๋งท ์‚ฌ์šฉ + DateTimeFormatter.ofPattern(formats.todayTimePattern) + .withLocale(displayLocale), + ) + String.format(formats.tomorrow, formattedTime) + } + // 4. ๊ทธ ์™ธ์˜ ๊ฒฝ์šฐ (๋…„๋„๊ฐ€ ๊ฐ™๊ณ , ์˜ค๋Š˜์ด๋‚˜ ๋‚ด์ผ์ด ์•„๋‹Œ ๋‹ค๋ฅธ ๋‚ ) 'thisYear' ํฌ๋งท ์ ์šฉ + else -> { + val formattedDateTime = alarmOccurrenceDateTime.format( + DateTimeFormatter.ofPattern(formats.thisYearDatePattern) + .withLocale(displayLocale), + ) + String.format(formats.thisYear, formattedDateTime) + } + } + } catch (e: DateTimeParseException) { + Log.e("AlarmDateTimeFormatter", "Invalid date format: $deliveryDateTimeString", e) + formats.noAlarm + } catch (e: Exception) { + Log.e( + "AlarmDateTimeFormatter", + "Error formatting delivery date time: $deliveryDateTimeString", + e, + ) + formats.noAlarm + } + } + + fun getFormattedEarliestUpcomingAlarmDeliveryTime( + alarms: List, + formats: DeliveryTimeFormats, + now: LocalDateTime = LocalDateTime.now(clock), + ): String { + val earliestAlarmDateTime = alarms + .filter { it.isAlarmActive } + .mapNotNull { alarm -> + try { + calculateNextOccurrence(alarm.hour, alarm.minute, alarm.repeatDays, now) + } catch (e: Exception) { + Log.e( + "AlarmDateTimeFormatter", + "Error calculating next occurrence for alarm: $alarm", + e, + ) + null // ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ null๋กœ ์ฒ˜๋ฆฌ + } + } + .minOrNull() + + val deliveryDateTimeString = earliestAlarmDateTime?.format( + DateTimeFormatter.ofPattern(DATE_TIME_FORMAT).withLocale(displayLocale), + ) ?: NO_ALARM_STRING + + return formatDeliveryDateTimeString(deliveryDateTimeString, formats, now) + } + + fun formatTimeDifference( + baseTime: LocalDateTime, + futureTime: LocalDateTime, + formats: TimeDifferenceFormats, + ): String { + if (!futureTime.isAfter(baseTime)) { + return formats.soonFormat + } + + val duration = Duration.between(baseTime, futureTime) + val totalMinutes = duration.toMinutes() + + if (totalMinutes < 1) { + return formats.soonFormat + } + + val days = duration.toDays() + val remainingHours = duration.toHours() % 24 + val remainingMinutes = duration.toMinutes() % 60 + + return when { + days > 0 -> String.format( + formats.daysHoursMinutesFormat, + days, + remainingHours, + remainingMinutes, + ) + + remainingHours > 0 -> String.format( + formats.hoursMinutesFormat, + remainingHours, + remainingMinutes, + ) + + else -> String.format(formats.minutesFormat, remainingMinutes) + } + } +} diff --git a/feature/home/src/main/res/values/strings.xml b/feature/home/src/main/res/values/strings.xml index cfe4c73c..b2266b16 100644 --- a/feature/home/src/main/res/values/strings.xml +++ b/feature/home/src/main/res/values/strings.xml @@ -32,6 +32,33 @@ ํ†  ๊ณตํœด์ผ ์•Œ๋žŒ ๋„๊ธฐ + ๋ฏธ์…˜ + %1$s, %2$dํšŒ + ํ”๋“ค๊ธฐ + ํ„ฐ์น˜ํ•˜๊ธฐ + ์—†์Œ + + + ๋ฏธ์…˜ + + ๋“ฑ๋ก๋œ ๋ฏธ์…˜์ด ์—†์–ด์š” + ์ƒˆ ๋ฏธ์…˜์„ ์ถ”๊ฐ€ํ•ด๋ณด์„ธ์š” + ๋ฏธ์…˜์ถ”๊ฐ€ + + ๋ฏธ์…˜ ๋ณ€๊ฒฝ + ์™„๋ฃŒ + + ๋ฏธ์…˜ ์„ ํƒ + ์„ ํƒ๋จ + + %dํšŒ + + ํšŸ์ˆ˜ + ์‰ฌ์›€ + ์–ด๋ ค์›€ + ๋ฏธ๋ฆฌ๋ณด๊ธฐ + ๋ฏธ์…˜ ์ €์žฅ + %s, %s ์•ˆ ํ•จ @@ -92,4 +119,12 @@ ์•Œ๋žŒ ๋ฏธ๋ฃจ๊ธฐ ๋‚จ์€ ์‹œ๊ฐ„ + + %1$d์ผ %2$d์‹œ๊ฐ„ ํ›„์— ์šธ๋ ค์š” + %1$d์‹œ๊ฐ„ %2$d๋ถ„ ํ›„์— ์šธ๋ ค์š” + %d๋ถ„ ํ›„์— ์šธ๋ ค์š” + ๊ณง ์šธ๋ ค์š” + + ๋‹ค์‹œ ๋ณด์ง€ ์•Š๊ธฐ + ๋‹ซ๊ธฐ diff --git a/feature/home/src/test/kotlin/com/yapp/home/util/AlarmDateTimeFormatterTest.kt b/feature/home/src/test/kotlin/com/yapp/home/util/AlarmDateTimeFormatterTest.kt new file mode 100644 index 00000000..c6ab9554 --- /dev/null +++ b/feature/home/src/test/kotlin/com/yapp/home/util/AlarmDateTimeFormatterTest.kt @@ -0,0 +1,205 @@ +package com.yapp.home.util + +import com.yapp.domain.model.Alarm +import com.yapp.domain.model.AlarmDay +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import java.time.Clock +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.util.Locale + +class AlarmDateTimeFormatterTest { + + private lateinit var formatter: AlarmDateTimeFormatter + private val fixedNow = LocalDateTime.of(2023, 10, 26, 10, 0, 0) // ๋ชฉ์š”์ผ + private val fixedClock = Clock.fixed(fixedNow.atZone(ZoneId.of("Asia/Seoul")).toInstant(), ZoneId.of("Asia/Seoul")) + private val testLocale: Locale = Locale.KOREA + + @Before + fun `ํ…Œ์ŠคํŠธ_์ค€๋น„`() { + formatter = AlarmDateTimeFormatter(clock = fixedClock, displayLocale = testLocale) + } + + private fun getLocalizedFormatter(pattern: String): DateTimeFormatter { + return DateTimeFormatter.ofPattern(pattern).withLocale(testLocale) + } + + private val deliveryFormats = AlarmDateTimeFormatter.DeliveryTimeFormats( + noAlarm = "๋ฐ›์„ ์ˆ˜ ์žˆ๋Š” ์šด์„ธ๊ฐ€ ์—†์–ด์š”", + today = "%1\$s ๋„์ฐฉ", + tomorrow = "๋‚ด์ผ %1\$s ๋„์ฐฉ", + thisYear = "%1\$s ๋„์ฐฉ", + otherYear = "%1\$s ๋„์ฐฉ", + todayTimePattern = "a h:mm", + thisYearDatePattern = "M์›” d์ผ a h:mm", + otherYearDatePattern = "yy๋…„ M์›” d์ผ a h:mm" + ) + + @Test + fun `๊ฐ€์žฅ๋น ๋ฅธ_์•Œ๋žŒ์‹œ๊ฐ„_ํฌ๋งทํŒ…_ํ™œ์„ฑ์•Œ๋žŒ_์—†์œผ๋ฉด_์ˆ˜์ •๋œ_์•Œ๋žŒ์—†์Œ_๋ฐ˜ํ™˜`() { + // given + val alarms = listOf(Alarm(id = 1, hour = 14, minute = 0, repeatDays = 0, isAlarmActive = false)) + + // when + val result = formatter.getFormattedEarliestUpcomingAlarmDeliveryTime(alarms, deliveryFormats, fixedNow) + + // then + assertEquals(deliveryFormats.noAlarm, result) + } + + @Test + fun `๊ฐ€์žฅ๋น ๋ฅธ_์•Œ๋žŒ์‹œ๊ฐ„_ํฌ๋งทํŒ…_์˜ค๋Š˜_๋ฏธ๋ž˜_ํ™œ์„ฑ์•Œ๋žŒ_ํ•˜๋‚˜๋ฉด_์ˆ˜์ •๋œ_์˜ค๋Š˜ํ˜•์‹_๋ฐ˜ํ™˜`() { + // given + val alarms = listOf(Alarm(id = 1, hour = 14, minute = 30, repeatDays = 0, isAlarmActive = true)) + val expectedTime = LocalDateTime.of(2023, 10, 26, 14, 30) + val expected = String.format(deliveryFormats.today, expectedTime.format(getLocalizedFormatter(deliveryFormats.todayTimePattern))) + + // when + val result = formatter.getFormattedEarliestUpcomingAlarmDeliveryTime(alarms, deliveryFormats, fixedNow) + + // then + assertEquals(expected, result) + } + + @Test + fun `๊ฐ€์žฅ๋น ๋ฅธ_์•Œ๋žŒ์‹œ๊ฐ„_ํฌ๋งทํŒ…_๋‚ด์ผ_ํ™œ์„ฑ์•Œ๋žŒ_ํ•˜๋‚˜๋ฉด_์ˆ˜์ •๋œ_๋‚ด์ผํ˜•์‹_๋ฐ˜ํ™˜`() { + // given + val alarms = listOf(Alarm(id = 1, hour = 8, minute = 0, repeatDays = 0, isAlarmActive = true)) + val expectedTime = fixedNow.toLocalDate().plusDays(1).atTime(8, 0) + val expected = String.format(deliveryFormats.tomorrow, expectedTime.format(getLocalizedFormatter(deliveryFormats.todayTimePattern))) + + // when + val result = formatter.getFormattedEarliestUpcomingAlarmDeliveryTime(alarms, deliveryFormats, fixedNow) + + // then + assertEquals(expected, result) + } + + @Test + fun `๊ฐ€์žฅ๋น ๋ฅธ_์•Œ๋žŒ์‹œ๊ฐ„_ํฌ๋งทํŒ…_์˜ฌํ•ด_๋‹ค๋ฅธ๋‚ ์งœ๋ฉด_์ˆ˜์ •๋œ_์˜ฌํ•ดํ˜•์‹_๋ฐ˜ํ™˜`() { + // given + val alarms = listOf(Alarm(id = 1, hour = 14, minute = 30, repeatDays = AlarmDay.SUN.bitValue, isAlarmActive = true)) + val expectedTime = LocalDateTime.of(2023, 10, 29, 14, 30) + val expected = String.format(deliveryFormats.thisYear, expectedTime.format(getLocalizedFormatter(deliveryFormats.thisYearDatePattern))) + + // when + val result = formatter.getFormattedEarliestUpcomingAlarmDeliveryTime(alarms, deliveryFormats, fixedNow) + + // then + assertEquals(expected, result) + } + + @Test + fun `๊ฐ€์žฅ๋น ๋ฅธ_์•Œ๋žŒ์‹œ๊ฐ„_ํฌ๋งทํŒ…_๋‹ค๋ฅธํ•ด๋ฉด_์ˆ˜์ •๋œ_๋‹ค๋ฅธํ•ดํ˜•์‹_๋ฐ˜ํ™˜`() { + // given + val now = LocalDateTime.of(2023, 12, 31, 10, 0, 0) + val alarms = listOf(Alarm(id = 1, hour = 9, minute = 0, repeatDays = 0, isAlarmActive = true)) + val expectedTime = LocalDateTime.of(2024, 1, 1, 9, 0) + val expected = String.format(deliveryFormats.otherYear, expectedTime.format(getLocalizedFormatter(deliveryFormats.otherYearDatePattern))) + + // when + val result = formatter.getFormattedEarliestUpcomingAlarmDeliveryTime(alarms, deliveryFormats, now) + + // then + assertEquals(expected, result) + } + + @Test + fun `๊ฐ€์žฅ๋น ๋ฅธ_์•Œ๋žŒ์‹œ๊ฐ„_ํฌ๋งทํŒ…_์—ฌ๋Ÿฌ_ํ™œ์„ฑ์•Œ๋žŒ์ค‘_๊ฐ€์žฅ๋น ๋ฅธ๊ฒƒ_์ •ํ™•ํžˆ_ํฌ๋งทํŒ…_์ˆ˜์ •๋œํ˜•์‹`() { + // given + val alarms = listOf( + Alarm(id = 1, hour = 15, minute = 0, repeatDays = 0, isAlarmActive = true), // ์˜ค๋Š˜ 15:00 + Alarm(id = 2, hour = 12, minute = 0, repeatDays = 0, isAlarmActive = true), // ์˜ค๋Š˜ 12:00 (์ด๊ฒŒ ๋” ๋น ๋ฆ„) + Alarm(id = 3, hour = 9, minute = 0, repeatDays = 0, isAlarmActive = false), + Alarm(id = 4, hour = 8, minute = 0, repeatDays = AlarmDay.FRI.bitValue, isAlarmActive = true) // ๋‚ด์ผ 08:00 + ) + val expectedTime = LocalDateTime.of(2023, 10, 26, 12, 0) + val expected = String.format(deliveryFormats.today, expectedTime.format(getLocalizedFormatter(deliveryFormats.todayTimePattern))) + + // when + val result = formatter.getFormattedEarliestUpcomingAlarmDeliveryTime(alarms, deliveryFormats, fixedNow) + + // then + assertEquals(expected, result) + } + + @Test + fun `๋‚ ์งœ์‹œ๊ฐ„๋ฌธ์ž์—ด_ํฌ๋งทํŒ…_์ž˜๋ชป๋œ_๋‚ ์งœํ˜•์‹์ด๋ฉด_์ˆ˜์ •๋œ_์•Œ๋žŒ์—†์Œ_๋ฐ˜ํ™˜`() { + // given + val alarms = emptyList() + + // when + val result = formatter.getFormattedEarliestUpcomingAlarmDeliveryTime(alarms, deliveryFormats, fixedNow) + + // then + assertEquals(deliveryFormats.noAlarm, result) + } + + private val timeFormats = AlarmDateTimeFormatter.TimeDifferenceFormats( + daysHoursMinutesFormat = "%1\$d์ผ %2\$d์‹œ๊ฐ„ %3\$d๋ถ„ ํ›„์— ์šธ๋ ค์š”", + hoursMinutesFormat = "%1\$d์‹œ๊ฐ„ %2\$d๋ถ„ ํ›„์— ์šธ๋ ค์š”", + minutesFormat = "%1\$d๋ถ„ ํ›„์— ์šธ๋ ค์š”", + soonFormat = "๊ณง ์šธ๋ ค์š”" + ) + + @Test + fun `์‹œ๊ฐ„์ฐจ์ด_ํฌ๋งทํŒ…_์ฐจ์ด์—†๊ฑฐ๋‚˜_๊ณผ๊ฑฐ๋ฉด_๊ณง์šธ๋ ค์š”_๋ฐ˜ํ™˜`() { + // when & then + assertEquals(timeFormats.soonFormat, formatter.formatTimeDifference(fixedNow, fixedNow, timeFormats)) + assertEquals(timeFormats.soonFormat, formatter.formatTimeDifference(fixedNow, fixedNow.minusMinutes(1), timeFormats)) + } + + @Test + fun `์‹œ๊ฐ„์ฐจ์ด_ํฌ๋งทํŒ…_1๋ถ„๋ฏธ๋งŒ_์ฐจ์ด๋ฉด_๊ณง์šธ๋ ค์š”_๋ฐ˜ํ™˜`() { + // given + val future = fixedNow.plusSeconds(30) + + // when + val result = formatter.formatTimeDifference(fixedNow, future, timeFormats) + + // then + assertEquals(timeFormats.soonFormat, result) + } + + @Test + fun `์‹œ๊ฐ„์ฐจ์ด_ํฌ๋งทํŒ…_25๋ถ„_์ฐจ์ด๋ฉด_์ •ํ™•ํ•œ_๋ฌธ์ž์—ด_๋ฐ˜ํ™˜`() { + // given + val futureTime = fixedNow.plusMinutes(25) + val expected = String.format(testLocale, timeFormats.minutesFormat, 25L) + + // when + val result = formatter.formatTimeDifference(fixedNow, futureTime, timeFormats) + + // then + assertEquals(expected, result) + } + + @Test + fun `์‹œ๊ฐ„์ฐจ์ด_ํฌ๋งทํŒ…_70๋ถ„_์ฐจ์ด๋ฉด_์ •ํ™•ํ•œ_๋ฌธ์ž์—ด_๋ฐ˜ํ™˜`() { + // given + val futureTime = fixedNow.plusMinutes(70) + val expected = String.format(testLocale, timeFormats.hoursMinutesFormat, 1L, 10L) + + // when + val result = formatter.formatTimeDifference(fixedNow, futureTime, timeFormats) + + // then + assertEquals(expected, result) + } + + @Test + fun `์‹œ๊ฐ„์ฐจ์ด_ํฌ๋งทํŒ…_1์ผ_1์‹œ๊ฐ„_5๋ถ„_์ฐจ์ด๋ฉด_์ •ํ™•ํ•œ_๋ฌธ์ž์—ด_๋ฐ˜ํ™˜`() { + // given + val futureTime = fixedNow.plusDays(1).plusHours(1).plusMinutes(5) + val expected = String.format(testLocale, timeFormats.daysHoursMinutesFormat, 1L, 1L, 5L) + + // when + val result = formatter.formatTimeDifference(fixedNow, futureTime, timeFormats) + + // then + assertEquals(expected, result) + } +} diff --git a/feature/mission/build.gradle.kts b/feature/mission/build.gradle.kts index 95ffd72c..eda23f95 100644 --- a/feature/mission/build.gradle.kts +++ b/feature/mission/build.gradle.kts @@ -15,7 +15,6 @@ dependencies { implementation(projects.core.media) implementation(projects.core.alarm) implementation(projects.domain) - implementation(projects.core.datastore) implementation(libs.orbit.core) implementation(libs.orbit.compose) implementation(libs.orbit.viewmodel) diff --git a/feature/mission/src/main/java/com/yapp/mission/MissionContract.kt b/feature/mission/src/main/java/com/yapp/mission/MissionContract.kt index f1812c49..8e5c61d4 100644 --- a/feature/mission/src/main/java/com/yapp/mission/MissionContract.kt +++ b/feature/mission/src/main/java/com/yapp/mission/MissionContract.kt @@ -1,15 +1,17 @@ package com.yapp.mission +import com.yapp.domain.MissionMode import com.yapp.domain.model.MissionType sealed class MissionContract { data class State( - val missionType: MissionType = MissionType.Click, + val missionMode: MissionMode = MissionMode.REAL, + val missionType: MissionType = MissionType.TAP, val isMissionTypeLoading: Boolean = true, + val missionCount: Int = 10, + val currentCount: Int = 0, val isMissionCompleted: Boolean = false, - val shakeCount: Int = 0, - val clickCount: Int = 0, val playWhenClick: Boolean = false, val showFinalAnimation: Boolean = false, val isFlipped: Boolean = false, @@ -20,16 +22,16 @@ sealed class MissionContract { ) : com.yapp.ui.base.UiState sealed class Action { + data object NavigateBack : Action() data object ShakeCard : Action() data object ClickCard : Action() data object ShowExitDialog : Action() data object HideExitDialog : Action() - data object RetryPostFortune : Action() } sealed class SideEffect : com.yapp.ui.base.SideEffect { data object NavigateToFortune : SideEffect() - + data object NavigateToHome : SideEffect() data object NavigateBack : SideEffect() } } diff --git a/feature/mission/src/main/java/com/yapp/mission/MissionNavGraph.kt b/feature/mission/src/main/java/com/yapp/mission/MissionNavGraph.kt index a24f0545..5befebf4 100644 --- a/feature/mission/src/main/java/com/yapp/mission/MissionNavGraph.kt +++ b/feature/mission/src/main/java/com/yapp/mission/MissionNavGraph.kt @@ -1,6 +1,5 @@ package com.yapp.mission -import androidx.compose.runtime.LaunchedEffect import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import androidx.navigation.navDeepLink @@ -8,6 +7,7 @@ import androidx.navigation.navOptions import com.yapp.common.navigation.OrbitNavigator import com.yapp.common.navigation.extensions.sharedHiltViewModel import com.yapp.common.navigation.route.MissionRoute +import org.orbitmvi.orbit.compose.collectSideEffect fun NavGraphBuilder.missionScreen( navigator: OrbitNavigator, @@ -15,30 +15,48 @@ fun NavGraphBuilder.missionScreen( composable( deepLinks = listOf( navDeepLink { - uriPattern = "orbitapp://mission?notificationId={notificationId}" + uriPattern = "orbitapp://mission?notificationId={notificationId}&missionType={missionType}&missionCount={missionCount}" }, ), ) { val viewModel = it.sharedHiltViewModel(navigator.navController) - LaunchedEffect(viewModel) { - viewModel.container.sideEffectFlow.collect { sideEffect -> - when (sideEffect) { - MissionContract.SideEffect.NavigateToFortune -> { - navigator.navigateToFortune( - navOptions = navOptions { - popUpTo(MissionRoute) { - inclusive = true - } - }, - ) + viewModel.collectSideEffect { + handleSideEffect(it, navigator) + } + + MissionRoute( + navigator = navigator, + viewModel = viewModel, + ) + } +} + +private fun handleSideEffect( + sideEffect: MissionContract.SideEffect, + navigator: OrbitNavigator, +) { + when (sideEffect) { + MissionContract.SideEffect.NavigateToFortune -> { + navigator.navigateToFortune( + navOptions = navOptions { + popUpTo { + inclusive = true } + }, + ) + } - MissionContract.SideEffect.NavigateBack -> navigator.navigateBack() - } - } + MissionContract.SideEffect.NavigateToHome -> { + navigator.navigateToHome( + navOptions = navOptions { + popUpTo { + inclusive = true + } + }, + ) } - MissionRoute(viewModel) + MissionContract.SideEffect.NavigateBack -> navigator.navigateBack() } } diff --git a/feature/mission/src/main/java/com/yapp/mission/MissionScreen.kt b/feature/mission/src/main/java/com/yapp/mission/MissionScreen.kt index cd42cdd1..65249b8c 100644 --- a/feature/mission/src/main/java/com/yapp/mission/MissionScreen.kt +++ b/feature/mission/src/main/java/com/yapp/mission/MissionScreen.kt @@ -1,5 +1,6 @@ package com.yapp.mission +import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler import androidx.compose.animation.Crossfade @@ -9,14 +10,21 @@ import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues 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.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -31,6 +39,7 @@ import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -38,7 +47,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.yapp.analytics.AnalyticsEvent import com.yapp.analytics.AnalyticsHelper import com.yapp.analytics.LocalAnalyticsHelper +import com.yapp.common.navigation.OrbitNavigator import com.yapp.designsystem.theme.OrbitTheme +import com.yapp.domain.MissionMode import com.yapp.domain.model.MissionType import com.yapp.mission.component.FlipCard import com.yapp.mission.component.MissionProgressBar @@ -47,9 +58,13 @@ import com.yapp.ui.component.lottie.LottieAnimation import com.yapp.ui.extensions.customClickable import com.yapp.ui.utils.heightForScreenPercentage import com.yapp.ui.utils.paddingForScreenPercentage +import feature.mission.R @Composable -fun MissionRoute(viewModel: MissionViewModel = hiltViewModel()) { +fun MissionRoute( + viewModel: MissionViewModel = hiltViewModel(), + navigator: OrbitNavigator, +) { val state by viewModel.container.stateFlow.collectAsStateWithLifecycle() val context = LocalContext.current @@ -59,6 +74,21 @@ fun MissionRoute(viewModel: MissionViewModel = hiltViewModel()) { } } + BackHandler { + if (state.missionMode == MissionMode.PREVIEW) { + navigator.navigateBack() + return@BackHandler + } + + viewModel.processAction( + if (state.showExitDialog) { + MissionContract.Action.HideExitDialog + } else { + MissionContract.Action.ShowExitDialog + }, + ) + } + LaunchedEffect(Unit) { shakeDetector.start() } @@ -76,10 +106,6 @@ fun MissionRoute(viewModel: MissionViewModel = hiltViewModel()) { ) } -/** - * Mission ์ƒํƒœ์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ํ™”๋ฉด์„ ๊ตฌ์„ฑํ•˜๋Š” ๋ฉ”์ธ ์ปจํ…Œ์ด๋„ˆ. - * ๋กœ๋”ฉ, ์ฝ˜ํ…์ธ , ์„ฑ๊ณต ์˜ค๋ฒ„๋ ˆ์ด, ๋‹ค์ด์–ผ๋กœ๊ทธ ๋“ฑ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ ํฌํ•จ. - */ @Composable fun MissionScreen( stateProvider: () -> MissionContract.State, @@ -89,18 +115,10 @@ fun MissionScreen( val state = stateProvider() val analytics = LocalAnalyticsHelper.current - BackHandler { - eventDispatcher( - if (state.showExitDialog) { - MissionContract.Action.HideExitDialog - } else { - MissionContract.Action.ShowExitDialog - }, - ) - } - - Box(modifier = Modifier.fillMaxSize()) { - if (state.isMissionTypeLoading) { + Box( + modifier = Modifier.fillMaxSize(), + ) { + if (state.isMissionTypeLoading || state.missionType == MissionType.NONE) { MissionLoadingScreen() return } @@ -112,7 +130,10 @@ fun MissionScreen( modifier = Modifier.matchParentSize(), ) - MissionContent(state, eventDispatcher) + MissionContent( + state = state, + eventDispatcher = eventDispatcher, + ) if (state.showExitDialog) { ExitDialog(state, eventDispatcher, onFinish, analytics) @@ -122,17 +143,35 @@ fun MissionScreen( MissionSuccessOverlay() } - state.errorMessage?.let { - ErrorDialog(message = it) { - eventDispatcher(MissionContract.Action.RetryPostFortune) + if (state.missionMode == MissionMode.PREVIEW) { + val insets = WindowInsets.navigationBars.asPaddingValues() + + Button( + onClick = { + eventDispatcher(MissionContract.Action.NavigateBack) + }, + shape = CircleShape, + colors = ButtonDefaults.buttonColors( + containerColor = OrbitTheme.colors.white, + contentColor = OrbitTheme.colors.gray_900, + ), + contentPadding = PaddingValues( + horizontal = 24.dp, + vertical = 12.dp, + ), + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = insets.calculateBottomPadding() + 28.dp), + ) { + Text( + text = stringResource(id = R.string.mission_preview_exit), + style = OrbitTheme.typography.body1SemiBold, + ) } } } } -/** - * ๋ฏธ์…˜ ์ฝ˜ํ…์ธ  ๋ณธ๋ฌธ. TopBar, ์ง„ํ–‰ ๋ฐ”, ์ƒํƒœ๋ณ„ ๊ฒŒ์ž„ ํฌํ•จ. - */ @Composable fun MissionContent( state: MissionContract.State, @@ -142,32 +181,39 @@ fun MissionContent( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, ) { - MissionTopAppBar(onExit = { eventDispatcher(MissionContract.Action.ShowExitDialog) }) + MissionTopAppBar( + mode = state.missionMode, + onExit = { eventDispatcher(MissionContract.Action.ShowExitDialog) }, + ) MissionProgressBarSection(state) MissionLabel(state) Spacer(modifier = Modifier.heightForScreenPercentage(0.0665f)) when (state.missionType) { - is MissionType.Shake -> { - if (state.shakeCount == 0) { + MissionType.SHAKE -> { + if (state.currentCount == 0) { MissionShakeInitialImage() } else { - FlipCard(state = state, eventDispatcher = eventDispatcher) + FlipCard(state) } } - is MissionType.Click -> { + MissionType.TAP -> { MissionClickCard(state, eventDispatcher) } + + MissionType.NONE -> { + Log.e("MissionContent", "Invalid or NONE MissionType: ${state.missionType}") + } } } } -/** - * '๋‚˜๊ฐ€๊ธฐ' ๋ฒ„ํŠผ์ด ํฌํ•จ๋œ ๋ฏธ์…˜ ์ƒ๋‹จ ์•ฑ๋ฐ” ์˜์—ญ. - */ @Composable -fun MissionTopAppBar(onExit: () -> Unit) { +fun MissionTopAppBar( + mode: MissionMode, + onExit: () -> Unit, +) { Spacer(modifier = Modifier.heightForScreenPercentage(0.066f)) Box( modifier = Modifier @@ -176,43 +222,41 @@ fun MissionTopAppBar(onExit: () -> Unit) { contentAlignment = Alignment.TopEnd, ) { Row( - modifier = Modifier.customClickable( - rippleEnabled = false, - fadeOnPress = true, - pressedAlpha = 0.5f, - onClick = onExit, - ), + modifier = Modifier + .height(26.dp) + .customClickable( + rippleEnabled = false, + fadeOnPress = true, + pressedAlpha = 0.5f, + onClick = onExit, + ), ) { - Icon( - painter = painterResource(id = core.designsystem.R.drawable.ic_cancel), - contentDescription = null, - tint = OrbitTheme.colors.white, - modifier = Modifier.size(24.dp), - ) - Text( - text = "๋‚˜๊ฐ€๊ธฐ", - color = OrbitTheme.colors.white, - style = OrbitTheme.typography.body1SemiBold, - modifier = Modifier - .padding(start = 4.dp) - .align(Alignment.CenterVertically), - ) + if (mode == MissionMode.REAL) { + Icon( + painter = painterResource(id = core.designsystem.R.drawable.ic_cancel), + contentDescription = null, + tint = OrbitTheme.colors.white, + modifier = Modifier.size(24.dp), + ) + Text( + text = stringResource(id = R.string.exit), + color = OrbitTheme.colors.white, + style = OrbitTheme.typography.body1SemiBold, + modifier = Modifier + .padding(start = 4.dp) + .align(Alignment.CenterVertically), + ) + } } } } -/** - * ๋ฏธ์…˜ ์ง„ํ–‰๋„ ProgressBar ์„น์…˜. - */ @Composable fun MissionProgressBarSection(state: MissionContract.State) { Spacer(modifier = Modifier.heightForScreenPercentage(0.0246f)) MissionProgressBar( - currentProgress = when (state.missionType) { - is MissionType.Shake -> state.shakeCount - is MissionType.Click -> state.clickCount - }, - totalProgress = 10, + currentProgress = state.currentCount, + totalProgress = state.missionCount, modifier = Modifier .fillMaxWidth() .height(5.dp) @@ -221,14 +265,15 @@ fun MissionProgressBarSection(state: MissionContract.State) { Spacer(modifier = Modifier.heightForScreenPercentage(0.06f)) } -/** - * ๋ฏธ์…˜ ์•ˆ๋‚ด ๋ฌธ๊ตฌ ๋ฐ ํ˜„์žฌ ์นด์šดํŠธ. - */ @Composable fun MissionLabel(state: MissionContract.State) { - val instruction = - if (state.missionType is MissionType.Shake) "10ํšŒ๋ฅผ ํ”๋“ค์–ด ๋ถ€์ ์„ ๋’ค์ง‘์–ด์ค˜" else "10ํšŒ๋ฅผ ๋ˆŒ๋Ÿฌ ํŽธ์ง€๋ฅผ ์—ด์–ด์ค˜" - val count = if (state.missionType is MissionType.Shake) state.shakeCount else state.clickCount + val instruction = stringResource( + id = when (state.missionType) { + MissionType.SHAKE -> R.string.mission_instruction_shake + else -> R.string.mission_instruction_tap + }, + state.missionCount, + ) Text( text = instruction, @@ -237,15 +282,12 @@ fun MissionLabel(state: MissionContract.State) { ) Spacer(modifier = Modifier.heightForScreenPercentage(0.005f)) Text( - text = count.toString(), + text = state.currentCount.toString(), color = OrbitTheme.colors.white, style = OrbitTheme.typography.displaySemiBold, ) } -/** - * ํ”๋“ค๊ธฐ ๋ฏธ์…˜ ์ดˆ๊ธฐ ์ด๋ฏธ์ง€. - */ @Composable fun MissionShakeInitialImage() { Image( @@ -257,15 +299,12 @@ fun MissionShakeInitialImage() { ) } -/** - * ํด๋ฆญ ๋ฏธ์…˜ ์นด๋“œ. ํด๋ฆญ ์‹œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋ฐ ์ƒํƒœ ๋ณ€ํ™”. - */ @Composable fun MissionClickCard( state: MissionContract.State, eventDispatcher: (MissionContract.Action) -> Unit, ) { - if (state.clickCount == 0) { + if (state.currentCount == 0) { Image( painter = painterResource(id = core.designsystem.R.drawable.ic_mission_main_letter), contentDescription = null, @@ -295,9 +334,6 @@ fun MissionClickCard( } } -/** - * ๋ฏธ์…˜ ์ข…๋ฃŒ ์‹œ ๋‚˜๊ฐ€๊ธฐ ๋‹ค์ด์–ผ๋กœ๊ทธ. - */ @Composable fun ExitDialog( state: MissionContract.State, @@ -306,18 +342,19 @@ fun ExitDialog( analytics: AnalyticsHelper, ) { OrbitDialog( - title = "๋‚˜๊ฐ€๋ฉด ์šด์„ธ๋ฅผ ๋ฐ›์„ ์ˆ˜ ์—†์–ด์š”", - message = "๋ฏธ์…˜์„ ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š๊ณ  ๋‚˜๊ฐ€์‹œ๊ฒ ์–ด์š”?", - confirmText = "๋‚˜๊ฐ€๊ธฐ", - cancelText = "์ทจ์†Œ", + title = stringResource(id = R.string.mission_exit_dialog_title), + message = stringResource(id = R.string.mission_exit_dialog_message), + confirmText = stringResource(id = R.string.exit), + cancelText = stringResource(id = R.string.cancel), onConfirm = { analytics.logEvent( AnalyticsEvent( type = "mission_fail", properties = mapOf( AnalyticsEvent.MissionPropertiesKeys.MISSION_TYPE to when (state.missionType) { - is MissionType.Shake -> "shake" - is MissionType.Click -> "click" + MissionType.SHAKE -> "shake" + MissionType.TAP -> "click" + else -> "" }, ), ), @@ -328,9 +365,6 @@ fun ExitDialog( ) } -/** - * ๋ฏธ์…˜ ์„ฑ๊ณต ์‹œ ์˜ค๋ฒ„๋ ˆ์ด ํ™”๋ฉด. - */ @Composable fun MissionSuccessOverlay() { Box( @@ -354,7 +388,7 @@ fun MissionSuccessOverlay() { play = true, ) Text( - text = "๋ฏธ์…˜ ์„ฑ๊ณต!", + text = stringResource(id = R.string.mission_success), color = OrbitTheme.colors.white, style = OrbitTheme.typography.title1Bold, modifier = Modifier @@ -365,22 +399,6 @@ fun MissionSuccessOverlay() { } } -/** - * ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ๋‹ค์ด์–ผ๋กœ๊ทธ. - */ -@Composable -fun ErrorDialog(message: String, onConfirm: () -> Unit) { - OrbitDialog( - title = "์˜ค๋ฅ˜", - message = message, - confirmText = "ํ™•์ธ", - onConfirm = onConfirm, - ) -} - -/** - * ๋กœ๋”ฉ ํ™”๋ฉด. ๋ฏธ์…˜ ํƒ€์ž… ๋กœ๋”ฉ ์ค‘์— ํ‘œ์‹œ. - */ @Composable fun MissionLoadingScreen() { Box( @@ -394,16 +412,36 @@ fun MissionLoadingScreen() { } } +@Composable +@Preview +private fun MissionRouteReal() { + MissionScreen( + stateProvider = { + MissionContract.State( + isMissionTypeLoading = false, + missionType = MissionType.TAP, + currentCount = 0, + showFinalAnimation = false, + playWhenClick = false, + showExitDialog = false, + isMissionCompleted = false, + ) + }, + eventDispatcher = {}, + onFinish = {}, + ) +} + @Composable @Preview private fun MissionRoutePreview() { MissionScreen( stateProvider = { MissionContract.State( + missionMode = MissionMode.PREVIEW, isMissionTypeLoading = false, - missionType = MissionType.Shake, - shakeCount = 0, - clickCount = 0, + missionType = MissionType.TAP, + currentCount = 0, showFinalAnimation = false, playWhenClick = false, showExitDialog = false, diff --git a/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt b/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt index 38512436..b5e1c45d 100644 --- a/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt +++ b/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt @@ -1,25 +1,26 @@ package com.yapp.mission import android.app.Application -import android.util.Log import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.viewModelScope +import androidx.lifecycle.ViewModel import com.yapp.alarm.pendingIntent.interaction.createAlarmDismissIntent import com.yapp.analytics.AnalyticsEvent import com.yapp.analytics.AnalyticsHelper -import com.yapp.datastore.UserPreferences +import com.yapp.domain.MissionMode +import com.yapp.domain.model.FortuneCreateStatus import com.yapp.domain.model.MissionType import com.yapp.domain.repository.FortuneRepository -import com.yapp.domain.usecase.GetMissionTypeUseCase import com.yapp.media.haptic.HapticFeedbackManager import com.yapp.media.haptic.HapticType -import com.yapp.ui.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.coroutines.flow.first +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.syntax.simple.intent +import org.orbitmvi.orbit.syntax.simple.postSideEffect +import org.orbitmvi.orbit.syntax.simple.reduce +import org.orbitmvi.orbit.viewmodel.container import javax.inject.Inject @HiltViewModel @@ -27,145 +28,132 @@ class MissionViewModel @Inject constructor( private val analyticsHelper: AnalyticsHelper, private val hapticFeedbackManager: HapticFeedbackManager, private val fortuneRepository: FortuneRepository, - private val userPreferences: UserPreferences, - private val getMissionTypeUseCase: GetMissionTypeUseCase, private val app: Application, - savedStateHandle: SavedStateHandle, -) : BaseViewModel( - MissionContract.State(), -) { - init { - savedStateHandle.get("notificationId")?.toLong()?.let { - sendAlarmDismissIntent(it) - } - loadRemoteMissionType() - } - - private fun loadRemoteMissionType() { - viewModelScope.launch { - val missionType = getMissionTypeUseCase.execute() - updateState { - copy( - missionType = missionType, - isMissionTypeLoading = false, - ) - } - } + private val savedStateHandle: SavedStateHandle, +) : ViewModel(), ContainerHost { + + override val container: Container = container( + initialState = MissionContract.State(), + ) { + sendAlarmDismissIntent() + loadMissionInfo( + missionTypeRaw = savedStateHandle.get("missionType"), + missionCountRaw = savedStateHandle.get("missionCount"), + missionModeRaw = savedStateHandle.get("missionMode"), + ) } fun processAction(action: MissionContract.Action) { when (action) { - is MissionContract.Action.ShakeCard -> handleShake() - is MissionContract.Action.ClickCard -> handleClick() + is MissionContract.Action.NavigateBack -> navigateBack() + is MissionContract.Action.ShakeCard -> handleMissionProgress(MissionType.SHAKE) + is MissionContract.Action.ClickCard -> handleMissionProgress(MissionType.TAP) is MissionContract.Action.ShowExitDialog -> showExitDialog() is MissionContract.Action.HideExitDialog -> hideExitDialog() - is MissionContract.Action.RetryPostFortune -> retryPostFortune() } } - private fun showExitDialog() { - updateState { copy(showExitDialog = true) } - } + private fun sendAlarmDismissIntent() { + val notificationId = savedStateHandle.get("notificationId")?.toLongOrNull() ?: return + val missionType = savedStateHandle.get("missionType")?.toIntOrNull() ?: -1 + val missionCount = savedStateHandle.get("missionCount")?.toIntOrNull() ?: -1 - private fun hideExitDialog() { - updateState { copy(showExitDialog = false) } + val alarmDismissIntent = createAlarmDismissIntent( + context = app, + notificationId = notificationId, + missionType = missionType, + missionCount = missionCount, + ) + app.sendBroadcast(alarmDismissIntent) } - private fun handleShake() = viewModelScope.launch { - if (currentState.missionType !is MissionType.Shake) return@launch - - val currentCount = currentState.shakeCount - if (currentCount < 9) { - performHapticSuccess() - updateState { copy(shakeCount = currentCount + 1) } - } else if (!currentState.isFlipped) { - completeMission(type = "shake") - updateState { - copy( - isMissionCompleted = true, - shakeCount = 10, - isFlipped = true, - ) - } - delay(500) + private fun loadMissionInfo( + missionTypeRaw: String?, + missionCountRaw: String?, + missionModeRaw: String?, + ) = intent { + val missionType = missionTypeRaw?.toIntOrNull() ?: MissionType.TAP.value + val missionCount = missionCountRaw?.toIntOrNull() ?: 10 + val missionMode = MissionMode.fromRaw(missionModeRaw) + + reduce { + state.copy( + missionMode = missionMode, + missionType = MissionType.fromInt(missionType), + missionCount = missionCount, + isMissionTypeLoading = false, + ) } } - private fun handleClick() = viewModelScope.launch { - if (currentState.missionType !is MissionType.Click) return@launch + private fun navigateBack() = intent { + postSideEffect(MissionContract.SideEffect.NavigateBack) + } - val currentCount = currentState.clickCount - if (currentCount < 9) { - performHapticSuccess() - logMissionSuccess("click") - updateState { copy(clickCount = currentCount + 1, playWhenClick = true) } - delay(500) - updateState { copy(playWhenClick = false) } - } else { - updateState { - copy( - clickCount = 10, - showFinalAnimation = true, - ) - } - postFortune() - delay(500) - updateState { copy(isMissionCompleted = true) } - } + private fun showExitDialog() = intent { + reduce { state.copy(showExitDialog = true) } } - private fun postFortune() { - viewModelScope.launch { - val userId = userPreferences.userIdFlow.firstOrNull() ?: return@launch - val result = runCatching { - withContext(Dispatchers.IO) { - fortuneRepository.postFortune(userId) - } - } + private fun hideExitDialog() = intent { + reduce { state.copy(showExitDialog = false) } + } - result.onSuccess { - val data = it.getOrThrow() - userPreferences.saveFortuneId(data.id) - userPreferences.saveFortuneScore(data.avgFortuneScore) + private fun handleMissionProgress(missionType: MissionType) = intent { + val isLast = state.currentCount >= state.missionCount - 1 + val nextCount = state.currentCount + 1 - emitSideEffect(MissionContract.SideEffect.NavigateToFortune) - }.onFailure { error -> - Log.e("MissionViewModel", "์šด์„ธ ๋ฐ์ดํ„ฐ ์š”์ฒญ ์‹คํŒจ: ${error.message}") - updateState { copy(errorMessage = error.message) } - } - } - } + performHapticSuccess() - private fun retryPostFortune() { - viewModelScope.launch { - val userId = userPreferences.userIdFlow.firstOrNull() ?: return@launch - val result = runCatching { - withContext(Dispatchers.IO) { - fortuneRepository.postFortune(userId) - } + if (isLast) { + completeMission(type = missionType.name.lowercase()) + reduce { + state.copy( + isMissionCompleted = true, + currentCount = state.missionCount, + showFinalAnimation = true, + ) + } + delay(500) + } else { + val transientState = if (missionType == MissionType.TAP) { + state.copy(currentCount = nextCount, playWhenClick = true) + } else { + state.copy(currentCount = nextCount) } - result.onSuccess { - val data = it.getOrThrow() - userPreferences.saveFortuneId(data.id) - userPreferences.saveFortuneScore(data.avgFortuneScore) + reduce { transientState } - emitSideEffect(MissionContract.SideEffect.NavigateToFortune) - }.onFailure { - Log.e("MissionViewModel", "์šด์„ธ ์žฌ์š”์ฒญ ์‹คํŒจ: ${it.message}") - navigateToHome() + if (missionType == MissionType.TAP) { + delay(500) + reduce { state.copy(playWhenClick = false) } } } } - private fun completeMission(type: String) { + private fun completeMission(type: String) = intent { performHapticSuccess() logMissionSuccess(type) - postFortune() - } - private fun performHapticSuccess() { - hapticFeedbackManager.performHapticFeedback(HapticType.SUCCESS) + if (state.missionMode != MissionMode.REAL) { + postSideEffect(MissionContract.SideEffect.NavigateBack) + return@intent + } + + val fortuneCreateStatus = fortuneRepository.fortuneCreateStatusFlow.first() + val hasUnseenFortune = fortuneRepository.hasUnseenFortuneFlow.first() + + val shouldOpenFortune = ( + fortuneCreateStatus is FortuneCreateStatus.Creating || + fortuneCreateStatus is FortuneCreateStatus.Success && hasUnseenFortune + ) + + postSideEffect( + if (shouldOpenFortune) { + MissionContract.SideEffect.NavigateToFortune + } else { + MissionContract.SideEffect.NavigateBack + }, + ) } private fun logMissionSuccess(type: String) { @@ -179,15 +167,7 @@ class MissionViewModel @Inject constructor( ) } - private fun navigateToHome() { - emitSideEffect(MissionContract.SideEffect.NavigateToFortune) - } - - private fun sendAlarmDismissIntent(id: Long) { - val alarmDismissIntent = createAlarmDismissIntent( - context = app, - notificationId = id, - ) - app.sendBroadcast(alarmDismissIntent) + private fun performHapticSuccess() { + hapticFeedbackManager.performHapticFeedback(HapticType.SUCCESS) } } diff --git a/feature/mission/src/main/java/com/yapp/mission/component/FlipCard.kt b/feature/mission/src/main/java/com/yapp/mission/component/FlipCard.kt index 630ba25a..14e41a39 100644 --- a/feature/mission/src/main/java/com/yapp/mission/component/FlipCard.kt +++ b/feature/mission/src/main/java/com/yapp/mission/component/FlipCard.kt @@ -25,7 +25,6 @@ import com.yapp.mission.MissionContract @Composable fun FlipCard( state: MissionContract.State, - eventDispatcher: (MissionContract.Action) -> Unit, ) { val rotationZ = remember { Animatable(0f) } val rotationY = remember { Animatable(state.rotationY) } @@ -49,8 +48,8 @@ fun FlipCard( } } - LaunchedEffect(state.shakeCount) { - if (state.shakeCount in 1..9) { + LaunchedEffect(state.currentCount) { + if (state.currentCount in 1..state.missionCount - 1) { rotationZ.animateTo( targetValue = -20f, animationSpec = tween(durationMillis = 66, easing = LinearEasing), @@ -109,7 +108,6 @@ fun FlipCardPreview() { ) { FlipCard( state = state.copy(rotationY = rotationY, rotationZ = rotationZ), - eventDispatcher = {}, ) } } diff --git a/feature/mission/src/main/res/values/strings.xml b/feature/mission/src/main/res/values/strings.xml new file mode 100644 index 00000000..3fd93606 --- /dev/null +++ b/feature/mission/src/main/res/values/strings.xml @@ -0,0 +1,14 @@ + + + ๋‚˜๊ฐ€๊ธฐ + ์ทจ์†Œ + ํ™•์ธ + ์˜ค๋ฅ˜ + + ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ข…๋ฃŒ + ๋‚˜๊ฐ€๋ฉด ์šด์„ธ๋ฅผ ๋ฐ›์„ ์ˆ˜ ์—†์–ด์š” + ๋ฏธ์…˜์„ ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š๊ณ  ๋‚˜๊ฐ€์‹œ๊ฒ ์–ด์š”? + ๋ฏธ์…˜ ์„ฑ๊ณต! + %1$dํšŒ๋ฅผ ํ”๋“ค์–ด ๋ถ€์ ์„ ๋’ค์ง‘์–ด์ค˜ + %1$dํšŒ๋ฅผ ๋ˆŒ๋Ÿฌ ํŽธ์ง€๋ฅผ ์—ด์–ด์ค˜ + diff --git a/feature/navigator/.gitignore b/feature/navigator/.gitignore deleted file mode 100644 index 42afabfd..00000000 --- a/feature/navigator/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/feature/navigator/build.gradle.kts b/feature/navigator/build.gradle.kts deleted file mode 100644 index a8db6273..00000000 --- a/feature/navigator/build.gradle.kts +++ /dev/null @@ -1,26 +0,0 @@ -import com.yapp.convention.setNamespace - -plugins { - id("orbit.android.feature") -} - -android { - setNamespace("feature.navigator") -} - -dependencies { - implementation(projects.core.common) - implementation(projects.core.analytics) - implementation(libs.orbit.core) - implementation(libs.orbit.compose) - implementation(libs.orbit.viewmodel) - implementation(libs.kotlin.reflect) - implementation(projects.feature.home) - implementation(projects.feature.alarmInteraction) - implementation(projects.feature.onboarding) - implementation(projects.feature.mission) - implementation(projects.feature.fortune) - implementation(projects.feature.setting) - implementation(projects.feature.splash) - implementation(projects.feature.webview) -} diff --git a/feature/navigator/consumer-rules.pro b/feature/navigator/consumer-rules.pro deleted file mode 100644 index e69de29b..00000000 diff --git a/feature/navigator/proguard-rules.pro b/feature/navigator/proguard-rules.pro deleted file mode 100644 index 481bb434..00000000 --- a/feature/navigator/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# 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/feature/onboarding/build.gradle.kts b/feature/onboarding/build.gradle.kts index 6c1d22e0..e573da87 100644 --- a/feature/onboarding/build.gradle.kts +++ b/feature/onboarding/build.gradle.kts @@ -14,10 +14,10 @@ dependencies { implementation(projects.core.analytics) implementation(projects.core.media) implementation(projects.domain) - implementation(projects.core.datastore) implementation(libs.orbit.core) implementation(libs.orbit.compose) implementation(libs.orbit.viewmodel) + implementation(libs.compose.material) implementation(libs.coil.compose) implementation(libs.coil.gif) implementation(libs.accompanist.permission) diff --git a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingAccessScreen.kt b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingAccessScreen.kt index abc83d5c..1485ed59 100644 --- a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingAccessScreen.kt +++ b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingAccessScreen.kt @@ -21,9 +21,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height 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.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -48,7 +46,6 @@ import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionStatus -import com.google.accompanist.permissions.shouldShowRationale import com.yapp.analytics.AnalyticsEvent import com.yapp.analytics.LocalAnalyticsHelper import com.yapp.designsystem.theme.OrbitTheme @@ -238,8 +235,6 @@ fun OnboardingAccessScreen( modifier = Modifier .fillMaxSize() .background(OrbitTheme.colors.gray_900) - .statusBarsPadding() - .navigationBarsPadding() .imePadding(), ) { if (!hasRequestedPermission) { diff --git a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingAlarmTimeSelectionScreen.kt b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingAlarmTimeSelectionScreen.kt index 963b03ba..8ccd007f 100644 --- a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingAlarmTimeSelectionScreen.kt +++ b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingAlarmTimeSelectionScreen.kt @@ -20,6 +20,7 @@ import com.yapp.designsystem.theme.OrbitTheme import com.yapp.ui.component.timepicker.OrbitPicker import com.yapp.ui.utils.heightForScreenPercentage import feature.onboarding.R +import java.time.LocalTime @Composable fun OnboardingAlarmTimeSelectionRoute( @@ -57,8 +58,8 @@ fun OnboardingAlarmTimeSelectionRoute( ) }, onBackClick = { viewModel.processAction(OnboardingContract.Action.PreviousStep) }, - setAlarmTime = { isAm, hour, minute -> - viewModel.processAction(OnboardingContract.Action.SetAlarmTime(isAm, hour, minute)) + setAlarmTime = { newTime -> + viewModel.processAction(OnboardingContract.Action.SetAlarmTime(newTime)) }, ) } @@ -69,7 +70,7 @@ fun OnboardingAlarmTimeSelectionScreen( totalSteps: Int, onNextClick: () -> Unit, onBackClick: () -> Unit, - setAlarmTime: (String, Int, Int) -> Unit, + setAlarmTime: (LocalTime) -> Unit, ) { OnboardingScreen( currentStep = currentStep, @@ -100,8 +101,8 @@ fun OnboardingAlarmTimeSelectionScreen( OrbitPicker( modifier = Modifier.padding(top = 90.dp), - ) { amPm, hour, minute -> - setAlarmTime(amPm, hour, minute) + ) { newTime -> + setAlarmTime(newTime) } } } @@ -116,7 +117,7 @@ fun OnboardingAlarmTimeSelectionScreenPreview() { totalSteps = 0, onNextClick = {}, onBackClick = {}, - setAlarmTime = { _, _, _ -> }, + setAlarmTime = { _ -> }, ) } } diff --git a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingBirthdayScreen.kt b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingBirthdayScreen.kt index 39868a89..ee9180dc 100644 --- a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingBirthdayScreen.kt +++ b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingBirthdayScreen.kt @@ -10,9 +10,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height 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.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -104,8 +102,6 @@ fun OnboardingBirthdayScreen( modifier = Modifier .fillMaxSize() .background(OrbitTheme.colors.gray_900) - .statusBarsPadding() - .navigationBarsPadding() .imePadding(), ) { OnBoardingTopAppBar( diff --git a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingCompleteScreen2.kt b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingCompleteScreen2.kt index f99e8771..f0e752e1 100644 --- a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingCompleteScreen2.kt +++ b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingCompleteScreen2.kt @@ -9,10 +9,8 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -57,8 +55,6 @@ fun OnboardingCompleteScreen2( modifier = Modifier .fillMaxSize() .background(OrbitTheme.colors.gray_900) - .statusBarsPadding() - .navigationBarsPadding() .imePadding(), ) { OnBoardingTopAppBar( diff --git a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingContract.kt b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingContract.kt index 5e8b83c2..95dc24db 100644 --- a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingContract.kt +++ b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingContract.kt @@ -1,12 +1,13 @@ package com.yapp.onboarding import com.yapp.ui.base.UiState +import java.time.LocalTime sealed class OnboardingContract { data class State( val currentStep: Int = 1, - val timeState: AlarmTimeState = AlarmTimeState(), + val selectedTime: LocalTime = LocalTime.of(1, 0), val textFieldValue: String = "", val showWarning: Boolean = false, val isButtonEnabled: Boolean = false, @@ -18,7 +19,6 @@ sealed class OnboardingContract { val isBirthDateValid: Boolean = false, val isBirthTimeValid: Boolean = false, val isValid: Boolean = false, - val isBottomSheetOpen: Boolean = false, val isShowWarningDialog: Boolean = false, ) : UiState { val birthDateFormatted: String @@ -43,23 +43,18 @@ sealed class OnboardingContract { } } - data class AlarmTimeState( - val selectedAmPm: String = "์˜ค์ „", - val selectedHour: Int = 1, - val selectedMinute: Int = 0, - ) - sealed class Action { data object NextStep : Action() data object PreviousStep : Action() - data class SetAlarmTime(val isAm: String, val hour: Int, val minute: Int) : Action() + data class SetAlarmTime(val newTime: LocalTime) : Action() data object CreateAlarm : Action() data class UpdateField(val value: String, val fieldType: FieldType) : Action() data object Reset : Action() data object Submit : Action() data class UpdateGender(val gender: String) : Action() data class UpdateBirthDate(val lunar: String, val year: Int, val month: Int, val day: Int) : Action() - data object ToggleBottomSheet : Action() + data object ShowBottomSheet : Action() + data object HideBottomSheet : Action() data object CompleteOnboarding : Action() data class OpenWebView(val url: String) : Action() data object ShowWarningDialog : Action() @@ -76,6 +71,10 @@ sealed class OnboardingContract { data object NavigateBack : SideEffect() + data object ShowBottomSheet : SideEffect() + + data object HideBottomSheet : SideEffect() + data object OnboardingCompleted : SideEffect() data class OpenWebView(val url: String) : SideEffect() diff --git a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingGenderScreen.kt b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingGenderScreen.kt index c9ba857d..01f0df00 100644 --- a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingGenderScreen.kt +++ b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingGenderScreen.kt @@ -1,5 +1,6 @@ package com.yapp.onboarding +import android.net.Uri import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -21,18 +22,27 @@ 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.navigation.navOptions import com.yapp.analytics.AnalyticsEvent import com.yapp.analytics.LocalAnalyticsHelper +import com.yapp.common.navigation.OrbitNavigator +import com.yapp.common.navigation.route.OnboardingBaseRoute import com.yapp.designsystem.theme.OrbitTheme import com.yapp.onboarding.component.UserInfoBottomSheet +import com.yapp.ui.component.bottomsheet.OrbitBottomSheetLayout +import com.yapp.ui.component.bottomsheet.OrbitBottomSheetState +import com.yapp.ui.component.bottomsheet.rememberOrbitBottomSheetState import com.yapp.ui.component.dialog.OrbitDialog import com.yapp.ui.toggle.OrbitGenderToggle import com.yapp.ui.utils.heightForScreenPercentage import com.yapp.ui.utils.paddingForScreenPercentage import feature.onboarding.R +import org.orbitmvi.orbit.compose.collectSideEffect @Composable fun OnboardingGenderRoute( + navigator: OrbitNavigator, + bottomSheetState: OrbitBottomSheetState, viewModel: OnboardingViewModel, ) { val state by viewModel.container.stateFlow.collectAsStateWithLifecycle() @@ -50,90 +60,152 @@ fun OnboardingGenderRoute( ) } - BackHandler { - viewModel.processAction(OnboardingContract.Action.PreviousStep) + viewModel.collectSideEffect { sideEffect -> + handleSideEffect( + sideEffect = sideEffect, + navigator = navigator, + bottomSheetState = bottomSheetState, + state = state, + processAction = viewModel::processAction, + ) } OnboardingGenderScreen( state = state, + bottomSheetState = bottomSheetState, currentStep = 5, totalSteps = 6, - onNextClick = { viewModel.processAction(OnboardingContract.Action.ToggleBottomSheet) }, - onBackClick = { viewModel.processAction(OnboardingContract.Action.PreviousStep) }, - onGenderSelect = { gender -> - analyticsHelper.logEvent( - AnalyticsEvent( - type = "onboarding_gender_select", - properties = mapOf( - AnalyticsEvent.OnboardingPropertiesKeys.GENDER to gender, - ), - ), - ) - viewModel.processAction(OnboardingContract.Action.UpdateGender(gender)) - }, - onDismissRequest = { - viewModel.processAction(OnboardingContract.Action.ToggleBottomSheet) - }, - onConfirmRequest = { - viewModel.processAction(OnboardingContract.Action.ToggleBottomSheet) - viewModel.processAction(OnboardingContract.Action.Submit) - }, + processAction = viewModel::processAction, ) } +private suspend fun handleSideEffect( + sideEffect: OnboardingContract.SideEffect, + navigator: OrbitNavigator, + bottomSheetState: OrbitBottomSheetState, + state: OnboardingContract.State, + processAction: (OnboardingContract.Action) -> Unit, +) { + when (sideEffect) { + is OnboardingContract.SideEffect.NavigateToNextStep -> { + navigator.navigateToOnboardingNextStep(sideEffect.currentStep) + } + + OnboardingContract.SideEffect.NavigateBack -> { + processAction(OnboardingContract.Action.Reset) + navigator.navigateBack() + } + + is OnboardingContract.SideEffect.ShowBottomSheet -> { + bottomSheetState.show { + UserInfoBottomSheet( + name = state.userName, + gender = state.selectedGender ?: "๋ฌด์ง€๊ฐœ", + birthDate = state.birthDateFormatted, + birthTime = state.birthTimeFormatted, + onDismiss = { + processAction(OnboardingContract.Action.HideBottomSheet) + }, + onConfirm = { + processAction(OnboardingContract.Action.HideBottomSheet) + processAction(OnboardingContract.Action.Submit) + }, + ) + } + } + + is OnboardingContract.SideEffect.HideBottomSheet -> { + bottomSheetState.hide() + } + + OnboardingContract.SideEffect.OnboardingCompleted -> { + navigator.navigateToHome( + navOptions = navOptions { + popUpTo { + inclusive = true + } + }, + ) + } + + is OnboardingContract.SideEffect.OpenWebView -> { + navigator.navigateToWebView(Uri.encode(sideEffect.url)) + } + } +} + @OptIn(ExperimentalMaterial3Api::class) @Composable fun OnboardingGenderScreen( state: OnboardingContract.State, + bottomSheetState: OrbitBottomSheetState, currentStep: Int, totalSteps: Int, - onNextClick: () -> Unit, - onBackClick: () -> Unit, - onGenderSelect: (String) -> Unit, - onDismissRequest: () -> Unit, - onConfirmRequest: () -> Unit, + processAction: (OnboardingContract.Action) -> Unit, + logEvent: (AnalyticsEvent) -> Unit = { }, ) { - OnboardingScreen( - currentStep = currentStep, - totalSteps = totalSteps, - isButtonEnabled = state.selectedGender != null, - onNextClick = onNextClick, - onBackClick = onBackClick, - buttonLabel = "๋‹ค์Œ", - ) { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Spacer(modifier = Modifier.heightForScreenPercentage(0.05f)) - Text( - text = stringResource(id = R.string.onboarding_step6_text_title), - style = OrbitTheme.typography.heading1SemiBold, - color = OrbitTheme.colors.white, - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - ) + BackHandler { + if (state.isShowWarningDialog) { + processAction(OnboardingContract.Action.HideWarningDialog) + } else if (bottomSheetState.state.isVisible) { + processAction(OnboardingContract.Action.HideBottomSheet) + } else { + processAction(OnboardingContract.Action.PreviousStep) + } + } - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 38.dp) - .paddingForScreenPercentage(topPercentage = 0.11f), - horizontalArrangement = Arrangement.spacedBy(15.dp), + OrbitBottomSheetLayout(sheetState = bottomSheetState) { + OnboardingScreen( + currentStep = currentStep, + totalSteps = totalSteps, + isButtonEnabled = state.selectedGender != null, + onNextClick = { + processAction(OnboardingContract.Action.ShowBottomSheet) + }, + onBackClick = { + processAction(OnboardingContract.Action.PreviousStep) + }, + buttonLabel = "๋‹ค์Œ", + ) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, ) { - Box(modifier = Modifier.weight(1f)) { - OrbitGenderToggle( - label = "๋‚จ์„ฑ", - isSelected = state.selectedGender == "๋‚จ์„ฑ", - onToggle = { onGenderSelect("๋‚จ์„ฑ") }, - ) - } - Box(modifier = Modifier.weight(1f)) { - OrbitGenderToggle( - label = "์—ฌ์„ฑ", - isSelected = state.selectedGender == "์—ฌ์„ฑ", - onToggle = { onGenderSelect("์—ฌ์„ฑ") }, - ) + Spacer(modifier = Modifier.heightForScreenPercentage(0.05f)) + Text( + text = stringResource(id = R.string.onboarding_step6_text_title), + style = OrbitTheme.typography.heading1SemiBold, + color = OrbitTheme.colors.white, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 38.dp) + .paddingForScreenPercentage(topPercentage = 0.11f), + horizontalArrangement = Arrangement.spacedBy(15.dp), + ) { + listOf("๋‚จ์„ฑ", "์—ฌ์„ฑ").forEach { gender -> + Box(modifier = Modifier.weight(1f)) { + OrbitGenderToggle( + label = gender, + isSelected = state.selectedGender == gender, + onToggle = { + logEvent( + AnalyticsEvent( + type = "onboarding_gender_select", + properties = mapOf( + AnalyticsEvent.OnboardingPropertiesKeys.GENDER to gender, + ), + ), + ) + processAction(OnboardingContract.Action.UpdateGender(gender)) + }, + ) + } + } } } } @@ -144,21 +216,9 @@ fun OnboardingGenderScreen( title = stringResource(id = R.string.onboarding_warning_dialog_title), message = stringResource(id = R.string.onboarding_warning_dialog_message), confirmText = stringResource(id = R.string.onboarding_warning_dialog_btn_confirm), - onConfirm = { - onConfirmRequest() - }, + onConfirm = { processAction(OnboardingContract.Action.HideWarningDialog) }, ) } - - UserInfoBottomSheet( - isSheetOpen = state.isBottomSheetOpen, - onDismissRequest = onDismissRequest, - onConfirmRequest = onConfirmRequest, - name = state.userName, - gender = state.selectedGender ?: "๋ฌด์ง€๊ฐœ", - birthDate = state.birthDateFormatted, - birthTime = state.birthTimeFormatted, - ) } @Composable @@ -168,12 +228,9 @@ fun OnboardingGenderScreenPreview() { state = OnboardingContract.State( isButtonEnabled = true, ), + bottomSheetState = rememberOrbitBottomSheetState(), currentStep = 0, totalSteps = 0, - onNextClick = {}, - onBackClick = {}, - onGenderSelect = {}, - onDismissRequest = {}, - onConfirmRequest = {}, + processAction = {}, ) } diff --git a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingNavGraph.kt b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingNavGraph.kt index ee57af2e..a642d861 100644 --- a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingNavGraph.kt +++ b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingNavGraph.kt @@ -1,7 +1,6 @@ package com.yapp.onboarding import android.net.Uri -import androidx.compose.runtime.LaunchedEffect import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import androidx.navigation.navOptions @@ -10,108 +9,106 @@ import com.yapp.common.navigation.OrbitNavigator import com.yapp.common.navigation.extensions.sharedHiltViewModel import com.yapp.common.navigation.route.OnboardingBaseRoute import com.yapp.common.navigation.route.OnboardingDestination -import kotlinx.coroutines.flow.collectLatest +import com.yapp.ui.component.bottomsheet.OrbitBottomSheetState +import org.orbitmvi.orbit.compose.collectSideEffect fun NavGraphBuilder.onboardingNavGraph( navigator: OrbitNavigator, + bottomSheetState: OrbitBottomSheetState, ) { navigation(startDestination = OnboardingDestination.Explain) { composable { val viewModel = it.sharedHiltViewModel(navigator.navController) - LaunchedEffect(viewModel) { - viewModel.container.sideEffectFlow.collectLatest { sideEffect -> - handleSideEffect(sideEffect, navigator, viewModel) - } + + viewModel.collectSideEffect { sideEffect -> + handleOnboardingCommonSideEffect(sideEffect, navigator, viewModel::processAction) } + OnboardingExplainRoute(viewModel) } composable { val viewModel = it.sharedHiltViewModel(navigator.navController) - LaunchedEffect(viewModel) { - viewModel.container.sideEffectFlow.collectLatest { sideEffect -> - handleSideEffect(sideEffect, navigator, viewModel) - } + + viewModel.collectSideEffect { sideEffect -> + handleOnboardingCommonSideEffect(sideEffect, navigator, viewModel::processAction) } + OnboardingAlarmTimeSelectionRoute(viewModel) } composable { val viewModel = it.sharedHiltViewModel(navigator.navController) - LaunchedEffect(viewModel) { - viewModel.container.sideEffectFlow.collectLatest { sideEffect -> - handleSideEffect(sideEffect, navigator, viewModel) - } + + viewModel.collectSideEffect { sideEffect -> + handleOnboardingCommonSideEffect(sideEffect, navigator, viewModel::processAction) } + OnboardingBirthdayRoute(viewModel) } composable { val viewModel = it.sharedHiltViewModel(navigator.navController) - LaunchedEffect(viewModel) { - viewModel.container.sideEffectFlow.collectLatest { sideEffect -> - handleSideEffect(sideEffect, navigator, viewModel) - } + + viewModel.collectSideEffect { sideEffect -> + handleOnboardingCommonSideEffect(sideEffect, navigator, viewModel::processAction) } + OnboardingTimeOfBirthRoute(viewModel) } composable { val viewModel = it.sharedHiltViewModel(navigator.navController) - LaunchedEffect(viewModel) { - viewModel.container.sideEffectFlow.collectLatest { sideEffect -> - handleSideEffect(sideEffect, navigator, viewModel) - } + + viewModel.collectSideEffect { sideEffect -> + handleOnboardingCommonSideEffect(sideEffect, navigator, viewModel::processAction) } + OnboardingNameRoute(viewModel) } composable { val viewModel = it.sharedHiltViewModel(navigator.navController) - LaunchedEffect(viewModel) { - viewModel.container.sideEffectFlow.collectLatest { sideEffect -> - handleSideEffect(sideEffect, navigator, viewModel) - } - } - OnboardingGenderRoute(viewModel) + + OnboardingGenderRoute(navigator, bottomSheetState, viewModel) } composable { val viewModel = it.sharedHiltViewModel(navigator.navController) - LaunchedEffect(viewModel) { - viewModel.container.sideEffectFlow.collectLatest { sideEffect -> - handleSideEffect(sideEffect, navigator, viewModel) - } + + viewModel.collectSideEffect { sideEffect -> + handleOnboardingCommonSideEffect(sideEffect, navigator, viewModel::processAction) } + OnboardingAccessRoute(viewModel) } composable { val viewModel = it.sharedHiltViewModel(navigator.navController) - LaunchedEffect(viewModel) { - viewModel.container.sideEffectFlow.collectLatest { sideEffect -> - handleSideEffect(sideEffect, navigator, viewModel) - } + + viewModel.collectSideEffect { sideEffect -> + handleOnboardingCommonSideEffect(sideEffect, navigator, viewModel::processAction) } + OnboardingCompleteRoute(viewModel) } composable { val viewModel = it.sharedHiltViewModel(navigator.navController) - LaunchedEffect(viewModel) { - viewModel.container.sideEffectFlow.collectLatest { sideEffect -> - handleSideEffect(sideEffect, navigator, viewModel) - } + + viewModel.collectSideEffect { sideEffect -> + handleOnboardingCommonSideEffect(sideEffect, navigator, viewModel::processAction) } + OnboardingCompleteRoute2(viewModel) } } } -private fun handleSideEffect( +private fun handleOnboardingCommonSideEffect( sideEffect: OnboardingContract.SideEffect, navigator: OrbitNavigator, - viewModel: OnboardingViewModel, + processAction: (OnboardingContract.Action) -> Unit, ) { when (sideEffect) { is OnboardingContract.SideEffect.NavigateToNextStep -> { @@ -119,14 +116,14 @@ private fun handleSideEffect( } OnboardingContract.SideEffect.NavigateBack -> { - viewModel.processAction(OnboardingContract.Action.Reset) + processAction(OnboardingContract.Action.Reset) navigator.navigateBack() } OnboardingContract.SideEffect.OnboardingCompleted -> { navigator.navigateToHome( navOptions = navOptions { - popUpTo(OnboardingBaseRoute) { + popUpTo { inclusive = true } }, @@ -136,5 +133,7 @@ private fun handleSideEffect( is OnboardingContract.SideEffect.OpenWebView -> { navigator.navigateToWebView(Uri.encode(sideEffect.url)) } + + else -> { } } } diff --git a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingViewModel.kt b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingViewModel.kt index ec145349..13fc35eb 100644 --- a/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingViewModel.kt +++ b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingViewModel.kt @@ -2,21 +2,26 @@ package com.yapp.onboarding import android.util.Log import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.viewModelScope +import androidx.lifecycle.ViewModel import com.yapp.analytics.AnalyticsEvent import com.yapp.analytics.AnalyticsHelper import com.yapp.common.navigation.route.OnboardingDestination -import com.yapp.datastore.UserPreferences import com.yapp.domain.model.Alarm import com.yapp.domain.model.AlarmDay import com.yapp.domain.model.toRepeatDays import com.yapp.domain.repository.SignUpRepository +import com.yapp.domain.repository.UserInfoRepository import com.yapp.domain.usecase.AlarmUseCase import com.yapp.media.haptic.HapticFeedbackManager import com.yapp.media.haptic.HapticType -import com.yapp.ui.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.syntax.simple.intent +import org.orbitmvi.orbit.syntax.simple.postSideEffect +import org.orbitmvi.orbit.syntax.simple.reduce +import org.orbitmvi.orbit.viewmodel.container +import java.time.LocalTime import javax.inject.Inject import kotlin.reflect.KClass @@ -24,32 +29,36 @@ import kotlin.reflect.KClass class OnboardingViewModel @Inject constructor( private val analyticsHelper: AnalyticsHelper, private val signUpRepository: SignUpRepository, - private val userPreferences: UserPreferences, + private val userInfoRepository: UserInfoRepository, private val alarmUseCase: AlarmUseCase, private val hapticFeedbackManager: HapticFeedbackManager, private val savedStateHandle: SavedStateHandle, -) : BaseViewModel( - initialState = OnboardingContract.State( - currentStep = savedStateHandle["currentStep"] ?: 1, - birthDate = savedStateHandle["birthDate"] ?: "", - birthType = savedStateHandle["birthType"] ?: "์–‘๋ ฅ", - ), -) { +) : ViewModel(), ContainerHost { + + override val container: Container = container( + initialState = OnboardingContract.State( + currentStep = savedStateHandle["currentStep"] ?: 1, + birthDate = savedStateHandle["birthDate"] ?: "", + birthType = savedStateHandle["birthType"] ?: "์–‘๋ ฅ", + ), + ) + private val currentRoute: KClass? - get() = OnboardingDestination.routes.getOrNull(currentState.currentStep) + get() = OnboardingDestination.routes.getOrNull(container.stateFlow.value.currentStep) fun processAction(action: OnboardingContract.Action) { when (action) { is OnboardingContract.Action.NextStep -> moveToNextStep() is OnboardingContract.Action.PreviousStep -> moveToPreviousStep() - is OnboardingContract.Action.SetAlarmTime -> setAlarmTime(action.isAm, action.hour, action.minute) + is OnboardingContract.Action.SetAlarmTime -> setAlarmTime(action.newTime) is OnboardingContract.Action.CreateAlarm -> createAlarm() is OnboardingContract.Action.UpdateField -> updateField(action.value, action.fieldType) is OnboardingContract.Action.UpdateBirthDate -> updateBirthDate(action.lunar, action.year, action.month, action.day) is OnboardingContract.Action.Reset -> resetFields() is OnboardingContract.Action.Submit -> submitUserInfo() is OnboardingContract.Action.UpdateGender -> updateGender(action.gender) - is OnboardingContract.Action.ToggleBottomSheet -> toggleBottomSheet() + is OnboardingContract.Action.ShowBottomSheet -> showBottomSheet() + is OnboardingContract.Action.HideBottomSheet -> hideBottomSheet() is OnboardingContract.Action.CompleteOnboarding -> completeOnboarding() is OnboardingContract.Action.OpenWebView -> openWebView(action.url) is OnboardingContract.Action.ShowWarningDialog -> showWarningDialog() @@ -57,122 +66,103 @@ class OnboardingViewModel @Inject constructor( } } - private fun submitUserInfo() { - viewModelScope.launch { - val state = container.stateFlow.value - - val result = signUpRepository.postSignUp( - name = state.userName, - calendarType = state.birthType, - birthDate = state.birthDate, - birthTime = state.birthTime, - gender = state.selectedGender ?: "", - ) + private fun submitUserInfo() = intent { + val result = signUpRepository.postSignUp( + name = state.userName, + calendarType = state.birthType, + birthDate = state.birthDate, + birthTime = state.birthTime, + gender = state.selectedGender ?: "", + ) - if (result.isSuccess) { - val userId = result.getOrNull() ?: return@launch - val userName = state.userName - userPreferences.saveUserId(userId) - userPreferences.saveUserName(userName) - - analyticsHelper.setUserId(userId) - analyticsHelper.logEvent( - AnalyticsEvent( - type = "onboarding_complete", - properties = mapOf( - AnalyticsEvent.OnboardingPropertiesKeys.STEP to "ํ™˜์˜2", - ), + if (result.isSuccess) { + val userId = result.getOrNull() ?: return@intent + val userName = state.userName + userInfoRepository.saveUserId(userId) + userInfoRepository.saveUserName(userName) + + analyticsHelper.setUserId(userId) + analyticsHelper.logEvent( + AnalyticsEvent( + type = "onboarding_complete", + properties = mapOf( + AnalyticsEvent.OnboardingPropertiesKeys.STEP to "ํ™˜์˜2", ), - ) + ), + ) - updateState { copy(isBottomSheetOpen = false) } - moveToNextStep() - } else { - processAction(OnboardingContract.Action.ShowWarningDialog) - } + moveToNextStep() + } else { + showWarningDialog() } } - private fun moveToNextStep() { - val currentStep = container.stateFlow.value.currentStep + private fun moveToNextStep() = intent { + val currentStep = state.currentStep val nextStep = currentStep + 1 val nextRoute = OnboardingDestination.getNextRouteForStep(currentStep) - savedStateHandle["birthDate"] = currentState.birthDate - savedStateHandle["birthType"] = currentState.birthType + savedStateHandle["birthDate"] = state.birthDate + savedStateHandle["birthType"] = state.birthType if (nextRoute != null) { savedStateHandle["currentStep"] = nextStep - updateState { copy(currentStep = nextStep) } - emitSideEffect(OnboardingContract.SideEffect.NavigateToNextStep(currentStep)) + reduce { state.copy(currentStep = nextStep) } + postSideEffect(OnboardingContract.SideEffect.NavigateToNextStep(currentStep)) } else { - emitSideEffect(OnboardingContract.SideEffect.OnboardingCompleted) + postSideEffect(OnboardingContract.SideEffect.OnboardingCompleted) } } - private fun moveToPreviousStep() { - val currentStep = container.stateFlow.value.currentStep + private fun moveToPreviousStep() = intent { + val currentStep = state.currentStep if (currentStep > 1) { val previousStep = currentStep - 1 savedStateHandle["currentStep"] = previousStep - updateState { copy(currentStep = previousStep) } - emitSideEffect(OnboardingContract.SideEffect.NavigateBack) + reduce { state.copy(currentStep = previousStep) } + postSideEffect(OnboardingContract.SideEffect.NavigateBack) } } - private fun setAlarmTime(amPm: String, hour: Int, minute: Int) { + private fun setAlarmTime(newTime: LocalTime) = intent { hapticFeedbackManager.performHapticFeedback(HapticType.LIGHT_TICK) - val newTimeState = currentState.timeState.copy( - selectedAmPm = amPm, - selectedHour = hour, - selectedMinute = minute, - ) - updateState { - copy( - timeState = newTimeState, - ) - } + reduce { state.copy(selectedTime = newTime) } } - private fun createAlarm() { - viewModelScope.launch { - alarmUseCase.getAlarmSounds().onSuccess { sounds -> - val defaultSoundIndex = sounds.indexOfFirst { it.title == "Homecoming" }.takeIf { it >= 0 } ?: 0 - val defaultSoundUri = sounds[defaultSoundIndex] - - val newAlarm = Alarm( - isAm = currentState.timeState.selectedAmPm == "์˜ค์ „", - hour = currentState.timeState.selectedHour, - minute = currentState.timeState.selectedMinute, - repeatDays = setOf(AlarmDay.MON, AlarmDay.TUE, AlarmDay.WED, AlarmDay.THU, AlarmDay.FRI).toRepeatDays(), - isSnoozeEnabled = true, - snoozeInterval = 5, - snoozeCount = 5, - soundUri = "${defaultSoundUri.uri}", - ) - - alarmUseCase.insertAlarm( - alarm = newAlarm, - ).onSuccess { - emitSideEffect(OnboardingContract.SideEffect.OnboardingCompleted) - }.onFailure { - Log.e("OnboardingViewModel", "Failed to create alarm", it) - } - }.onFailure { - Log.e("OnboardingViewModel", "Failed to get alarm sounds", it) + private fun createAlarm() = intent { + alarmUseCase.getAlarmSounds().onSuccess { sounds -> + val defaultSoundIndex = sounds.indexOfFirst { it.title == "Homecoming" }.takeIf { it >= 0 } ?: 0 + val defaultSoundUri = sounds[defaultSoundIndex] + + val newAlarm = Alarm( + hour = state.selectedTime.hour, + minute = state.selectedTime.minute, + repeatDays = setOf(AlarmDay.MON, AlarmDay.TUE, AlarmDay.WED, AlarmDay.THU, AlarmDay.FRI).toRepeatDays(), + isSnoozeEnabled = true, + snoozeInterval = 5, + snoozeCount = 5, + soundUri = "${defaultSoundUri.uri}", + ) + + alarmUseCase.insertAlarm( + alarm = newAlarm, + ).onFailure { + Log.e("OnboardingViewModel", "Failed to create alarm", it) } + }.onFailure { + Log.e("OnboardingViewModel", "Failed to get alarm sounds", it) } } - private fun updateField(value: String, fieldType: OnboardingContract.FieldType) { + private fun updateField(value: String, fieldType: OnboardingContract.FieldType) = intent { when (fieldType) { OnboardingContract.FieldType.TIME -> { val isComplete = value.length == 5 val isValid = isComplete && value.matches(fieldType.validationRegex) - updateState { - copy( + reduce { + state.copy( textFieldValue = value, birthTime = if (isValid) value else "", showWarning = isComplete && !isValid, @@ -187,8 +177,8 @@ class OnboardingViewModel @Inject constructor( val truncatedValue = OnboardingContract.truncateTextToLimit(value) val isValid = truncatedValue.matches(fieldType.validationRegex) - updateState { - copy( + reduce { + state.copy( textFieldValue = truncatedValue, userName = truncatedValue, showWarning = !isValid, @@ -200,8 +190,8 @@ class OnboardingViewModel @Inject constructor( } } - private fun updateBirthDate(lunar: String, year: Int, month: Int, day: Int) { - if (currentRoute != OnboardingDestination.Birthday::class) return + private fun updateBirthDate(lunar: String, year: Int, month: Int, day: Int) = intent { + if (currentRoute != OnboardingDestination.Birthday::class) return@intent val formattedDate = "$year-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}" @@ -209,8 +199,8 @@ class OnboardingViewModel @Inject constructor( savedStateHandle["birthDate"] = formattedDate savedStateHandle["birthType"] = lunar - updateState { - copy( + reduce { + state.copy( birthDate = formattedDate, birthType = lunar, isBirthDateValid = true, @@ -218,9 +208,9 @@ class OnboardingViewModel @Inject constructor( } } - private fun resetFields() { - updateState { - copy( + private fun resetFields() = intent { + reduce { + state.copy( textFieldValue = "", showWarning = false, isButtonEnabled = false, @@ -229,31 +219,32 @@ class OnboardingViewModel @Inject constructor( } } - private fun updateGender(gender: String) { - updateState { copy(selectedGender = gender, isButtonEnabled = true) } + private fun updateGender(gender: String) = intent { + reduce { state.copy(selectedGender = gender, isButtonEnabled = true) } } - private fun toggleBottomSheet() { - val isCurrentlyOpen = container.stateFlow.value.isBottomSheetOpen - updateState { copy(isBottomSheetOpen = !isCurrentlyOpen) } + private fun showBottomSheet() = intent { + postSideEffect(OnboardingContract.SideEffect.ShowBottomSheet) } - private fun completeOnboarding() { - viewModelScope.launch { - userPreferences.setOnboardingCompleted() - emitSideEffect(OnboardingContract.SideEffect.OnboardingCompleted) - } + private fun hideBottomSheet() = intent { + postSideEffect(OnboardingContract.SideEffect.HideBottomSheet) + } + + private fun completeOnboarding() = intent { + userInfoRepository.setOnboardingCompleted() + postSideEffect(OnboardingContract.SideEffect.OnboardingCompleted) } - private fun openWebView(url: String) { - emitSideEffect(OnboardingContract.SideEffect.OpenWebView(url)) + private fun openWebView(url: String) = intent { + postSideEffect(OnboardingContract.SideEffect.OpenWebView(url)) } - private fun showWarningDialog() { - updateState { copy(isShowWarningDialog = true) } + private fun showWarningDialog() = intent { + reduce { state.copy(isShowWarningDialog = true) } } - private fun hideWarningDialog() { - updateState { copy(isShowWarningDialog = false) } + private fun hideWarningDialog() = intent { + reduce { state.copy(isShowWarningDialog = false) } } } diff --git a/feature/onboarding/src/main/java/com/yapp/onboarding/component/UserInfoBottomSheet.kt b/feature/onboarding/src/main/java/com/yapp/onboarding/component/UserInfoBottomSheet.kt index ce6b6629..af935235 100644 --- a/feature/onboarding/src/main/java/com/yapp/onboarding/component/UserInfoBottomSheet.kt +++ b/feature/onboarding/src/main/java/com/yapp/onboarding/component/UserInfoBottomSheet.kt @@ -7,9 +7,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.SheetState import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -17,7 +15,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.yapp.designsystem.theme.OrbitTheme -import com.yapp.ui.component.OrbitBottomSheet import com.yapp.ui.component.button.OrbitButton import com.yapp.ui.utils.paddingForScreenPercentage import com.yapp.ui.utils.widthForScreenPercentage @@ -26,80 +23,70 @@ import feature.onboarding.R @OptIn(ExperimentalMaterial3Api::class) @Composable fun UserInfoBottomSheet( - sheetState: SheetState = rememberModalBottomSheetState(), - isSheetOpen: Boolean, - onDismissRequest: () -> Unit, - onConfirmRequest: () -> Unit, name: String, gender: String, birthDate: String, birthTime: String, + onDismiss: () -> Unit, + onConfirm: () -> Unit, ) { - if (isSheetOpen) { - OrbitBottomSheet( - isSheetOpen = isSheetOpen, - sheetState = sheetState, - onDismissRequest = onDismissRequest, - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .paddingForScreenPercentage(allPercentage = 0.03f), - ) { - Text( - text = stringResource(R.string.onboarding_step6_bs_title), - modifier = Modifier - .paddingForScreenPercentage( - topPercentage = 0.005f, - bottomPercentage = 0.027f, - ), - style = OrbitTheme.typography.heading2SemiBold, - color = OrbitTheme.colors.white, - ) - UserInfoRow(label = stringResource(R.string.onboarding_step6_bs_name), value = name) - UserInfoRow( - label = stringResource(R.string.onboarding_step6_bs_gender), - value = gender, - ) - UserInfoRow( - label = stringResource(R.string.onboarding_step6_bs_birth), - value = birthDate, - ) - UserInfoRow( - label = stringResource(R.string.onboarding_step6_bs_time), - value = birthTime, - ) + Column( + modifier = Modifier + .fillMaxWidth() + .paddingForScreenPercentage(allPercentage = 0.03f), + ) { + Text( + text = stringResource(R.string.onboarding_step6_bs_title), + modifier = Modifier + .paddingForScreenPercentage( + topPercentage = 0.005f, + bottomPercentage = 0.027f, + ), + style = OrbitTheme.typography.heading2SemiBold, + color = OrbitTheme.colors.white, + ) + UserInfoRow(label = stringResource(R.string.onboarding_step6_bs_name), value = name) + UserInfoRow( + label = stringResource(R.string.onboarding_step6_bs_gender), + value = gender, + ) + UserInfoRow( + label = stringResource(R.string.onboarding_step6_bs_birth), + value = birthDate, + ) + UserInfoRow( + label = stringResource(R.string.onboarding_step6_bs_time), + value = birthTime, + ) - Row( - modifier = Modifier - .fillMaxWidth() - .paddingForScreenPercentage(topPercentage = 0.032f), - verticalAlignment = Alignment.CenterVertically, - ) { - OrbitButton( - label = stringResource(R.string.onboarding_step6_bs_btn_dismiss), - modifier = Modifier.weight(1f), - onClick = onDismissRequest, - enabled = true, - containerColor = OrbitTheme.colors.gray_600, - contentColor = OrbitTheme.colors.white, - pressedContainerColor = OrbitTheme.colors.gray_500, - pressedContentColor = OrbitTheme.colors.white.copy(alpha = 0.7f), - shape = RoundedCornerShape(12.dp), - ) - Spacer(modifier = Modifier.widthForScreenPercentage(0.032f)) - OrbitButton( - label = stringResource(R.string.onboarding_step6_bs_btn_confirm), - modifier = Modifier.weight(1f), - onClick = onConfirmRequest, - enabled = true, - pressedContainerColor = OrbitTheme.colors.main.copy(alpha = 0.8f), - pressedContentColor = OrbitTheme.colors.gray_600, - shape = RoundedCornerShape(12.dp), + Row( + modifier = Modifier + .fillMaxWidth() + .paddingForScreenPercentage(topPercentage = 0.032f), + verticalAlignment = Alignment.CenterVertically, + ) { + OrbitButton( + label = stringResource(R.string.onboarding_step6_bs_btn_dismiss), + modifier = Modifier.weight(1f), + onClick = onDismiss, + enabled = true, + containerColor = OrbitTheme.colors.gray_600, + contentColor = OrbitTheme.colors.white, + pressedContainerColor = OrbitTheme.colors.gray_500, + pressedContentColor = OrbitTheme.colors.white.copy(alpha = 0.7f), + shape = RoundedCornerShape(12.dp), + ) + Spacer(modifier = Modifier.widthForScreenPercentage(0.032f)) + OrbitButton( + label = stringResource(R.string.onboarding_step6_bs_btn_confirm), + modifier = Modifier.weight(1f), + onClick = onConfirm, + enabled = true, + pressedContainerColor = OrbitTheme.colors.main.copy(alpha = 0.8f), + pressedContentColor = OrbitTheme.colors.gray_600, + shape = RoundedCornerShape(12.dp), - ) - } - } + ) } } } @@ -134,12 +121,11 @@ fun UserInfoRow( @Preview fun UserInfoBottomSheetPreview() { UserInfoBottomSheet( - isSheetOpen = true, - onDismissRequest = { }, - onConfirmRequest = { }, name = "ํ™๊ธธ๋™", gender = "๋‚จ์„ฑ", birthDate = "1990๋…„ 1์›” 1์ผ", birthTime = "12:00", + onDismiss = { }, + onConfirm = { }, ) } diff --git a/feature/setting/build.gradle.kts b/feature/setting/build.gradle.kts index 96e74f94..5ea1d850 100644 --- a/feature/setting/build.gradle.kts +++ b/feature/setting/build.gradle.kts @@ -13,7 +13,6 @@ dependencies { implementation(projects.core.common) implementation(projects.core.analytics) implementation(projects.domain) - implementation(projects.core.datastore) implementation(libs.orbit.core) implementation(libs.orbit.compose) implementation(libs.orbit.viewmodel) diff --git a/feature/setting/src/main/java/com/yapp/setting/EditBirthdayScreen.kt b/feature/setting/src/main/java/com/yapp/setting/EditBirthdayScreen.kt index 1870ceea..b8e2116b 100644 --- a/feature/setting/src/main/java/com/yapp/setting/EditBirthdayScreen.kt +++ b/feature/setting/src/main/java/com/yapp/setting/EditBirthdayScreen.kt @@ -32,14 +32,14 @@ fun EditBirthdayRoute( EditBirthdayScreen( state = state, - onBack = { viewModel.onAction(SettingContract.Action.PreviousStep) }, + onBack = { viewModel.processAction(SettingContract.Action.PreviousStep) }, onConfirmExit = { - viewModel.onAction(SettingContract.Action.HideDialog) - viewModel.onAction(SettingContract.Action.PreviousStep) + viewModel.processAction(SettingContract.Action.HideDialog) + viewModel.processAction(SettingContract.Action.PreviousStep) }, - onCancelDialog = { viewModel.onAction(SettingContract.Action.HideDialog) }, + onCancelDialog = { viewModel.processAction(SettingContract.Action.HideDialog) }, onUpdateBirthDate = { lunar, year, month, day -> - viewModel.onAction( + viewModel.processAction( SettingContract.Action.UpdateBirthDate( lunar, year, @@ -48,7 +48,7 @@ fun EditBirthdayRoute( ), ) }, - onConfirm = { viewModel.onAction(SettingContract.Action.ConfirmAndNavigateBack) }, + onConfirm = { viewModel.processAction(SettingContract.Action.ConfirmAndNavigateBack) }, ) } diff --git a/feature/setting/src/main/java/com/yapp/setting/EditProfileScreen.kt b/feature/setting/src/main/java/com/yapp/setting/EditProfileScreen.kt index 6df4257a..f65daca3 100644 --- a/feature/setting/src/main/java/com/yapp/setting/EditProfileScreen.kt +++ b/feature/setting/src/main/java/com/yapp/setting/EditProfileScreen.kt @@ -55,36 +55,36 @@ fun EditProfileRoute( LaunchedEffect(state.shouldFetchUserInfo) { if (state.shouldFetchUserInfo) { - viewModel.onAction(SettingContract.Action.RefreshUserInfo) + viewModel.processAction(SettingContract.Action.RefreshUserInfo) } } EditProfileScreen( state = state, - onBack = { viewModel.onAction(SettingContract.Action.ShowDialog) }, - onUpdateName = { name -> viewModel.onAction(SettingContract.Action.UpdateName(name)) }, - onToggleGender = { isMale -> viewModel.onAction(SettingContract.Action.ToggleGender(isMale)) }, + onBack = { viewModel.processAction(SettingContract.Action.ShowDialog) }, + onUpdateName = { name -> viewModel.processAction(SettingContract.Action.UpdateName(name)) }, + onToggleGender = { isMale -> viewModel.processAction(SettingContract.Action.ToggleGender(isMale)) }, onToggleTimeUnknown = { isChecked -> - viewModel.onAction( + viewModel.processAction( SettingContract.Action.ToggleTimeUnknown( isChecked, ), ) }, onUpdateTimeOfBirth = { time -> - viewModel.onAction( + viewModel.processAction( SettingContract.Action.UpdateTimeOfBirth( time, ), ) }, - onNavigateToEditBirthday = { viewModel.onAction(SettingContract.Action.NavigateToEditBirthday) }, + onNavigateToEditBirthday = { viewModel.processAction(SettingContract.Action.NavigateToEditBirthday) }, onConfirmExit = { - viewModel.onAction(SettingContract.Action.HideDialog) - viewModel.onAction(SettingContract.Action.PreviousStep) + viewModel.processAction(SettingContract.Action.HideDialog) + viewModel.processAction(SettingContract.Action.PreviousStep) }, - onCancelDialog = { viewModel.onAction(SettingContract.Action.HideDialog) }, - onSaveUserInfo = { viewModel.onAction(SettingContract.Action.SubmitUserInfo) }, + onCancelDialog = { viewModel.processAction(SettingContract.Action.HideDialog) }, + onSaveUserInfo = { viewModel.processAction(SettingContract.Action.SubmitUserInfo) }, ) } diff --git a/feature/setting/src/main/java/com/yapp/setting/EditProfileViewModel.kt b/feature/setting/src/main/java/com/yapp/setting/EditProfileViewModel.kt index 166388b3..3dfdcaf2 100644 --- a/feature/setting/src/main/java/com/yapp/setting/EditProfileViewModel.kt +++ b/feature/setting/src/main/java/com/yapp/setting/EditProfileViewModel.kt @@ -1,25 +1,29 @@ package com.yapp.setting import android.util.Log -import androidx.lifecycle.viewModelScope -import com.yapp.datastore.UserPreferences +import androidx.lifecycle.ViewModel import com.yapp.domain.model.EditUser import com.yapp.domain.repository.UserInfoRepository -import com.yapp.ui.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.launch +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.syntax.simple.intent +import org.orbitmvi.orbit.syntax.simple.postSideEffect +import org.orbitmvi.orbit.syntax.simple.reduce +import org.orbitmvi.orbit.viewmodel.container import javax.inject.Inject @HiltViewModel class EditProfileViewModel @Inject constructor( private val userInfoRepository: UserInfoRepository, - private val userPreferences: UserPreferences, -) : BaseViewModel( - SettingContract.State(), -) { - fun onAction(action: SettingContract.Action) = intent { +) : ViewModel(), ContainerHost { + + override val container: Container = container( + initialState = SettingContract.State(), + ) + + fun processAction(action: SettingContract.Action) { when (action) { is SettingContract.Action.UpdateName -> updateName(action.name) is SettingContract.Action.UpdateBirthDate -> updateBirthDate(action) @@ -28,63 +32,73 @@ class EditProfileViewModel @Inject constructor( is SettingContract.Action.ToggleGender -> toggleGender(action.isMale) is SettingContract.Action.ToggleTimeUnknown -> toggleTimeUnknown(action.isChecked) is SettingContract.Action.UpdateTimeOfBirth -> updateTimeOfBirth(action.time) - is SettingContract.Action.ConfirmAndNavigateBack -> emitSideEffect(SettingContract.SideEffect.NavigateBack) - is SettingContract.Action.Reset -> updateState { SettingContract.State() } - SettingContract.Action.ShowDialog -> updateState { copy(isDialogVisible = true) } - SettingContract.Action.HideDialog -> updateState { copy(isDialogVisible = false) } + is SettingContract.Action.ConfirmAndNavigateBack -> navigateBack() + is SettingContract.Action.Reset -> resetState() + SettingContract.Action.ShowDialog -> showDialog() + SettingContract.Action.HideDialog -> hideDialog() SettingContract.Action.PreviousStep -> previousStep() SettingContract.Action.SubmitUserInfo -> submitUserInfo() is SettingContract.Action.NavigateToEditBirthday -> navigateToEditBirthday() - is SettingContract.Action.RefreshUserInfo -> { - if (currentState.shouldFetchUserInfo) { - refreshUserInfo() - } - } + is SettingContract.Action.RefreshUserInfo -> refreshUserInfo() else -> {} } } - private fun updateName(name: String) = updateState { - copy(name = name, isNameValid = validateName(name)) + private fun updateName(name: String) = intent { + reduce { + state.copy(name = name, isNameValid = validateName(name)) + } } private fun validateName(name: String): Boolean { return SettingContract.FieldType.NAME.validationRegex.matches(name) } - private fun updateBirthDate(action: SettingContract.Action.UpdateBirthDate) = updateState { - val formattedDate = "${action.year}-${action.month.toString().padStart(2, '0')}-${ - action.day.toString().padStart(2, '0') - }" - copy(birthDate = formattedDate) + private fun updateBirthDate(action: SettingContract.Action.UpdateBirthDate) = intent { + reduce { + val formattedDate = "${action.year}-${action.month.toString().padStart(2, '0')}-${ + action.day.toString().padStart(2, '0') + }" + state.copy(birthDate = formattedDate) + } } - private fun updateCalendarType(calendarType: String) = updateState { - copy(birthType = calendarType) + private fun updateCalendarType(calendarType: String) = intent { + reduce { + state.copy(birthType = calendarType) + } } - private fun updateGender(gender: String) = updateState { - copy(selectedGender = gender) + private fun updateGender(gender: String) = intent { + reduce { + state.copy(selectedGender = gender) + } } - private fun toggleGender(isMale: Boolean) = updateState { - copy( - isMaleSelected = isMale, - isFemaleSelected = !isMale, - selectedGender = if (isMale) "๋‚จ์„ฑ" else "์—ฌ์„ฑ", - ) + private fun toggleGender(isMale: Boolean) = intent { + reduce { + state.copy( + isMaleSelected = isMale, + isFemaleSelected = !isMale, + selectedGender = if (isMale) "๋‚จ์„ฑ" else "์—ฌ์„ฑ", + ) + } } - private fun toggleTimeUnknown(isChecked: Boolean) = updateState { - val newState = copy( - isTimeUnknown = isChecked, - timeOfBirth = if (isChecked) "์‹œ๊ฐ„๋ชจ๋ฆ„" else "", - ) - newState.copy(isTimeValid = validateTimeOfBirth(newState.timeOfBirth, isChecked)) + private fun toggleTimeUnknown(isChecked: Boolean) = intent { + reduce { + val newState = state.copy( + isTimeUnknown = isChecked, + timeOfBirth = if (isChecked) "์‹œ๊ฐ„๋ชจ๋ฆ„" else "", + ) + newState.copy(isTimeValid = validateTimeOfBirth(newState.timeOfBirth, isChecked)) + } } - private fun updateTimeOfBirth(time: String) = updateState { - copy(timeOfBirth = time, isTimeValid = validateTimeOfBirth(time, isTimeUnknown)) + private fun updateTimeOfBirth(time: String) = intent { + reduce { + state.copy(timeOfBirth = time, isTimeValid = validateTimeOfBirth(time, state.isTimeUnknown)) + } } private fun validateTimeOfBirth(time: String, isTimeUnknown: Boolean): Boolean { @@ -95,47 +109,44 @@ class EditProfileViewModel @Inject constructor( } } - private fun fetchUserInfo(userId: Long) { - viewModelScope.launch { - userInfoRepository.getUserInfo(userId) - .onSuccess { user -> - val (initialYear, initialMonth, initialDay) = user.birthDate.split("-") - - updateState { - copy( - name = user.name, - isNameValid = validateName(user.name), - initialYear = initialYear, - initialMonth = initialMonth, - initialDay = initialDay, - birthType = user.calendarType, - birthDate = user.birthDate, - selectedGender = user.gender, - timeOfBirth = user.birthTime ?: "99:99", - isTimeUnknown = user.birthTime == "์‹œ๊ฐ„๋ชจ๋ฆ„", - isTimeValid = validateTimeOfBirth( - user.birthTime ?: "", - user.birthTime == "์‹œ๊ฐ„๋ชจ๋ฆ„", - ), - isMaleSelected = user.gender == "๋‚จ์„ฑ", - isFemaleSelected = user.gender == "์—ฌ์„ฑ", - ) - } + private fun fetchUserInfo(userId: Long) = intent { + userInfoRepository.getUserInfo(userId) + .onSuccess { user -> + val (initialYear, initialMonth, initialDay) = user.birthDate.split("-") + + reduce { + state.copy( + name = user.name, + isNameValid = validateName(user.name), + initialYear = initialYear, + initialMonth = initialMonth, + initialDay = initialDay, + birthType = user.calendarType, + birthDate = user.birthDate, + selectedGender = user.gender, + timeOfBirth = user.birthTime ?: "99:99", + isTimeUnknown = user.birthTime == "์‹œ๊ฐ„๋ชจ๋ฆ„", + isTimeValid = validateTimeOfBirth( + user.birthTime ?: "", + user.birthTime == "์‹œ๊ฐ„๋ชจ๋ฆ„", + ), + isMaleSelected = user.gender == "๋‚จ์„ฑ", + isFemaleSelected = user.gender == "์—ฌ์„ฑ", + ) } - .onFailure { error -> - Log.e("EditProfileViewModel", "์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ: ${error.message}") - } - } + } + .onFailure { error -> + Log.e("EditProfileViewModel", "์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ: ${error.message}") + } } - private fun previousStep() { - updateState { copy(shouldFetchUserInfo = true) } - emitSideEffect(SettingContract.SideEffect.NavigateBack) + private fun previousStep() = intent { + reduce { state.copy(shouldFetchUserInfo = true) } + postSideEffect(SettingContract.SideEffect.NavigateBack) } - private fun submitUserInfo() = viewModelScope.launch { - val userId = userPreferences.userIdFlow.firstOrNull() ?: return@launch - val state = container.stateFlow.value + private fun submitUserInfo() = intent { + val userId = userInfoRepository.userIdFlow.firstOrNull() ?: return@intent val updatedUser = EditUser( name = state.name, @@ -148,8 +159,8 @@ class EditProfileViewModel @Inject constructor( val result = userInfoRepository.updateUserInfo(userId, updatedUser) if (result.isSuccess) { - userPreferences.saveUserName(state.name) - emitSideEffect(SettingContract.SideEffect.NavigateToSettingRoute) + userInfoRepository.saveUserName(state.name) + postSideEffect(SettingContract.SideEffect.NavigateToSettingRoute) } else { Log.e("EditProfileViewModel", "์‚ฌ์šฉ์ž ์ •๋ณด ์ˆ˜์ • ์‹คํŒจ") } @@ -159,17 +170,31 @@ class EditProfileViewModel @Inject constructor( return formattedDate.replace(Regex("[^0-9-]"), "") } - private fun navigateToEditBirthday() { - updateState { copy(shouldFetchUserInfo = false) } - emitSideEffect(SettingContract.SideEffect.NavigateToEditBirthday) + private fun navigateBack() = intent { + postSideEffect(SettingContract.SideEffect.NavigateBack) } - private fun refreshUserInfo() { - viewModelScope.launch { - val userId = userPreferences.userIdFlow.firstOrNull() - if (userId != null) { - fetchUserInfo(userId) - } + private fun resetState() = intent { + reduce { SettingContract.State() } + } + + private fun showDialog() = intent { + reduce { state.copy(isDialogVisible = true) } + } + + private fun hideDialog() = intent { + reduce { state.copy(isDialogVisible = false) } + } + + private fun refreshUserInfo() = intent { + val userId = userInfoRepository.userIdFlow.firstOrNull() + if (userId != null) { + fetchUserInfo(userId) } } + + private fun navigateToEditBirthday() = intent { + reduce { state.copy(shouldFetchUserInfo = false) } + postSideEffect(SettingContract.SideEffect.NavigateToEditBirthday) + } } diff --git a/feature/setting/src/main/java/com/yapp/setting/SettingNavGraph.kt b/feature/setting/src/main/java/com/yapp/setting/SettingNavGraph.kt index 054c523b..0114df93 100644 --- a/feature/setting/src/main/java/com/yapp/setting/SettingNavGraph.kt +++ b/feature/setting/src/main/java/com/yapp/setting/SettingNavGraph.kt @@ -5,7 +5,6 @@ import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.tween import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically -import androidx.compose.runtime.LaunchedEffect import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import androidx.navigation.navOptions @@ -14,7 +13,7 @@ import com.yapp.common.navigation.OrbitNavigator import com.yapp.common.navigation.extensions.sharedHiltViewModel import com.yapp.common.navigation.route.SettingBaseRoute import com.yapp.common.navigation.route.SettingDestination -import kotlinx.coroutines.flow.collectLatest +import org.orbitmvi.orbit.compose.collectSideEffect fun NavGraphBuilder.settingNavGraph( navigator: OrbitNavigator, @@ -25,10 +24,8 @@ fun NavGraphBuilder.settingNavGraph( composable { val viewModel = it.sharedHiltViewModel(navigator.navController) - LaunchedEffect(viewModel) { - viewModel.container.sideEffectFlow.collectLatest { sideEffect -> - handleSideEffect(sideEffect, navigator) - } + viewModel.collectSideEffect { + handleSideEffect(it, navigator) } SettingRoute(viewModel) @@ -37,10 +34,8 @@ fun NavGraphBuilder.settingNavGraph( composable { val viewModel = it.sharedHiltViewModel(navigator.navController) - LaunchedEffect(viewModel) { - viewModel.container.sideEffectFlow.collectLatest { sideEffect -> - handleSideEffect(sideEffect, navigator) - } + viewModel.collectSideEffect { + handleSideEffect(it, navigator) } EditProfileRoute(viewModel) @@ -86,10 +81,8 @@ fun NavGraphBuilder.settingNavGraph( ) { val viewModel = it.sharedHiltViewModel(navigator.navController) - LaunchedEffect(viewModel) { - viewModel.container.sideEffectFlow.collectLatest { sideEffect -> - handleSideEffect(sideEffect, navigator) - } + viewModel.collectSideEffect { + handleSideEffect(it, navigator) } EditBirthdayRoute(viewModel) diff --git a/feature/setting/src/main/java/com/yapp/setting/SettingScreen.kt b/feature/setting/src/main/java/com/yapp/setting/SettingScreen.kt index 03f79fd0..9f4eca47 100644 --- a/feature/setting/src/main/java/com/yapp/setting/SettingScreen.kt +++ b/feature/setting/src/main/java/com/yapp/setting/SettingScreen.kt @@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.HorizontalDivider 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 @@ -40,15 +39,12 @@ fun SettingRoute( val state by viewModel.container.stateFlow.collectAsStateWithLifecycle() val context = LocalContext.current - LaunchedEffect(key1 = Unit) { - viewModel.onAction(SettingContract.Action.RefreshUserInfo) - } SettingScreen( state = state, onNavigateToEditProfile = { - viewModel.onAction(SettingContract.Action.NavigateToEditProfile) + viewModel.processAction(SettingContract.Action.NavigateToEditProfile) }, - onBackClick = { viewModel.onAction(SettingContract.Action.PreviousStep) }, + onBackClick = { viewModel.processAction(SettingContract.Action.PreviousStep) }, onInquiryClick = { val kakaoUrl = "http://pf.kakao.com/_ykqxjn" val kakaoSchemeUrl = "kakaoplus://plusfriend/home/_ykqxjn" @@ -58,18 +54,18 @@ fun SettingRoute( try { context.startActivity(kakaoIntent) // ์นด์นด์˜คํ†ก ์•ฑ์œผ๋กœ ์ด๋™ } catch (e: Exception) { - viewModel.onAction( + viewModel.processAction( SettingContract.Action.OpenWebView(kakaoUrl), // ์•ฑ์ด ์—†์œผ๋ฉด ์›น๋ทฐ๋กœ ์—ด๊ธฐ ) } }, onTermsClick = { - viewModel.onAction( + viewModel.processAction( SettingContract.Action.OpenWebView("https://www.orbitalarm.net/terms.html"), ) }, onPrivacyPolicyClick = { - viewModel.onAction( + viewModel.processAction( SettingContract.Action.OpenWebView("https://www.orbitalarm.net/privacy.html"), ) }, diff --git a/feature/setting/src/main/java/com/yapp/setting/SettingViewModel.kt b/feature/setting/src/main/java/com/yapp/setting/SettingViewModel.kt index 2e0773c0..c72828d6 100644 --- a/feature/setting/src/main/java/com/yapp/setting/SettingViewModel.kt +++ b/feature/setting/src/main/java/com/yapp/setting/SettingViewModel.kt @@ -1,26 +1,37 @@ package com.yapp.setting import android.util.Log -import androidx.lifecycle.viewModelScope -import com.yapp.datastore.UserPreferences +import androidx.lifecycle.ViewModel import com.yapp.domain.repository.UserInfoRepository -import com.yapp.ui.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.launch +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.syntax.simple.intent +import org.orbitmvi.orbit.syntax.simple.postSideEffect +import org.orbitmvi.orbit.syntax.simple.reduce +import org.orbitmvi.orbit.syntax.simple.repeatOnSubscription +import org.orbitmvi.orbit.viewmodel.container import javax.inject.Inject @HiltViewModel class SettingViewModel @Inject constructor( private val userInfoRepository: UserInfoRepository, - private val userPreferences: UserPreferences, -) : BaseViewModel( - SettingContract.State(), -) { - fun onAction(action: SettingContract.Action) = intent { +) : ViewModel(), ContainerHost { + + override val container: Container = container( + initialState = SettingContract.State(), + ) { + intent { + repeatOnSubscription { + refreshUserInfo() + } + } + } + + fun processAction(action: SettingContract.Action) = intent { when (action) { - SettingContract.Action.PreviousStep -> emitSideEffect(SettingContract.SideEffect.NavigateBack) + SettingContract.Action.PreviousStep -> navigateBack() SettingContract.Action.NavigateToEditProfile -> navigateToEditProfile() is SettingContract.Action.OpenWebView -> openWebView(action.url) SettingContract.Action.RefreshUserInfo -> refreshUserInfo() @@ -28,40 +39,40 @@ class SettingViewModel @Inject constructor( } } - private fun fetchUserInfo(userId: Long) { - viewModelScope.launch { - userInfoRepository.getUserInfo(userId) - .onSuccess { user -> - updateState { - copy( - initialLoading = false, - name = user.name, - birthDate = user.birthDate, - selectedGender = user.gender, - timeOfBirth = user.birthTime.toString(), - ) - } - } - .onFailure { error -> - Log.e("SettingViewModel", "์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ: ${error.message}") + private fun fetchUserInfo(userId: Long) = intent { + userInfoRepository.getUserInfo(userId) + .onSuccess { user -> + reduce { + state.copy( + initialLoading = false, + name = user.name, + birthDate = user.birthDate, + selectedGender = user.gender, + timeOfBirth = user.birthTime.toString(), + ) } - } + } + .onFailure { error -> + Log.e("SettingViewModel", "์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ: ${error.message}") + } } - private fun navigateToEditProfile() { - emitSideEffect(SettingContract.SideEffect.NavigateToEditProfile) + private fun navigateBack() = intent { + postSideEffect(SettingContract.SideEffect.NavigateBack) } - private fun openWebView(url: String) { - emitSideEffect(SettingContract.SideEffect.OpenWebView(url)) + private fun navigateToEditProfile() = intent { + postSideEffect(SettingContract.SideEffect.NavigateToEditProfile) } - private fun refreshUserInfo() { - viewModelScope.launch { - val userId = userPreferences.userIdFlow.firstOrNull() - if (userId != null) { - fetchUserInfo(userId) - } + private fun openWebView(url: String) = intent { + postSideEffect(SettingContract.SideEffect.OpenWebView(url)) + } + + private fun refreshUserInfo() = intent { + val userId = userInfoRepository.userIdFlow.firstOrNull() + if (userId != null) { + fetchUserInfo(userId) } } } diff --git a/feature/splash/build.gradle.kts b/feature/splash/build.gradle.kts index 0377f847..a0212a52 100644 --- a/feature/splash/build.gradle.kts +++ b/feature/splash/build.gradle.kts @@ -12,7 +12,7 @@ dependencies { implementation(projects.core.ui) implementation(projects.core.common) implementation(projects.core.analytics) - implementation(projects.core.datastore) + implementation(projects.domain) implementation(libs.orbit.core) implementation(libs.orbit.compose) implementation(libs.orbit.viewmodel) diff --git a/feature/splash/src/main/java/com/yapp/splash/SplashScreen.kt b/feature/splash/src/main/java/com/yapp/splash/SplashScreen.kt index 272dea8a..6068cd1b 100644 --- a/feature/splash/src/main/java/com/yapp/splash/SplashScreen.kt +++ b/feature/splash/src/main/java/com/yapp/splash/SplashScreen.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -23,7 +22,7 @@ import androidx.navigation.navOptions import com.yapp.common.navigation.OrbitNavigator import com.yapp.common.navigation.route.SplashRoute import com.yapp.designsystem.theme.OrbitTheme -import kotlinx.coroutines.flow.collectLatest +import org.orbitmvi.orbit.compose.collectSideEffect @Composable fun SplashRoute( @@ -31,37 +30,41 @@ fun SplashRoute( viewModel: SplashViewModel = hiltViewModel(), ) { val state by viewModel.container.stateFlow.collectAsStateWithLifecycle() - val sideEffect = viewModel.container.sideEffectFlow - LaunchedEffect(sideEffect) { - sideEffect.collectLatest { effect -> - when (effect) { - is SplashContract.SideEffect.NavigateToOnboarding -> { - navigator.navigateToOnboarding( - navOptions = navOptions { - popUpTo(SplashRoute) { - inclusive = true - } - }, - ) - } - - is SplashContract.SideEffect.NavigateToHome -> { - navigator.navigateToHome( - navOptions = navOptions { - popUpTo(SplashRoute) { - inclusive = true - } - }, - ) - } - } - } + viewModel.collectSideEffect { + handleSideEffects(it, navigator) } SplashScreen(state = state) } +private fun handleSideEffects( + sideEffect: SplashContract.SideEffect, + navigator: OrbitNavigator, +) { + when (sideEffect) { + is SplashContract.SideEffect.NavigateToOnboarding -> { + navigator.navigateToOnboarding( + navOptions = navOptions { + popUpTo(SplashRoute) { + inclusive = true + } + }, + ) + } + + is SplashContract.SideEffect.NavigateToHome -> { + navigator.navigateToHome( + navOptions = navOptions { + popUpTo(SplashRoute) { + inclusive = true + } + }, + ) + } + } +} + @Composable fun SplashScreen( state: SplashContract.State, diff --git a/feature/splash/src/main/java/com/yapp/splash/SplashViewModel.kt b/feature/splash/src/main/java/com/yapp/splash/SplashViewModel.kt index db697f4b..e14bd222 100644 --- a/feature/splash/src/main/java/com/yapp/splash/SplashViewModel.kt +++ b/feature/splash/src/main/java/com/yapp/splash/SplashViewModel.kt @@ -1,49 +1,52 @@ package com.yapp.splash -import androidx.lifecycle.viewModelScope -import com.yapp.datastore.UserPreferences -import com.yapp.ui.base.BaseViewModel +import androidx.lifecycle.ViewModel +import com.yapp.domain.repository.UserInfoRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.first +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.syntax.simple.intent +import org.orbitmvi.orbit.syntax.simple.postSideEffect +import org.orbitmvi.orbit.syntax.simple.reduce +import org.orbitmvi.orbit.viewmodel.container import javax.inject.Inject @HiltViewModel class SplashViewModel @Inject constructor( - private val userPreferences: UserPreferences, -) : BaseViewModel( - initialState = SplashContract.State(), -) { - init { + private val userInfoRepository: UserInfoRepository, +) : ViewModel(), ContainerHost { + + override val container: Container = container( + initialState = SplashContract.State(), + ) { startSplashAnimation() } - private fun startSplashAnimation() { - viewModelScope.launch { - updateState { copy(isVisible = true) } - delay(1500) - updateState { copy(isVisible = false) } - delay(1000) + private fun startSplashAnimation() = intent { + reduce { state.copy(isVisible = true) } + delay(1500) + reduce { state.copy(isVisible = false) } + delay(1000) - checkUserState() - } + checkUserState() } - private fun checkUserState() { - viewModelScope.launch { - combine( - userPreferences.userIdFlow, - userPreferences.onboardingCompletedFlow, - ) { userId, onboardingCompleted -> - Pair(userId, onboardingCompleted) - }.collect { (userId, onboardingCompleted) -> - if (userId != null && onboardingCompleted) { - emitSideEffect(SplashContract.SideEffect.NavigateToHome) - } else { - emitSideEffect(SplashContract.SideEffect.NavigateToOnboarding) - } + private fun checkUserState() = intent { + combine( + userInfoRepository.userIdFlow, + userInfoRepository.onboardingCompletedFlow, + ) { userId, onboardingCompleted -> + Pair(userId, onboardingCompleted) + }.first { (userId, onboardingCompleted) -> + if (userId != null && onboardingCompleted) { + postSideEffect(SplashContract.SideEffect.NavigateToHome) + } else { + postSideEffect(SplashContract.SideEffect.NavigateToOnboarding) } + true } } } diff --git a/gradle.properties b/gradle.properties index 20e2a015..e0d20494 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,4 +20,5 @@ kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true \ No newline at end of file +android.nonTransitiveRClass=true +android.experimental.androidTest.useUnifiedTestPlatform=false diff --git a/gradle/dependencyGraph.gradle b/gradle/dependencyGraph.gradle index 904cf4cd..6f117039 100644 --- a/gradle/dependencyGraph.gradle +++ b/gradle/dependencyGraph.gradle @@ -1,5 +1,3 @@ -// from: https://github.com/DroidKaigi/conference-app-2021/blob/main/gradle/dependencyGraph.gradle -// from: https://github.com/JakeWharton/SdkSearch/blob/3351cad9bfacb0a364858e843774147143f58c7a/gradle/projectDependencyGraph.gradle tasks.register('projectDependencyGraph') { doLast { def dotFileName = 'project.dot' @@ -9,7 +7,7 @@ tasks.register('projectDependencyGraph') { dot << 'digraph {\n' dot << " graph [label=\"${rootProject.name}\\n \",labelloc=t,fontsize=30,ranksep=1.4];\n" - dot << ' node [style=filled, fillcolor="#bbbbbb"];\n' + dot << ' node [style=filled, fillcolor="#bbbbbb"];\n' // ๊ธฐ๋ณธ ๋…ธ๋“œ ์ƒ‰์ƒ dot << ' rankdir=TB;\n' def rootProjects = [] @@ -29,27 +27,41 @@ tasks.register('projectDependencyGraph') { def androidDynamicFeatureProjects = [] def javaProjects = [] + // --- ๋ชจ๋“ˆ ์œ ํ˜•์„ ์ €์žฅํ•  ๋ฆฌ์ŠคํŠธ ์ถ”๊ฐ€ --- + def featureModules = [] + def coreModules = [] + def dataModules = [] + def domainModules = [] // domain ๋ชจ๋“ˆ ๋ฆฌ์ŠคํŠธ ์ถ”๊ฐ€ + queue = [rootProject] while (!queue.isEmpty()) { def project = queue.remove(0) queue.addAll(project.childProjects.values()) - if (project.plugins.hasPlugin('org.jetbrains.kotlin.multiplatform')) { - multiplatformProjects.add(project) + // --- ๋ชจ๋“ˆ ๊ฒฝ๋กœ/์ด๋ฆ„์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ชจ๋“ˆ ์œ ํ˜• ์‹๋ณ„ --- + // ํ”„๋กœ์ ํŠธ์˜ ๋ชจ๋“ˆ ๋ช…๋ช… ๊ทœ์น™์— ๋งž๊ฒŒ ์กฐ๊ฑด์„ ์ˆ˜์ •ํ•˜์„ธ์š”. + // ์šฐ์„ ์ˆœ์œ„๋ฅผ ๊ณ ๋ คํ•˜์—ฌ ๋ฐฐ์น˜ (๋” ๊ตฌ์ฒด์ ์ธ ์กฐ๊ฑด์ด ์œ„๋กœ) + if (project.path.startsWith(':feature')) { + featureModules.add(project) + } else if (project.path.contains(':domain')) { // domain ๋ชจ๋“ˆ ์‹๋ณ„ ์กฐ๊ฑด (์˜ˆ: ':user:domain', ':product:domain') + domainModules.add(project) + } else if (project.path.contains(':core')) { + coreModules.add(project) + } else if (project.path.startsWith(':data')) { + dataModules.add(project) } - if (project.plugins.hasPlugin('kotlin2js')) { + // --- ๊ธฐ์กด ํ”Œ๋Ÿฌ๊ทธ์ธ ๊ธฐ๋ฐ˜ ์‹๋ณ„ ๋กœ์ง ์œ ์ง€ --- + else if (project.plugins.hasPlugin('org.jetbrains.kotlin.multiplatform')) { + multiplatformProjects.add(project) + } else if (project.plugins.hasPlugin('kotlin2js')) { jsProjects.add(project) - } - if (project.plugins.hasPlugin('com.android.application')) { + } else if (project.plugins.hasPlugin('com.android.application')) { androidProjects.add(project) - } - if (project.plugins.hasPlugin('com.android.library')) { + } else if (project.plugins.hasPlugin('com.android.library')) { androidLibraryProjects.add(project) - } - if (project.plugins.hasPlugin('com.android.dynamic-feature')) { + } else if (project.plugins.hasPlugin('com.android.dynamic-feature')) { androidDynamicFeatureProjects.add(project) - } - if (project.plugins.hasPlugin('java-library') || project.plugins.hasPlugin('java')) { + } else if (project.plugins.hasPlugin('java-library') || project.plugins.hasPlugin('java')) { javaProjects.add(project) } @@ -83,7 +95,18 @@ tasks.register('projectDependencyGraph') { traits.add('shape=box') } - if (multiplatformProjects.contains(project)) { + // --- ํŠน์ • ๋ชจ๋“ˆ ์œ ํ˜• ์ƒ‰์ƒ ์šฐ์„  ์ง€์ • --- + if (featureModules.contains(project)) { + traits.add('fillcolor="#FFC0CB"') // ํ•‘ํฌ (Feature) + } else if (domainModules.contains(project)) { + traits.add('fillcolor="#DAF7A6"') // ์˜ˆ: ๋ผ์ดํŠธ ๊ทธ๋ฆฐ/์˜๋กœ์šฐ (Domain) - ์ƒ‰์ƒ ๋ณ€๊ฒฝ ๊ฐ€๋Šฅ + } else if (coreModules.contains(project)) { + traits.add('fillcolor="#ADD8E6"') // ๋ผ์ดํŠธ ๋ธ”๋ฃจ (Core) + } else if (dataModules.contains(project)) { + traits.add('fillcolor="#90EE90"') // ๋ผ์ดํŠธ ๊ทธ๋ฆฐ (Data) + } + // --- ๊ธฐ์กด ํ”Œ๋Ÿฌ๊ทธ์ธ ๊ธฐ๋ฐ˜ ์ƒ‰์ƒ ์ง€์ • ๋กœ์ง --- + else if (multiplatformProjects.contains(project)) { traits.add('fillcolor="#ffd2b3"') } else if (jsProjects.contains(project)) { traits.add('fillcolor="#ffffba"') @@ -96,7 +119,7 @@ tasks.register('projectDependencyGraph') { } else if (javaProjects.contains(project)) { traits.add('fillcolor="#ffb3ba"') } else { - traits.add('fillcolor="#eeeeee"') + traits.add('fillcolor="#eeeeee"') // ๊ทธ ์™ธ ๊ธฐ๋ณธ ์ƒ‰์ƒ } dot << " \"${project.path}\" [${traits.join(", ")}];\n" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 51c70bb2..2d1248b6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,14 +22,14 @@ ktlint = "11.5.1" kotlin = "2.0.0" kotlinx-serialization-json = "1.7.0" kotlinx-coroutines = "1.9.0-RC" -kotlinx-datetime = "0.4.0" kotlinx-collections = "0.3.7" ## AndroidX androidx-app-compat = "1.7.0" androidx-core = "1.15.0" androidx-datastore = "1.1.1" -androidx-room = "2.6.1" +androidx-room = "2.7.2" +androidx-work = "2.10.3" androidx-lifecycle = "2.8.7" @@ -45,16 +45,12 @@ activity-compose = "1.9.3" ## Hilt hilt = "2.51.1" hilt-navigation-compose = "1.2.0" +hilt-work = "1.2.0" ## Third Party okhttp = "4.12.0" retrofit = "2.11.0" -retrofit-kotlinx-serialization-json = "1.0.0" -coil = "2.4.0" -sentry = "5.0.0" -sentry-android = "8.0.0" -sentry-compose = "8.0.0" -gson = "2.11.0" +coil = "2.7.0" # Google Libraries Versions google-service = "4.4.2" @@ -64,12 +60,14 @@ firebase-app-distribution = "5.1.0" firebase-crashlytics = "3.0.3" ## Test -junit = "4.13.2" +junit4 = "4.13.2" mockito = "3.3.3" +mockk = "1.13.9" robolectric = "4.9" androidx-test-ext-junit = "1.2.0" androidx-test-runner = "1.6.0" androidx-test = "1.6.0" +jacoco = "0.8.10" ## Others timber = "5.0.1" @@ -80,7 +78,6 @@ process-pheonix = "3.0.0" lottie = "6.1.0" accompanist = "0.37.0" materialAndroid = "1.7.5" -flexible-bottomsheet = "0.1.5" amplitude = "1.20.3" [libraries] @@ -92,7 +89,6 @@ ksp-gradle-plugin = { group = "com.google.devtools.ksp", name = "com.google.devt ## Kotlin Libraries kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" } -kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinx-datetime" } kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } @@ -105,8 +101,12 @@ androidx-datastore = { group = "androidx.datastore", name = "datastore-preferenc androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "androidx-room" } androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "androidx-room" } androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "androidx-room" } +androidx-room-testing = { group = "androidx.room", name = "room-testing", version.ref = "androidx-room" } androidx-room-paging = { group = "androidx.room", name = "room-paging", version.ref = "androidx-room" } androidx-annotation = { group = "androidx.annotation", name = "annotation", version.ref = "annotation" } +androidx-hilt-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "hilt-work" } +androidx-work-runtime = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "androidx-work" } +androidx-work-testing = { group = "androidx.work", name = "work-testing", version.ref = "androidx-work" } ## Compose Libraries activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" } @@ -134,7 +134,7 @@ hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref hilt-android-testing = { group = "com.google.dagger", name = "hilt-android-testing", version.ref = "hilt" } hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" } hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hilt-navigation-compose" } - +hilt-worker = { group = "androidx.hilt", name = "hilt-work", version.ref = "hilt-work" } # Orbit orbit-core = { group = "org.orbit-mvi", name = "orbit-core", version.ref = "orbit" } @@ -146,9 +146,6 @@ retrofit-core = { group = "com.squareup.retrofit2", name = "retrofit", version.r retrofit-kotlin-serialization = { module = "com.squareup.retrofit2:converter-kotlinx-serialization", version.ref = "retrofit" } okhttp-bom = { group = "com.squareup.okhttp3", name = "okhttp-bom", version.ref = "okhttp" } okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } -flexible-bottomsheet = { group = "com.github.skydoves", name = "flexible-bottomsheet-material3", version.ref = "flexible-bottomsheet" } -#sentry-android = { group = "io.sentry", name = "sentry-android", version.ref = "sentry-android" } -#sentry-compose = { group = "io.sentry", name = "sentry-compose", version.ref = "sentry-compose" } # Google Libraries firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebase-bom" } @@ -163,8 +160,9 @@ timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "tim coil = { group = "io.coil-kt", name = "coil", version.ref = "coil" } ## Test Libraries -junit = { group = "junit", name = "junit", version.ref = "junit" } +junit4 = { group = "junit", name = "junit", version.ref = "junit4" } mockito = { group = "org.mockito", name = "mockito-core", version.ref = "mockito" } +mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" } androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" } androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidx-test-runner" } @@ -191,10 +189,10 @@ kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", versi ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" } android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" } +room = { id = "androidx.room", version.ref = "androidx-room" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } android-test = { id = "com.android.test", version.ref = "android-gradle-plugin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } google-service = { id = "com.google.gms.google-services", version.ref = "google-service" } firebase-app-distribution = { id = "com.google.firebase.appdistribution", version.ref = "firebase-app-distribution" } firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebase-crashlytics" } -#sentry = { id = "io.sentry.android.gradle", version.ref = "sentry" } diff --git a/project.dot.png b/project.dot.png index 51f22f9d..77a616a5 100644 Binary files a/project.dot.png and b/project.dot.png differ diff --git a/settings.gradle.kts b/settings.gradle.kts index c2db5f10..984c9fc9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -33,10 +33,8 @@ include(":core:buildconfig") include(":data") include(":domain") include(":feature") -include(":core:security") include(":core:ui") include(":feature:home") -include(":feature:navigator") include(":feature:onboarding") include(":feature:mission") include(":feature:fortune") @@ -48,3 +46,4 @@ include(":feature:splash") include(":feature:webview") include(":core:analytics") include(":core:remoteconfig") +include(":core:database")