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..5625c9ea --- /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: false + 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_ci.yml b/.github/workflows/android_ci.yml index b8974a0d..0330be6d 100644 --- a/.github/workflows/android_ci.yml +++ b/.github/workflows/android_ci.yml @@ -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..933fbfe3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -29,22 +29,27 @@ 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.firebase.analytics) implementation(libs.firebase.crashlytics) implementation(libs.play.services.ads) + implementation(libs.kotlin.reflect) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 51ec3303..3b4a973a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -30,7 +30,7 @@ android:value="@string/admob_app_id" /> { + 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..aa5803c4 100644 --- a/build-logic/src/main/java/orbit.android.feature.gradle.kts +++ b/build-logic/src/main/java/orbit.android.feature.gradle.kts @@ -6,12 +6,6 @@ plugins { id("orbit.android.compose") } -android { - defaultConfig { - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - } -} - configureHiltAndroid() dependencies { @@ -19,7 +13,6 @@ dependencies { 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..e007649e 100644 --- a/core/alarm/src/main/java/com/yapp/alarm/AlarmConstants.kt +++ b/core/alarm/src/main/java/com/yapp/alarm/AlarmConstants.kt @@ -16,8 +16,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..2691e78b --- /dev/null +++ b/core/alarm/src/main/java/com/yapp/alarm/AndroidAlarmScheduler.kt @@ -0,0 +1,91 @@ +package com.yapp.alarm + +import android.app.AlarmManager +import android.app.Application +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 { + + 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) + + alarmManager.setExactAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, + triggerMillis, + pendingIntent, + ) + } + + private fun setNonRepeatingAlarm(alarm: Alarm) { + val triggerMillis = alarmTimeCalculator.calculateNonRepeatingTimeMillis(alarm) + val pendingIntent = createAlarmReceiverPendingIntentForSchedule(app, alarm) + + 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) + + 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..075651be 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 @@ -35,8 +35,15 @@ fun createAlarmDismissIntent( 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 +55,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..4f7a4ceb 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,7 +6,7 @@ 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.repository.FortuneRepository import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -20,7 +20,7 @@ import javax.inject.Inject 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 @@ -30,7 +30,7 @@ class AlarmInteractionActivityReceiver(private val activity: ComponentActivity) if (!isSnoozed) { CoroutineScope(Dispatchers.IO).launch { - val fortuneDate = userPreferences.fortuneDateFlow.firstOrNull() + val fortuneDate = fortuneRepository.fortuneDateFlow.firstOrNull() val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE) if (fortuneDate != todayDate) { 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..878cf68e 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,13 +7,13 @@ 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.toTimeString +import com.yapp.domain.repository.FortuneRepository import com.yapp.domain.usecase.AlarmUseCase import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope @@ -31,10 +31,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 @@ -107,21 +107,21 @@ class AlarmReceiver : BroadcastReceiver() { ), ), ) - val existingId = userPreferences.firstDismissedAlarmIdFlow.firstOrNull() + val existingId = fortuneRepository.firstDismissedAlarmIdFlow.firstOrNull() if (existingId == null) { // 첫 번째 알람 해제 기록 - userPreferences.saveFirstDismissedAlarmId(alarmId) + fortuneRepository.saveFirstDismissedAlarmId(alarmId) } else if (existingId != alarmId) { // 두 번째 알람 해제 감지 - 기존 기록 삭제 - userPreferences.clearDismissedAlarmId() + fortuneRepository.clearDismissedAlarmId() } } - alarmHelper.cancelSnoozedAlarm(alarmId) + androidAlarmScheduler.cancelSnoozedAlarm(alarmId) } else { Log.e("AlarmReceiver", "알람 ID 수신 실패") } - alarmHelper.cancelSnoozedAlarm(alarmId) + androidAlarmScheduler.cancelSnoozedAlarm(alarmId) context.stopService(alarmServiceIntent) sendBroadCastToCloseAlarmInteractionActivity(context) @@ -152,8 +152,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, @@ -167,7 +166,7 @@ class AlarmReceiver : BroadcastReceiver() { ) context.stopService(Intent(context, AlarmService::class.java)) - alarmHelper.scheduleAlarm(updatedAlarm) + androidAlarmScheduler.scheduleAlarm(updatedAlarm) } private fun sendBroadCastToCloseAlarmInteractionActivity(context: Context) { 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..9d21144d 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,7 +3,7 @@ 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 @@ -18,7 +18,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 @@ -33,7 +33,7 @@ class RescheduleAlarmReceiver : BroadcastReceiver() { CoroutineScope(Dispatchers.IO).launch { alarmUseCase.getAllAlarms().collect { alarms -> alarms.forEach { alarm -> - alarmHelper.scheduleAlarm(alarm) + androidAlarmScheduler.scheduleAlarm(alarm) } } } 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..70cc193d 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,13 @@ 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.domain.model.Alarm import com.yapp.domain.model.AlarmDay +import com.yapp.domain.repository.FortuneRepository import com.yapp.domain.usecase.AlarmUseCase import com.yapp.media.sound.SoundPlayer import dagger.hilt.android.AndroidEntryPoint @@ -51,10 +50,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 fortuneRepository: FortuneRepository private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) @@ -103,7 +102,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)) } } @@ -126,7 +125,7 @@ class AlarmService : Service() { } private suspend fun shouldNavigateToMission(): Boolean { - val fortuneDate = userPreferences.fortuneDateFlow.firstOrNull() + val fortuneDate = fortuneRepository.fortuneDateFlow.firstOrNull() val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE) return fortuneDate != todayDate } @@ -135,17 +134,26 @@ class AlarmService : Service() { val alarmAlertPendingIntent = createAlarmAlertPendingIntent(applicationContext, alarm) - val alarmDismissPendingIntent = if (shouldNavigateToMission) { + /*val alarmDismissPendingIntent = if (shouldNavigateToMission) { createNavigateToMissionPendingIntent( applicationContext = applicationContext, notificationId = alarm.id, + missionType = alarm.missionType.value, + missionCount = alarm.missionCount, ) } else { createAlarmDismissPendingIntent( applicationContext = applicationContext, pendingIntentId = alarm.id, ) - } + }*/ + + val alarmDismissPendingIntent = createNavigateToMissionPendingIntent( + applicationContext = applicationContext, + notificationId = alarm.id, + missionType = alarm.missionType.value, + missionCount = alarm.missionCount, + ) val snoozePendingIntent = if (alarm.isSnoozeEnabled && alarm.snoozeCount != 0) { createAlarmSnoozePendingIntent(applicationContext, alarm) 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..2ffd29d6 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,15 @@ 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, +) { + companion object { + const val route = "mission" + } +} 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/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/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/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..10276dfb 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,9 +1,6 @@ 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 @@ -22,11 +19,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( @@ -51,13 +43,11 @@ object NetworkModule { fun provideAuthOkHttpClient( loggingInterceptor: HttpLoggingInterceptor, authInterceptor: Interceptor, - authenticator: OrbitAuthenticator, ): OkHttpClient = OkHttpClient.Builder() .retryOnConnectionFailure(true) .addInterceptor(loggingInterceptor) .addInterceptor(authInterceptor) - .authenticator(authenticator) .build() @Provides @@ -106,8 +96,4 @@ object NetworkModule { .baseUrl(buildConfigFieldProvider.get().baseUrl) .client(okHttpClient) .build() - - @Provides - @Singleton - fun provideAuthInterceptor(interceptor: AuthenticationIntercept): Interceptor = interceptor } 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/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/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..eb9fb350 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,17 +1,19 @@ 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 { + val firstDismissedAlarmIdFlow: Flow + 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 suspend fun getAlarm(id: Long): Alarm? suspend fun deleteAlarm(id: Long): Int + suspend fun saveFirstDismissedAlarmId(alarmId: Long) + suspend fun clearDismissedAlarmId() } 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..7c7425b2 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,9 @@ 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.datastore.UserPreferences import com.yapp.domain.model.Alarm import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -10,30 +11,21 @@ import javax.inject.Inject class AlarmLocalDataSourceImpl @Inject constructor( private val alarmDao: AlarmDao, + private val userPreferences: UserPreferences, ) : AlarmLocalDataSource { + override val firstDismissedAlarmIdFlow: Flow = userPreferences.firstDismissedAlarmIdFlow + override fun getAllAlarms(): Flow> { return alarmDao.getAllAlarms() .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) } @@ -53,4 +45,12 @@ class AlarmLocalDataSourceImpl @Inject constructor( override suspend fun deleteAlarm(id: Long): Int { return alarmDao.deleteAlarm(id) } + + override suspend fun saveFirstDismissedAlarmId(alarmId: Long) { + userPreferences.saveFirstDismissedAlarmId(alarmId) + } + + override suspend fun clearDismissedAlarmId() { + userPreferences.clearDismissedAlarmId() + } } 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..149234f7 --- /dev/null +++ b/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSource.kt @@ -0,0 +1,20 @@ +package com.yapp.data.local.datasource + +import kotlinx.coroutines.flow.Flow + +interface FortuneLocalDataSource { + val fortuneIdFlow: Flow + val fortuneDateFlow: Flow + val fortuneImageIdFlow: Flow + val fortuneScoreFlow: Flow + val hasNewFortuneFlow: Flow + val firstDismissedAlarmIdFlow: Flow + + suspend fun saveFortuneId(fortuneId: Long) + suspend fun markFortuneAsChecked() + suspend fun saveFortuneImageId(imageResId: Int) + suspend fun saveFortuneScore(score: Int) + suspend fun saveFirstDismissedAlarmId(alarmId: Long) + suspend fun clearDismissedAlarmId() + suspend fun clearFortuneId() +} 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..6dbe10f9 --- /dev/null +++ b/data/src/main/java/com/yapp/data/local/datasource/FortuneLocalDataSourceImpl.kt @@ -0,0 +1,44 @@ +package com.yapp.data.local.datasource + +import com.yapp.datastore.UserPreferences +import javax.inject.Inject + +class FortuneLocalDataSourceImpl @Inject constructor( + private val userPreferences: UserPreferences, +) : FortuneLocalDataSource { + + override val fortuneIdFlow = userPreferences.fortuneIdFlow + override val fortuneDateFlow = userPreferences.fortuneDateFlow + override val fortuneImageIdFlow = userPreferences.fortuneImageIdFlow + override val fortuneScoreFlow = userPreferences.fortuneScoreFlow + override val hasNewFortuneFlow = userPreferences.hasNewFortuneFlow + override val firstDismissedAlarmIdFlow = userPreferences.firstDismissedAlarmIdFlow + + override suspend fun saveFortuneId(fortuneId: Long) { + userPreferences.saveFortuneId(fortuneId) + } + + override suspend fun markFortuneAsChecked() { + userPreferences.markFortuneAsChecked() + } + + override suspend fun saveFortuneImageId(imageResId: Int) { + userPreferences.saveFortuneImageId(imageResId) + } + + override suspend fun saveFortuneScore(score: Int) { + userPreferences.saveFortuneScore(score) + } + + override suspend fun saveFirstDismissedAlarmId(alarmId: Long) { + userPreferences.saveFirstDismissedAlarmId(alarmId) + } + + override suspend fun clearDismissedAlarmId() { + userPreferences.clearDismissedAlarmId() + } + + override suspend fun clearFortuneId() { + userPreferences.clearFortuneId() + } +} 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..37b4fc5a --- /dev/null +++ b/data/src/main/java/com/yapp/data/local/datasource/UserLocalDataSource.kt @@ -0,0 +1,14 @@ +package com.yapp.data.local.datasource + +import kotlinx.coroutines.flow.Flow + +interface UserLocalDataSource { + val userIdFlow: Flow + val userNameFlow: Flow + val onboardingCompletedFlow: Flow + + suspend fun saveUserId(userId: Long) + suspend fun saveUserName(userName: String) + suspend fun setOnboardingCompleted() + 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..7e7d4324 --- /dev/null +++ b/data/src/main/java/com/yapp/data/local/datasource/UserLocalDataSourceImpl.kt @@ -0,0 +1,30 @@ +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 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 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..458db14b 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,7 +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 @@ -15,11 +14,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(@NoneAuth 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/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 84% 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..a1c72135 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 @@ -16,6 +16,8 @@ class AlarmRepositoryImpl @Inject constructor( private val ringtoneManagerHelper: RingtoneManagerHelper, private val soundPlayer: SoundPlayer, ) : AlarmRepository { + override val firstDismissedAlarmIdFlow: Flow = alarmLocalDataSource.firstDismissedAlarmIdFlow + override suspend fun getAlarmSounds(): Result> = runCatching { ringtoneManagerHelper.getAlarmSounds().map { (title, uri) -> AlarmSound(title, uri) @@ -45,14 +47,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()) @@ -97,4 +93,12 @@ class AlarmRepositoryImpl @Inject constructor( throw Exception("No rows deleted") } } + + override suspend fun saveFirstDismissedAlarmId(alarmId: Long) { + alarmLocalDataSource.saveFirstDismissedAlarmId(alarmId) + } + + override suspend fun clearDismissedAlarmId() { + alarmLocalDataSource.clearDismissedAlarmId() + } } 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..d3abb0e9 --- /dev/null +++ b/data/src/main/java/com/yapp/data/repositoryimpl/FortuneRepositoryImpl.kt @@ -0,0 +1,43 @@ +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.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 fortuneDateFlow: Flow = fortuneLocalDataSource.fortuneDateFlow + override val fortuneImageIdFlow: Flow = fortuneLocalDataSource.fortuneImageIdFlow + override val fortuneScoreFlow: Flow = fortuneLocalDataSource.fortuneScoreFlow + override val hasNewFortuneFlow: Flow = fortuneLocalDataSource.hasNewFortuneFlow + override val firstDismissedAlarmIdFlow: Flow = fortuneLocalDataSource.firstDismissedAlarmIdFlow + + override suspend fun saveFortuneId(fortuneId: Long) = fortuneLocalDataSource.saveFortuneId(fortuneId) + override suspend fun markFortuneAsChecked() = fortuneLocalDataSource.markFortuneAsChecked() + override suspend fun saveFortuneImageId(imageResId: Int) = fortuneLocalDataSource.saveFortuneImageId(imageResId) + override suspend fun saveFortuneScore(score: Int) = fortuneLocalDataSource.saveFortuneScore(score) + override suspend fun saveFirstDismissedAlarmId(alarmId: Long) = fortuneLocalDataSource.saveFirstDismissedAlarmId(alarmId) + override suspend fun clearDismissedAlarmId() = fortuneLocalDataSource.clearDismissedAlarmId() + override suspend fun clearFortuneId() = fortuneLocalDataSource.clearFortuneId() + + override suspend fun postFortune(userId: Long): Result { + return fortuneRemoteDataSource.postFortune(userId) + .mapCatching { fortuneResponse -> + fortuneResponse.toDomain() + } + } + + override suspend fun getFortune(fortuneId: Long): Result { + return fortuneRemoteDataSource.getFortune(fortuneId) + .mapCatching { fortuneResponse -> + fortuneResponse.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/remote/repositoryimpl/UserInfoRepositoryImpl.kt b/data/src/main/java/com/yapp/data/repositoryimpl/UserInfoRepositoryImpl.kt similarity index 57% rename from data/src/main/java/com/yapp/data/remote/repositoryimpl/UserInfoRepositoryImpl.kt rename to data/src/main/java/com/yapp/data/repositoryimpl/UserInfoRepositoryImpl.kt index c4720bb6..d96ca6be 100644 --- a/data/src/main/java/com/yapp/data/remote/repositoryimpl/UserInfoRepositoryImpl.kt +++ b/data/src/main/java/com/yapp/data/repositoryimpl/UserInfoRepositoryImpl.kt @@ -1,16 +1,28 @@ -package com.yapp.data.remote.repositoryimpl +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 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 clearUserData() = userLocalDataSource.clearUserData() + override suspend fun getUserInfo(userId: Long): Result { return userInfoDataSource.getUserInfo(userId) .mapCatching { userResponse -> 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/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/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..f7dac361 100644 --- a/domain/src/main/java/com/yapp/domain/repository/AlarmRepository.kt +++ b/domain/src/main/java/com/yapp/domain/repository/AlarmRepository.kt @@ -6,6 +6,8 @@ import com.yapp.domain.model.AlarmSound import kotlinx.coroutines.flow.Flow interface AlarmRepository { + val firstDismissedAlarmIdFlow: Flow + suspend fun getAlarmSounds(): Result> fun initializeSoundPlayer(uri: Uri) fun playAlarmSound(volume: Int) @@ -13,12 +15,12 @@ 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 suspend fun getAlarm(id: Long): Result suspend fun deleteAlarm(id: Long): Result + suspend fun saveFirstDismissedAlarmId(alarmId: Long) + suspend fun clearDismissedAlarmId() } 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..23598e3c 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,23 @@ package com.yapp.domain.repository -import com.yapp.domain.model.fortune.Fortune +import com.yapp.domain.model.Fortune +import kotlinx.coroutines.flow.Flow interface FortuneRepository { + val fortuneIdFlow: Flow + val fortuneDateFlow: Flow + val fortuneImageIdFlow: Flow + val fortuneScoreFlow: Flow + val hasNewFortuneFlow: Flow + val firstDismissedAlarmIdFlow: Flow + + suspend fun saveFortuneId(fortuneId: Long) + suspend fun markFortuneAsChecked() + suspend fun saveFortuneImageId(imageResId: Int) + suspend fun saveFortuneScore(score: Int) + suspend fun saveFirstDismissedAlarmId(alarmId: Long) + suspend fun clearDismissedAlarmId() + suspend fun clearFortuneId() 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..bda28291 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,18 @@ 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 + + suspend fun saveUserId(userId: Long) + suspend fun saveUserName(userName: String) + suspend fun setOnboardingCompleted() + 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/AlarmInteractionNavGraph.kt b/feature/alarm-interaction/src/main/java/com/yapp/alarm/interaction/AlarmInteractionNavGraph.kt index bc8d0035..d5ee04d9 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 @@ -71,9 +71,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..ea4d0b68 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 { 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..c8fb991a 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,16 +44,9 @@ 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( @@ -62,6 +55,17 @@ internal fun AlarmActionRoute( ) } +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, 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..30d95dfe 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,17 +2,20 @@ 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.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.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 @@ -23,81 +26,81 @@ import javax.inject.Inject @HiltViewModel class AlarmActionViewModel @Inject constructor( private val app: Application, - private val userPreferences: UserPreferences, + private val fortuneRepository: FortuneRepository, savedStateHandle: SavedStateHandle, -) : BaseViewModel( - AlarmActionContract.State(), -) { +) : ViewModel(), ContainerHost { + + override val container: Container = container( + initialState = AlarmActionContract.State(), + ) { + fetchIsFirstMission() + 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 + private fun fetchIsFirstMission() = intent { + val fortuneDate = fortuneRepository.fortuneDateFlow.firstOrNull() + val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE) + val isFirstMission = fortuneDate != todayDate - updateState { - copy(isFirstMission = isFirstMission) - } + reduce { + state.copy(isFirstMission = isFirstMission) } } - 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)) } } 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..0cb9b177 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,14 +49,8 @@ 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 }, 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..2d362032 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,16 +2,18 @@ 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 @@ -23,67 +25,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.fortuneDateFlow.firstOrNull() + val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE) + 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) } } diff --git a/feature/fortune/build.gradle.kts b/feature/fortune/build.gradle.kts index ac291d36..ae450155 100644 --- a/feature/fortune/build.gradle.kts +++ b/feature/fortune/build.gradle.kts @@ -12,7 +12,6 @@ dependencies { implementation(projects.core.ui) implementation(projects.core.common) implementation(projects.core.analytics) - implementation(projects.core.datastore) implementation(libs.orbit.core) implementation(libs.orbit.compose) implementation(libs.orbit.viewmodel) 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..f5109a3b 100644 --- a/feature/fortune/src/main/java/com/yapp/fortune/FortuneScreen.kt +++ b/feature/fortune/src/main/java/com/yapp/fortune/FortuneScreen.kt @@ -105,15 +105,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 +123,7 @@ fun FortuneRoute( ), ), ) - viewModel.onAction(FortuneContract.Action.NavigateToHome) + viewModel.processAction(FortuneContract.Action.NavigateToHome) }, ) } 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..034c1590 100644 --- a/feature/fortune/src/main/java/com/yapp/fortune/FortuneViewModel.kt +++ b/feature/fortune/src/main/java/com/yapp/fortune/FortuneViewModel.kt @@ -3,19 +3,19 @@ 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.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.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 java.time.LocalDate import java.time.format.DateTimeFormatter import javax.inject.Inject @@ -24,32 +24,51 @@ import javax.inject.Inject 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(), + ) { + loadFortune() + } + + 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 loadFortune() = intent { + val fortuneId = fortuneRepository.fortuneIdFlow.firstOrNull() + val firstDismissedAlarmId = fortuneRepository.firstDismissedAlarmIdFlow.firstOrNull() + val fortuneDate = fortuneRepository.fortuneDateFlow.firstOrNull() + fortuneId?.let { fetchAndUpdateFortune(it, firstDismissedAlarmId, fortuneDate) } + } + + private fun fetchAndUpdateFortune(fortuneId: Long, firstDismissedAlarmId: Long?, fortuneDate: String?) = 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( + reduce { + state.copy( isLoading = false, dailyFortuneTitle = formattedTitle, dailyFortuneDescription = fortune.dailyFortuneDescription, @@ -61,50 +80,41 @@ class FortuneViewModel @Inject constructor( } }.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/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/home/build.gradle.kts b/feature/home/build.gradle.kts index b8a4b12d..9ef0f667 100644 --- a/feature/home/build.gradle.kts +++ b/feature/home/build.gradle.kts @@ -12,9 +12,7 @@ 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) 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/home/HomeContract.kt b/feature/home/src/main/java/com/yapp/home/HomeContract.kt index ee3f5dd1..dee0ecef 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 { 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..5e5fae4a 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,10 @@ 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 const val ADD_ALARM_RESULT_KEY = "addAlarmResult" const val UPDATE_ALARM_RESULT_KEY = "updateAlarmResult" 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..19d45307 100644 --- a/feature/home/src/main/java/com/yapp/home/HomeScreen.kt +++ b/feature/home/src/main/java/com/yapp/home/HomeScreen.kt @@ -64,11 +64,11 @@ 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.ui.component.dialog.OrbitDialog import com.yapp.ui.component.lottie.LottieAnimation @@ -77,7 +77,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 +87,6 @@ fun HomeRoute( snackBarHostState: SnackbarHostState, ) { val state by viewModel.container.stateFlow.collectAsStateWithLifecycle() - val sideEffect = viewModel.container.sideEffectFlow val coroutineScope = rememberCoroutineScope() @@ -120,49 +120,56 @@ 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( + stateProvider = { state }, + eventDispatcher = 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 @@ -898,7 +905,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, 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..ae347cea 100644 --- a/feature/home/src/main/java/com/yapp/home/HomeViewModel.kt +++ b/feature/home/src/main/java/com/yapp/home/HomeViewModel.kt @@ -1,23 +1,26 @@ package com.yapp.home 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 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 @@ -25,15 +28,21 @@ import javax.inject.Inject 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, +) : ViewModel(), ContainerHost { + + override val container: Container = container( + initialState = HomeContract.State(), + ) { + intent { + repeatOnSubscription { + loadAllAlarms() + loadDailyFortuneState() + loadUserName() + } + } } fun processAction(action: HomeContract.Action) { @@ -68,17 +77,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 +97,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 +258,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 +286,129 @@ 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 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" - - updateState { copy(deliveryTime = formatDeliveryTime(deliveryTime)) } + private fun editAlarm(alarmId: Long) = intent { + postSideEffect(HomeContract.SideEffect.NavigateToEditAlarm(alarmId)) } - private fun getNextAlarmDateWithTime(isAm: Boolean, hour: Int, minute: Int, repeatDays: Int): LocalDateTime { - val now = LocalDateTime.now() - - 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) - } + 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"), + ) - // 비트마스크 기반 반복 요일 추출 - 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")), - ) - } - } catch (e: Exception) { - resourceProvider.getString(R.string.home_fortune_no_alarm) - } + val formattedTime = alarmDateTimeFormatter.getFormattedEarliestUpcomingAlarmDeliveryTime( + alarms = alarms, + formats = deliveryTimeFormats, + ) + reduce { state.copy(deliveryTime = formattedTime) } } - private fun loadDailyFortune() { - viewModelScope.launch { - val fortuneDate = userPreferences.fortuneDateFlow.firstOrNull() - val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE) - - Log.d("HomeViewModel", "fortuneDate: $fortuneDate, todayDate: $todayDate") + private fun loadDailyFortune() = intent { + val fortuneDate = fortuneRepository.fortuneDateFlow.firstOrNull() + val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE) - if (fortuneDate != todayDate) { - processAction(HomeContract.Action.ShowNoDailyFortuneDialog) - } else { - userPreferences.markFortuneAsChecked() - emitSideEffect(HomeContract.SideEffect.NavigateToFortune) - } + if (fortuneDate != todayDate) { + processAction(HomeContract.Action.ShowNoDailyFortuneDialog) + } else { + fortuneRepository.markFortuneAsChecked() + postSideEffect(HomeContract.SideEffect.NavigateToFortune) } } - 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 loadDailyFortuneState() = intent { + val todayDate = LocalDate.now().format(DateTimeFormatter.ISO_DATE) + + combine( + fortuneRepository.fortuneDateFlow, + fortuneRepository.fortuneScoreFlow, + fortuneRepository.hasNewFortuneFlow, + ) { fortuneDate, fortuneScore, hasNewFortune -> + val isTodayFortuneAvailable = fortuneDate == todayDate + val finalFortuneScore = if (isTodayFortuneAvailable) fortuneScore ?: -1 else -1 + + Pair(finalFortuneScore, hasNewFortune) + }.collect { (finalFortuneScore, hasNewFortune) -> + reduce { + state.copy( + lastFortuneScore = finalFortuneScore, + hasNewFortune = hasNewFortune, + isToolTipVisible = hasNewFortune, + ) } } } - private fun loadUserName() { - viewModelScope.launch { - userPreferences.userNameFlow.collect { userName -> - updateState { copy(name = userName ?: "") } - } + 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() } } 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 82% 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..eb99cf01 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,6 +44,11 @@ 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, @@ -74,12 +78,14 @@ 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 SaveMission(val type: MissionType, val count: Int) : Action() + data class NavigateToMissionPreview(val missionType: MissionType, val missionCount: Int) : Action() data class SetSnoozeInterval(val index: Int) : Action() data class SetSnoozeRepeatCount(val index: Int) : Action() data object ToggleVibrationOption : Action() @@ -90,6 +96,7 @@ sealed class AlarmAddEditContract { } sealed class BottomSheetType { + data object MissionSetting : BottomSheetType() data object SnoozeSetting : BottomSheetType() data object SoundSetting : BottomSheetType() } @@ -97,6 +104,11 @@ 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 SaveAlarm(val id: Long) : SideEffect() data class UpdateAlarm(val id: Long) : SideEffect() @@ -118,11 +130,12 @@ 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() } diff --git a/feature/home/src/main/java/com/yapp/alarm/addedit/AlarmAddEditScreen.kt b/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditScreen.kt similarity index 68% rename from feature/home/src/main/java/com/yapp/alarm/addedit/AlarmAddEditScreen.kt rename to feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditScreen.kt index d479e3f6..8bc32ab5 100644 --- a/feature/home/src/main/java/com/yapp/alarm/addedit/AlarmAddEditScreen.kt +++ b/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditScreen.kt @@ -1,4 +1,4 @@ -package com.yapp.alarm.addedit +package com.yapp.home.alarm.addedit import android.net.Uri import androidx.activity.compose.BackHandler @@ -32,7 +32,6 @@ 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 @@ -49,18 +48,20 @@ 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.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.button.OrbitButton import com.yapp.ui.component.dialog.OrbitDialog import com.yapp.ui.component.lottie.LottieAnimation @@ -68,8 +69,10 @@ 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.CoroutineScope import kotlinx.coroutines.launch +import org.orbitmvi.orbit.compose.collectSideEffect +import java.time.LocalTime @Composable fun AlarmAddEditRoute( @@ -78,52 +81,11 @@ fun AlarmAddEditRoute( 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() - } - } - } - } + viewModel.collectSideEffect { + handleSideEffect(it, navigator, snackBarHostState, coroutineScope) } AlarmAddEditScreen( @@ -132,6 +94,59 @@ fun AlarmAddEditRoute( ) } +private suspend fun handleSideEffect( + sideEffect: AlarmAddEditContract.SideEffect, + navigator: OrbitNavigator, + snackBarHostState: SnackbarHostState, + coroutineScope: CoroutineScope, +) { + when (sideEffect) { + is AlarmAddEditContract.SideEffect.NavigateBack -> { + navigator.navigateBack() + } + is AlarmAddEditContract.SideEffect.NavigateToMissionPreview -> { + navigator.navigateToMissionPreview( + missionType = sideEffect.missionType.value, + missionCount = sideEffect.missionCount, + ) + } + 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( stateProvider: () -> AlarmAddEditContract.State, @@ -159,7 +174,9 @@ fun AlarmAddEditContent( eventDispatcher(AlarmAddEditContract.Action.CheckUnsavedChangesBeforeExit) } + val missionState = state.missionState val snoozeState = state.snoozeState + val missionBottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val snoozeBottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val soundBottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val scope = rememberCoroutineScope() @@ -179,13 +196,18 @@ fun AlarmAddEditContent( 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)) + initialTime = state.timeState.initialTime, + ) { newTime -> + eventDispatcher(AlarmAddEditContract.Action.SetAlarmTime(newTime)) } } + AlarmAddEditSelectDaysSection( + modifier = Modifier.padding(horizontal = 20.dp), + daysSelectionState = state.daySelectionState, + holidayState = state.holidayState, + processAction = eventDispatcher, + ) + Spacer(modifier = Modifier.height(12.dp)) AlarmAddEditSettingsSection( modifier = Modifier.padding(horizontal = 20.dp), state = state, @@ -205,6 +227,36 @@ fun AlarmAddEditContent( ) } + AlarmMissionBottomSheet( + sheetState = missionBottomSheetState, + missionType = missionState.missionType, + missionCount = missionState.missionCount, + isSheetOpen = state.bottomSheetState == AlarmAddEditContract.BottomSheetType.MissionSetting, + onDismiss = { + scope.launch { + missionBottomSheetState.hide() + }.invokeOnCompletion { + eventDispatcher(AlarmAddEditContract.Action.ToggleBottomSheet(AlarmAddEditContract.BottomSheetType.MissionSetting)) + } + }, + onSaveMission = { missionType, missionCount -> + eventDispatcher( + AlarmAddEditContract.Action.SaveMission( + type = missionType, + count = missionCount, + ), + ) + }, + onPreviewMission = { missionType, missionCount -> + eventDispatcher( + AlarmAddEditContract.Action.NavigateToMissionPreview( + missionType = missionType, + missionCount = missionCount, + ), + ) + }, + ) + AlarmSnoozeBottomSheet( snoozeEnabled = snoozeState.isSnoozeEnabled, snoozeIntervalIndex = snoozeState.snoozeIntervalIndex, @@ -403,13 +455,36 @@ private fun AlarmAddEditSettingsSection( shape = RoundedCornerShape(12.dp), ), ) { - AlarmAddEditSelectDaysSection( - state = state.daySelectionState, - processAction = processAction, - ) - AlarmAddEditDisableHolidaySwitch( - state = state.holidayState, - processAction = processAction, + 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.ToggleBottomSheet( + AlarmAddEditContract.BottomSheetType.MissionSetting, + ), + ) + }, ) Spacer( modifier = Modifier @@ -418,7 +493,6 @@ private fun AlarmAddEditSettingsSection( .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) { @@ -539,79 +613,96 @@ private fun AlarmAddEditSettingItem( @Composable private fun AlarmAddEditSelectDaysSection( - state: AlarmAddEditContract.AlarmDaySelectionState, + 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.padding(horizontal = 20.dp, vertical = 16.dp), + modifier = modifier + .fillMaxWidth() + .background( + color = OrbitTheme.colors.gray_800, + shape = RoundedCornerShape(12.dp), + ) + .clip( + shape = RoundedCornerShape(12.dp), + ), + verticalArrangement = Arrangement.spacedBy(20.dp), ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, + Column( + modifier = Modifier.padding(horizontal = 20.dp, vertical = 16.dp), ) { - 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) - }, - ) - } + 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.height(12.dp)) + Spacer(modifier = Modifier.weight(1f)) - 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), + AlarmCheckItem( + label = stringResource(id = R.string.alarm_add_edit_weekdays), + isPressed = daysSelectionState.isWeekdaysChecked, onClick = { - processAction(AlarmAddEditContract.Action.ToggleSpecificDaySelection(day)) + 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 - .fillMaxWidth() - .padding( - start = 20.dp, - end = 20.dp, - bottom = 16.dp, - ), + modifier = modifier, verticalAlignment = Alignment.CenterVertically, ) { Icon( @@ -644,9 +735,7 @@ fun AlarmAddEditSettingsSectionPreview() { AlarmAddEditSettingsSection( state = AlarmAddEditContract.State( timeState = AlarmAddEditContract.AlarmTimeState( - currentAmPm = "AM", - currentHour = 9, - currentMinute = 30, + currentTime = LocalTime.of(19, 30), ), daySelectionState = AlarmAddEditContract.AlarmDaySelectionState( isWeekdaysChecked = true, @@ -675,25 +764,32 @@ fun AlarmAddEditSettingItemPreview() { @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, - ), + OrbitTheme { + Box( + modifier = Modifier.background( + color = OrbitTheme.colors.gray_900, + ), + ) { + AlarmAddEditScreen( + stateProvider = { + 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, + ), + ) + }, + eventDispatcher = { }, ) - }, - eventDispatcher = { }, - ) + } + } } 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..fc68790e --- /dev/null +++ b/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditViewModel.kt @@ -0,0 +1,563 @@ +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, + 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.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.SaveMission -> saveMission(action.type, action.count) + is AlarmAddEditContract.Action.NavigateToMissionPreview -> navigateToMissionPreview(action.missionType, action.missionCount) + 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() = 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 saveMission(type: MissionType, count: Int) = intent { + val newMissionState = state.missionState.copy( + missionType = type, + missionCount = count, + ) + reduce { + state.copy(missionState = newMissionState) + } + } + + private fun toggleSnoozeOption() = intent { + val newSnoozeState = state.snoozeState.copy( + isSnoozeEnabled = !state.snoozeState.isSnoozeEnabled, + ) + reduce { + state.copy(snoozeState = newSnoozeState) + } + } + + private fun setSnoozeInterval(index: Int) = intent { + val newSnoozeState = state.snoozeState.copy(snoozeIntervalIndex = index) + reduce { + state.copy(snoozeState = newSnoozeState) + } + } + + private fun setSnoozeRepeatCount(index: Int) = intent { + val newSnoozeState = state.snoozeState.copy(snoozeCountIndex = index) + reduce { + state.copy(snoozeState = newSnoozeState) + } + } + + private fun toggleVibrationOption() = intent { + val newSoundState = state.soundState.copy(isVibrationEnabled = !state.soundState.isVibrationEnabled) + + if (newSoundState.isVibrationEnabled) { + hapticFeedbackManager.performHapticFeedback(HapticType.SUCCESS) + } + reduce { + state.copy(soundState = newSoundState) + } + } + + private fun toggleSoundOption() = intent { + val newSoundState = state.soundState.copy(isSoundEnabled = !state.soundState.isSoundEnabled) + if (!newSoundState.isSoundEnabled) { + alarmUseCase.stopAlarmSound() + } + reduce { + state.copy(soundState = newSoundState) + } + } + + private fun adjustSoundVolume(volume: Int) = intent { + val newSoundState = state.soundState.copy(soundVolume = volume) + alarmUseCase.updateAlarmVolume(volume) + reduce { + state.copy(soundState = newSoundState) + } + } + + private fun selectAlarmSound(index: Int) = intent { + val newSoundState = state.soundState.copy(soundIndex = index) + reduce { + state.copy(soundState = newSoundState) + } + + val selectedSound = state.soundState.sounds[index] + alarmUseCase.initializeSoundPlayer(selectedSound.uri) + alarmUseCase.playAlarmSound(state.soundState.soundVolume) + } + + private fun toggleBottomSheet(sheetType: AlarmAddEditContract.BottomSheetType) = intent { + val newBottomSheetState = if (state.bottomSheetState == sheetType) { + if (state.bottomSheetState == AlarmAddEditContract.BottomSheetType.SoundSetting) { + alarmUseCase.stopAlarmSound() + } + null + } else { + sheetType + } + reduce { + state.copy(bottomSheetState = newBottomSheetState) + } + } + + 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..9cb8669e --- /dev/null +++ b/feature/home/src/main/java/com/yapp/home/alarm/component/bottomsheet/AlarmMissionBottomSheet.kt @@ -0,0 +1,661 @@ +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.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.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.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.component.SelectorItems +import com.yapp.ui.component.OrbitBottomSheet +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") +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun AlarmMissionBottomSheet( + sheetState: SheetState, + missionType: MissionType, + missionCount: Int, + isSheetOpen: Boolean, + onDismiss: () -> Unit, + onSaveMission: (MissionType, Int) -> Unit, + onPreviewMission: (MissionType, Int) -> Unit, +) { + var currentStep by remember { mutableStateOf(AlarmMissionSelectBottomSheetType.MISSION_SETTING) } + + var selectedMissionType by remember { mutableStateOf(missionType) } + var selectedMissionCount by remember { mutableIntStateOf(missionCount) } + + OrbitBottomSheet( + isSheetOpen = isSheetOpen, + sheetState = sheetState, + onDismissRequest = { + currentStep = AlarmMissionSelectBottomSheetType.MISSION_SETTING + onDismiss() + }, + ) { + when (currentStep) { + AlarmMissionSelectBottomSheetType.MISSION_SETTING -> { + if (selectedMissionType == MissionType.NONE) { + MissionAddContent { + currentStep = AlarmMissionSelectBottomSheetType.MISSION_SELECT + } + } else { + MissionSettingContent( + missionType = missionType, + missionCount = missionCount, + onDetail = { + currentStep = AlarmMissionSelectBottomSheetType.MISSION_DETAIL + }, + onDelete = { + selectedMissionType = MissionType.NONE + onSaveMission(selectedMissionType, selectedMissionCount) + }, + onChange = { + currentStep = AlarmMissionSelectBottomSheetType.MISSION_SELECT + }, + onDone = { + onSaveMission(selectedMissionType, selectedMissionCount) + onDismiss() + }, + ) + } + } + + AlarmMissionSelectBottomSheetType.MISSION_SELECT -> { + MissionSelectContent( + onBack = { + currentStep = AlarmMissionSelectBottomSheetType.MISSION_SETTING + }, + onClose = { + currentStep = AlarmMissionSelectBottomSheetType.MISSION_SETTING + onDismiss() + }, + onSelect = { mission -> + selectedMissionType = mission + currentStep = AlarmMissionSelectBottomSheetType.MISSION_DETAIL + }, + ) + } + + AlarmMissionSelectBottomSheetType.MISSION_DETAIL -> { + MissionDetailContent( + missionType = selectedMissionType, + selectedMissionCount = selectedMissionCount, + onCountChange = { count -> + selectedMissionCount = count + }, + onBack = { + currentStep = AlarmMissionSelectBottomSheetType.MISSION_SELECT + }, + onClose = { + currentStep = AlarmMissionSelectBottomSheetType.MISSION_SETTING + onDismiss() + }, + onSave = { + currentStep = AlarmMissionSelectBottomSheetType.MISSION_SETTING + onSaveMission(selectedMissionType, selectedMissionCount) + onDismiss() + }, + onPreview = { + onPreviewMission(selectedMissionType, selectedMissionCount) + }, + ) + } + } + } +} + +@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, + 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_bottom_sheet_title), + onBack = onBack, + onClose = onClose, + ) + + Column( + modifier = Modifier.padding(horizontal = 12.dp), + ) { + MissionTypeItem( + missionType = MissionType.SHAKE, + onClick = { + onSelect(MissionType.SHAKE) + }, + ) + MissionTypeItem( + missionType = MissionType.TAP, + onClick = { + onSelect(MissionType.TAP) + }, + ) + } + } +} + +@Composable +private fun MissionTypeItem( + missionType: MissionType, + onClick: () -> Unit, +) { + val (iconRes, titleRes) = missionType.displayData() + val title = stringResource(id = titleRes) + + Row( + modifier = Modifier + .fillMaxWidth() + .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, + ) + } +} + +@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 = 24.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 { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + AlarmMissionBottomSheet( + sheetState = sheetState, + missionType = MissionType.SHAKE, + missionCount = 15, + isSheetOpen = true, + onDismiss = {}, + onSaveMission = { _, _ -> }, + onPreviewMission = { _, _ -> }, + ) + } +} diff --git a/feature/home/src/main/java/com/yapp/alarm/component/bottomsheet/AlarmSnoozeBottomSheet.kt b/feature/home/src/main/java/com/yapp/home/alarm/component/bottomsheet/AlarmSnoozeBottomSheet.kt similarity index 81% rename from feature/home/src/main/java/com/yapp/alarm/component/bottomsheet/AlarmSnoozeBottomSheet.kt rename to feature/home/src/main/java/com/yapp/home/alarm/component/bottomsheet/AlarmSnoozeBottomSheet.kt index 8e2b9109..82e4caeb 100644 --- a/feature/home/src/main/java/com/yapp/alarm/component/bottomsheet/AlarmSnoozeBottomSheet.kt +++ b/feature/home/src/main/java/com/yapp/home/alarm/component/bottomsheet/AlarmSnoozeBottomSheet.kt @@ -1,7 +1,6 @@ -package com.yapp.alarm.component.bottomsheet +package com.yapp.home.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 @@ -26,9 +25,9 @@ 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.component.SelectorItems 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 @@ -183,62 +182,6 @@ private fun SelectorSection( } } -@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 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 99% 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..e98fe962 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 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..9aaec0b3 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 @@ -48,10 +48,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 @@ -253,7 +253,6 @@ internal fun AlarmBottomSheetContent( onClick = onClickAlarm, onLongPress = onLongPressAlarm, onToggleSelect = onToggleSelect, - isAm = alarm.isAm, hour = alarm.hour, minute = alarm.minute, isActive = alarm.isAlarmActive, 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..cf91a124 100644 --- a/feature/home/src/main/res/values/strings.xml +++ b/feature/home/src/main/res/values/strings.xml @@ -32,6 +32,30 @@ 공휴일 알람 끄기 + 미션 + %1$s, %2$d회 + 흔들기 + 터치하기 + 없음 + + + 미션 + + 등록된 미션이 없어요 + 새 미션을 추가해보세요 + 미션추가 + + 미션 변경 + 완료 + + %d회 + + 횟수 + 쉬움 + 어려움 + 미리보기 + 미션 저장 + %s, %s 안 함 @@ -92,4 +116,9 @@ 알람 미루기 남은 시간 + + %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..9aa81001 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,6 +22,7 @@ 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() @@ -29,7 +32,7 @@ sealed class MissionContract { 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..247a9e5c 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(MissionRoute.route) { + inclusive = true } + }, + ) + } - MissionContract.SideEffect.NavigateBack -> navigator.navigateBack() - } - } + MissionContract.SideEffect.NavigateToHome -> { + navigator.navigateToHome( + navOptions = navOptions { + popUpTo(MissionRoute.route) { + 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..ad178833 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) @@ -127,12 +148,36 @@ fun MissionScreen( 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 +187,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 +228,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 +271,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 +288,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 +305,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 +340,6 @@ fun MissionClickCard( } } -/** - * 미션 종료 시 나가기 다이얼로그. - */ @Composable fun ExitDialog( state: MissionContract.State, @@ -306,18 +348,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 +371,6 @@ fun ExitDialog( ) } -/** - * 미션 성공 시 오버레이 화면. - */ @Composable fun MissionSuccessOverlay() { Box( @@ -354,7 +394,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 +405,16 @@ fun MissionSuccessOverlay() { } } -/** - * 오류 발생 시 다이얼로그. - */ @Composable fun ErrorDialog(message: String, onConfirm: () -> Unit) { OrbitDialog( - title = "오류", + title = stringResource(id = R.string.error), message = message, - confirmText = "확인", + confirmText = stringResource(id = R.string.confirm), onConfirm = onConfirm, ) } -/** - * 로딩 화면. 미션 타입 로딩 중에 표시. - */ @Composable fun MissionLoadingScreen() { Box( @@ -394,16 +428,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..33650e31 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,28 @@ 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.MissionType import com.yapp.domain.repository.FortuneRepository -import com.yapp.domain.usecase.GetMissionTypeUseCase +import com.yapp.domain.repository.UserInfoRepository 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 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,141 +30,131 @@ 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 userInfoRepository: UserInfoRepository, private val app: Application, savedStateHandle: SavedStateHandle, -) : BaseViewModel( - MissionContract.State(), -) { - init { +) : ViewModel(), ContainerHost { + + override val container: Container = container( + initialState = MissionContract.State(), + ) { savedStateHandle.get("notificationId")?.toLong()?.let { sendAlarmDismissIntent(it) } - loadRemoteMissionType() - } - - private fun loadRemoteMissionType() { - viewModelScope.launch { - val missionType = getMissionTypeUseCase.execute() - updateState { - copy( - missionType = missionType, - isMissionTypeLoading = false, - ) - } - } + 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 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 hideExitDialog() { - updateState { copy(showExitDialog = false) } + private fun navigateBack() = intent { + postSideEffect(MissionContract.SideEffect.NavigateBack) } - 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 showExitDialog() = intent { + reduce { state.copy(showExitDialog = true) } } - private fun handleClick() = viewModelScope.launch { - if (currentState.missionType !is MissionType.Click) return@launch + private fun hideExitDialog() = intent { + reduce { state.copy(showExitDialog = false) } + } - 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, + private fun handleMissionProgress(missionType: MissionType) = intent { + val isLast = state.currentCount >= state.missionCount - 1 + val nextCount = state.currentCount + 1 + + performHapticSuccess() + + if (isLast) { + completeMission(type = missionType.name.lowercase()) + reduce { + state.copy( + isMissionCompleted = true, + currentCount = state.missionCount, showFinalAnimation = true, ) } - postFortune() delay(500) - updateState { copy(isMissionCompleted = true) } - } - } - - private fun postFortune() { - viewModelScope.launch { - val userId = userPreferences.userIdFlow.firstOrNull() ?: return@launch - val result = runCatching { - withContext(Dispatchers.IO) { - fortuneRepository.postFortune(userId) - } + } 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 { error -> - Log.e("MissionViewModel", "운세 데이터 요청 실패: ${error.message}") - updateState { copy(errorMessage = error.message) } + if (missionType == MissionType.TAP) { + delay(500) + reduce { state.copy(playWhenClick = false) } } } } - private fun retryPostFortune() { - viewModelScope.launch { - val userId = userPreferences.userIdFlow.firstOrNull() ?: return@launch - val result = runCatching { - withContext(Dispatchers.IO) { - fortuneRepository.postFortune(userId) - } - } + private fun postFortune(isRetry: Boolean = false) = intent { + val userId = userInfoRepository.userIdFlow.firstOrNull() ?: return@intent + + val result = withContext(Dispatchers.IO) { + fortuneRepository.postFortune(userId) + } - result.onSuccess { - val data = it.getOrThrow() - userPreferences.saveFortuneId(data.id) - userPreferences.saveFortuneScore(data.avgFortuneScore) + result.onSuccess { data -> + fortuneRepository.saveFortuneId(data.id) + fortuneRepository.saveFortuneScore(data.avgFortuneScore) - emitSideEffect(MissionContract.SideEffect.NavigateToFortune) - }.onFailure { - Log.e("MissionViewModel", "운세 재요청 실패: ${it.message}") + postSideEffect(MissionContract.SideEffect.NavigateToFortune) + }.onFailure { error -> + if (isRetry) { navigateToHome() + } else { + reduce { state.copy(errorMessage = error.message) } } } } - private fun completeMission(type: String) { + fun retryPostFortune() { + postFortune(isRetry = true) + } + + private fun completeMission(type: String) = intent { performHapticSuccess() logMissionSuccess(type) - postFortune() + if (state.missionMode == MissionMode.REAL) { + postFortune() + } else { + postSideEffect(MissionContract.SideEffect.NavigateBack) + } } private fun performHapticSuccess() { @@ -179,8 +172,8 @@ class MissionViewModel @Inject constructor( ) } - private fun navigateToHome() { - emitSideEffect(MissionContract.SideEffect.NavigateToFortune) + private fun navigateToHome() = intent { + postSideEffect(MissionContract.SideEffect.NavigateToHome) } private fun sendAlarmDismissIntent(id: Long) { 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..8a44d192 100644 --- a/feature/onboarding/build.gradle.kts +++ b/feature/onboarding/build.gradle.kts @@ -14,7 +14,6 @@ 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) 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/OnboardingContract.kt b/feature/onboarding/src/main/java/com/yapp/onboarding/OnboardingContract.kt index 5e8b83c2..86bf3029 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, @@ -43,16 +44,10 @@ 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() 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..7b0677fa 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,7 +9,7 @@ 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 org.orbitmvi.orbit.compose.collectSideEffect fun NavGraphBuilder.onboardingNavGraph( navigator: OrbitNavigator, @@ -18,90 +17,72 @@ fun NavGraphBuilder.onboardingNavGraph( navigation(startDestination = OnboardingDestination.Explain) { composable { val viewModel = it.sharedHiltViewModel(navigator.navController) - LaunchedEffect(viewModel) { - viewModel.container.sideEffectFlow.collectLatest { sideEffect -> - handleSideEffect(sideEffect, navigator, viewModel) - } + viewModel.collectSideEffect { + handleSideEffect(it, navigator, viewModel) } OnboardingExplainRoute(viewModel) } composable { val viewModel = it.sharedHiltViewModel(navigator.navController) - LaunchedEffect(viewModel) { - viewModel.container.sideEffectFlow.collectLatest { sideEffect -> - handleSideEffect(sideEffect, navigator, viewModel) - } + viewModel.collectSideEffect { + handleSideEffect(it, navigator, viewModel) } OnboardingAlarmTimeSelectionRoute(viewModel) } composable { val viewModel = it.sharedHiltViewModel(navigator.navController) - LaunchedEffect(viewModel) { - viewModel.container.sideEffectFlow.collectLatest { sideEffect -> - handleSideEffect(sideEffect, navigator, viewModel) - } + viewModel.collectSideEffect { + handleSideEffect(it, navigator, viewModel) } OnboardingBirthdayRoute(viewModel) } composable { val viewModel = it.sharedHiltViewModel(navigator.navController) - LaunchedEffect(viewModel) { - viewModel.container.sideEffectFlow.collectLatest { sideEffect -> - handleSideEffect(sideEffect, navigator, viewModel) - } + viewModel.collectSideEffect { + handleSideEffect(it, navigator, viewModel) } OnboardingTimeOfBirthRoute(viewModel) } composable { val viewModel = it.sharedHiltViewModel(navigator.navController) - LaunchedEffect(viewModel) { - viewModel.container.sideEffectFlow.collectLatest { sideEffect -> - handleSideEffect(sideEffect, navigator, viewModel) - } + viewModel.collectSideEffect { + handleSideEffect(it, navigator, viewModel) } OnboardingNameRoute(viewModel) } composable { val viewModel = it.sharedHiltViewModel(navigator.navController) - LaunchedEffect(viewModel) { - viewModel.container.sideEffectFlow.collectLatest { sideEffect -> - handleSideEffect(sideEffect, navigator, viewModel) - } + viewModel.collectSideEffect { + handleSideEffect(it, navigator, viewModel) } OnboardingGenderRoute(viewModel) } composable { val viewModel = it.sharedHiltViewModel(navigator.navController) - LaunchedEffect(viewModel) { - viewModel.container.sideEffectFlow.collectLatest { sideEffect -> - handleSideEffect(sideEffect, navigator, viewModel) - } + viewModel.collectSideEffect { + handleSideEffect(it, navigator, viewModel) } OnboardingAccessRoute(viewModel) } composable { val viewModel = it.sharedHiltViewModel(navigator.navController) - LaunchedEffect(viewModel) { - viewModel.container.sideEffectFlow.collectLatest { sideEffect -> - handleSideEffect(sideEffect, navigator, viewModel) - } + viewModel.collectSideEffect { + handleSideEffect(it, navigator, viewModel) } OnboardingCompleteRoute(viewModel) } composable { val viewModel = it.sharedHiltViewModel(navigator.navController) - LaunchedEffect(viewModel) { - viewModel.container.sideEffectFlow.collectLatest { sideEffect -> - handleSideEffect(sideEffect, navigator, viewModel) - } + viewModel.collectSideEffect { + handleSideEffect(it, navigator, viewModel) } OnboardingCompleteRoute2(viewModel) } 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..7281a9a5 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,25 +29,28 @@ 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) @@ -57,122 +65,106 @@ 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) - } + reduce { state.copy(isBottomSheetOpen = false) } + 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) - } + 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, + ).onSuccess { + postSideEffect(OnboardingContract.SideEffect.OnboardingCompleted) }.onFailure { - Log.e("OnboardingViewModel", "Failed to get alarm sounds", it) + 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 +179,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 +192,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 +201,8 @@ class OnboardingViewModel @Inject constructor( savedStateHandle["birthDate"] = formattedDate savedStateHandle["birthType"] = lunar - updateState { - copy( + reduce { + state.copy( birthDate = formattedDate, birthType = lunar, isBirthDateValid = true, @@ -218,9 +210,9 @@ class OnboardingViewModel @Inject constructor( } } - private fun resetFields() { - updateState { - copy( + private fun resetFields() = intent { + reduce { + state.copy( textFieldValue = "", showWarning = false, isButtonEnabled = false, @@ -229,31 +221,29 @@ 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 toggleBottomSheet() = intent { + val isCurrentlyOpen = state.isBottomSheetOpen + reduce { state.copy(isBottomSheetOpen = !isCurrentlyOpen) } } - private fun completeOnboarding() { - viewModelScope.launch { - userPreferences.setOnboardingCompleted() - emitSideEffect(OnboardingContract.SideEffect.OnboardingCompleted) - } + 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/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..c10bb87c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,14 +22,13 @@ 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-lifecycle = "2.8.7" @@ -49,12 +48,7 @@ hilt-navigation-compose = "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" # Google Libraries Versions google-service = "4.4.2" @@ -64,12 +58,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 +76,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 +87,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,6 +99,7 @@ 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" } @@ -135,7 +130,6 @@ hilt-android-testing = { group = "com.google.dagger", name = "hilt-android-testi 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" } - # Orbit orbit-core = { group = "org.orbit-mvi", name = "orbit-core", version.ref = "orbit" } orbit-compose = { group = "org.orbit-mvi", name = "orbit-compose", version.ref = "orbit" } @@ -146,9 +140,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 +154,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 +183,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..070d91b3 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")