├── .github ├── ISSUE_TEMPLATE │ ├── 🌟-refactor.md │ ├── 🐞-bug.md │ └── 🔨-feature.md └── pull_request_template.md ├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── com │ └── woory │ └── almostthere │ ├── AlmostThereApp.kt │ ├── di │ ├── DataModule.kt │ ├── DatabaseModule.kt │ ├── DispatchersModule.kt │ └── NetworkModule.kt │ └── initializer │ ├── KakaoSdkInitializer.kt │ ├── ThreeTenInitializer.kt │ └── TimberInitializer.kt ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── Configuration.kt ├── data ├── .gitignore ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── com │ └── woory │ └── almostthere │ └── data │ ├── model │ ├── LocationModel.kt │ ├── LocationSearchModel.kt │ ├── MagneticInfoModel.kt │ ├── PathModel.kt │ ├── PromiseAlarmModel.kt │ ├── PromiseHistoryModel.kt │ ├── PromiseModel.kt │ ├── RouteType.kt │ ├── UserHpModel.kt │ ├── UserModel.kt │ ├── UserPreferencesModel.kt │ ├── UserRankingModel.kt │ └── UserStateModel.kt │ ├── repository │ ├── DefaultPromiseRepository.kt │ ├── DefaultRouteRepository.kt │ ├── DefaultUserRepository.kt │ ├── PromiseRepository.kt │ ├── RouteRepository.kt │ └── UserRepository.kt │ ├── source │ ├── DatabaseDataSource.kt │ └── NetworkDataSource.kt │ └── util │ └── Const.kt ├── database ├── .gitignore ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── com │ └── woory │ └── almostthere │ └── database │ ├── AlmostThereDatabase.kt │ ├── OffsetDateTimeConverter.kt │ ├── PromiseAlarmDao.kt │ ├── entity │ ├── PromiseAlarmEntity.kt │ └── mapper │ │ └── PromiseAlarmMapper.kt │ ├── preferences │ ├── UserPreferences.kt │ └── mapper │ │ └── UserPreferencesMapper.kt │ └── source │ └── DefaultDatabaseDataSource.kt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── network ├── .gitignore ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── com │ └── woory │ └── almostthere │ └── network │ ├── DefaultNetworkDataSource.kt │ ├── model │ ├── AddressInfoResponse.kt │ ├── LocationSearchResponse.kt │ ├── MagneticInfoDocument.kt │ ├── PromiseData.kt │ ├── UserHpDocument.kt │ ├── UserLocationDocument.kt │ └── mapper │ │ ├── AddedUserHpMapper.kt │ │ ├── LocationSearchMapper.kt │ │ ├── MagneticDataMapper.kt │ │ ├── ModelMapper.kt │ │ ├── PromiseDataMapper.kt │ │ ├── UserLocationMapper.kt │ │ └── UserModelMapper.kt │ ├── service │ ├── ODsayService.kt │ └── TMapService.kt │ └── util │ ├── InviteCodeUtil.kt │ └── TimeConverter.kt ├── presentation ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── libs │ ├── tmap-sdk-1.1.aar │ └── vsm-tmap-sdk-v2-android-1.6.60.aar ├── proguard-rules.pro └── src │ ├── androidTest │ └── kotlin │ │ └── com │ │ └── woory │ │ └── almostthere │ │ └── presentation │ │ └── ExampleInstrumentedTest.kt │ ├── debug │ ├── ic_woory_icon-playstore.png │ └── res │ │ ├── drawable │ │ └── ic_woory_icon_foreground.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_woory_icon.xml │ │ └── ic_woory_icon_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_woory_icon.png │ │ └── ic_woory_icon_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_woory_icon.png │ │ └── ic_woory_icon_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_woory_icon.png │ │ └── ic_woory_icon_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_woory_icon.png │ │ └── ic_woory_icon_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_woory_icon.png │ │ └── ic_woory_icon_round.png │ │ └── values │ │ └── ic_woory_icon_background.xml │ ├── main │ ├── AndroidManifest.xml │ ├── kotlin │ │ └── com │ │ │ └── woory │ │ │ └── almostthere │ │ │ └── presentation │ │ │ ├── background │ │ │ ├── HiltBroadcastReceiver.kt │ │ │ ├── alarm │ │ │ │ └── AlarmFunctions.kt │ │ │ ├── notification │ │ │ │ ├── NotificationChannelProvider.kt │ │ │ │ └── NotificationProvider.kt │ │ │ ├── receiver │ │ │ │ ├── AlarmReceiver.kt │ │ │ │ ├── AlarmTouchReceiver.kt │ │ │ │ └── BootBroadcastReceiver.kt │ │ │ ├── service │ │ │ │ ├── AlarmRestartService.kt │ │ │ │ ├── PromiseAlarmRegisterService.kt │ │ │ │ ├── PromiseFinishService.kt │ │ │ │ ├── PromiseGameService.kt │ │ │ │ └── PromiseReadyService.kt │ │ │ └── util │ │ │ │ └── Extensions.kt │ │ │ ├── binding │ │ │ ├── BindingAdapter.kt │ │ │ ├── ButtonBinding.kt │ │ │ ├── ImageViewBinding.kt │ │ │ ├── TextInputLayoutBinding.kt │ │ │ ├── TextViewBinding.kt │ │ │ └── ViewBinding.kt │ │ │ ├── di │ │ │ └── PresentationModule.kt │ │ │ ├── extension │ │ │ └── LifecycleExtension.kt │ │ │ ├── model │ │ │ ├── AlarmState.kt │ │ │ ├── Color.kt │ │ │ ├── Location.kt │ │ │ ├── MagneticInfo.kt │ │ │ ├── ProfileImage.kt │ │ │ ├── Promise.kt │ │ │ ├── PromiseAlarm.kt │ │ │ ├── PromiseHistory.kt │ │ │ ├── UiState.kt │ │ │ ├── User.kt │ │ │ ├── UserHp.kt │ │ │ ├── WooryTMapCircle.kt │ │ │ ├── exception │ │ │ │ ├── AlmostThereException.kt │ │ │ │ └── CreatingPromiseException.kt │ │ │ ├── mapper │ │ │ │ ├── UiModelMapper.kt │ │ │ │ ├── alarm │ │ │ │ │ └── PromiseAlarmMapper.kt │ │ │ │ ├── gaming │ │ │ │ │ └── BottomSheetProfile.kt │ │ │ │ ├── history │ │ │ │ │ └── PromiseHistoryMapper.kt │ │ │ │ ├── location │ │ │ │ │ ├── GeoPointMapper.kt │ │ │ │ │ ├── LocationMapper.kt │ │ │ │ │ └── UserLocationMapper.kt │ │ │ │ ├── magnetic │ │ │ │ │ └── MagneticInfoMapper.kt │ │ │ │ ├── promise │ │ │ │ │ ├── PromiseDataMapper.kt │ │ │ │ │ └── PromiseMapper.kt │ │ │ │ ├── searchlocation │ │ │ │ │ └── SearchResultMapper.kt │ │ │ │ └── user │ │ │ │ │ ├── AddedUserHpMapper.kt │ │ │ │ │ ├── UserDataMapper.kt │ │ │ │ │ ├── UserMapper.kt │ │ │ │ │ ├── UserProfileImageMapper.kt │ │ │ │ │ └── UserRankingMapper.kt │ │ │ └── user │ │ │ │ └── gameresult │ │ │ │ ├── UserRanking.kt │ │ │ │ └── UserSplitMoneyItem.kt │ │ │ ├── ui │ │ │ ├── BaseActivity.kt │ │ │ ├── BaseDialogFragment.kt │ │ │ ├── BaseFragment.kt │ │ │ ├── BaseViewHolder.kt │ │ │ ├── creatingpromise │ │ │ │ ├── CheckPromiseDataDialog.kt │ │ │ │ ├── CreatingPromiseActivity.kt │ │ │ │ ├── CreatingPromiseFragment.kt │ │ │ │ ├── CreatingPromiseUiState.kt │ │ │ │ ├── CreatingPromiseViewModel.kt │ │ │ │ ├── ProfileFragment.kt │ │ │ │ └── locationsearch │ │ │ │ │ ├── LocationSearchFragment.kt │ │ │ │ │ ├── LocationSearchResult.kt │ │ │ │ │ ├── LocationSearchResultAdapter.kt │ │ │ │ │ ├── LocationSearchResultFragment.kt │ │ │ │ │ ├── LocationSearchResultViewModel.kt │ │ │ │ │ └── LocationSearchViewModel.kt │ │ │ ├── customview │ │ │ │ ├── HPBar.kt │ │ │ │ ├── RankBadge.kt │ │ │ │ └── topitemresize │ │ │ │ │ ├── TopItemResizeAdapter.kt │ │ │ │ │ ├── TopItemResizeDecoration.kt │ │ │ │ │ └── TopItemResizeScrollListener.kt │ │ │ ├── gameresult │ │ │ │ ├── GameResultActivity.kt │ │ │ │ ├── GameResultFragment.kt │ │ │ │ ├── GameResultViewModel.kt │ │ │ │ ├── SplitMoneyFragment.kt │ │ │ │ ├── SplitMoneyLogic.kt │ │ │ │ ├── TotalCostFragment.kt │ │ │ │ ├── UserRankingAdapter.kt │ │ │ │ └── UserSplitMoneyAdapter.kt │ │ │ ├── gaming │ │ │ │ ├── CharacterFragment.kt │ │ │ │ ├── GamingActivity.kt │ │ │ │ ├── GamingFragment.kt │ │ │ │ ├── GamingRankingAdapter.kt │ │ │ │ ├── GamingViewModel.kt │ │ │ │ ├── ShakeDeviceFragment.kt │ │ │ │ └── ShakeEventListener.kt │ │ │ ├── history │ │ │ │ ├── PromiseHistoryActivity.kt │ │ │ │ ├── PromiseHistoryAdapter.kt │ │ │ │ ├── PromiseHistoryType.kt │ │ │ │ ├── PromiseHistoryViewModel.kt │ │ │ │ └── RankBadgeType.kt │ │ │ ├── join │ │ │ │ ├── FormState.kt │ │ │ │ ├── JoinActivity.kt │ │ │ │ ├── JoinViewModel.kt │ │ │ │ ├── ProfileActivity.kt │ │ │ │ └── ProfileViewModel.kt │ │ │ ├── main │ │ │ │ ├── FindPromiseFragment.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ └── MainFragment.kt │ │ │ ├── promiseinfo │ │ │ │ ├── PromiseInfoActivity.kt │ │ │ │ ├── PromiseInfoFragment.kt │ │ │ │ ├── PromiseInfoViewModel.kt │ │ │ │ ├── PromiseUiState.kt │ │ │ │ ├── PromiseUserAdapter.kt │ │ │ │ └── ReadyStatus.kt │ │ │ └── splash │ │ │ │ └── SplashActivity.kt │ │ │ └── util │ │ │ ├── ActivityContext.kt │ │ │ ├── Constants.kt │ │ │ ├── DistanceUtil.kt │ │ │ ├── Exceptions.kt │ │ │ ├── InviteCodeUtil.kt │ │ │ ├── KonfettiPresets.kt │ │ │ ├── ResourceManager.kt │ │ │ ├── SoftKeyboardUtils.kt │ │ │ ├── Tag.kt │ │ │ ├── TimeConverter.kt │ │ │ ├── TimeUtils.kt │ │ │ ├── Utils.kt │ │ │ ├── ViewExtension.kt │ │ │ ├── ViewUtils.kt │ │ │ └── flow │ │ │ └── EventFlow.kt │ └── res │ │ ├── anim │ │ ├── blink.xml │ │ ├── slide_in_from_down.xml │ │ ├── slide_in_from_left.xml │ │ ├── slide_in_from_right.xml │ │ ├── slide_in_from_up.xml │ │ ├── slide_out_to_down.xml │ │ ├── slide_out_to_left.xml │ │ ├── slide_out_to_right.xml │ │ └── slide_out_to_up.xml │ │ ├── color │ │ └── button_state_color.xml │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── bg_bottom_sheet.xml │ │ ├── bg_btn_of_map.xml │ │ ├── bg_button_clicked.xml │ │ ├── bg_button_unclicked.xml │ │ ├── bg_character_img.xml │ │ ├── bg_game_result.xml │ │ ├── bg_hp_bar.xml │ │ ├── bg_lying_shiba_in_menu.xml │ │ ├── bg_lying_shiba_in_profile.xml │ │ ├── bg_menu_green.xml │ │ ├── bg_menu_indigo.xml │ │ ├── bg_menu_peach.xml │ │ ├── bg_menu_pink.xml │ │ ├── bg_menu_sky.xml │ │ ├── bg_modest_shiba_in_menu.xml │ │ ├── bg_modest_shiba_in_profile.xml │ │ ├── bg_profile.xml │ │ ├── bg_ranking.xml │ │ ├── bg_round_btn.xml │ │ ├── bg_sleepy_shiba_in_menu.xml │ │ ├── bg_sleepy_shiba_in_profile.xml │ │ ├── bg_speech_bubble.xml │ │ ├── bg_speech_bubble_body.xml │ │ ├── bg_speech_bubble_tail.xml │ │ ├── bg_splash_img.xml │ │ ├── bg_stubborn_shiba_in_menu.xml │ │ ├── bg_stubborn_shiba_in_profile.xml │ │ ├── bg_sudden_shiba_in_menu.xml │ │ ├── bg_sudden_shiba_in_profile.xml │ │ ├── bg_tag.xml │ │ ├── ic_app.xml │ │ ├── ic_baseline_location_searching.xml │ │ ├── ic_baseline_timer.xml │ │ ├── ic_bomb.xml │ │ ├── ic_calendar.xml │ │ ├── ic_crown.xml │ │ ├── ic_destination.xml │ │ ├── ic_destination_flag.xml │ │ ├── ic_goal_marker.xml │ │ ├── ic_home.xml │ │ ├── ic_hourglass.xml │ │ ├── ic_invite_code.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_location.xml │ │ ├── ic_nickname.xml │ │ ├── ic_outline_info.xml │ │ ├── ic_people.xml │ │ ├── ic_rank_label.xml │ │ ├── ic_refresh.xml │ │ ├── ic_search.xml │ │ ├── ic_splash_bubble.xml │ │ ├── ic_switch.xml │ │ ├── ic_time.xml │ │ ├── ic_today.xml │ │ └── shape_thumb.xml │ │ ├── font-v26 │ │ └── font.xml │ │ ├── font │ │ ├── sans400.otf │ │ └── sans500.otf │ │ ├── layout │ │ ├── activity_creating_promise.xml │ │ ├── activity_game_result.xml │ │ ├── activity_gaming.xml │ │ ├── activity_join.xml │ │ ├── activity_main.xml │ │ ├── activity_profile.xml │ │ ├── activity_promise_history.xml │ │ ├── activity_promise_info.xml │ │ ├── activity_promises.xml │ │ ├── activity_splash.xml │ │ ├── bottomsheet_gaming.xml │ │ ├── bottomsheet_gaming_promise_info.xml │ │ ├── customview_character_marker.xml │ │ ├── customview_hp_bar.xml │ │ ├── dialog_fragment_check_promise_data.xml │ │ ├── dialog_fragment_shake_device.xml │ │ ├── dialog_fragment_total_cost.xml │ │ ├── fragment_character.xml │ │ ├── fragment_creating_promise.xml │ │ ├── fragment_find_promise.xml │ │ ├── fragment_game_result.xml │ │ ├── fragment_gaming.xml │ │ ├── fragment_location_search.xml │ │ ├── fragment_location_search_result.xml │ │ ├── fragment_main.xml │ │ ├── fragment_profile.xml │ │ ├── fragment_promise_info.xml │ │ ├── fragment_promises.xml │ │ ├── fragment_split_money.xml │ │ ├── item_balance.xml │ │ ├── item_gaming_ranking.xml │ │ ├── item_location_search_result.xml │ │ ├── item_promise_history.xml │ │ ├── item_promise_user.xml │ │ ├── item_ranking.xml │ │ ├── item_user_split_money.xml │ │ ├── layout_character_img.xml │ │ ├── layout_double_button.xml │ │ ├── layout_history_progress.xml │ │ ├── layout_map_icon_info.xml │ │ ├── layout_map_icon_location.xml │ │ ├── layout_progressbar.xml │ │ ├── layout_submit_button.xml │ │ └── layout_toolbar.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── navigation │ │ ├── nav_creating_promise.xml │ │ ├── nav_game_result.xml │ │ └── nav_main.xml │ │ ├── raw │ │ ├── beer.json │ │ ├── finish.json │ │ ├── money.json │ │ └── shake_device.json │ │ ├── values-night │ │ ├── colors.xml │ │ └── themes.xml │ │ ├── values │ │ ├── attrs_hp_bar.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ ├── styles.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── kotlin │ └── com │ └── woory │ └── almostthere │ └── presentation │ ├── ExampleUnitTest.kt │ └── ui │ └── gameresult │ └── SplitMoneyLogicTest.kt └── settings.gradle.kts /.github/ISSUE_TEMPLATE/🌟-refactor.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F31F Refactor" 3 | about: Refactoring 시 작성해주세요. 4 | title: "${리팩토링 사항} 리팩토링" 5 | labels: "\U0001F31F 리팩토링, \U0001F528 기능" 6 | assignees: '' 7 | 8 | --- 9 | 10 | # 목차 11 | - [기존 구현 사항](#기존-구현-사항) 12 | - [개선 사항](#개선-사항) 13 | 14 | 15 | # 기존 구현 사항 16 | > 기존 구현 사항을 작성합니다. 17 | > 18 | > ex. 각 모듈에서 사용하는 데이터 클래스가 뒤죽박죽이고, 일관성이 없다. 19 | 20 |
21 | 22 | # 개선 사항 23 | > 개선 사항을 작성합니다. 24 | > 25 | > ex. ~과 같이 통일했다. 26 | 27 |
28 | 29 | # 메모 30 | ## 학습 31 | > 학습 키워드 혹은 내용을 작성합니다. 없으면 비워도 좋습니다. 32 | 33 | ## 참고 사이트 34 | > 개선시키면서 (본문에 포함하지않은) 참고한 사이트를 정리합니다. 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/🐞-bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41E Bug" 3 | about: Bug 발생 시 작성해주세요. 4 | title: "<버그 요약> 버그 해결" 5 | labels: "\U0001F41E 버그" 6 | assignees: '' 7 | 8 | --- 9 | 10 | # 목차 11 | - [개요](#개요) 12 | - [발견 사항](#발견-사항) 13 | - [스크린샷/GIF](#스크린샷gif) 14 | - [제안](#제안) 15 | - [메모](#메모) 16 | - [학습](#학습) 17 | - [참고 사이트](#참고-사이트) 18 | 19 | # 개요 20 | > 버그가 어느 부분에서 발생하는지 적습니다. 21 | > 22 | > ex. Fragment에 MapView를 적용할 때 발생한 에러 23 | 24 |
25 | 26 | # 발견 사항 27 | > 구체적으로 버그가 언제, 어떻게, 왜 발생하는지 적습니다. 28 | > 29 | > ex. 30 | > 31 | > `java.lang.ClassCastException: dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper cannot be cast to android.app.Activity` 32 | > 33 | > Fragment에서 Context를 가져올 때 FragmentContextWrapper를 사용하는데, 여기서 가져오는 Context가 Activity의 Context가 아니어서 발생하는 문제. 34 | > 35 | > 이를 Hilt에서는 인식하지 못해 런타임 시에 크래시가 발생한다. 36 | 37 |
38 | 39 | # 스크린샷/GIF 40 | > 문제 발생을 스크린샷 or Gif로 넣습니다. 41 | 42 |
43 | 44 | # 제안 45 | > 문제를 해결하기 위한 방안을 제시합니다. 46 | > 47 | > ex. 기존 MapView를 FrameLayout으로 대체하고 코드 상에서 동적으로 추가한다. 그리고 이때 context를 baseContext로 넣어주자. 48 | 49 |
50 | 51 | # 메모 52 | ## 학습 53 | > 버그를 해결하면서 학습한 내용 or 학습 키워드를 정리합니다. 54 | > 55 | > 만약 없다면 비워둬도 괜찮습니다! 56 | 57 | ## 참고 사이트 58 | > 해결하면서 참고한 사이트를 적습니다. 59 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/🔨-feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F528 Feature" 3 | about: Feature 구현 시 작성해주세요. 4 | title: "<기능 이름> 기능 구현" 5 | labels: "\U0001F528 기능" 6 | assignees: '' 7 | 8 | --- 9 | 10 | # 목차 11 | - [개요](#개요) 12 | - [문제 정의](#문제-정의) 13 | - [제안 작업](#제안-작업) 14 | - [성공 기준](#성공-기준) 15 | - [요구사항](#요구사항) 16 | - [필수 요구사항](#필수-요구사항) 17 | - [선택 요구사항](#선택-요구사항) 18 | - [비요구사항](#비요구사항) 19 | - [설계 및 구현](#설계-및-구현) 20 | - [설계](#설계) 21 | - [체크 포인트](#체크-포인트) 22 | - [메모](#메모) 23 | - [학습](#학습) 24 | - [참고 사이트](#참고-사이트) 25 | 26 | # 개요 27 | ## 문제 정의 28 | > 이 작업을 통해 해결하려는 문제를 정의합니다. 29 | 30 | > ex. 현재 쿼리는 캐시되어있지만, 성능이 평균 이하이며, 이로 인해 프론트엔드에서 로딩 시간이 느려지고 있습니다. 31 | 32 | ## 제안 작업 33 | > 문제에 대한 해결책과 그 이유에 대한 전체적인 개요를 입력합니다. 34 | 35 | ## 성공 기준 36 | > 이 프로젝트를 성공으로 간주하려면 아래의 기준을 충족해야 합니다. 37 | 38 |
39 | 40 | # 요구사항 41 | ## 필수 요구사항 42 | > 현재 프로젝트의 요구사항을 입력합니다. 43 | 44 | ## 선택 요구사항 45 | > 다음에 할 예정인 요구사항을 나열하세요. 46 | 47 | ## 비요구사항 48 | > 프로젝트 범위를 벗어난 모든 사항을 열거하세요. 49 | 50 |
51 | 52 | # 설계 및 구현 53 | ## 설계 54 | > 설계 내용을 자유롭게 작성합니다. 구현 로직 등을 적어도 좋습니다. 55 | > 56 | > ⚠️ 어떤 기술 or 스킬을 선택하기로 했다면 왜 선택했는지를 적어야 합니다. ⚠️ 57 | > 58 | > ex. 약속 코드 생성을 어떻게 구현할까? 59 | > 1. 서버에서 하는 방법 60 | > - 가장 이상적인 방법 61 | > - 그러나 서버 개발자가 아니므로 현재로서는 어려움 62 | > 2. 로컬에서 하는 방법 63 | > - 서버에서 코드를 불러와 while문으로 확인하는 방법 64 | > - 시간은 들더라도 로컬에서 할 수 있는 최선의 방법 65 | > 66 | > 현 프로젝트 상황에서 서버를 건들기가 어려우므로 2번을 선택했다. 67 | > 68 | 69 |
70 | 71 | ## 체크 포인트 72 | > 해당 이슈를 해결하기 위한 Task를 작게 잘라서 작성합니다. 73 | > 74 | > ex. [ ] 위치 권한 묻기 구현 75 | 76 |
77 | 78 | ## 이슈 79 | > 설계와 다르게 진행이 되었던 작업에 대해 작성합니다. 80 | > 81 | > ex. 설계에서는 Parcelize를 사용하기로 했는데 하지 못하게 되었다. 82 | > 그래서 ~ 방식으로 우선 해결했다. 83 | 84 |
85 | 86 | ## 추후 보완 사항 87 | > 추후 리팩토링 할 작업에 대해 정리합니다. 88 | 89 |
90 | 91 | # 메모 92 | ## 학습 93 | > 학습 정리 or 학습 키워드를 적습니다. 학습하고 난 후 링크를 첨부해도 좋습니다. 94 | 95 | ## 참고 사이트 96 | > 구현 시에 참고한 사이트를 작성합니다. 97 | > 98 | > 위 설계에 첨부해도 좋습니다. 그 외 설계에 담기가 어려웠던 참고 사이트를 적어주시면 됩니다. 99 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Related Issue 2 | 3 | - resolved boostcampwm-2022/android11-almost-there#7 4 | 5 | ## 작업 내용 요약 6 | 7 | - 약속 참여 화면 구현 8 | 9 | ## Checklist 10 | 11 | - [x] Merge 되는 브랜치가 올바른가? 12 | - [x] Reviewers, Assignees, Labels, Projects, Milestone 설정을 했는가? 13 | - [x] 스스로 코드 리뷰를 충분히 진행했는가? 14 | - [x] 프로젝트의 스타일 가이드라인(컨벤션)을 준수하는가? -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | /.idea 18 | .gradle/ 19 | build/ 20 | 21 | # Local configuration file (sdk path, etc) 22 | local.properties 23 | 24 | # Proguard folder generated by Eclipse 25 | proguard/ 26 | 27 | # Log Files 28 | *.log 29 | 30 | # Android Studio Navigation editor temp files 31 | .navigation/ 32 | 33 | # Android Studio captures folder 34 | captures/ 35 | 36 | # Intellij 37 | *.iml 38 | .idea/workspace.xml 39 | .idea/tasks.xml 40 | .idea/gradle.xml 41 | .idea/dictionaries 42 | .idea/libraries 43 | app/.idea/ 44 | 45 | # Mac 46 | *.DS_Store 47 | 48 | # Keystore files 49 | *.jks 50 | 51 | # External native build folder generated in Android Studio 2.2 and later 52 | .externalNativeBuild 53 | 54 | # Google Services (e.g. APIs or Firebase) 55 | google-services.json -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/src/main/kotlin/com/woory/almostthere/AlmostThereApp.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class AlmostThereApp : Application() -------------------------------------------------------------------------------- /app/src/main/kotlin/com/woory/almostthere/di/DataModule.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.di 2 | 3 | import com.woory.almostthere.data.repository.DefaultPromiseRepository 4 | import com.woory.almostthere.data.repository.DefaultRouteRepository 5 | import com.woory.almostthere.data.repository.DefaultUserRepository 6 | import com.woory.almostthere.data.repository.PromiseRepository 7 | import com.woory.almostthere.data.repository.RouteRepository 8 | import com.woory.almostthere.data.repository.UserRepository 9 | import dagger.Binds 10 | import dagger.Module 11 | import dagger.hilt.InstallIn 12 | import dagger.hilt.components.SingletonComponent 13 | 14 | @Module 15 | @InstallIn(SingletonComponent::class) 16 | interface DataModule { 17 | 18 | @Binds 19 | fun bindsPromiseRepository(defaultPromiseRepository: DefaultPromiseRepository): PromiseRepository 20 | 21 | @Binds 22 | fun bindsUserRepository(defaultUserRepository: DefaultUserRepository): UserRepository 23 | 24 | @Binds 25 | fun bindsRouteRepository(defaultRouteRepository: DefaultRouteRepository): RouteRepository 26 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/woory/almostthere/di/DispatchersModule.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.di 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.components.SingletonComponent 7 | import kotlinx.coroutines.CoroutineScope 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.SupervisorJob 10 | import javax.inject.Singleton 11 | 12 | @Module 13 | @InstallIn(SingletonComponent::class) 14 | object DispatchersModule { 15 | 16 | @Provides 17 | @Singleton 18 | fun provideIOScope(): CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) 19 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/woory/almostthere/initializer/KakaoSdkInitializer.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.initializer 2 | 3 | import android.content.Context 4 | import androidx.startup.Initializer 5 | import com.kakao.sdk.common.KakaoSdk 6 | import com.woory.almostthere.BuildConfig 7 | 8 | class KakaoSdkInitializer : Initializer { 9 | 10 | override fun create(context: Context) { 11 | KakaoSdk.init(context, BuildConfig.KAKAO_NATIVE_APP_KEY) 12 | } 13 | 14 | override fun dependencies(): List>> = emptyList() 15 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/woory/almostthere/initializer/ThreeTenInitializer.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.initializer 2 | 3 | import android.content.Context 4 | import androidx.startup.Initializer 5 | import com.jakewharton.threetenabp.AndroidThreeTen 6 | 7 | class ThreeTenInitializer : Initializer { 8 | 9 | override fun create(context: Context) { 10 | AndroidThreeTen.init(context) 11 | } 12 | 13 | override fun dependencies(): List>> = emptyList() 14 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/woory/almostthere/initializer/TimberInitializer.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.initializer 2 | 3 | import android.content.Context 4 | import androidx.startup.Initializer 5 | import com.woory.almostthere.BuildConfig 6 | import timber.log.Timber 7 | 8 | class TimberInitializer : Initializer { 9 | 10 | override fun create(context: Context) { 11 | if (BuildConfig.DEBUG) { 12 | Timber.plant(Timber.DebugTree()) 13 | } 14 | } 15 | 16 | override fun dependencies(): List>> = emptyList() 17 | } -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | @Suppress("DSL_SCOPE_VIOLATION") 3 | plugins { 4 | alias(libs.plugins.spotless) 5 | } 6 | 7 | buildscript { 8 | repositories { 9 | google() 10 | mavenCentral() 11 | gradlePluginPortal() 12 | maven(url = "https://devrepo.kakao.com/nexus/content/groups/public/") 13 | } 14 | 15 | dependencies { 16 | classpath(libs.agp) 17 | classpath(libs.kotlin.gradle.plugin) 18 | classpath(libs.hilt.plugin) 19 | classpath(libs.google.services) 20 | classpath(libs.navigation.safeArgs) 21 | } 22 | } 23 | 24 | subprojects { 25 | apply(plugin = rootProject.libs.plugins.spotless.get().pluginId) 26 | 27 | tasks.withType().all { 28 | kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() 29 | kotlinOptions.freeCompilerArgs += listOf( 30 | "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", 31 | "-Xopt-in=kotlin.time.ExperimentalTime", 32 | ) 33 | } 34 | 35 | extensions.configure { 36 | kotlin { 37 | targetExclude("$buildDir/**/*.kt") 38 | ktlint() 39 | trimTrailingWhitespace() 40 | endWithNewline() 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | mavenCentral() 7 | } -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/Configuration.kt: -------------------------------------------------------------------------------- 1 | object Configuration { 2 | 3 | const val BUILD_TOOLS_VERSION = "33.0.1" 4 | const val COMPILE_SDK = 33 5 | const val TARGET_SDK = 33 6 | const val MIN_SDK = 23 7 | const val MAJOR_VERSION = 0 8 | const val MINOR_VERSION = 0 9 | const val PATCH_VERSION = 1 10 | const val VERSION_NAME = "$MAJOR_VERSION.$MINOR_VERSION.$PATCH_VERSION" 11 | const val VERSION_CODE = 1 12 | } -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /data/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @Suppress("DSL_SCOPE_VIOLATION") 2 | plugins { 3 | id(libs.plugins.android.library.get().pluginId) 4 | id(libs.plugins.kotlin.android.get().pluginId) 5 | id(libs.plugins.kotlin.kapt.get().pluginId) 6 | } 7 | 8 | android { 9 | namespace = "com.woory.almostthere.data" 10 | buildToolsVersion = Configuration.BUILD_TOOLS_VERSION 11 | compileSdk = Configuration.COMPILE_SDK 12 | 13 | defaultConfig { 14 | minSdk = Configuration.MIN_SDK 15 | targetSdk = Configuration.TARGET_SDK 16 | } 17 | } 18 | 19 | dependencies { 20 | implementation(libs.threeten) 21 | 22 | // DI 23 | implementation(libs.hilt.android) 24 | kapt(libs.hilt.compiler) 25 | 26 | // Coroutines 27 | implementation(libs.coroutines) 28 | testImplementation(libs.coroutines) 29 | testImplementation(libs.coroutines.test) 30 | 31 | // Unit test 32 | testImplementation(libs.junit) 33 | androidTestImplementation(libs.androidx.junit) 34 | androidTestImplementation(libs.androidx.espresso) 35 | } -------------------------------------------------------------------------------- /data/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /data/src/main/kotlin/com/woory/almostthere/data/model/LocationModel.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.data.model 2 | 3 | data class LocationModel(val geoPoint: GeoPointModel, val address: String) 4 | 5 | data class GeoPointModel(val latitude: Double, val longitude: Double) -------------------------------------------------------------------------------- /data/src/main/kotlin/com/woory/almostthere/data/model/LocationSearchModel.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.data.model 2 | 3 | data class LocationSearchModel( 4 | val name: String, 5 | val address: LocationModel 6 | ) -------------------------------------------------------------------------------- /data/src/main/kotlin/com/woory/almostthere/data/model/MagneticInfoModel.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.data.model 2 | 3 | import org.threeten.bp.OffsetDateTime 4 | 5 | data class MagneticInfoModel( 6 | val gameCode: String, 7 | val centerPoint: GeoPointModel, 8 | val radius: Double, 9 | val initialRadius: Double, 10 | val updatedAt: OffsetDateTime 11 | ) -------------------------------------------------------------------------------- /data/src/main/kotlin/com/woory/almostthere/data/model/PathModel.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.data.model 2 | 3 | data class PathModel( 4 | val routeType: RouteType, 5 | val distance: Int, 6 | val time: Int, 7 | ) { 8 | val velocity = distance.toDouble() / time 9 | } -------------------------------------------------------------------------------- /data/src/main/kotlin/com/woory/almostthere/data/model/PromiseAlarmModel.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.data.model 2 | 3 | import org.threeten.bp.OffsetDateTime 4 | 5 | data class PromiseAlarmModel( 6 | val alarmCode: Int, 7 | val promiseCode: String, 8 | val status: String, 9 | val startTime: OffsetDateTime, 10 | val endTime: OffsetDateTime 11 | ) -------------------------------------------------------------------------------- /data/src/main/kotlin/com/woory/almostthere/data/model/PromiseHistoryModel.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.data.model 2 | 3 | data class PromiseHistoryModel( 4 | val promise: PromiseModel, 5 | val magnetic: MagneticInfoModel? = null, 6 | val users: List? = null 7 | ) -------------------------------------------------------------------------------- /data/src/main/kotlin/com/woory/almostthere/data/model/PromiseModel.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.data.model 2 | 3 | import org.threeten.bp.OffsetDateTime 4 | 5 | data class PromiseModel( 6 | val code: String, 7 | val data: PromiseDataModel 8 | ) 9 | 10 | data class PromiseDataModel( 11 | val promiseLocation: LocationModel, 12 | val promiseDateTime: OffsetDateTime, 13 | val gameDateTime: OffsetDateTime, 14 | val host: UserModel, 15 | val users: List 16 | ) -------------------------------------------------------------------------------- /data/src/main/kotlin/com/woory/almostthere/data/model/RouteType.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.data.model 2 | 3 | enum class RouteType { 4 | WALK, PUBLIC_TRANSIT, CAR, NONE 5 | } -------------------------------------------------------------------------------- /data/src/main/kotlin/com/woory/almostthere/data/model/UserHpModel.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.data.model 2 | 3 | import org.threeten.bp.OffsetDateTime 4 | 5 | data class UserHpModel( 6 | val userId: String, 7 | val hp: Int, 8 | val arrived: Boolean, 9 | val lost: Boolean, 10 | val updatedAt: OffsetDateTime 11 | ) -------------------------------------------------------------------------------- /data/src/main/kotlin/com/woory/almostthere/data/model/UserModel.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.data.model 2 | 3 | data class UserModel( 4 | val userId: String, 5 | val data: UserDataModel 6 | ) 7 | 8 | data class UserDataModel( 9 | val name: String, 10 | val profileImage: UserProfileImageModel 11 | ) 12 | 13 | data class UserProfileImageModel( 14 | val color: String, 15 | val imageIndex: Int 16 | ) -------------------------------------------------------------------------------- /data/src/main/kotlin/com/woory/almostthere/data/model/UserPreferencesModel.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.data.model 2 | 3 | data class UserPreferencesModel( 4 | val userID: String, 5 | val createdAt: Long 6 | ) -------------------------------------------------------------------------------- /data/src/main/kotlin/com/woory/almostthere/data/model/UserRankingModel.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.data.model 2 | 3 | data class UserRankingModel( 4 | val userId: String, 5 | val userData: UserDataModel, 6 | val hp: Int, 7 | val rankingNumber: Int 8 | ) -------------------------------------------------------------------------------- /data/src/main/kotlin/com/woory/almostthere/data/model/UserStateModel.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.data.model 2 | 3 | data class UserLocationModel( 4 | val id: String, 5 | val location: GeoPointModel, 6 | val updatedAt: Long, 7 | ) -------------------------------------------------------------------------------- /data/src/main/kotlin/com/woory/almostthere/data/repository/DefaultRouteRepository.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.data.repository 2 | 3 | import com.woory.almostthere.data.model.GeoPointModel 4 | import com.woory.almostthere.data.model.PathModel 5 | import com.woory.almostthere.data.model.RouteType 6 | import com.woory.almostthere.data.source.NetworkDataSource 7 | import javax.inject.Inject 8 | 9 | class DefaultRouteRepository @Inject constructor( 10 | private val networkDataSource: NetworkDataSource 11 | ) : RouteRepository { 12 | override suspend fun getMaximumVelocity( 13 | start: GeoPointModel, 14 | dest: GeoPointModel 15 | ): Result { 16 | return runCatching { 17 | val defaultPathModel = PathModel(RouteType.NONE, 0, Int.MAX_VALUE) 18 | 19 | with(networkDataSource) { 20 | arrayOf( 21 | getPublicTransitRoute(start, dest), 22 | getCarRoute(start, dest), 23 | getWalkRoute(start, dest) 24 | ).map { it.getOrDefault(defaultPathModel) } 25 | .filter { it.time != 0 } 26 | .map { it.velocity } 27 | .max() 28 | } 29 | } 30 | } 31 | 32 | override suspend fun getMinimumTime( 33 | start: GeoPointModel, 34 | dest: GeoPointModel 35 | ): Result { 36 | return runCatching { 37 | val defaultPathModel = PathModel(RouteType.NONE, 0, 0) 38 | 39 | with(networkDataSource) { 40 | arrayOf( 41 | getPublicTransitRoute(start, dest), 42 | getCarRoute(start, dest), 43 | getWalkRoute(start, dest) 44 | ).map { it.getOrDefault(defaultPathModel) } 45 | .filter { it.time != 0 }.minOfOrNull { it.time } ?: -1 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /data/src/main/kotlin/com/woory/almostthere/data/repository/DefaultUserRepository.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.data.repository 2 | 3 | import com.woory.almostthere.data.model.UserPreferencesModel 4 | import com.woory.almostthere.data.source.DatabaseDataSource 5 | import kotlinx.coroutines.flow.Flow 6 | import javax.inject.Inject 7 | 8 | class DefaultUserRepository @Inject constructor( 9 | private val databaseDataSource: DatabaseDataSource, 10 | ) : UserRepository { 11 | 12 | override val userPreferences: Flow = 13 | databaseDataSource.getUserPreferences() 14 | } -------------------------------------------------------------------------------- /data/src/main/kotlin/com/woory/almostthere/data/repository/RouteRepository.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.data.repository 2 | 3 | import com.woory.almostthere.data.model.GeoPointModel 4 | 5 | interface RouteRepository { 6 | 7 | suspend fun getMaximumVelocity(start: GeoPointModel, dest: GeoPointModel): Result 8 | 9 | suspend fun getMinimumTime(start: GeoPointModel, dest: GeoPointModel): Result 10 | } -------------------------------------------------------------------------------- /data/src/main/kotlin/com/woory/almostthere/data/repository/UserRepository.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.data.repository 2 | 3 | import com.woory.almostthere.data.model.UserPreferencesModel 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | interface UserRepository { 7 | 8 | val userPreferences: Flow 9 | } -------------------------------------------------------------------------------- /data/src/main/kotlin/com/woory/almostthere/data/source/DatabaseDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.data.source 2 | 3 | import com.woory.almostthere.data.model.PromiseAlarmModel 4 | import com.woory.almostthere.data.model.PromiseModel 5 | import com.woory.almostthere.data.model.UserPreferencesModel 6 | import kotlinx.coroutines.flow.Flow 7 | 8 | interface DatabaseDataSource { 9 | 10 | suspend fun setPromiseAlarmByPromiseModel(promiseModel: PromiseModel): Result 11 | 12 | suspend fun setPromiseAlarmByPromiseAlarmModel(promiseAlarmModel: PromiseAlarmModel): Result 13 | 14 | suspend fun getAll(): Result> 15 | 16 | suspend fun getPromiseAlarmSortedByStartTime(): Result> 17 | 18 | suspend fun getPromiseAlarmSortedByEndTime(): Result> 19 | 20 | suspend fun getPromiseAlarmWhereCode(promiseCode: String): Result 21 | 22 | fun getJoinedPromises(): Flow> 23 | 24 | fun getUserPreferences(): Flow 25 | } -------------------------------------------------------------------------------- /data/src/main/kotlin/com/woory/almostthere/data/util/Const.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.data.util 2 | 3 | const val MAGNETIC_FIELD_UPDATE_TERM_SECOND = 60 -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /database/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @Suppress("DSL_SCOPE_VIOLATION") 2 | plugins { 3 | id(libs.plugins.android.library.get().pluginId) 4 | id(libs.plugins.kotlin.android.get().pluginId) 5 | id(libs.plugins.kotlin.kapt.get().pluginId) 6 | } 7 | 8 | android { 9 | namespace = "com.woory.almostthere.database" 10 | buildToolsVersion = Configuration.BUILD_TOOLS_VERSION 11 | compileSdk = Configuration.COMPILE_SDK 12 | 13 | defaultConfig { 14 | minSdk = Configuration.MIN_SDK 15 | targetSdk = Configuration.TARGET_SDK 16 | } 17 | } 18 | 19 | dependencies { 20 | // Modules 21 | implementation(project(":data")) 22 | 23 | // DI 24 | implementation(libs.hilt.android) 25 | kapt(libs.hilt.compiler) 26 | 27 | // Room 28 | implementation(libs.androidx.room.ktx) 29 | implementation(libs.androidx.room.runtime) 30 | kapt(libs.androidx.room.compiler) 31 | kapt(libs.room.persistence) 32 | testImplementation(libs.androidx.room.testing) 33 | 34 | // DataStore 35 | implementation(libs.datastore.preferences) 36 | 37 | // For Time stuff 38 | implementation(libs.threeten) 39 | 40 | // Unit test 41 | testImplementation(libs.junit) 42 | androidTestImplementation(libs.androidx.junit) 43 | androidTestImplementation(libs.androidx.espresso) 44 | } -------------------------------------------------------------------------------- /database/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /database/src/main/kotlin/com/woory/almostthere/database/AlmostThereDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.database 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import androidx.room.TypeConverters 6 | import com.woory.almostthere.database.entity.PromiseAlarmEntity 7 | 8 | @Database( 9 | entities = [PromiseAlarmEntity::class], 10 | version = 1, 11 | exportSchema = true 12 | ) 13 | @TypeConverters(value = [OffsetDateTimeConverter::class]) 14 | abstract class AlmostThereDatabase : RoomDatabase() { 15 | 16 | abstract fun promiseAlarmDao(): PromiseAlarmDao 17 | } -------------------------------------------------------------------------------- /database/src/main/kotlin/com/woory/almostthere/database/OffsetDateTimeConverter.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.database 2 | 3 | import androidx.room.TypeConverter 4 | import org.threeten.bp.Instant 5 | import org.threeten.bp.OffsetDateTime 6 | import org.threeten.bp.ZoneId 7 | 8 | class OffsetDateTimeConverter { 9 | 10 | private val zoneId: ZoneId = ZoneId.of("Asia/Seoul") 11 | 12 | @TypeConverter 13 | fun fromEpochMillis(epochMillis: Long): OffsetDateTime = 14 | OffsetDateTime.ofInstant(Instant.ofEpochMilli(epochMillis), zoneId) 15 | 16 | @TypeConverter 17 | fun fromOffsetDateTime(date: OffsetDateTime): Long = date.toInstant().toEpochMilli() 18 | } -------------------------------------------------------------------------------- /database/src/main/kotlin/com/woory/almostthere/database/PromiseAlarmDao.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.database 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy 6 | import androidx.room.Query 7 | import com.woory.almostthere.database.entity.PromiseAlarmEntity 8 | import kotlinx.coroutines.flow.Flow 9 | import org.threeten.bp.OffsetDateTime 10 | 11 | @Dao 12 | interface PromiseAlarmDao { 13 | 14 | @Insert(onConflict = OnConflictStrategy.REPLACE) 15 | suspend fun setPromiseAlarm(gameTimeEntity: PromiseAlarmEntity) 16 | 17 | @Query("SELECT * FROM promise_alarm WHERE end_time > :currentTime") 18 | suspend fun getAll( 19 | currentTime: Long = OffsetDateTime.now().toInstant().toEpochMilli() 20 | ): List 21 | 22 | @Query("SELECT * From promise_alarm ORDER BY datetime(start_time)") 23 | suspend fun getPromiseAlarmSortedByStartTime(): List 24 | 25 | @Query("SELECT * From promise_alarm ORDER BY datetime(end_time)") 26 | suspend fun getPromiseAlarmSortedByEndTime(): List 27 | 28 | @Query("SELECT * FROM promise_alarm WHERE promiseCode=:promiseCode") 29 | suspend fun getPromiseAlarmWhereCode(promiseCode: String): PromiseAlarmEntity 30 | 31 | @Query("SELECT * FROM promise_alarm ORDER BY end_time ASC") 32 | fun getJoinedPromises(): Flow> 33 | } -------------------------------------------------------------------------------- /database/src/main/kotlin/com/woory/almostthere/database/entity/PromiseAlarmEntity.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.database.entity 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | import org.threeten.bp.OffsetDateTime 7 | 8 | @Entity(tableName = "promise_alarm") 9 | data class PromiseAlarmEntity( 10 | @PrimaryKey(autoGenerate = true) val alarmCode: Int = 0, 11 | @ColumnInfo val promiseCode: String, 12 | @ColumnInfo(name = "status") val status: String, 13 | @ColumnInfo(name = "start_time") val startTime: OffsetDateTime, 14 | @ColumnInfo(name = "end_time") val endTime: OffsetDateTime 15 | ) -------------------------------------------------------------------------------- /database/src/main/kotlin/com/woory/almostthere/database/entity/mapper/PromiseAlarmMapper.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.database.entity.mapper 2 | 3 | import com.woory.almostthere.data.model.PromiseAlarmModel 4 | import com.woory.almostthere.data.model.PromiseModel 5 | import com.woory.almostthere.database.entity.PromiseAlarmEntity 6 | 7 | fun PromiseModel.asPromiseAlarmEntity() = 8 | PromiseAlarmEntity( 9 | promiseCode = code, 10 | status = "READY", 11 | startTime = data.gameDateTime, 12 | endTime = data.promiseDateTime, 13 | ) 14 | 15 | fun PromiseAlarmEntity.asPromiseAlarmModel() = 16 | PromiseAlarmModel( 17 | alarmCode = alarmCode, 18 | promiseCode = promiseCode, 19 | status = status, 20 | startTime = startTime, 21 | endTime = endTime 22 | ) 23 | 24 | fun PromiseAlarmModel.asPromiseAlarmEntity() = 25 | PromiseAlarmEntity( 26 | alarmCode = alarmCode, 27 | promiseCode = promiseCode, 28 | status = status, 29 | startTime = startTime, 30 | endTime = endTime 31 | ) 32 | 33 | fun List.asPromiseAlarmModel() = 34 | map { 35 | it.asPromiseAlarmModel() 36 | } -------------------------------------------------------------------------------- /database/src/main/kotlin/com/woory/almostthere/database/preferences/UserPreferences.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.database.preferences 2 | 3 | import androidx.datastore.preferences.core.longPreferencesKey 4 | import androidx.datastore.preferences.core.stringPreferencesKey 5 | 6 | data class UserPreferences( 7 | val userID: String, 8 | val createdAt: Long 9 | ) { 10 | 11 | companion object { 12 | val USER_ID = stringPreferencesKey("user_id") 13 | val CREATED_AT = longPreferencesKey("created_at") 14 | } 15 | } -------------------------------------------------------------------------------- /database/src/main/kotlin/com/woory/almostthere/database/preferences/mapper/UserPreferencesMapper.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.database.preferences.mapper 2 | 3 | import com.woory.almostthere.data.model.UserPreferencesModel 4 | import com.woory.almostthere.database.preferences.UserPreferences 5 | 6 | fun UserPreferences.asUserPreferencesModel() = 7 | UserPreferencesModel( 8 | userID = userID, 9 | createdAt = createdAt 10 | ) 11 | 12 | fun UserPreferencesModel.asUserPreferences() = 13 | UserPreferences( 14 | userID = userID, 15 | createdAt = createdAt 16 | ) -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | 8 | # Specifies the JVM arguments used for the daemon process. 9 | # The setting is particularly useful for tweaking memory settings. 10 | # https://docs.gradle.org/current/userguide/build_environment.html#sec:configuring_jvm_memory 11 | org.gradle.jvmargs=-Xmx6g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g 12 | 13 | # https://docs.gradle.org/current/userguide/build_cache.html 14 | org.gradle.caching=true 15 | 16 | # When configured, Gradle will run in incubating parallel mode. 17 | # This option should only be used with decoupled projects. More details, visit 18 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 19 | org.gradle.parallel=true 20 | 21 | # Configure only necessary projects, useful with multimodule projects 22 | org.gradle.configureondemand=true 23 | 24 | # AndroidX package structure to make it clearer which packages are bundled with the 25 | # Android operating system, and which are packaged with your app's APK 26 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 27 | android.useAndroidX=true 28 | # Automatically convert third-party libraries to use AndroidX 29 | android.enableJetifier=true 30 | 31 | # Kotlin code style for this project: "official" or "obsolete": 32 | kotlin.code.style=official 33 | 34 | # Enables namespacing of each library's R class so that its R class includes only the 35 | # resources declared in the library itself and none from the library's dependencies, 36 | # thereby reducing the size of the R class for that library 37 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android11-almost-there/9d03b07e5124a8a0ee1a55e7667142dbca9a66c6/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /network/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /network/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties 2 | 3 | @Suppress("DSL_SCOPE_VIOLATION") 4 | plugins { 5 | id(libs.plugins.android.library.get().pluginId) 6 | id(libs.plugins.kotlin.android.get().pluginId) 7 | id(libs.plugins.kotlin.kapt.get().pluginId) 8 | id(libs.plugins.google.services.get().pluginId) 9 | } 10 | 11 | android { 12 | namespace = "com.woory.almostthere.network" 13 | buildToolsVersion = Configuration.BUILD_TOOLS_VERSION 14 | compileSdk = Configuration.COMPILE_SDK 15 | 16 | defaultConfig { 17 | minSdk = Configuration.MIN_SDK 18 | targetSdk = Configuration.TARGET_SDK 19 | 20 | buildConfigField("String", "MAP_API_KEY", getApiKey("MAP_API_KEY")) 21 | buildConfigField("String", "ODSAY_API_KEY", getApiKey("ODSAY_API_KEY")) 22 | } 23 | 24 | testOptions { 25 | unitTests.isReturnDefaultValues = true 26 | } 27 | } 28 | 29 | dependencies { 30 | // Modules 31 | implementation(project(":data")) 32 | 33 | // DI 34 | implementation(libs.hilt.android) 35 | kapt(libs.hilt.compiler) 36 | 37 | // Retrofit 38 | implementation(libs.retrofit) 39 | implementation(libs.retrofit.moshi) 40 | 41 | // Moshi 42 | implementation(libs.moshi) 43 | implementation(libs.moshi.kotlin) 44 | 45 | // Firebase Services 46 | implementation(libs.firebase.bom) 47 | implementation(libs.firebase.analytics) 48 | implementation(libs.firebase.firestore) 49 | 50 | implementation(libs.threeten) 51 | 52 | // Unit test 53 | testImplementation(libs.coroutines.test) 54 | testImplementation(libs.junit) 55 | androidTestImplementation(libs.androidx.junit) 56 | androidTestImplementation(libs.androidx.espresso) 57 | } 58 | 59 | fun getApiKey(propertyKey: String): String = gradleLocalProperties(rootDir).getProperty(propertyKey) -------------------------------------------------------------------------------- /network/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /network/src/main/kotlin/com/woory/almostthere/network/model/AddressInfoResponse.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.network.model 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | @JsonClass(generateAdapter = true) 7 | data class AddressInfoResponse( 8 | @field:Json(name = "addressInfo") val addressInfo: AddressInfo, 9 | ) 10 | 11 | @JsonClass(generateAdapter = true) 12 | data class AddressInfo( 13 | @field:Json(name = "fullAddress") val fullAddress: String, 14 | @field:Json(name = "addressType") val addressType: String, 15 | @field:Json(name = "city_do") val city_do: String, 16 | @field:Json(name = "gu_gun") val gu_gun: String, 17 | @field:Json(name = "eup_myun") val eup_myun: String, 18 | @field:Json(name = "adminDong") val adminDong: String, 19 | @field:Json(name = "adminDongCode") val adminDongCode: String, 20 | @field:Json(name = "legalDong") val legalDong: String, 21 | @field:Json(name = "legalDongCode") val legalDongCode: String, 22 | @field:Json(name = "ri") val ri: String, 23 | @field:Json(name = "bunji") val bunji: String, 24 | @field:Json(name = "roadName") val roadName: String, 25 | @field:Json(name = "buildingIndex") val buildingIndex: String, 26 | @field:Json(name = "buildingName") val buildingName: String, 27 | @field:Json(name = "mappingDistance") val mappingDistance: String, 28 | @field:Json(name = "roadCode") val roadCode: String 29 | ) 30 | -------------------------------------------------------------------------------- /network/src/main/kotlin/com/woory/almostthere/network/model/LocationSearchResponse.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.network.model 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | @JsonClass(generateAdapter = true) 7 | data class LocationSearchResponse( 8 | @field:Json(name = "addressInfo") val searchPoiInfo: SearchPoiInfo, 9 | ) 10 | 11 | @JsonClass(generateAdapter = true) 12 | data class SearchPoiInfo( 13 | @field:Json(name = "totalCount") val totalCount: String, 14 | @field:Json(name = "count") val count: String, 15 | @field:Json(name = "page") val page: String, 16 | @field:Json(name = "pois") val pois: PoiList 17 | ) 18 | 19 | @JsonClass(generateAdapter = true) 20 | data class PoiList( 21 | @field:Json(name = "poi") val poi: List 22 | ) 23 | 24 | @JsonClass(generateAdapter = true) 25 | data class Poi( 26 | @field:Json(name = "name") val name: String, 27 | @field:Json(name = "noorLat") val noorLat: String, 28 | @field:Json(name = "noorLon") val noorLon: String, 29 | @field:Json(name = "upperAddrName") val upperAddrName: String, 30 | @field:Json(name = "middleAddrName") val middleAddrName: String, 31 | @field:Json(name = "lowerAddrName") val lowerAddrName: String, 32 | @field:Json(name = "detailAddrName") val detailAddrName: String, 33 | @field:Json(name = "newAddressList") val newAddressList: NewAddressList, 34 | ) 35 | 36 | @JsonClass(generateAdapter = true) 37 | data class NewAddressList( 38 | @field:Json(name = "newAddress") val newAddress: List 39 | ) 40 | 41 | @JsonClass(generateAdapter = true) 42 | data class NewAddress( 43 | @field:Json(name = "centerLat") val centerLat: String, 44 | @field:Json(name = "centerLon") val centerLon: String, 45 | @field:Json(name = "fullAddressRoad") val fullAddressRoad: String, 46 | ) 47 | -------------------------------------------------------------------------------- /network/src/main/kotlin/com/woory/almostthere/network/model/MagneticInfoDocument.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.network.model 2 | 3 | import com.google.firebase.Timestamp 4 | import com.google.firebase.firestore.GeoPoint 5 | 6 | data class MagneticInfoDocument( 7 | val centerPoint: GeoPoint = GeoPoint(37.5559, 126.9723), 8 | val gameCode: String = "", 9 | val radius: Double = 1.0, 10 | val initialRadius: Double = 1.0, 11 | val timeStamp: Timestamp = Timestamp(1, 1) 12 | ) -------------------------------------------------------------------------------- /network/src/main/kotlin/com/woory/almostthere/network/model/PromiseData.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.network.model 2 | 3 | import com.google.firebase.Timestamp 4 | import com.google.firebase.firestore.GeoPoint 5 | 6 | data class PromiseDocument( 7 | val address: String = "", 8 | val code: String = "", 9 | val destination: GeoPoint = GeoPoint(0.0, 0.0), 10 | val host: PromiseParticipantField = PromiseParticipantField(), 11 | val gameTime: Timestamp = Timestamp(1, 1), 12 | val promiseTime: Timestamp = Timestamp(1, 1), 13 | val users: List = listOf(), 14 | val finished: Boolean = false, 15 | val started: Boolean = false, 16 | ) 17 | 18 | data class PromiseParticipantField( 19 | val userImage: UserImageInfoField = UserImageInfoField(), 20 | val userName: String = "", 21 | val userId: String = "" 22 | ) 23 | 24 | data class UserImageInfoField( 25 | val color: String = "", 26 | val imageIdx: Int = 0 27 | ) -------------------------------------------------------------------------------- /network/src/main/kotlin/com/woory/almostthere/network/model/UserHpDocument.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.network.model 2 | 3 | import com.google.firebase.Timestamp 4 | 5 | data class UserHpDocument( 6 | val userId: String = "", 7 | val hp: Int = 100, 8 | val arrived: Boolean = false, 9 | val lost: Boolean = false, 10 | val updatedAt: Timestamp = Timestamp(1, 1) 11 | ) -------------------------------------------------------------------------------- /network/src/main/kotlin/com/woory/almostthere/network/model/UserLocationDocument.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.network.model 2 | 3 | import com.google.firebase.firestore.GeoPoint 4 | import org.threeten.bp.OffsetDateTime 5 | 6 | data class UserLocationDocument( 7 | val id: String = "", 8 | val location: GeoPoint = GeoPoint(0.0, 0.0), 9 | val updatedAt: Long = OffsetDateTime.now().toInstant().toEpochMilli() 10 | ) -------------------------------------------------------------------------------- /network/src/main/kotlin/com/woory/almostthere/network/model/mapper/AddedUserHpMapper.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.network.model.mapper 2 | 3 | import com.woory.almostthere.data.model.UserHpModel 4 | import com.woory.almostthere.network.model.UserHpDocument 5 | import com.woory.almostthere.network.util.TimeConverter.asOffsetDate 6 | import com.woory.almostthere.network.util.TimeConverter.asTimeStamp 7 | 8 | object AddedUserHpMapper : ModelMapper { 9 | override fun asModel(domain: UserHpModel): UserHpDocument = 10 | UserHpDocument( 11 | userId = domain.userId, 12 | hp = domain.hp, 13 | arrived = domain.arrived, 14 | lost = domain.lost, 15 | updatedAt = domain.updatedAt.asTimeStamp() 16 | ) 17 | 18 | override fun asDomain(model: UserHpDocument): UserHpModel = 19 | UserHpModel( 20 | userId = model.userId, 21 | hp = model.hp, 22 | arrived = model.arrived, 23 | lost = model.lost, 24 | updatedAt = model.updatedAt.asOffsetDate() 25 | ) 26 | } 27 | 28 | internal fun UserHpModel.asModel() = AddedUserHpMapper.asModel(this) 29 | 30 | internal fun UserHpDocument.asDomain() = AddedUserHpMapper.asDomain(this) -------------------------------------------------------------------------------- /network/src/main/kotlin/com/woory/almostthere/network/model/mapper/LocationSearchMapper.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.network.model.mapper 2 | 3 | import com.woory.almostthere.data.model.GeoPointModel 4 | import com.woory.almostthere.data.model.LocationModel 5 | import com.woory.almostthere.data.model.LocationSearchModel 6 | import com.woory.almostthere.network.model.Poi 7 | 8 | object LocationSearchMapper { 9 | 10 | fun asDomain(model: Poi): LocationSearchModel = 11 | LocationSearchModel( 12 | name = model.name, 13 | address = LocationModel( 14 | geoPoint = GeoPointModel(model.noorLat.toDouble(), model.noorLon.toDouble()), 15 | address = model.newAddressList.newAddress[0].fullAddressRoad 16 | ) 17 | ) 18 | } 19 | 20 | fun Poi.asDomain() = LocationSearchMapper.asDomain(this) -------------------------------------------------------------------------------- /network/src/main/kotlin/com/woory/almostthere/network/model/mapper/MagneticDataMapper.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.network.model.mapper 2 | 3 | import com.google.firebase.firestore.GeoPoint 4 | import com.woory.almostthere.data.model.GeoPointModel 5 | import com.woory.almostthere.data.model.MagneticInfoModel 6 | import com.woory.almostthere.network.model.MagneticInfoDocument 7 | import com.woory.almostthere.network.util.TimeConverter.asOffsetDate 8 | import com.woory.almostthere.network.util.TimeConverter.asTimeStamp 9 | 10 | object MagneticDataMapper : ModelMapper { 11 | override fun asModel(domain: MagneticInfoModel): MagneticInfoDocument = 12 | MagneticInfoDocument( 13 | gameCode = domain.gameCode, 14 | centerPoint = GeoPoint( 15 | domain.centerPoint.latitude, 16 | domain.centerPoint.longitude 17 | ), 18 | radius = domain.radius, 19 | initialRadius = domain.initialRadius, 20 | timeStamp = domain.updatedAt.asTimeStamp() 21 | ) 22 | 23 | override fun asDomain(model: MagneticInfoDocument): MagneticInfoModel = 24 | MagneticInfoModel( 25 | model.gameCode, 26 | GeoPointModel( 27 | model.centerPoint.latitude, 28 | model.centerPoint.longitude 29 | ), 30 | model.radius, 31 | model.initialRadius, 32 | model.timeStamp.asOffsetDate() 33 | ) 34 | } 35 | 36 | internal fun MagneticInfoDocument.asDomain(): MagneticInfoModel = 37 | MagneticDataMapper.asDomain(this) 38 | 39 | internal fun MagneticInfoModel.asModel(): MagneticInfoDocument = 40 | MagneticDataMapper.asModel(this) -------------------------------------------------------------------------------- /network/src/main/kotlin/com/woory/almostthere/network/model/mapper/ModelMapper.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.network.model.mapper 2 | 3 | interface ModelMapper { 4 | 5 | fun asModel(domain: Domain): Model 6 | 7 | fun asDomain(model: Model): Domain 8 | } -------------------------------------------------------------------------------- /network/src/main/kotlin/com/woory/almostthere/network/model/mapper/UserLocationMapper.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.network.model.mapper 2 | 3 | import com.google.firebase.firestore.GeoPoint 4 | import com.woory.almostthere.data.model.GeoPointModel 5 | import com.woory.almostthere.data.model.UserLocationModel 6 | import com.woory.almostthere.network.model.UserLocationDocument 7 | 8 | object UserLocationMapper : ModelMapper { 9 | override fun asModel(domain: UserLocationModel): UserLocationDocument = UserLocationDocument( 10 | id = domain.id, 11 | location = GeoPoint(domain.location.latitude, domain.location.longitude), 12 | updatedAt = domain.updatedAt 13 | ) 14 | 15 | override fun asDomain(model: UserLocationDocument): UserLocationModel = UserLocationModel( 16 | id = model.id, 17 | location = GeoPointModel(model.location.latitude, model.location.longitude), 18 | updatedAt = model.updatedAt 19 | ) 20 | } 21 | 22 | internal fun UserLocationModel.asModel() = UserLocationMapper.asModel(this) 23 | 24 | internal fun UserLocationDocument.asDomain() = UserLocationMapper.asDomain(this) -------------------------------------------------------------------------------- /network/src/main/kotlin/com/woory/almostthere/network/model/mapper/UserModelMapper.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.network.model.mapper 2 | 3 | import com.woory.almostthere.data.model.UserDataModel 4 | import com.woory.almostthere.data.model.UserModel 5 | import com.woory.almostthere.data.model.UserProfileImageModel 6 | import com.woory.almostthere.network.model.PromiseParticipantField 7 | import com.woory.almostthere.network.model.UserImageInfoField 8 | 9 | internal fun UserModel.asPromiseParticipant() = PromiseParticipantField( 10 | userImage = this.data.profileImage.asUserImageField(), 11 | userName = this.data.name, 12 | userId = this.userId 13 | ) 14 | 15 | internal fun UserProfileImageModel.asUserImageField() = UserImageInfoField( 16 | color = this.color, 17 | imageIdx = this.imageIndex 18 | ) 19 | 20 | internal fun PromiseParticipantField.asUserModel() = UserModel( 21 | userId = userId, 22 | data = UserDataModel(userName, userImage.asUserImage()) 23 | ) 24 | 25 | internal fun UserImageInfoField.asUserImage() = UserProfileImageModel( 26 | color = this.color, 27 | imageIndex = this.imageIdx 28 | ) -------------------------------------------------------------------------------- /network/src/main/kotlin/com/woory/almostthere/network/service/ODsayService.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.network.service 2 | 3 | import okhttp3.ResponseBody 4 | import retrofit2.http.GET 5 | import retrofit2.http.Query 6 | 7 | interface ODsayService { 8 | 9 | @GET("searchPubTransPathT") 10 | suspend fun getPublicTransitRoute( 11 | @Query("apiKey") apiKey: String, 12 | @Query("SX") sx: Double, 13 | @Query("SY") sy: Double, 14 | @Query("EX") ex: Double, 15 | @Query("EY") ey: Double, 16 | ): ResponseBody 17 | } -------------------------------------------------------------------------------- /network/src/main/kotlin/com/woory/almostthere/network/service/TMapService.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.network.service 2 | 3 | import com.woory.almostthere.network.model.AddressInfoResponse 4 | import com.woory.almostthere.network.model.LocationSearchResponse 5 | import okhttp3.ResponseBody 6 | import retrofit2.http.GET 7 | import retrofit2.http.POST 8 | import retrofit2.http.Query 9 | 10 | interface TMapService { 11 | 12 | @GET("tmap/geo/reversegeocoding") 13 | suspend fun getReverseGeoCoding( 14 | @Query("version") version: Int = 1, 15 | @Query("lat") lat: String, 16 | @Query("lon") lon: String, 17 | @Query("coordType") coordType: String = "WGS84GEO", 18 | @Query("addressType") addressType: String = "A03", 19 | @Query("newAddressExtend") newAddressExtend: String = "Y" 20 | ): AddressInfoResponse 21 | 22 | @GET("tmap/pois") 23 | suspend fun getSearchedLocation( 24 | @Query("version") version: Int = 1, 25 | @Query("searchKeyword") searchKeyword: String, 26 | @Query("count") count: Int = 20 27 | ): LocationSearchResponse 28 | 29 | @POST("tmap/routes") 30 | suspend fun getCarRoute( 31 | @Query("version") version: Int = 1, 32 | @Query("startX") startX: Double, 33 | @Query("startY") startY: Double, 34 | @Query("endX") endX: Double, 35 | @Query("endY") endY: Double, 36 | ): ResponseBody 37 | 38 | @POST("tmap/routes/pedestrian") 39 | suspend fun getWalkRoute( 40 | @Query("version") version: Int = 1, 41 | @Query("startX") startX: Double, 42 | @Query("startY") startY: Double, 43 | @Query("endX") endX: Double, 44 | @Query("endY") endY: Double, 45 | @Query("startName") startName: String = "START", 46 | @Query("endName") endName: String = "END", 47 | ): ResponseBody 48 | } -------------------------------------------------------------------------------- /network/src/main/kotlin/com/woory/almostthere/network/util/InviteCodeUtil.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.network.util 2 | 3 | object InviteCodeUtil { 4 | 5 | private const val MAX_INVITE_CODE_LENGTH = 7 6 | private val codePool = (('A'..'Z') + (0..9)).shuffled() 7 | private val inviteCodeRegex = 8 | """[A-Z0-9]{$MAX_INVITE_CODE_LENGTH}""".toRegex(RegexOption.IGNORE_CASE) 9 | 10 | fun getRandomInviteCode(): String { 11 | val builder = StringBuilder() 12 | 13 | repeat(MAX_INVITE_CODE_LENGTH) { 14 | builder.append(codePool.random()) 15 | } 16 | 17 | return builder.toString() 18 | } 19 | 20 | fun String.isValidInviteCode(): Boolean = matches(inviteCodeRegex) 21 | } -------------------------------------------------------------------------------- /network/src/main/kotlin/com/woory/almostthere/network/util/TimeConverter.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.network.util 2 | 3 | import com.google.firebase.Timestamp 4 | import org.threeten.bp.Instant 5 | import org.threeten.bp.OffsetDateTime 6 | import org.threeten.bp.ZoneId 7 | 8 | object TimeConverter { 9 | 10 | private val zoneId: ZoneId = ZoneId.of("Asia/Seoul") 11 | 12 | fun Timestamp.asMillis(): Long = seconds * 1000 + (nanoseconds / 1000000) 13 | 14 | fun Timestamp.asOffsetDate(): OffsetDateTime { 15 | return OffsetDateTime.ofInstant(Instant.ofEpochMilli(asMillis()), zoneId) 16 | } 17 | 18 | fun Long.asTimeStamp() = Timestamp(this / 1000, (this % 1000).toInt() * 1000000) 19 | 20 | fun OffsetDateTime.asTimeStamp(): Timestamp = 21 | Timestamp(toEpochSecond(), nano) 22 | } -------------------------------------------------------------------------------- /presentation/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /presentation/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android11-almost-there/9d03b07e5124a8a0ee1a55e7667142dbca9a66c6/presentation/consumer-rules.pro -------------------------------------------------------------------------------- /presentation/libs/tmap-sdk-1.1.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android11-almost-there/9d03b07e5124a8a0ee1a55e7667142dbca9a66c6/presentation/libs/tmap-sdk-1.1.aar -------------------------------------------------------------------------------- /presentation/libs/vsm-tmap-sdk-v2-android-1.6.60.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android11-almost-there/9d03b07e5124a8a0ee1a55e7667142dbca9a66c6/presentation/libs/vsm-tmap-sdk-v2-android-1.6.60.aar -------------------------------------------------------------------------------- /presentation/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /presentation/src/androidTest/kotlin/com/woory/almostthere/presentation/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.soulplay.presentation 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.* 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.soulplay.presentation.test", appContext.packageName) 21 | } 22 | } -------------------------------------------------------------------------------- /presentation/src/debug/ic_woory_icon-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android11-almost-there/9d03b07e5124a8a0ee1a55e7667142dbca9a66c6/presentation/src/debug/ic_woory_icon-playstore.png -------------------------------------------------------------------------------- /presentation/src/debug/res/mipmap-anydpi-v26/ic_woory_icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /presentation/src/debug/res/mipmap-anydpi-v26/ic_woory_icon_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /presentation/src/debug/res/mipmap-hdpi/ic_woory_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android11-almost-there/9d03b07e5124a8a0ee1a55e7667142dbca9a66c6/presentation/src/debug/res/mipmap-hdpi/ic_woory_icon.png -------------------------------------------------------------------------------- /presentation/src/debug/res/mipmap-hdpi/ic_woory_icon_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android11-almost-there/9d03b07e5124a8a0ee1a55e7667142dbca9a66c6/presentation/src/debug/res/mipmap-hdpi/ic_woory_icon_round.png -------------------------------------------------------------------------------- /presentation/src/debug/res/mipmap-mdpi/ic_woory_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android11-almost-there/9d03b07e5124a8a0ee1a55e7667142dbca9a66c6/presentation/src/debug/res/mipmap-mdpi/ic_woory_icon.png -------------------------------------------------------------------------------- /presentation/src/debug/res/mipmap-mdpi/ic_woory_icon_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android11-almost-there/9d03b07e5124a8a0ee1a55e7667142dbca9a66c6/presentation/src/debug/res/mipmap-mdpi/ic_woory_icon_round.png -------------------------------------------------------------------------------- /presentation/src/debug/res/mipmap-xhdpi/ic_woory_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android11-almost-there/9d03b07e5124a8a0ee1a55e7667142dbca9a66c6/presentation/src/debug/res/mipmap-xhdpi/ic_woory_icon.png -------------------------------------------------------------------------------- /presentation/src/debug/res/mipmap-xhdpi/ic_woory_icon_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android11-almost-there/9d03b07e5124a8a0ee1a55e7667142dbca9a66c6/presentation/src/debug/res/mipmap-xhdpi/ic_woory_icon_round.png -------------------------------------------------------------------------------- /presentation/src/debug/res/mipmap-xxhdpi/ic_woory_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android11-almost-there/9d03b07e5124a8a0ee1a55e7667142dbca9a66c6/presentation/src/debug/res/mipmap-xxhdpi/ic_woory_icon.png -------------------------------------------------------------------------------- /presentation/src/debug/res/mipmap-xxhdpi/ic_woory_icon_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android11-almost-there/9d03b07e5124a8a0ee1a55e7667142dbca9a66c6/presentation/src/debug/res/mipmap-xxhdpi/ic_woory_icon_round.png -------------------------------------------------------------------------------- /presentation/src/debug/res/mipmap-xxxhdpi/ic_woory_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android11-almost-there/9d03b07e5124a8a0ee1a55e7667142dbca9a66c6/presentation/src/debug/res/mipmap-xxxhdpi/ic_woory_icon.png -------------------------------------------------------------------------------- /presentation/src/debug/res/mipmap-xxxhdpi/ic_woory_icon_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android11-almost-there/9d03b07e5124a8a0ee1a55e7667142dbca9a66c6/presentation/src/debug/res/mipmap-xxxhdpi/ic_woory_icon_round.png -------------------------------------------------------------------------------- /presentation/src/debug/res/values/ic_woory_icon_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #BFE3C9 4 | -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/background/HiltBroadcastReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.background 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import androidx.annotation.CallSuper 7 | 8 | abstract class HiltBroadcastReceiver : BroadcastReceiver() { 9 | 10 | @CallSuper 11 | override fun onReceive(p0: Context?, p1: Intent?) { } 12 | } 13 | -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/background/receiver/AlarmTouchReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.background.receiver 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import com.woory.almostthere.presentation.background.service.PromiseAlarmRegisterService 7 | import com.woory.almostthere.presentation.background.service.PromiseReadyService 8 | import com.woory.almostthere.presentation.background.util.asPromiseAlarm 9 | import com.woory.almostthere.presentation.background.util.putPromiseAlarm 10 | import com.woory.almostthere.presentation.background.util.startServiceBp 11 | import com.woory.almostthere.presentation.model.AlarmState 12 | import com.woory.almostthere.presentation.model.PromiseAlarm 13 | 14 | class AlarmTouchReceiver : BroadcastReceiver() { 15 | 16 | override fun onReceive(context: Context?, intent: Intent?) { 17 | context ?: return 18 | intent ?: return 19 | 20 | val promiseAlarm = intent.asPromiseAlarm() 21 | 22 | when (promiseAlarm.state) { 23 | AlarmState.READY -> { 24 | onTouchPromiseReadyNotification(context, promiseAlarm) 25 | } 26 | AlarmState.START -> { 27 | 28 | } 29 | AlarmState.END -> { 30 | 31 | } 32 | } 33 | } 34 | 35 | private fun onTouchPromiseReadyNotification( 36 | context: Context, 37 | promiseAlarm: PromiseAlarm 38 | ) { 39 | Intent(context, PromiseAlarmRegisterService::class.java).run { 40 | putPromiseAlarm(promiseAlarm.copy(state = AlarmState.START)) 41 | context.startServiceBp(this) 42 | } 43 | 44 | Intent(context, PromiseReadyService::class.java).run { 45 | putPromiseAlarm(promiseAlarm) 46 | context.startServiceBp(this) 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/background/receiver/BootBroadcastReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.background.receiver 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import com.woory.almostthere.presentation.background.service.AlarmRestartService 7 | import com.woory.almostthere.presentation.background.util.startServiceBp 8 | 9 | class BootBroadcastReceiver : BroadcastReceiver() { 10 | 11 | override fun onReceive(context: Context?, intent: Intent?) { 12 | context ?: return 13 | intent ?: return 14 | 15 | if (intent.action.equals(Intent.ACTION_BOOT_COMPLETED)) { 16 | val restartIntent = Intent(context, AlarmRestartService::class.java) 17 | context.startServiceBp(restartIntent) 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/background/util/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.background.util 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Build 6 | import com.woory.almostthere.presentation.model.PromiseAlarm 7 | import com.woory.almostthere.presentation.model.asAlarmState 8 | import com.woory.almostthere.presentation.util.PROMISE_CODE_KEY 9 | import com.woory.almostthere.presentation.util.TimeConverter.asMillis 10 | import com.woory.almostthere.presentation.util.TimeConverter.asOffsetDateTime 11 | 12 | fun Intent.putPromiseAlarm(promiseAlarm: PromiseAlarm) { 13 | this.putExtra("alarmCode", promiseAlarm.alarmCode) 14 | this.putExtra(PROMISE_CODE_KEY, promiseAlarm.promiseCode) 15 | this.putExtra("state", promiseAlarm.state.current) 16 | this.putExtra("startTime", promiseAlarm.startTime.asMillis()) 17 | this.putExtra("endTime", promiseAlarm.endTime.asMillis()) 18 | } 19 | 20 | fun Intent.asPromiseAlarm(): PromiseAlarm { 21 | val extras = this.extras ?: throw IllegalArgumentException("is extras null") 22 | val alarmCode = extras.getInt("alarmCode") 23 | val promiseCode = 24 | extras.getString(PROMISE_CODE_KEY) ?: throw IllegalArgumentException("is promise code null") 25 | val state = extras.getString("state") ?: throw IllegalArgumentException("is state null") 26 | val startTime = extras.getLong("startTime") 27 | val endTime = extras.getLong("endTime") 28 | 29 | return PromiseAlarm( 30 | alarmCode = alarmCode, 31 | promiseCode = promiseCode, 32 | state = state.asAlarmState(), 33 | startTime = startTime.asOffsetDateTime(), 34 | endTime = endTime.asOffsetDateTime() 35 | ) 36 | } 37 | 38 | fun Context.startServiceBp(intent: Intent) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 39 | this.startForegroundService(intent) 40 | } else { 41 | this.startService(intent) 42 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/binding/ButtonBinding.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.binding 2 | 3 | import android.widget.Button 4 | import androidx.databinding.BindingAdapter 5 | import com.woory.almostthere.presentation.ui.join.FormState 6 | 7 | @BindingAdapter("enabled") 8 | fun Button.bindEnabled(state: FormState) { 9 | isEnabled = state is FormState.Valid 10 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/binding/ImageViewBinding.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.binding 2 | 3 | import android.widget.ImageView 4 | import androidx.databinding.BindingAdapter 5 | import com.woory.almostthere.presentation.model.ProfileImage 6 | 7 | @BindingAdapter("src") 8 | fun ImageView.bindImage(index: Int) { 9 | val imageDrawable = ProfileImage.values()[index].getDrawableImage(context) 10 | 11 | setImageDrawable(imageDrawable) 12 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/binding/TextInputLayoutBinding.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.binding 2 | 3 | import androidx.databinding.BindingAdapter 4 | import com.google.android.material.textfield.TextInputLayout 5 | import com.woory.almostthere.presentation.R 6 | import com.woory.almostthere.presentation.ui.join.FormState 7 | 8 | @BindingAdapter("stateMessage") 9 | fun TextInputLayout.bindStateMessage(state: FormState) { 10 | error = if (state is FormState.Invalid) { 11 | context.getString(R.string.invalid_invite_code) 12 | } else { 13 | "" 14 | } 15 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/binding/TextViewBinding.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.binding 2 | 3 | import android.util.TypedValue 4 | import android.widget.TextView 5 | import androidx.appcompat.widget.AppCompatTextView 6 | import androidx.databinding.BindingAdapter 7 | import com.woory.almostthere.presentation.R 8 | import com.woory.almostthere.presentation.model.Promise 9 | import com.woory.almostthere.presentation.ui.history.PromiseHistoryViewType 10 | import com.woory.almostthere.presentation.util.TimeUtils.getDurationStringInMinuteToDay 11 | import org.threeten.bp.OffsetDateTime 12 | 13 | @BindingAdapter(value = ["itemStateType", "itemStatePromise"], requireAll = true) 14 | fun AppCompatTextView.bindItemState(type: PromiseHistoryViewType, promise: Promise) { 15 | val currentDateTime = OffsetDateTime.now() 16 | val beforeStartTime = 17 | getDurationStringInMinuteToDay(context, currentDateTime, promise.data.gameDateTime) 18 | val beforeEndTime = 19 | getDurationStringInMinuteToDay(context, currentDateTime, promise.data.promiseDateTime) 20 | 21 | text = when (type) { 22 | PromiseHistoryViewType.BEFORE -> context.getString( 23 | R.string.history_item_state_before, 24 | beforeStartTime 25 | ) 26 | PromiseHistoryViewType.ONGOING -> context.getString( 27 | R.string.history_item_state_ongoing, 28 | beforeEndTime 29 | ) 30 | PromiseHistoryViewType.END -> context.getString(R.string.promises_end) 31 | } 32 | } 33 | 34 | @BindingAdapter("date_time") 35 | fun AppCompatTextView.bindDateTime(dateTime: OffsetDateTime) = with(dateTime) { 36 | text = context.getString( 37 | R.string.date_time_template, 38 | year, 39 | monthValue, 40 | dayOfMonth, 41 | hour, 42 | minute 43 | ) 44 | } 45 | 46 | @BindingAdapter("textSize") 47 | fun TextView.bindTextSize(size: Int) { 48 | setTextSize(TypedValue.COMPLEX_UNIT_SP, size.toFloat()) 49 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/binding/ViewBinding.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.binding 2 | 3 | import android.view.View 4 | import android.view.ViewGroup.MarginLayoutParams 5 | import android.widget.FrameLayout 6 | import androidx.databinding.BindingAdapter 7 | import com.woory.almostthere.presentation.ui.history.PromiseHistoryViewType 8 | 9 | @BindingAdapter("toGone") 10 | fun View.bindToGone(isGone: Boolean) { 11 | visibility = if (isGone) { 12 | View.GONE 13 | } else { 14 | View.VISIBLE 15 | } 16 | } 17 | 18 | @BindingAdapter("itemRankLabel") 19 | fun View.bindItemRankLabel(type: PromiseHistoryViewType) { 20 | visibility = when (type) { 21 | PromiseHistoryViewType.BEFORE -> View.GONE 22 | else -> View.VISIBLE 23 | } 24 | } 25 | 26 | @BindingAdapter("itemHP") 27 | fun View.bindItemHP(type: PromiseHistoryViewType) { 28 | visibility = when (type) { 29 | PromiseHistoryViewType.ONGOING -> View.VISIBLE 30 | else -> View.GONE 31 | } 32 | } 33 | 34 | @BindingAdapter("visibilityByType") 35 | fun FrameLayout.bindVisibilityByType(type: PromiseHistoryViewType) { 36 | visibility = when (type) { 37 | PromiseHistoryViewType.END -> View.GONE 38 | else -> View.VISIBLE 39 | } 40 | } 41 | 42 | @BindingAdapter("layoutMarginHorizontal") 43 | fun View.bindMarinHorizontal(dimen: Float) { 44 | layoutParams = (layoutParams as MarginLayoutParams).apply { 45 | marginStart = dimen.toInt() 46 | marginEnd = dimen.toInt() 47 | } 48 | } 49 | 50 | @BindingAdapter("layoutMarginVertical") 51 | fun View.bindMarinVertical(dimen: Float) { 52 | layoutParams = (layoutParams as MarginLayoutParams).apply { 53 | topMargin = dimen.toInt() 54 | bottomMargin = dimen.toInt() 55 | } 56 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/di/PresentationModule.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.di 2 | 3 | import androidx.lifecycle.SavedStateHandle 4 | import com.woory.almostthere.presentation.ui.history.PromiseHistoryType 5 | import com.woory.almostthere.presentation.ui.join.ProfileActivity.Companion.PROMISE_CODE_KEY 6 | import dagger.Module 7 | import dagger.Provides 8 | import dagger.hilt.InstallIn 9 | import dagger.hilt.android.components.ViewModelComponent 10 | import dagger.hilt.android.scopes.ViewModelScoped 11 | import javax.inject.Qualifier 12 | 13 | @Module 14 | @InstallIn(ViewModelComponent::class) 15 | object PresentationModule { 16 | 17 | @Qualifier 18 | @Retention(AnnotationRetention.RUNTIME) 19 | annotation class Code 20 | 21 | @Provides 22 | @ViewModelScoped 23 | @Code 24 | fun provideCode(savedStateHandle: SavedStateHandle): String? = 25 | savedStateHandle[PROMISE_CODE_KEY] 26 | 27 | @Qualifier 28 | @Retention(AnnotationRetention.RUNTIME) 29 | annotation class HistoryType 30 | 31 | @Provides 32 | @ViewModelScoped 33 | @HistoryType 34 | fun providePromiseHistoryType(savedStateHandle: SavedStateHandle): PromiseHistoryType = 35 | savedStateHandle["promise_history_type"] ?: throw IllegalStateException() 36 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/extension/LifecycleExtension.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.extension 2 | 3 | import androidx.lifecycle.Lifecycle 4 | import androidx.lifecycle.LifecycleOwner 5 | import androidx.lifecycle.lifecycleScope 6 | import androidx.lifecycle.repeatOnLifecycle 7 | import kotlinx.coroutines.CoroutineScope 8 | import kotlinx.coroutines.launch 9 | 10 | fun LifecycleOwner.repeatOnStarted(block: suspend CoroutineScope.() -> Unit) { 11 | lifecycleScope.launch { 12 | lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED, block) 13 | } 14 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/model/AlarmState.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.model 2 | 3 | enum class AlarmState(val current: String) { 4 | READY("READY"), START("START"), END("END") 5 | } 6 | 7 | fun String.asAlarmState() = when (this) { 8 | "READY" -> AlarmState.READY 9 | "START" -> AlarmState.START 10 | "END" -> AlarmState.END 11 | else -> throw IllegalArgumentException() 12 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/model/Color.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.model 2 | 3 | import com.woory.almostthere.presentation.util.getHex 4 | import java.util.Random 5 | 6 | data class Color( 7 | val red: Int, 8 | val green: Int, 9 | val blue: Int 10 | ) { 11 | 12 | override fun toString(): String = "#${getHex(red)}${getHex(green)}${getHex(blue)}" 13 | 14 | companion object { 15 | 16 | private val random = Random() 17 | private const val MAX_COLOR_VALUE = 255 18 | 19 | fun getRandomColor(): Color = 20 | Color( 21 | random.nextInt(MAX_COLOR_VALUE), 22 | random.nextInt(MAX_COLOR_VALUE), 23 | random.nextInt(MAX_COLOR_VALUE) 24 | ) 25 | } 26 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/model/Location.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.model 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | 6 | @Parcelize 7 | data class Location(val geoPoint: GeoPoint, val address: String) : Parcelable 8 | 9 | @Parcelize 10 | data class GeoPoint(val latitude: Double, val longitude: Double) : Parcelable 11 | 12 | @Parcelize 13 | data class UserLocation(val token: String, val geoPoint: GeoPoint, val updatedAt: Long) : Parcelable -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/model/MagneticInfo.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.model 2 | 3 | import org.threeten.bp.OffsetDateTime 4 | 5 | data class MagneticInfo( 6 | val gameCode: String, 7 | val centerPoint: GeoPoint, 8 | val radius: Double, 9 | val initialRadius: Double, 10 | val updatedAt: OffsetDateTime 11 | ) 12 | -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/model/ProfileImage.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.model 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.graphics.drawable.Drawable 6 | import androidx.annotation.DrawableRes 7 | import com.woory.almostthere.presentation.R 8 | 9 | enum class ProfileImage(@DrawableRes private val imageResId: Int) { 10 | MODEST(R.drawable.bg_modest_shiba_in_profile), 11 | LYING(R.drawable.bg_lying_shiba_in_profile), 12 | SLEEPY(R.drawable.bg_sleepy_shiba_in_profile), 13 | STUBBORN(R.drawable.bg_stubborn_shiba_in_profile), 14 | SUDDEN(R.drawable.bg_sudden_shiba_in_profile); 15 | 16 | @SuppressLint("UseCompatLoadingForDrawables") 17 | fun getDrawableImage(context: Context): Drawable = 18 | context.resources.getDrawable(imageResId, null) 19 | 20 | companion object { 21 | private val imageArray = values().indices 22 | 23 | fun getRandomImage(): Int = imageArray.random() 24 | } 25 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/model/Promise.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.model 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | import org.threeten.bp.OffsetDateTime 6 | 7 | @Parcelize 8 | data class Promise( 9 | val code: String, 10 | val data: PromiseData 11 | ) : Parcelable 12 | 13 | @Parcelize 14 | data class PromiseData( 15 | val promiseLocation: Location, 16 | val promiseDateTime: OffsetDateTime, 17 | val gameDateTime: OffsetDateTime, 18 | val host: User, 19 | val users: List 20 | ) : Parcelable -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/model/PromiseAlarm.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.model 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | import org.threeten.bp.OffsetDateTime 6 | 7 | @Parcelize 8 | data class PromiseAlarm( 9 | val alarmCode: Int, 10 | val promiseCode: String, 11 | val state: AlarmState, 12 | val startTime: OffsetDateTime, 13 | val endTime: OffsetDateTime 14 | ) : Parcelable 15 | -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/model/PromiseHistory.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.model 2 | 3 | data class PromiseHistory( 4 | val userId: String? = null, 5 | val promise: Promise, 6 | val magnetic: MagneticInfo? = null, 7 | val users: List? = null 8 | ) { 9 | 10 | val ranking: List? 11 | get() = 12 | users?.sortedByDescending { user -> user.hp } 13 | ?.map { it.userId } 14 | ?.map { userId -> 15 | promise.data.users.filter { userInfo -> 16 | userId == userInfo.userId 17 | } 18 | .map { it.data.name } 19 | }?.flatten() 20 | 21 | val userHP: Int 22 | get() = users?.firstOrNull() { it.userId == userId }?.hp ?: 100 23 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/model/UiState.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.model 2 | 3 | import com.woory.almostthere.presentation.model.exception.AlmostThereException 4 | 5 | sealed class UiState { 6 | 7 | object Loading : UiState() 8 | 9 | data class Error(val errorType: AlmostThereException) : UiState() 10 | 11 | data class Success(val data: T) : UiState() 12 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/model/User.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.model 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | 6 | @Parcelize 7 | data class ReadyUser( 8 | val isReady: Boolean, 9 | val user: User, 10 | ) : Parcelable 11 | 12 | @Parcelize 13 | data class User( 14 | val userId: String, 15 | val data: UserData 16 | ) : Parcelable 17 | 18 | @Parcelize 19 | data class UserData( 20 | val name: String, 21 | val profileImage: UserProfileImage 22 | ) : Parcelable 23 | 24 | @Parcelize 25 | data class UserProfileImage( 26 | val color: String, 27 | val imageIndex: Int 28 | ) : Parcelable 29 | -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/model/UserHp.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.model 2 | 3 | import org.threeten.bp.OffsetDateTime 4 | 5 | data class UserHp( 6 | val userId: String, 7 | val hp: Int, 8 | val arrived: Boolean, 9 | val lost: Boolean, 10 | val updatedAt: OffsetDateTime 11 | ) -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/model/WooryTMapCircle.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.model 2 | 3 | import android.graphics.Color 4 | import com.skt.tmap.overlay.TMapCircle 5 | 6 | data class WooryTMapCircle( 7 | private val lat: Double, 8 | private val lon: Double, 9 | private val r: Double 10 | ) : TMapCircle(MAGNETIC_CIRCLE_KEY, lat, lon) { 11 | init { 12 | radius = r 13 | circleWidth = 2f 14 | areaAlpha = 10 15 | areaColor = Color.RED 16 | lineColor = Color.RED 17 | } 18 | 19 | companion object { 20 | private const val MAGNETIC_CIRCLE_KEY = "Magnetic" 21 | } 22 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/model/exception/AlmostThereException.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.model.exception 2 | 3 | import androidx.annotation.StringRes 4 | import com.woory.almostthere.presentation.R 5 | 6 | sealed class AlmostThereException(@StringRes open val messageResId: Int) { 7 | 8 | data class InvalidCodeException(@StringRes override val messageResId: Int = R.string.invalid_invite_code) : 9 | AlmostThereException(messageResId) 10 | 11 | data class AlreadyJoinedPromiseException(@StringRes override val messageResId: Int = R.string.already_join) : 12 | AlmostThereException(messageResId) 13 | 14 | data class AlreadyStartedPromiseException(@StringRes override val messageResId: Int = R.string.already_started) : 15 | AlmostThereException(messageResId) 16 | 17 | data class FetchFailedException(@StringRes override val messageResId: Int = R.string.fetch_data_fail_message) : 18 | AlmostThereException(messageResId) 19 | 20 | data class StoreFailedException(@StringRes override val messageResId: Int = R.string.store_data_fail_message) : 21 | AlmostThereException(messageResId) 22 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/model/exception/CreatingPromiseException.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.model.exception 2 | 3 | class InvalidGameTimeException() : IllegalArgumentException() 4 | 5 | class InvalidForCreatingPromiseDataEmpty() : IllegalArgumentException() 6 | 7 | class NotFoundSearchResult() : NullPointerException() -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/model/mapper/UiModelMapper.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.model.mapper 2 | 3 | interface UiModelMapper { 4 | 5 | fun asUiModel(domain: Domain): UiModel 6 | 7 | fun asDomain(uiModel: UiModel): Domain 8 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/model/mapper/alarm/PromiseAlarmMapper.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.model.mapper.alarm 2 | 3 | import com.woory.almostthere.data.model.PromiseAlarmModel 4 | import com.woory.almostthere.presentation.model.PromiseAlarm 5 | import com.woory.almostthere.presentation.model.asAlarmState 6 | import com.woory.almostthere.presentation.model.mapper.UiModelMapper 7 | 8 | object PromiseAlarmMapper : UiModelMapper { 9 | 10 | override fun asDomain(uiModel: PromiseAlarm): PromiseAlarmModel = 11 | PromiseAlarmModel( 12 | alarmCode = uiModel.alarmCode, 13 | promiseCode = uiModel.promiseCode, 14 | status = uiModel.state.current, 15 | startTime = uiModel.startTime, 16 | endTime = uiModel.endTime 17 | ) 18 | 19 | override fun asUiModel(domain: PromiseAlarmModel): PromiseAlarm = 20 | PromiseAlarm( 21 | alarmCode = domain.alarmCode, 22 | promiseCode = domain.promiseCode, 23 | state = domain.status.asAlarmState(), 24 | startTime = domain.startTime, 25 | endTime = domain.endTime 26 | ) 27 | } 28 | 29 | fun PromiseAlarm.asDomain(): PromiseAlarmModel = PromiseAlarmMapper.asDomain(this) 30 | 31 | fun PromiseAlarmModel.asUiModel(): PromiseAlarm = PromiseAlarmMapper.asUiModel(this) -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/model/mapper/gaming/BottomSheetProfile.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.model.mapper.gaming 2 | 3 | data class BottomSheetProfile( 4 | val name: String, 5 | val hp: Int, 6 | val rank: Int 7 | ) -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/model/mapper/location/GeoPointMapper.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.model.mapper.location 2 | 3 | import com.woory.almostthere.data.model.GeoPointModel 4 | import com.woory.almostthere.presentation.model.GeoPoint 5 | import com.woory.almostthere.presentation.model.mapper.UiModelMapper 6 | 7 | object GeoPointMapper : UiModelMapper { 8 | 9 | override fun asUiModel(domain: GeoPointModel): GeoPoint = 10 | GeoPoint( 11 | latitude = domain.latitude, 12 | longitude = domain.longitude 13 | ) 14 | 15 | override fun asDomain(uiModel: GeoPoint): GeoPointModel = 16 | GeoPointModel( 17 | latitude = uiModel.latitude, 18 | longitude = uiModel.longitude 19 | ) 20 | } 21 | 22 | fun GeoPoint.asDomain(): GeoPointModel = GeoPointMapper.asDomain(this) 23 | 24 | fun GeoPointModel.asUiModel(): GeoPoint = GeoPointMapper.asUiModel(this) -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/model/mapper/location/LocationMapper.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.model.mapper.location 2 | 3 | import com.woory.almostthere.data.model.LocationModel 4 | import com.woory.almostthere.presentation.model.Location 5 | import com.woory.almostthere.presentation.model.mapper.UiModelMapper 6 | 7 | object LocationMapper : UiModelMapper { 8 | 9 | override fun asUiModel(domain: LocationModel): Location = 10 | Location( 11 | geoPoint = domain.geoPoint.asUiModel(), 12 | address = domain.address 13 | ) 14 | 15 | override fun asDomain(uiModel: Location): LocationModel = 16 | LocationModel( 17 | geoPoint = uiModel.geoPoint.asDomain(), 18 | address = uiModel.address 19 | ) 20 | } 21 | 22 | fun Location.asDomain(): LocationModel = LocationMapper.asDomain(this) 23 | 24 | fun LocationModel.asUiModel(): Location = LocationMapper.asUiModel(this) -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/model/mapper/location/UserLocationMapper.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.model.mapper.location 2 | 3 | import com.woory.almostthere.data.model.GeoPointModel 4 | import com.woory.almostthere.data.model.UserLocationModel 5 | import com.woory.almostthere.presentation.model.GeoPoint 6 | import com.woory.almostthere.presentation.model.UserLocation 7 | import com.woory.almostthere.presentation.model.mapper.UiModelMapper 8 | 9 | object UserLocationMapper : UiModelMapper { 10 | override fun asUiModel(domain: UserLocationModel): UserLocation = 11 | UserLocation( 12 | token = domain.id, 13 | geoPoint = GeoPoint( 14 | domain.location.latitude, 15 | domain.location.longitude 16 | ), 17 | updatedAt = domain.updatedAt 18 | ) 19 | 20 | override fun asDomain(uiModel: UserLocation): UserLocationModel = 21 | UserLocationModel( 22 | id = uiModel.token, 23 | location = GeoPointModel( 24 | uiModel.geoPoint.latitude, 25 | uiModel.geoPoint.longitude 26 | ), 27 | updatedAt = uiModel.updatedAt 28 | ) 29 | } 30 | 31 | internal fun UserLocation.asDomain(): UserLocationModel = 32 | UserLocationMapper.asDomain(this) 33 | 34 | internal fun UserLocationModel.asUiModel(): UserLocation = 35 | UserLocationMapper.asUiModel(this) -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/model/mapper/magnetic/MagneticInfoMapper.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.model.mapper.magnetic 2 | 3 | import com.woory.almostthere.data.model.GeoPointModel 4 | import com.woory.almostthere.data.model.MagneticInfoModel 5 | import com.woory.almostthere.presentation.model.GeoPoint 6 | import com.woory.almostthere.presentation.model.MagneticInfo 7 | import com.woory.almostthere.presentation.model.mapper.UiModelMapper 8 | 9 | object MagneticInfoMapper : UiModelMapper { 10 | override fun asUiModel(domain: MagneticInfoModel): MagneticInfo = 11 | MagneticInfo( 12 | domain.gameCode, 13 | GeoPoint( 14 | domain.centerPoint.latitude, 15 | domain.centerPoint.longitude 16 | ), 17 | domain.radius, 18 | domain.initialRadius, 19 | domain.updatedAt 20 | ) 21 | 22 | override fun asDomain(uiModel: MagneticInfo): MagneticInfoModel = 23 | MagneticInfoModel( 24 | uiModel.gameCode, 25 | GeoPointModel( 26 | uiModel.centerPoint.latitude, 27 | uiModel.centerPoint.longitude 28 | ), 29 | uiModel.radius, 30 | uiModel.initialRadius, 31 | uiModel.updatedAt 32 | ) 33 | } 34 | 35 | internal fun MagneticInfo.asDomain(): MagneticInfoModel = 36 | MagneticInfoMapper.asDomain(this) 37 | 38 | internal fun MagneticInfoModel.asUiModel(): MagneticInfo = 39 | MagneticInfoMapper.asUiModel(this) -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/model/mapper/promise/PromiseDataMapper.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.model.mapper.promise 2 | 3 | import com.woory.almostthere.data.model.PromiseDataModel 4 | import com.woory.almostthere.presentation.model.PromiseData 5 | import com.woory.almostthere.presentation.model.mapper.UiModelMapper 6 | import com.woory.almostthere.presentation.model.mapper.location.asDomain 7 | import com.woory.almostthere.presentation.model.mapper.location.asUiModel 8 | import com.woory.almostthere.presentation.model.mapper.user.asDomain 9 | import com.woory.almostthere.presentation.model.mapper.user.asUiModel 10 | 11 | object PromiseDataMapper : UiModelMapper { 12 | 13 | override fun asDomain(uiModel: PromiseData): PromiseDataModel = 14 | PromiseDataModel( 15 | promiseLocation = uiModel.promiseLocation.asDomain(), 16 | promiseDateTime = uiModel.promiseDateTime, 17 | gameDateTime = uiModel.gameDateTime, 18 | host = uiModel.host.asDomain(), 19 | users = uiModel.users.map { it.asDomain() } 20 | ) 21 | 22 | override fun asUiModel(domain: PromiseDataModel): PromiseData = 23 | PromiseData( 24 | promiseLocation = domain.promiseLocation.asUiModel(), 25 | promiseDateTime = domain.promiseDateTime, 26 | gameDateTime = domain.gameDateTime, 27 | host = domain.host.asUiModel(), 28 | users = domain.users.map { it.asUiModel() } 29 | ) 30 | } 31 | 32 | fun PromiseData.asDomain(): PromiseDataModel = PromiseDataMapper.asDomain(this) 33 | 34 | fun PromiseDataModel.asUiModel(): PromiseData = PromiseDataMapper.asUiModel(this) 35 | -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/model/mapper/promise/PromiseMapper.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.model.mapper.promise 2 | 3 | import com.woory.almostthere.data.model.PromiseModel 4 | import com.woory.almostthere.presentation.model.Promise 5 | import com.woory.almostthere.presentation.model.mapper.UiModelMapper 6 | 7 | object PromiseMapper : 8 | UiModelMapper { 9 | 10 | override fun asDomain(uiModel: Promise): PromiseModel = 11 | PromiseModel( 12 | code = uiModel.code, 13 | data = uiModel.data.asDomain() 14 | ) 15 | 16 | override fun asUiModel(domain: PromiseModel): Promise = 17 | Promise( 18 | code = domain.code, 19 | data = domain.data.asUiModel() 20 | ) 21 | } 22 | 23 | 24 | fun Promise.asDomain(): PromiseModel = 25 | PromiseMapper.asDomain(this) 26 | 27 | fun PromiseModel.asUiModel(): Promise = 28 | PromiseMapper.asUiModel(this) -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/model/mapper/searchlocation/SearchResultMapper.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.model.mapper.searchlocation 2 | 3 | import com.woory.almostthere.data.model.LocationModel 4 | import com.woory.almostthere.data.model.LocationSearchModel 5 | import com.woory.almostthere.presentation.model.mapper.UiModelMapper 6 | import com.woory.almostthere.presentation.model.mapper.location.asDomain 7 | import com.woory.almostthere.presentation.model.mapper.location.asUiModel 8 | import com.woory.almostthere.presentation.ui.creatingpromise.locationsearch.LocationSearchResult 9 | 10 | object SearchResultMapper : UiModelMapper { 11 | override fun asUiModel(domain: LocationSearchModel): LocationSearchResult = 12 | LocationSearchResult( 13 | name = domain.name, 14 | address = domain.address.address, 15 | location = domain.address.geoPoint.asUiModel() 16 | ) 17 | 18 | override fun asDomain(uiModel: LocationSearchResult): LocationSearchModel = 19 | LocationSearchModel( 20 | name = uiModel.name, 21 | address = LocationModel( 22 | geoPoint = uiModel.location.asDomain(), 23 | address = uiModel.address 24 | ) 25 | ) 26 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/model/mapper/user/AddedUserHpMapper.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.model.mapper.user 2 | 3 | import com.woory.almostthere.data.model.UserHpModel 4 | import com.woory.almostthere.presentation.model.UserHp 5 | import com.woory.almostthere.presentation.model.mapper.UiModelMapper 6 | 7 | object AddedUserHpMapper : UiModelMapper { 8 | override fun asUiModel(domain: UserHpModel): UserHp = 9 | UserHp( 10 | userId = domain.userId, 11 | hp = domain.hp, 12 | arrived = domain.arrived, 13 | lost = domain.lost, 14 | updatedAt = domain.updatedAt 15 | ) 16 | 17 | override fun asDomain(uiModel: UserHp): UserHpModel = 18 | UserHpModel( 19 | userId = uiModel.userId, 20 | hp = uiModel.hp, 21 | arrived = uiModel.arrived, 22 | lost = uiModel.lost, 23 | updatedAt = uiModel.updatedAt 24 | ) 25 | } 26 | 27 | internal fun UserHpModel.asUiState() = AddedUserHpMapper.asUiModel(this) 28 | 29 | internal fun UserHp.asDomain() = AddedUserHpMapper.asDomain(this) -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/model/mapper/user/UserDataMapper.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.model.mapper.user 2 | 3 | import com.woory.almostthere.data.model.UserDataModel 4 | import com.woory.almostthere.presentation.model.UserData 5 | import com.woory.almostthere.presentation.model.mapper.UiModelMapper 6 | 7 | object UserDataMapper : UiModelMapper { 8 | 9 | override fun asUiModel(domain: UserDataModel): UserData = 10 | UserData( 11 | name = domain.name, 12 | profileImage = domain.profileImage.asUiModel() 13 | ) 14 | 15 | override fun asDomain(uiModel: UserData): UserDataModel = 16 | UserDataModel( 17 | name = uiModel.name, 18 | profileImage = uiModel.profileImage.asDomain() 19 | ) 20 | } 21 | 22 | fun UserData.asDomain(): UserDataModel = UserDataMapper.asDomain(this) 23 | 24 | fun UserDataModel.asUiModel(): UserData = UserDataMapper.asUiModel(this) -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/model/mapper/user/UserMapper.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.model.mapper.user 2 | 3 | import com.woory.almostthere.data.model.UserModel 4 | import com.woory.almostthere.presentation.model.User 5 | import com.woory.almostthere.presentation.model.mapper.UiModelMapper 6 | 7 | object UserMapper : UiModelMapper { 8 | 9 | override fun asUiModel(domain: UserModel): User = 10 | User( 11 | userId = domain.userId, 12 | data = domain.data.asUiModel() 13 | ) 14 | 15 | override fun asDomain(uiModel: User): UserModel = 16 | UserModel( 17 | userId = uiModel.userId, 18 | data = uiModel.data.asDomain() 19 | ) 20 | } 21 | 22 | fun User.asDomain(): UserModel = UserMapper.asDomain(this) 23 | 24 | fun UserModel.asUiModel(): User = UserMapper.asUiModel(this) -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/model/mapper/user/UserProfileImageMapper.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.model.mapper.user 2 | 3 | import com.woory.almostthere.data.model.UserProfileImageModel 4 | import com.woory.almostthere.presentation.model.UserProfileImage 5 | import com.woory.almostthere.presentation.model.mapper.UiModelMapper 6 | 7 | object UserProfileImageMapper : UiModelMapper { 8 | 9 | override fun asUiModel(domain: UserProfileImageModel): UserProfileImage = UserProfileImage( 10 | color = domain.color, 11 | imageIndex = domain.imageIndex 12 | ) 13 | 14 | override fun asDomain(uiModel: UserProfileImage): UserProfileImageModel = 15 | UserProfileImageModel( 16 | color = uiModel.color, 17 | imageIndex = uiModel.imageIndex 18 | ) 19 | } 20 | 21 | fun UserProfileImage.asDomain(): UserProfileImageModel = UserProfileImageMapper.asDomain(this) 22 | 23 | fun UserProfileImageModel.asUiModel(): UserProfileImage = UserProfileImageMapper.asUiModel(this) -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/model/mapper/user/UserRankingMapper.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.model.mapper.user 2 | 3 | import com.woory.almostthere.data.model.UserRankingModel 4 | import com.woory.almostthere.presentation.model.mapper.UiModelMapper 5 | import com.woory.almostthere.presentation.model.user.gameresult.UserRanking 6 | 7 | object UserRankingMapper : UiModelMapper { 8 | 9 | override fun asUiModel(domain: UserRankingModel): UserRanking = UserRanking( 10 | userId = domain.userId, 11 | userData = domain.userData.asUiModel(), 12 | hp = domain.hp, 13 | rankingNumber = domain.rankingNumber 14 | ) 15 | 16 | override fun asDomain(uiModel: UserRanking): UserRankingModel = 17 | UserRankingModel( 18 | userId = uiModel.userId, 19 | userData = uiModel.userData.asDomain(), 20 | hp = uiModel.hp, 21 | rankingNumber = uiModel.rankingNumber 22 | ) 23 | } 24 | 25 | fun UserRanking.asDomain(): UserRankingModel = UserRankingMapper.asDomain(this) 26 | 27 | fun UserRankingModel.asUiModel(): UserRanking = UserRankingMapper.asUiModel(this) -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/model/user/gameresult/UserRanking.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.model.user.gameresult 2 | 3 | import android.os.Parcelable 4 | import com.woory.almostthere.presentation.model.UserData 5 | import kotlinx.parcelize.Parcelize 6 | 7 | @Parcelize 8 | data class UserRanking( 9 | val userId: String, 10 | val userData: UserData, 11 | val hp: Int, 12 | val rankingNumber: Int 13 | ) : Parcelable 14 | -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/model/user/gameresult/UserSplitMoneyItem.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.model.user.gameresult 2 | 3 | import com.woory.almostthere.presentation.model.UserData 4 | 5 | sealed interface UserSplitMoneyItem { 6 | 7 | data class UserSplitMoney( 8 | val userId: String, 9 | val userData: UserData, 10 | val rankingNumber: Int, 11 | val moneyToPay: Int 12 | ) : UserSplitMoneyItem 13 | 14 | data class Balance(val value: Int) : UserSplitMoneyItem 15 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/ui/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.ui 2 | 3 | import android.os.Bundle 4 | import androidx.annotation.LayoutRes 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.appcompat.widget.Toolbar 7 | import androidx.databinding.DataBindingUtil 8 | import androidx.databinding.ViewDataBinding 9 | 10 | abstract class BaseActivity(@LayoutRes private val layoutResId: Int) : 11 | AppCompatActivity() { 12 | 13 | protected lateinit var binding: T 14 | 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | super.onCreate(savedInstanceState) 17 | 18 | binding = DataBindingUtil.setContentView(this, layoutResId).apply { 19 | lifecycleOwner = this@BaseActivity 20 | } 21 | } 22 | 23 | protected fun initToolbar(toolbar: Toolbar, toolbarTitle: String) = with(toolbar) { 24 | setSupportActionBar(this) 25 | 26 | supportActionBar?.apply { 27 | setDisplayHomeAsUpEnabled(true) 28 | setDisplayShowHomeEnabled(true) 29 | title = toolbarTitle 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/ui/BaseDialogFragment.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.ui 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.annotation.LayoutRes 8 | import androidx.databinding.DataBindingUtil 9 | import androidx.databinding.ViewDataBinding 10 | import androidx.fragment.app.DialogFragment 11 | 12 | abstract class BaseDialogFragment(@LayoutRes private val layoutId: Int) : 13 | DialogFragment() { 14 | 15 | private var _binding: T? = null 16 | protected val binding get() = _binding!! 17 | 18 | override fun onCreateView( 19 | inflater: LayoutInflater, 20 | container: ViewGroup?, 21 | savedInstanceState: Bundle? 22 | ): View? { 23 | _binding = DataBindingUtil.inflate(inflater, layoutId, container, false) 24 | binding.lifecycleOwner = viewLifecycleOwner 25 | return binding.root 26 | } 27 | 28 | override fun onDestroyView() { 29 | super.onDestroyView() 30 | _binding = null 31 | } 32 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/ui/BaseFragment.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.ui 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.annotation.LayoutRes 8 | import androidx.databinding.DataBindingUtil 9 | import androidx.databinding.ViewDataBinding 10 | import androidx.fragment.app.Fragment 11 | 12 | abstract class BaseFragment(@LayoutRes private val layoutId: Int) : 13 | Fragment() { 14 | 15 | private var _binding: T? = null 16 | protected val binding get() = _binding!! 17 | 18 | override fun onCreateView( 19 | inflater: LayoutInflater, 20 | container: ViewGroup?, 21 | savedInstanceState: Bundle? 22 | ): View? { 23 | _binding = DataBindingUtil.inflate(inflater, layoutId, container, false) 24 | binding.lifecycleOwner = viewLifecycleOwner 25 | return binding.root 26 | } 27 | 28 | override fun onDestroyView() { 29 | super.onDestroyView() 30 | _binding = null 31 | } 32 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/ui/BaseViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.ui 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.annotation.LayoutRes 6 | import androidx.databinding.ViewDataBinding 7 | import androidx.recyclerview.widget.RecyclerView 8 | 9 | abstract class BaseViewHolder( 10 | parent: ViewGroup, 11 | @LayoutRes private val layoutRes: Int 12 | ) : RecyclerView.ViewHolder(LayoutInflater.from(parent.context).inflate(layoutRes, parent, false)) { 13 | 14 | abstract val binding: VDB 15 | abstract fun bind(item: ITEM) 16 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/ui/creatingpromise/CreatingPromiseActivity.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.ui.creatingpromise 2 | 3 | import android.os.Bundle 4 | import android.view.MenuItem 5 | import androidx.navigation.fragment.NavHostFragment 6 | import com.woory.almostthere.presentation.R 7 | import com.woory.almostthere.presentation.databinding.ActivityCreatingPromiseBinding 8 | import com.woory.almostthere.presentation.ui.BaseActivity 9 | import dagger.hilt.android.AndroidEntryPoint 10 | 11 | @AndroidEntryPoint 12 | class CreatingPromiseActivity : 13 | BaseActivity(R.layout.activity_creating_promise) { 14 | 15 | private val navController by lazy { 16 | val container = 17 | supportFragmentManager.findFragmentById(R.id.fragment_creating_promise) as NavHostFragment 18 | container.navController 19 | } 20 | 21 | override fun onCreate(savedInstanceState: Bundle?) { 22 | super.onCreate(savedInstanceState) 23 | initToolbar(binding.containerToolbar.toolbar, getString(R.string.promise_creation)) 24 | } 25 | 26 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 27 | when (item.itemId) { 28 | android.R.id.home -> { 29 | if (navController.currentDestination?.id == R.id.nav_profile_frag) { 30 | finish() 31 | } else { 32 | navController.popBackStack() 33 | } 34 | return true 35 | } 36 | } 37 | return super.onOptionsItemSelected(item) 38 | } 39 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/ui/creatingpromise/CreatingPromiseUiState.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.ui.creatingpromise 2 | 3 | sealed class CreatingPromiseUiState { 4 | object Success: CreatingPromiseUiState() 5 | object Loading: CreatingPromiseUiState() 6 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/ui/creatingpromise/ProfileFragment.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.ui.creatingpromise 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.fragment.app.activityViewModels 6 | import androidx.navigation.fragment.findNavController 7 | import com.woory.almostthere.presentation.R 8 | import com.woory.almostthere.presentation.databinding.FragmentProfileBinding 9 | import com.woory.almostthere.presentation.ui.BaseFragment 10 | import dagger.hilt.android.AndroidEntryPoint 11 | 12 | @AndroidEntryPoint 13 | class ProfileFragment : 14 | BaseFragment(R.layout.fragment_profile) { 15 | 16 | private val viewModel: CreatingPromiseViewModel by activityViewModels() 17 | 18 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 19 | super.onViewCreated(view, savedInstanceState) 20 | 21 | binding.vm = viewModel 22 | 23 | binding.btnNext.setOnClickListener { 24 | findNavController().navigate(R.id.nav_creating_promise_frag) 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/ui/creatingpromise/locationsearch/LocationSearchResult.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.ui.creatingpromise.locationsearch 2 | 3 | import com.woory.almostthere.presentation.model.GeoPoint 4 | 5 | data class LocationSearchResult( 6 | val name: String, 7 | val address: String, 8 | val location: GeoPoint 9 | ) -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/ui/customview/RankBadge.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.ui.customview 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.Color 6 | import android.graphics.Paint 7 | import android.util.AttributeSet 8 | import androidx.annotation.ColorRes 9 | import androidx.appcompat.widget.AppCompatTextView 10 | import androidx.core.content.ContextCompat 11 | import com.woory.almostthere.presentation.R 12 | import com.woory.almostthere.presentation.util.getDip 13 | 14 | class RankBadge constructor( 15 | context: Context, 16 | attrs: AttributeSet? = null 17 | ) : AppCompatTextView(context, attrs) { 18 | 19 | @ColorRes 20 | var badgeColor = R.color.gold 21 | set(value) { 22 | field = value 23 | invalidate() 24 | } 25 | 26 | private val paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG) 27 | 28 | init { 29 | val padding = getDip(6f) 30 | setPadding(padding, padding, padding, padding) 31 | setTextAppearance(R.style.Caption) 32 | textSize = 12f 33 | setTextColor(Color.BLACK) 34 | background = null 35 | } 36 | 37 | override fun onDraw(canvas: Canvas) { 38 | canvas.drawCircle( 39 | width.toFloat() / 2, 40 | height.toFloat() / 2, 41 | minOf(width, height).toFloat() / 2, 42 | paint.apply { color = ContextCompat.getColor(context, badgeColor) } 43 | ) 44 | 45 | super.onDraw(canvas) 46 | } 47 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/ui/customview/topitemresize/TopItemResizeAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.ui.customview.topitemresize 2 | 3 | import androidx.databinding.ViewDataBinding 4 | import androidx.recyclerview.widget.DiffUtil 5 | import androidx.recyclerview.widget.ListAdapter 6 | import androidx.recyclerview.widget.RecyclerView 7 | 8 | abstract class TopItemResizeAdapter>( 9 | diffCallback: DiffUtil.ItemCallback 10 | ) : 11 | ListAdapter(diffCallback) { 12 | 13 | interface HighlightAble { 14 | fun setHighlight(value: Boolean) 15 | } 16 | 17 | abstract class ItemViewHolder(private val binding: VDB) : 18 | RecyclerView.ViewHolder(binding.root), HighlightAble { 19 | override fun setHighlight(value: Boolean) { 20 | if (value) { 21 | onHighlight(binding) 22 | } else { 23 | onNotHighlight(binding) 24 | } 25 | } 26 | 27 | abstract fun bind(item: T) 28 | abstract fun onHighlight(binding: VDB) 29 | abstract fun onNotHighlight(binding: VDB) 30 | } 31 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/ui/customview/topitemresize/TopItemResizeDecoration.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.ui.customview.topitemresize 2 | 3 | import android.graphics.Rect 4 | import android.view.View 5 | import androidx.core.view.children 6 | import androidx.recyclerview.widget.LinearLayoutManager 7 | import androidx.recyclerview.widget.RecyclerView 8 | 9 | class TopItemResizeDecoration : RecyclerView.ItemDecoration() { 10 | 11 | override fun getItemOffsets( 12 | outRect: Rect, 13 | view: View, 14 | parent: RecyclerView, 15 | state: RecyclerView.State 16 | ) { 17 | super.getItemOffsets(outRect, view, parent, state) 18 | if (parent.getChildAdapterPosition(view) + 1 == parent.adapter?.itemCount && parent.childCount != 1) { 19 | when (val layoutManager = parent.layoutManager) { 20 | is LinearLayoutManager -> { 21 | if (layoutManager.orientation == RecyclerView.VERTICAL) { 22 | val highlightViewHeight = parent.children.maxOf { it.height } 23 | outRect.bottom = parent.height - highlightViewHeight - parent.paddingTop - parent.paddingBottom 24 | } else { 25 | val highlightViewWidth = parent.children.maxOf { it.width } 26 | outRect.right = parent.width - highlightViewWidth - parent.paddingStart - parent.paddingEnd 27 | } 28 | } 29 | else -> throw IllegalArgumentException("TopItemResizeDecoration 은 LinearLayoutManager 일 때만 지원합니다.") 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/ui/gameresult/GameResultActivity.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.ui.gameresult 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import androidx.activity.viewModels 7 | import com.woory.almostthere.presentation.R 8 | import com.woory.almostthere.presentation.databinding.ActivityGameResultBinding 9 | import com.woory.almostthere.presentation.ui.BaseActivity 10 | import com.woory.almostthere.presentation.util.PROMISE_CODE_KEY 11 | import dagger.hilt.android.AndroidEntryPoint 12 | 13 | @AndroidEntryPoint 14 | class GameResultActivity : BaseActivity(R.layout.activity_game_result) { 15 | 16 | private val viewModel: GameResultViewModel by viewModels() 17 | 18 | override fun onCreate(savedInstanceState: Bundle?) { 19 | super.onCreate(savedInstanceState) 20 | 21 | initToolbar(binding.containerToolbar.toolbar, getString(R.string.calculate)) 22 | val gameCode = intent?.getStringExtra(PROMISE_CODE_KEY) 23 | viewModel.setGameCode(gameCode) 24 | } 25 | 26 | companion object { 27 | fun startActivity(context: Context, promiseCode: String) = 28 | context.startActivity(Intent(context, GameResultActivity::class.java).apply { 29 | putExtra(PROMISE_CODE_KEY, promiseCode) 30 | }) 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/ui/gameresult/SplitMoneyLogic.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.ui.gameresult 2 | 3 | import com.woory.almostthere.presentation.model.user.gameresult.UserRanking 4 | import com.woory.almostthere.presentation.model.user.gameresult.UserSplitMoneyItem 5 | 6 | object SplitMoneyLogic { 7 | 8 | private fun getRankingPayment( 9 | rankings: List, 10 | totalPayment: Int 11 | ): Map { 12 | val total = rankings.sumOf { it.rankingNumber } 13 | 14 | return (1..rankings.count()).associateWith { rankingNumber -> 15 | val rankingNumberCount = rankings.count { it.rankingNumber == rankingNumber } 16 | if (rankingNumberCount == 0) null 17 | else (totalPayment * (rankingNumber.toDouble() / total.toDouble())).toInt() 18 | }.filterNot { it.value == null } 19 | } 20 | 21 | fun calculatePayment(totalPayment: Int, _rankings: List): List { 22 | val rankings = _rankings.sortedBy { it.rankingNumber } 23 | val rankingPayment = getRankingPayment(rankings, totalPayment) 24 | return rankings.map { 25 | UserSplitMoneyItem.UserSplitMoney( 26 | userId = it.userId, 27 | userData = it.userData, 28 | rankingNumber = it.rankingNumber, 29 | moneyToPay = rankingPayment[it.rankingNumber] ?: 0 30 | ) 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/ui/gaming/CharacterFragment.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.ui.gaming 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.databinding.DataBindingUtil 8 | import com.google.android.material.bottomsheet.BottomSheetDialogFragment 9 | import com.woory.almostthere.presentation.R 10 | import com.woory.almostthere.presentation.databinding.FragmentCharacterBinding 11 | import dagger.hilt.android.AndroidEntryPoint 12 | 13 | @AndroidEntryPoint 14 | class CharacterFragment : BottomSheetDialogFragment() { 15 | 16 | private var _binding: FragmentCharacterBinding? = null 17 | val binding: FragmentCharacterBinding get() = requireNotNull(_binding) 18 | 19 | override fun onCreateView( 20 | inflater: LayoutInflater, 21 | container: ViewGroup?, 22 | savedInstanceState: Bundle? 23 | ): View { 24 | _binding = DataBindingUtil.inflate(inflater, R.layout.fragment_character, container, false) 25 | return binding.root 26 | } 27 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/ui/gaming/GamingRankingAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.ui.gaming 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.DiffUtil 6 | import androidx.recyclerview.widget.ListAdapter 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.woory.almostthere.presentation.databinding.ItemGamingRankingBinding 9 | import com.woory.almostthere.presentation.model.user.gameresult.UserRanking 10 | 11 | class GamingRankingAdapter : ListAdapter(diffUtil) { 12 | 13 | class ViewHolder(private val binding: ItemGamingRankingBinding) : 14 | RecyclerView.ViewHolder(binding.root) { 15 | fun bind(userRanking: UserRanking) { 16 | binding.userRanking = userRanking 17 | } 18 | } 19 | 20 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 21 | return ViewHolder( 22 | ItemGamingRankingBinding.inflate( 23 | LayoutInflater.from(parent.context), 24 | parent, 25 | false 26 | ) 27 | ) 28 | } 29 | 30 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 31 | holder.bind(getItem(position)) 32 | } 33 | 34 | companion object { 35 | val diffUtil = object : DiffUtil.ItemCallback() { 36 | override fun areItemsTheSame(oldItem: UserRanking, newItem: UserRanking): Boolean { 37 | return oldItem.userId == newItem.userId 38 | } 39 | 40 | override fun areContentsTheSame(oldItem: UserRanking, newItem: UserRanking): Boolean { 41 | return oldItem == newItem 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/ui/gaming/ShakeEventListener.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.ui.gaming 2 | 3 | import android.hardware.Sensor 4 | import android.hardware.SensorEvent 5 | import android.hardware.SensorEventListener 6 | import android.hardware.SensorManager 7 | import kotlin.math.sqrt 8 | 9 | class ShakeEventListener : SensorEventListener { 10 | 11 | private var shakeListener: OnShakeListener? = null 12 | private var shakeTimeStamp: Long = 0 13 | 14 | interface OnShakeListener { 15 | fun onShake() 16 | } 17 | 18 | fun setOnShakeListener(_shakeListener: OnShakeListener) { 19 | shakeListener = _shakeListener 20 | } 21 | 22 | override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {} 23 | 24 | override fun onSensorChanged(event: SensorEvent?) { 25 | event ?: return 26 | 27 | val x = event.values[0] 28 | val y = event.values[1] 29 | val z = event.values[2] 30 | 31 | val gX = x / SensorManager.GRAVITY_EARTH 32 | val gY = y / SensorManager.GRAVITY_EARTH 33 | val gZ = z / SensorManager.GRAVITY_EARTH 34 | 35 | val gForce = sqrt((gX * gX + gY * gY + gZ * gZ).toDouble()).toFloat() 36 | 37 | if (gForce > SHAKE_THRESHOLD_GRAVITY) { 38 | val now = System.currentTimeMillis() 39 | 40 | if (shakeTimeStamp + SHAKE_SLOP_TIME_MS > now) return 41 | 42 | shakeTimeStamp = now 43 | shakeListener?.onShake() 44 | } 45 | } 46 | 47 | companion object { 48 | private const val SHAKE_THRESHOLD_GRAVITY = 2.0F 49 | private const val SHAKE_SLOP_TIME_MS = 500 50 | } 51 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/ui/history/PromiseHistoryType.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.ui.history 2 | 3 | import android.content.Context 4 | import androidx.annotation.StringRes 5 | import com.woory.almostthere.presentation.R 6 | 7 | enum class PromiseHistoryType(@StringRes private val titleResId: Int) { 8 | PAST(R.string.past_promise_history), 9 | FUTURE(R.string.future_promise_history); 10 | 11 | fun getTitle(context: Context): String = context.getString(titleResId) 12 | } 13 | 14 | enum class PromiseHistoryViewType { 15 | BEFORE, ONGOING, END 16 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/ui/history/RankBadgeType.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.ui.history 2 | 3 | import androidx.annotation.ColorRes 4 | import androidx.annotation.StringRes 5 | import com.woory.almostthere.presentation.R 6 | 7 | enum class RankBadgeType( 8 | @StringRes val labelResId: Int, 9 | @ColorRes val colorResId: Int 10 | ) { 11 | RANK1(R.string.rank1, R.color.gold), 12 | RANK2(R.string.rank2, R.color.silver), 13 | RANK3(R.string.rank3, R.color.bronze) 14 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/ui/join/FormState.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.ui.join 2 | 3 | sealed class FormState { 4 | 5 | data class Valid( 6 | val message: String = "" 7 | ) : FormState() 8 | 9 | data class Invalid( 10 | val message: String = "" 11 | ) : FormState() 12 | 13 | object EMPTY : FormState() 14 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/ui/main/FindPromiseFragment.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.ui.main 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.navigation.fragment.findNavController 6 | import com.woory.almostthere.presentation.R 7 | import com.woory.almostthere.presentation.databinding.FragmentFindPromiseBinding 8 | import com.woory.almostthere.presentation.ui.BaseFragment 9 | import com.woory.almostthere.presentation.ui.history.PromiseHistoryType 10 | import dagger.hilt.android.AndroidEntryPoint 11 | 12 | @AndroidEntryPoint 13 | class FindPromiseFragment : 14 | BaseFragment(R.layout.fragment_find_promise) { 15 | 16 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 17 | super.onViewCreated(view, savedInstanceState) 18 | 19 | binding.containerEndedPromise.setOnClickListener { 20 | val action = MainFragmentDirections.startPromiseHistoryActivity(PromiseHistoryType.PAST) 21 | findNavController().navigate(action) 22 | } 23 | 24 | binding.containerSoonPromise.setOnClickListener { 25 | val action = MainFragmentDirections.startPromiseHistoryActivity(PromiseHistoryType.FUTURE) 26 | findNavController().navigate(action) 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/ui/main/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.ui.main 2 | 3 | import com.woory.almostthere.presentation.R 4 | import com.woory.almostthere.presentation.databinding.ActivityMainBinding 5 | import com.woory.almostthere.presentation.ui.BaseActivity 6 | import dagger.hilt.android.AndroidEntryPoint 7 | 8 | @AndroidEntryPoint 9 | class MainActivity : BaseActivity(R.layout.activity_main) -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/ui/main/MainFragment.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.ui.main 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.view.View 6 | import androidx.navigation.NavController 7 | import androidx.navigation.fragment.findNavController 8 | import com.woory.almostthere.presentation.R 9 | import com.woory.almostthere.presentation.databinding.FragmentMainBinding 10 | import com.woory.almostthere.presentation.ui.BaseFragment 11 | import com.woory.almostthere.presentation.ui.creatingpromise.CreatingPromiseActivity 12 | import com.woory.almostthere.presentation.util.animLeftToRightNavOptions 13 | import dagger.hilt.android.AndroidEntryPoint 14 | 15 | @AndroidEntryPoint 16 | class MainFragment : BaseFragment(R.layout.fragment_main) { 17 | 18 | private val navController: NavController by lazy { 19 | findNavController() 20 | } 21 | 22 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 23 | super.onViewCreated(view, savedInstanceState) 24 | binding.containerJoinPromise.setOnClickListener { 25 | navController.navigate( 26 | R.id.nav_join_act, 27 | null, 28 | animLeftToRightNavOptions 29 | ) 30 | } 31 | 32 | binding.containerCreatePromise.setOnClickListener { 33 | val intent = Intent(requireActivity(), CreatingPromiseActivity()::class.java) 34 | startActivity(intent) 35 | requireActivity().overridePendingTransition( 36 | R.anim.slide_in_from_right, 37 | R.anim.slide_out_to_left 38 | ) 39 | } 40 | 41 | binding.containerFindPromise.setOnClickListener { 42 | navController.navigate( 43 | R.id.nav_find_promise_frag, 44 | null, 45 | animLeftToRightNavOptions 46 | ) 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/ui/promiseinfo/PromiseInfoActivity.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.ui.promiseinfo 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import androidx.activity.viewModels 7 | import com.woory.almostthere.presentation.R 8 | import com.woory.almostthere.presentation.databinding.ActivityPromiseInfoBinding 9 | import com.woory.almostthere.presentation.ui.BaseActivity 10 | import com.woory.almostthere.presentation.util.NO_GAME_CODE_EXCEPTION 11 | import com.woory.almostthere.presentation.util.PROMISE_CODE_KEY 12 | import dagger.hilt.android.AndroidEntryPoint 13 | import dagger.hilt.android.internal.managers.FragmentComponentManager 14 | 15 | @AndroidEntryPoint 16 | class PromiseInfoActivity : 17 | BaseActivity(R.layout.activity_promise_info) { 18 | 19 | private val gameCode by lazy { 20 | intent?.getStringExtra(PROMISE_CODE_KEY) 21 | ?: throw NO_GAME_CODE_EXCEPTION 22 | } 23 | 24 | private val viewModel: PromiseInfoViewModel by viewModels() 25 | 26 | override fun onCreate(savedInstanceState: Bundle?) { 27 | super.onCreate(savedInstanceState) 28 | 29 | FragmentComponentManager.findActivity(this) 30 | initToolbar(binding.containerToolbar.toolbar, getString(R.string.promise_info)) 31 | viewModel.setGameCode(gameCode) 32 | } 33 | 34 | companion object { 35 | fun startActivity(context: Context, promiseCode: String) = 36 | context.startActivity(Intent(context, PromiseInfoActivity::class.java).apply { 37 | putExtra(PROMISE_CODE_KEY, promiseCode) 38 | }) 39 | } 40 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/ui/promiseinfo/PromiseUiState.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.ui.promiseinfo 2 | 3 | sealed class PromiseUiState { 4 | object Loading : PromiseUiState() 5 | object Success : PromiseUiState() 6 | object Fail : PromiseUiState() 7 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/ui/promiseinfo/ReadyStatus.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.ui.promiseinfo 2 | 3 | enum class ReadyStatus { 4 | READY, NOT, BEFORE, AFTER 5 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/ui/splash/SplashActivity.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.ui.splash 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.os.Handler 6 | import android.os.Looper 7 | import android.view.View 8 | import android.view.animation.AnimationUtils 9 | import com.woory.almostthere.presentation.R 10 | import com.woory.almostthere.presentation.databinding.ActivitySplashBinding 11 | import com.woory.almostthere.presentation.ui.BaseActivity 12 | import com.woory.almostthere.presentation.ui.main.MainActivity 13 | import dagger.hilt.android.AndroidEntryPoint 14 | 15 | @AndroidEntryPoint 16 | class SplashActivity : BaseActivity(R.layout.activity_splash) { 17 | 18 | private var handler = Handler(Looper.getMainLooper()) 19 | 20 | override fun onCreate(savedInstanceState: Bundle?) { 21 | super.onCreate(savedInstanceState) 22 | 23 | val anim = AnimationUtils.loadAnimation(this, R.anim.blink) 24 | binding.ivSplashImage.clipToOutline = true 25 | 26 | handler.postDelayed({ 27 | binding.tvNext.visibility = View.VISIBLE 28 | binding.tvNext.startAnimation(anim) 29 | 30 | binding.root.setOnClickListener { 31 | val intent = Intent(this, MainActivity::class.java) 32 | startActivity(intent) 33 | finish() 34 | } 35 | }, 2000) 36 | } 37 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/util/ActivityContext.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.util 2 | 3 | import android.content.Context 4 | import dagger.hilt.android.internal.managers.ViewComponentManager 5 | 6 | fun getActivityContext(context: Context): Context { 7 | return if (context is ViewComponentManager.FragmentContextWrapper) { 8 | context.baseContext 9 | } else context 10 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/util/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.util 2 | 3 | const val REQUIRE_PERMISSION_TEXT = "Permission is required" 4 | 5 | const val PROMISE_CODE_KEY = "PROMISE_CODE_KEY" -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/util/DistanceUtil.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.util 2 | 3 | import com.woory.almostthere.presentation.model.GeoPoint 4 | import kotlin.math.abs 5 | import kotlin.math.asin 6 | import kotlin.math.cos 7 | import kotlin.math.pow 8 | import kotlin.math.sin 9 | import kotlin.math.sqrt 10 | 11 | /** 12 | * @link https://shwjdqls.github.io/android-get-distance-between-locations/ 13 | */ 14 | object DistanceUtil { 15 | private const val R = 6372.8 * 1000 16 | 17 | /** 18 | * @param geo1 19 | * @param geo2 20 | * @return 거리의 m값값 21 | * */ 22 | fun getDistance(geo1: GeoPoint, geo2: GeoPoint): Double { 23 | val dLat = Math.toRadians(geo2.latitude - geo1.latitude) 24 | val dLon = Math.toRadians(geo2.longitude - geo1.longitude) 25 | val a = 26 | sin(dLat / 2).pow(2.0) + sin(dLon / 2).pow(2.0) * 27 | cos(Math.toRadians(geo1.latitude)) * cos(Math.toRadians(geo2.latitude) 28 | ) 29 | val c = 2 * asin(sqrt(a)) 30 | return abs(R * c) 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/util/Exceptions.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.util 2 | 3 | val NO_GAME_CODE_EXCEPTION = IllegalArgumentException("참여 코드가 없습니다.") -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/util/InviteCodeUtil.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.util 2 | 3 | import com.woory.almostthere.presentation.ui.join.FormState 4 | 5 | object InviteCodeUtil { 6 | 7 | private const val MAX_INVITE_CODE_LENGTH = 7 8 | private val inviteCodeRegex = 9 | """[A-Z0-9]{$MAX_INVITE_CODE_LENGTH}""".toRegex(RegexOption.IGNORE_CASE) 10 | 11 | fun getCodeState(code: String): FormState = if (code.isEmpty()) { 12 | FormState.EMPTY 13 | } else if (code.matches(inviteCodeRegex)) { 14 | FormState.Valid() 15 | } else { 16 | FormState.Invalid() 17 | } 18 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/util/KonfettiPresets.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.util 2 | 3 | import nl.dionsegijn.konfetti.core.Angle 4 | import nl.dionsegijn.konfetti.core.Party 5 | import nl.dionsegijn.konfetti.core.Position 6 | import nl.dionsegijn.konfetti.core.Rotation 7 | import nl.dionsegijn.konfetti.core.emitter.Emitter 8 | import nl.dionsegijn.konfetti.core.models.Size 9 | import java.util.concurrent.TimeUnit 10 | 11 | fun festive(): List { 12 | val party = Party( 13 | speed = 30f, 14 | maxSpeed = 50f, 15 | damping = 0.9f, 16 | angle = Angle.TOP, 17 | spread = 45, 18 | size = listOf(Size.SMALL, Size.LARGE), 19 | timeToLive = 3000L, 20 | rotation = Rotation(), 21 | colors = listOf(0xfce18a, 0xff726d, 0xf4306d, 0xb48def), 22 | emitter = Emitter(duration = 100, TimeUnit.MILLISECONDS).max(30), 23 | position = Position.Relative(0.5, 1.0) 24 | ) 25 | 26 | return listOf( 27 | party, 28 | party.copy( 29 | speed = 55f, 30 | maxSpeed = 65f, 31 | spread = 10, 32 | emitter = Emitter(duration = 100, TimeUnit.MILLISECONDS).max(10), 33 | ), 34 | party.copy( 35 | speed = 50f, 36 | maxSpeed = 60f, 37 | spread = 120, 38 | emitter = Emitter(duration = 100, TimeUnit.MILLISECONDS).max(40), 39 | ), 40 | party.copy( 41 | speed = 65f, 42 | maxSpeed = 80f, 43 | spread = 10, 44 | emitter = Emitter(duration = 100, TimeUnit.MILLISECONDS).max(10), 45 | ) 46 | ) 47 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/util/ResourceManager.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.util 2 | 3 | import android.content.Context 4 | import com.woory.almostthere.presentation.R 5 | import com.woory.almostthere.presentation.model.exception.InvalidForCreatingPromiseDataEmpty 6 | import com.woory.almostthere.presentation.model.exception.InvalidGameTimeException 7 | import com.woory.almostthere.presentation.model.exception.NotFoundSearchResult 8 | 9 | fun getExceptionMessage(context: Context, throwable: Throwable): String { 10 | return when (throwable) { 11 | is InvalidGameTimeException -> context.getString(R.string.invalid_game_date_time_message) 12 | is InvalidForCreatingPromiseDataEmpty -> context.getString(R.string.invalid_for_creating_promise_data_empty) 13 | is NotFoundSearchResult -> context.getString(R.string.not_found_search_result_message) 14 | else -> String.format(context.getString(R.string.unknown_error), throwable.message) 15 | } 16 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/util/SoftKeyboardUtils.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.util 2 | 3 | import android.graphics.Rect 4 | import android.view.ViewTreeObserver 5 | import android.view.Window 6 | 7 | class SoftKeyboardUtils( 8 | private val window: Window, 9 | private val onShowKeyboard: ((Int) -> Unit)? = null, 10 | private val onHideKeyboard: (() -> Unit)? = null 11 | ) { 12 | 13 | private val windowVisibleDisplayFrame = Rect() 14 | private var lastVisibleDecorViewHeight: Int = 0 15 | 16 | private val onGlobalLayoutListener = ViewTreeObserver.OnGlobalLayoutListener { 17 | window.decorView.getWindowVisibleDisplayFrame(windowVisibleDisplayFrame) 18 | val visibleDecorViewHeight = windowVisibleDisplayFrame.height() 19 | 20 | if (lastVisibleDecorViewHeight != 0) { 21 | if (lastVisibleDecorViewHeight > visibleDecorViewHeight + MIN_KEYBOARD_HEIGHT_PX) { 22 | val currentKeyboardHeight = 23 | window.decorView.height - windowVisibleDisplayFrame.bottom 24 | onShowKeyboard?.invoke(currentKeyboardHeight) 25 | } else if (lastVisibleDecorViewHeight + MIN_KEYBOARD_HEIGHT_PX < visibleDecorViewHeight) { 26 | onHideKeyboard?.invoke() 27 | } 28 | } 29 | 30 | lastVisibleDecorViewHeight = visibleDecorViewHeight 31 | } 32 | 33 | init { 34 | window.decorView.viewTreeObserver.addOnGlobalLayoutListener(onGlobalLayoutListener) 35 | } 36 | 37 | fun detachKeyboardListeners() { 38 | window.decorView.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalLayoutListener) 39 | } 40 | 41 | companion object { 42 | private const val MIN_KEYBOARD_HEIGHT_PX = 150 43 | } 44 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/util/Tag.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.util 2 | 3 | val Any.TAG get() = this::class.simpleName -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/util/TimeConverter.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.util 2 | 3 | import org.threeten.bp.Instant 4 | import org.threeten.bp.OffsetDateTime 5 | import org.threeten.bp.ZoneId 6 | import org.threeten.bp.ZoneOffset 7 | 8 | object TimeConverter { 9 | 10 | private val zoneId: ZoneId = ZoneId.of("Asia/Seoul") 11 | val zoneOffset: ZoneOffset = ZoneOffset.of("+09:00") 12 | 13 | fun OffsetDateTime.asMillis() = toInstant().toEpochMilli() 14 | 15 | fun Long.asOffsetDateTime(): OffsetDateTime = 16 | OffsetDateTime.ofInstant(Instant.ofEpochMilli(this), zoneId) 17 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/util/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.util 2 | 3 | import androidx.navigation.NavOptions 4 | import com.woory.almostthere.presentation.R 5 | import java.text.DecimalFormat 6 | 7 | val animRightToLeftNavOption = NavOptions.Builder().apply { 8 | setEnterAnim(R.anim.slide_in_from_right) 9 | setExitAnim(R.anim.slide_out_to_left) 10 | setPopEnterAnim(R.anim.slide_in_from_left) 11 | setPopExitAnim(R.anim.slide_out_to_right) 12 | }.build() 13 | 14 | val animLeftToRightNavOptions = NavOptions.Builder().apply { 15 | setEnterAnim(R.anim.slide_in_from_left) 16 | setExitAnim(R.anim.slide_out_to_right) 17 | setPopEnterAnim(R.anim.slide_in_from_right) 18 | setPopExitAnim(R.anim.slide_out_to_left) 19 | }.build() 20 | 21 | fun getHex(value: Int): String = "%02X".format(value) 22 | 23 | fun String.extractNumber(): Int = replace("[^0-9]".toRegex(), "").toInt() 24 | 25 | fun Int.getCommaNumber(): String { 26 | val formatter = DecimalFormat("#,###") 27 | return formatter.format(this) 28 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/util/ViewExtension.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.util 2 | 3 | import android.view.View 4 | import androidx.annotation.Px 5 | 6 | @Px 7 | fun View.getDip(value: Float) = (value * resources.displayMetrics.density).toInt() -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/util/ViewUtils.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.util 2 | 3 | import android.view.View 4 | import android.widget.ProgressBar 5 | import com.google.android.material.snackbar.Snackbar 6 | 7 | fun showSnackBar(view: View, message: String) = 8 | Snackbar.make( 9 | view, 10 | message, 11 | Snackbar.LENGTH_SHORT 12 | ).show() 13 | 14 | fun handleLoading(progressBar: ProgressBar, isLoading: Boolean) { 15 | progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE 16 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/com/woory/almostthere/presentation/util/flow/EventFlow.kt: -------------------------------------------------------------------------------- 1 | package com.woory.almostthere.presentation.util.flow 2 | 3 | import kotlinx.coroutines.InternalCoroutinesApi 4 | import kotlinx.coroutines.flow.Flow 5 | import kotlinx.coroutines.flow.FlowCollector 6 | import kotlinx.coroutines.flow.MutableSharedFlow 7 | import java.util.concurrent.atomic.AtomicBoolean 8 | 9 | interface EventFlow : Flow { 10 | 11 | companion object { 12 | const val DEFAULT_REPLAY: Int = 3 13 | } 14 | } 15 | 16 | interface MutableEventFlow : EventFlow, FlowCollector 17 | 18 | @Suppress("FunctionName") 19 | fun MutableEventFlow( 20 | replay: Int = EventFlow.DEFAULT_REPLAY 21 | ): MutableEventFlow = EventFlowImpl(replay) 22 | 23 | fun MutableEventFlow.asEventFlow(): EventFlow = ReadOnlyEventFlow(this) 24 | 25 | private class ReadOnlyEventFlow(flow: EventFlow) : EventFlow by flow 26 | 27 | private class EventFlowImpl( 28 | replay: Int 29 | ) : MutableEventFlow { 30 | 31 | private val flow: MutableSharedFlow> = MutableSharedFlow(replay = replay) 32 | 33 | @InternalCoroutinesApi 34 | override suspend fun collect(collector: FlowCollector) = flow.collect { slot -> 35 | if (!slot.markConsumed()) { 36 | collector.emit(slot.value) 37 | } 38 | } 39 | 40 | override suspend fun emit(value: T) { 41 | flow.emit(EventFlowSlot(value)) 42 | } 43 | } 44 | 45 | private class EventFlowSlot(val value: T) { 46 | 47 | private val consumed: AtomicBoolean = AtomicBoolean(false) 48 | 49 | fun markConsumed(): Boolean = consumed.getAndSet(true) 50 | } -------------------------------------------------------------------------------- /presentation/src/main/res/anim/blink.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | -------------------------------------------------------------------------------- /presentation/src/main/res/anim/slide_in_from_down.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /presentation/src/main/res/anim/slide_in_from_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | 12 | -------------------------------------------------------------------------------- /presentation/src/main/res/anim/slide_in_from_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | 12 | -------------------------------------------------------------------------------- /presentation/src/main/res/anim/slide_in_from_up.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | -------------------------------------------------------------------------------- /presentation/src/main/res/anim/slide_out_to_down.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | -------------------------------------------------------------------------------- /presentation/src/main/res/anim/slide_out_to_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | 12 | -------------------------------------------------------------------------------- /presentation/src/main/res/anim/slide_out_to_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | 12 | -------------------------------------------------------------------------------- /presentation/src/main/res/anim/slide_out_to_up.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | -------------------------------------------------------------------------------- /presentation/src/main/res/color/button_state_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/bg_bottom_sheet.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/bg_btn_of_map.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/bg_button_clicked.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/bg_button_unclicked.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/bg_character_img.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/bg_game_result.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/bg_hp_bar.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/bg_menu_green.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 13 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/bg_menu_indigo.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 13 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/bg_menu_peach.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 13 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/bg_menu_pink.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 13 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/bg_menu_sky.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 13 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/bg_profile.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/bg_ranking.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/bg_round_btn.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/bg_speech_bubble.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/bg_speech_bubble_body.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/bg_speech_bubble_tail.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/bg_splash_img.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/bg_tag.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_baseline_location_searching.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_baseline_timer.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_bomb.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_calendar.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_destination.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_goal_marker.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 17 | 20 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_home.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 20 | 21 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_hourglass.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_invite_code.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_location.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_nickname.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_outline_info.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_people.xml: -------------------------------------------------------------------------------- 1 | 4 | 6 | 8 | 10 | 12 | 13 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_rank_label.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_refresh.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_search.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_switch.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | 10 | 14 | 18 | 19 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_time.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_today.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/shape_thumb.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /presentation/src/main/res/font-v26/font.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 12 | -------------------------------------------------------------------------------- /presentation/src/main/res/font/sans400.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android11-almost-there/9d03b07e5124a8a0ee1a55e7667142dbca9a66c6/presentation/src/main/res/font/sans400.otf -------------------------------------------------------------------------------- /presentation/src/main/res/font/sans500.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/android11-almost-there/9d03b07e5124a8a0ee1a55e7667142dbca9a66c6/presentation/src/main/res/font/sans500.otf -------------------------------------------------------------------------------- /presentation/src/main/res/layout/activity_creating_promise.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | 20 | 21 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /presentation/src/main/res/layout/activity_game_result.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | 19 | 20 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /presentation/src/main/res/layout/activity_gaming.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | 21 | 22 | -------------------------------------------------------------------------------- /presentation/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /presentation/src/main/res/layout/activity_promise_info.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | 18 | 19 | 28 | 29 | -------------------------------------------------------------------------------- /presentation/src/main/res/layout/activity_promises.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 22 | 23 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /presentation/src/main/res/layout/customview_hp_bar.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /presentation/src/main/res/layout/fragment_character.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /presentation/src/main/res/layout/fragment_promises.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 19 | 20 | 26 | 27 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /presentation/src/main/res/layout/item_balance.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 17 | 18 | 27 | 28 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /presentation/src/main/res/layout/item_location_search_result.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 22 | 23 | 33 | 34 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /presentation/src/main/res/layout/layout_character_img.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 22 | 23 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /presentation/src/main/res/layout/layout_map_icon_info.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | -------------------------------------------------------------------------------- /presentation/src/main/res/layout/layout_map_icon_location.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | -------------------------------------------------------------------------------- /presentation/src/main/res/layout/layout_progressbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 21 | 22 | -------------------------------------------------------------------------------- /presentation/src/main/res/layout/layout_submit_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 17 | 18 |