diff --git a/.github/ISSUE_TEMPLATE/issue-template.md b/.github/ISSUE_TEMPLATE/issue-template.md new file mode 100644 index 00000000..f757cbf7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue-template.md @@ -0,0 +1,11 @@ +--- +name: Issue Template +about: issue template +title: '' +labels: '' +assignees: '' + +--- + +## ๐Ÿ“‹ Description + diff --git a/.github/ISSUE_TEMPLATE/issue_template.md b/.github/ISSUE_TEMPLATE/issue_template.md deleted file mode 100644 index d3947460..00000000 --- a/.github/ISSUE_TEMPLATE/issue_template.md +++ /dev/null @@ -1 +0,0 @@ -## ๐Ÿ“‹ Description diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index c7951abc..41c78ea1 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,11 +1,11 @@ ## โญ๏ธ Issue Number -#number +- #number ## ๐Ÿšฉ Summary - +- ## ๐Ÿ› ๏ธ Technical Concerns @@ -17,6 +17,11 @@ Content Content +## ๐Ÿ™‚ To Reviwer + +- Review Point +- Caution Point + ## ๐Ÿ“‹ To Do - diff --git a/.github/workflows/action-test.ci.yml b/.github/workflows/action-test.ci.yml deleted file mode 100644 index 5f34cad5..00000000 --- a/.github/workflows/action-test.ci.yml +++ /dev/null @@ -1,73 +0,0 @@ -name: test-ci - -# ํŠธ๋ฆฌ๊ฑฐ ์กฐ๊ฑด (push ํ•˜๊ฑฐ๋‚˜ PR ํ•˜๋ฉด ํ•˜๋‹จ์˜ jobs๋ฅผ ์‹คํ–‰ํ•˜๊ฒ ๋‹ค๋Š” ๋œป) -on: - push: - branches: - - main - - develop - - pull_request: - branches: - - main - - develop - -# ์ž‘์—… ์ค‘๋ณต ์‹คํ–‰ ๋ฐฉ์ง€ -concurrency: - group: ci-${{ github.ref }} - cancel-in-progress: true - -# ํŠธ๋ฆฌ๊ฑฐ ๋ฐœ์ƒ ์‹œ ์‹คํ–‰ํ•  ์ž‘์—…๋“ค -jobs: - test: - runs-on: macos-13 # iOS ํ”Œ๋žซํผ์—์„œ ์‹คํ–‰ - env: - DEVELOPER_DIR: "/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer" - strategy: - matrix: - xcode: - # - "14.1" # Swift 5.7 - # - "14.3" # Swift 5.8 - - "15.0" # Swift 5.9 - - steps: - - name: Runner Overview - run: system_profiler SPHardwareDataType SPSoftwareDataType SPDeveloperToolsDataType - - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Copy secrets to github action - env: - ENV: ${{ secrets.ENV }} # repository secrets ์—์„œ ๊ฐ€์ ธ์˜ด - OCCUPY_SECRET_DIR: ./KCS # ๋ ˆํฌ์ง€ํ† ๋ฆฌ ๋‚ด ํŒŒ์ผ ์˜ ์œ„์น˜ - OCCUPY_SECRET_DIR_FILE_NAME: Secret.xcconfig # ํŒŒ์ผ ์ด๋ฆ„ - - run: | - echo $ENV >> $OCCUPY_SECRET_DIR/$OCCUPY_SECRET_DIR_FILE_NAME - - # CocoaPod ์„ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ ์„ค์น˜ํ›„ ํ…Œ์ŠคํŠธ ์ง„ํ–‰ - - name: Install CocoaPods - run: | - pod install --repo-update --project-directory=KCS - - - name: Build - env: - platform: ${{ 'iOS Simulator' }} - run: | - # xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959) - device=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}' | sed -e "s/ Simulator$//"` - xcodebuild build-for-testing -workspace KCS/KCS.xcworkspace -scheme "KCS" -destination "platform=$platform,name=$device" -skipPackagePluginValidation -skipMacroValidation - - - name: Test - env: - platform: ${{ 'iOS Simulator' }} - run: | - # xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959) - device=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}' | sed -e "s/ Simulator$//"` - xcodebuild test-without-building -workspace KCS/KCS.xcworkspace -scheme "KCS" -destination "platform=$platform,name=$device" -skipPackagePluginValidation -skipMacroValidation - - - # - name: Test - # run: | - # xcodebuild clean test -workspace KCS/KCS.xcworkspace -scheme "KCS" -destination "platform=iOS Simulator,name=iPhone 14 Pro" diff --git a/KCS/.swiftlint.yml b/KCS/.swiftlint.yml index 0ffbd1ef..527bd03f 100644 --- a/KCS/.swiftlint.yml +++ b/KCS/.swiftlint.yml @@ -1,6 +1,7 @@ disabled_rules: - trailing_whitespace - + - file_length + opt_in_rules: - empty_string @@ -9,4 +10,6 @@ excluded: - KCS/Application line_length: 140 -file_length: 600 +cyclomatic_complexity: 20 +identifier_name: + max_length: 50 diff --git a/KCS/GoogleService-Info.plist b/KCS/GoogleService-Info.plist new file mode 100644 index 00000000..776fe7cf --- /dev/null +++ b/KCS/GoogleService-Info.plist @@ -0,0 +1,30 @@ + + + + + API_KEY + AIzaSyAHg3iyzwXwUd3TpKqM7jpl-EZaXUmiqHw + GCM_SENDER_ID + 127350323526 + PLIST_VERSION + 1 + BUNDLE_ID + com.kcs.nainga + PROJECT_ID + korea-certified-stores + STORAGE_BUCKET + korea-certified-stores.appspot.com + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:127350323526:ios:15feb5ff2ff45f42448d47 + + \ No newline at end of file diff --git a/KCS/KCS.xcodeproj/project.pbxproj b/KCS/KCS.xcodeproj/project.pbxproj index c9c0eb34..009f5bdf 100644 --- a/KCS/KCS.xcodeproj/project.pbxproj +++ b/KCS/KCS.xcodeproj/project.pbxproj @@ -13,6 +13,18 @@ 591A88812B384E600059E40F /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 591A88802B384E600059E40F /* HomeViewController.swift */; }; 591A88862B384E610059E40F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 591A88852B384E610059E40F /* Assets.xcassets */; }; 591A88892B384E610059E40F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 591A88872B384E610059E40F /* LaunchScreen.storyboard */; }; + 592262242B61203000CA5A11 /* DetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 592262232B61203000CA5A11 /* DetailView.swift */; }; + 59503A4A2B741F1E0006CF35 /* Secret.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 59503A492B741F1E0006CF35 /* Secret.xcconfig */; }; + 59503A4E2B751B0B0006CF35 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59503A4D2B751B0B0006CF35 /* SearchViewController.swift */; }; + 59503A522B751FCC0006CF35 /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59503A512B751FCC0006CF35 /* SearchViewModel.swift */; }; + 59503A542B751FD50006CF35 /* SearchViewModelImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59503A532B751FD50006CF35 /* SearchViewModelImpl.swift */; }; + 59503A562B756CCD0006CF35 /* SearchBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59503A552B756CCD0006CF35 /* SearchBarView.swift */; }; + 59503A582B758DCE0006CF35 /* SearchDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59503A572B758DCE0006CF35 /* SearchDTO.swift */; }; + 59503A5C2B75927F0006CF35 /* SearchStoreResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59503A5B2B75927F0006CF35 /* SearchStoreResponse.swift */; }; + 59503A5E2B7596990006CF35 /* FetchSearchStoresUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59503A5D2B7596990006CF35 /* FetchSearchStoresUseCase.swift */; }; + 59503A602B75970E0006CF35 /* FetchSearchStoresUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59503A5F2B75970E0006CF35 /* FetchSearchStoresUseCaseImpl.swift */; }; + 595EE2A52B693DE700CC01CE /* ErrorAlertMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 595EE2A42B693DE700CC01CE /* ErrorAlertMessage.swift */; }; + 596DDC4D2B6416AB00A4BBC4 /* SummaryViewContents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 596DDC4C2B6416AB00A4BBC4 /* SummaryViewContents.swift */; }; 5977BE582B5524D500725C90 /* RefreshButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5977BE572B5524D500725C90 /* RefreshButton.swift */; }; 5977BE5C2B5535A100725C90 /* FetchRefreshStoresUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5977BE5B2B5535A100725C90 /* FetchRefreshStoresUseCaseImpl.swift */; }; 5977BE5E2B5535C700725C90 /* FetchRefreshStoresUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5977BE5D2B5535C700725C90 /* FetchRefreshStoresUseCase.swift */; }; @@ -29,11 +41,24 @@ 598CC4D82B5D2E3C0043D064 /* FetchRefreshStoresUseCaseImplTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 598CC4D72B5D2E3C0043D064 /* FetchRefreshStoresUseCaseImplTests.swift */; }; 598CC4DB2B5D344C0043D064 /* MockSuccessStoreRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 598CC4DA2B5D344C0043D064 /* MockSuccessStoreRepository.swift */; }; 598CC4DD2B5D44940043D064 /* MockFailStoreRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 598CC4DC2B5D44940043D064 /* MockFailStoreRepository.swift */; }; + 59B886262B6A3A02005750EF /* StoreListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B886252B6A3A02005750EF /* StoreListViewController.swift */; }; + 59B886292B6A3F1E005750EF /* StoreTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B886282B6A3F1E005750EF /* StoreTableViewCell.swift */; }; + 59B8862B2B6A3F7F005750EF /* UITableViewCell+Identifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B8862A2B6A3F7F005750EF /* UITableViewCell+Identifier.swift */; }; + 59B886462B6A5CDC005750EF /* StoreListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B886452B6A5CDC005750EF /* StoreListViewModel.swift */; }; + 59B886482B6A5CE9005750EF /* StoreListViewModelImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B886472B6A5CE9005750EF /* StoreListViewModelImpl.swift */; }; + 59B8864A2B6A9CCB005750EF /* UIStackView+clear.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B886492B6A9CCB005750EF /* UIStackView+clear.swift */; }; + 59B886502B6AB9F7005750EF /* StoreTableViewCellContents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B8864F2B6AB9F7005750EF /* StoreTableViewCellContents.swift */; }; + 59B8865A2B6E3B40005750EF /* StoreInformationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B886592B6E3B40005750EF /* StoreInformationViewController.swift */; }; + 59B8865C2B6E4B8B005750EF /* StoreInformationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B8865B2B6E4B8B005750EF /* StoreInformationViewModel.swift */; }; + 59B8865E2B6E4B98005750EF /* StoreInformationViewModelImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B8865D2B6E4B98005750EF /* StoreInformationViewModelImpl.swift */; }; + 59B886602B6E7CF6005750EF /* UIViewController+Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B8865F2B6E7CF6005750EF /* UIViewController+Alert.swift */; }; + 59B886622B6E8484005750EF /* UISheetPresentationController+Detent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B886612B6E8484005750EF /* UISheetPresentationController+Detent.swift */; }; + 59B886642B6EC816005750EF /* SummaryViewHeightCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B886632B6EC816005750EF /* SummaryViewHeightCase.swift */; }; 59C306A42B4D7EBA00862625 /* Marker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59C306A32B4D7EBA00862625 /* Marker.swift */; }; 59C306A62B4D966C00862625 /* CertificationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59C306A52B4D966C00862625 /* CertificationType.swift */; }; 59C306A92B4FF9AF00862625 /* StoreRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59C306A82B4FF9AF00862625 /* StoreRepository.swift */; }; 59C306AD2B4FFAC700862625 /* StoreDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59C306AC2B4FFAC700862625 /* StoreDTO.swift */; }; - 59C306B02B4FFE4400862625 /* StoreResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59C306AF2B4FFE4400862625 /* StoreResponse.swift */; }; + 59C306B02B4FFE4400862625 /* RefreshStoreResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59C306AF2B4FFE4400862625 /* RefreshStoreResponse.swift */; }; 59C306B22B50001F00862625 /* StoreRepositoryImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59C306B12B50001F00862625 /* StoreRepositoryImpl.swift */; }; 59C306B42B50015500862625 /* APIResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59C306B32B50015500862625 /* APIResponse.swift */; }; 59C306B62B50027300862625 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59C306B52B50027300862625 /* Store.swift */; }; @@ -47,6 +72,7 @@ 59C306CF2B50399C00862625 /* RequestLocationDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59C306CE2B50399C00862625 /* RequestLocationDTO.swift */; }; 59C306D82B50650D00862625 /* Encodable+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59C306D72B50650D00862625 /* Encodable+.swift */; }; 59C306DC2B506F3D00862625 /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59C306DB2B506F3D00862625 /* NetworkError.swift */; }; + 59EC53802B69E5E2004DB2F9 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 59EC537F2B69E5E2004DB2F9 /* GoogleService-Info.plist */; }; 59F478B12B59BB00002FEF9E /* ImageRepositoryError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59F478B02B59BB00002FEF9E /* ImageRepositoryError.swift */; }; 59F478B32B59BDD6002FEF9E /* FetchImageUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59F478B22B59BDD6002FEF9E /* FetchImageUseCaseImpl.swift */; }; 59F478B52B59BE0B002FEF9E /* FetchImageUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59F478B42B59BE0B002FEF9E /* FetchImageUseCase.swift */; }; @@ -54,7 +80,6 @@ 59F478BD2B5AE180002FEF9E /* FetchStoresUseCaseImplTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59F478BC2B5AE180002FEF9E /* FetchStoresUseCaseImplTests.swift */; }; 59F478BF2B5BEA08002FEF9E /* RequestLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59F478BE2B5BEA08002FEF9E /* RequestLocation.swift */; }; 59F478C12B5D0D8D002FEF9E /* ImageRepositoryImplTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59F478C02B5D0D8D002FEF9E /* ImageRepositoryImplTests.swift */; }; - 8FE699E5DAEEDFE5A53D5E82 /* Pods_KCS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E11E3144529848C9A0FC6F77 /* Pods_KCS.framework */; }; A802D1F62B5277630091FDE7 /* CertificationLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A802D1F52B5277620091FDE7 /* CertificationLabel.swift */; }; A81EFBB32B5BC57800D0C0D7 /* OpenClosedContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81EFBB22B5BC57800D0C0D7 /* OpenClosedContent.swift */; }; A81EFBB52B5D477600D0C0D7 /* UILabel+.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81EFBB42B5D477600D0C0D7 /* UILabel+.swift */; }; @@ -68,21 +93,41 @@ A81EFBC72B5D597400D0C0D7 /* Pretendard-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = A81EFBBE2B5D597400D0C0D7 /* Pretendard-Bold.ttf */; }; A81EFBC82B5D597400D0C0D7 /* Pretendard-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = A81EFBBF2B5D597400D0C0D7 /* Pretendard-Light.ttf */; }; A81EFBCA2B5D5A2300D0C0D7 /* UIFont+.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81EFBC92B5D5A2300D0C0D7 /* UIFont+.swift */; }; + A821A3742B74B84700089B8F /* SplashViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A821A3732B74B84700089B8F /* SplashViewController.swift */; }; + A821A3762B74B9F900089B8F /* NetworkRepositoryImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = A821A3752B74B9F900089B8F /* NetworkRepositoryImpl.swift */; }; + A821A3782B74BAA700089B8F /* NetworkRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = A821A3772B74BAA700089B8F /* NetworkRepository.swift */; }; + A821A37A2B74BBE200089B8F /* CheckNetworkStatusUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = A821A3792B74BBE200089B8F /* CheckNetworkStatusUseCaseImpl.swift */; }; + A821A37C2B74BC4B00089B8F /* CheckNetworkStatusUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A821A37B2B74BC4B00089B8F /* CheckNetworkStatusUseCase.swift */; }; + A821A3802B74BDA200089B8F /* SplashViewModelImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = A821A37F2B74BDA200089B8F /* SplashViewModelImpl.swift */; }; + A821A3832B74C08600089B8F /* SplashViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A821A3822B74C08600089B8F /* SplashViewModel.swift */; }; + A83367B62B6F993F00E0A844 /* MoreStoreButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A83367B52B6F993F00E0A844 /* MoreStoreButton.swift */; }; + A83367B82B6FA0E700E0A844 /* FetchStores.swift in Sources */ = {isa = PBXBuildFile; fileRef = A83367B72B6FA0E700E0A844 /* FetchStores.swift */; }; + A83367BB2B709C0200E0A844 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A83367BA2B709C0200E0A844 /* OnboardingViewController.swift */; }; + A83367BD2B70A52900E0A844 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A83367BC2B70A52900E0A844 /* Storage.swift */; }; + A83367BF2B7246E700E0A844 /* FirstOnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A83367BE2B7246E700E0A844 /* FirstOnboardingView.swift */; }; + A83367C12B726E2600E0A844 /* SecondOnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A83367C02B726E2600E0A844 /* SecondOnboardingView.swift */; }; + A83367C32B72714C00E0A844 /* ThirdOnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A83367C22B72714B00E0A844 /* ThirdOnboardingView.swift */; }; + A83367C52B7271B900E0A844 /* FourthOnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A83367C42B7271B900E0A844 /* FourthOnboardingView.swift */; }; + A83367C72B72725700E0A844 /* FifthOnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A83367C62B72725700E0A844 /* FifthOnboardingView.swift */; }; A89087042B4E7F3500767225 /* FilterButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A89087032B4E7F3500767225 /* FilterButton.swift */; }; A890870A2B4EF00B00767225 /* SystemImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A89087092B4EF00B00767225 /* SystemImage.swift */; }; A890870D2B4EF91600767225 /* UIView+SetLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A890870C2B4EF91600767225 /* UIView+SetLayer.swift */; }; - A890870F2B4F836C00767225 /* StoreInformationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A890870E2B4F836C00767225 /* StoreInformationViewController.swift */; }; + A890870F2B4F836C00767225 /* SummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A890870E2B4F836C00767225 /* SummaryView.swift */; }; + A8A7E05B2B642EC900D015E5 /* DetailViewContents.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8A7E05A2B642EC900D015E5 /* DetailViewContents.swift */; }; + A8A7E05D2B64AF1300D015E5 /* StoreInformationViewConstraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8A7E05C2B64AF1200D015E5 /* StoreInformationViewConstraints.swift */; }; + A8A7E0602B64E62200D015E5 /* NMFMyPosition+.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8A7E05F2B64E62200D015E5 /* NMFMyPosition+.swift */; }; + A8A7E0622B652F0D00D015E5 /* MarkerContents.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8A7E0612B652F0D00D015E5 /* MarkerContents.swift */; }; A8ACB7D82B57BE7D00540BD1 /* StoreRepositoryError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8ACB7D72B57BE7D00540BD1 /* StoreRepositoryError.swift */; }; A8ACB7DB2B58B51A00540BD1 /* Date+.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8ACB7DA2B58B51A00540BD1 /* Date+.swift */; }; A8ACB7DD2B58E3DE00540BD1 /* OpenClosedType.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8ACB7DC2B58E3DE00540BD1 /* OpenClosedType.swift */; }; - A8ACB7DF2B594F4B00540BD1 /* StoreInformationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8ACB7DE2B594F4B00540BD1 /* StoreInformationViewModel.swift */; }; - A8ACB7E22B594F7400540BD1 /* StoreInformationViewModelImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8ACB7E12B594F7400540BD1 /* StoreInformationViewModelImpl.swift */; }; A8ACB7E82B595A2100540BD1 /* GetOpenClosedUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8ACB7E72B595A2100540BD1 /* GetOpenClosedUseCase.swift */; }; A8ACB7EA2B595A3E00540BD1 /* GetOpenClosedUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8ACB7E92B595A3E00540BD1 /* GetOpenClosedUseCaseImpl.swift */; }; A8ACB7ED2B59647400540BD1 /* OpeningHourError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8ACB7EC2B59647400540BD1 /* OpeningHourError.swift */; }; A8ACB7EF2B5AEBB900540BD1 /* GetStoreInformationUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8ACB7EE2B5AEBB800540BD1 /* GetStoreInformationUseCaseImpl.swift */; }; A8ACB7F12B5AEBE300540BD1 /* GetStoreInformationUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8ACB7F02B5AEBE300540BD1 /* GetStoreInformationUseCase.swift */; }; - F242B43374CDD61CC6F6A4D5 /* Pods_KCSUnitTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0FF39E6207057AD78DB44730 /* Pods_KCSUnitTest.framework */; }; + A8AE4B1B2B62A60B00632355 /* OpeningHoursCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8AE4B1A2B62A60B00632355 /* OpeningHoursCellView.swift */; }; + AE60727C9A543E3D0F3A0279 /* Pods_KCSUnitTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 71A6818C1431365A23C873FB /* Pods_KCSUnitTest.framework */; }; + BBE1483137890D1D37D0E308 /* Pods_KCS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A3902FBE673069073F47D82 /* Pods_KCS.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -96,8 +141,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 0FF39E6207057AD78DB44730 /* Pods_KCSUnitTest.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_KCSUnitTest.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 4BD0CCD4DBD4E121C26925E6 /* Pods-KCSUnitTest.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-KCSUnitTest.release.xcconfig"; path = "Target Support Files/Pods-KCSUnitTest/Pods-KCSUnitTest.release.xcconfig"; sourceTree = ""; }; + 2A59B3837A53AAB2D7A1E09C /* Pods-KCSUnitTest.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-KCSUnitTest.release.xcconfig"; path = "Target Support Files/Pods-KCSUnitTest/Pods-KCSUnitTest.release.xcconfig"; sourceTree = ""; }; 59053D0A2B3889A200D190CC /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = ""; }; 591A88792B384E600059E40F /* KCS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KCS.app; sourceTree = BUILT_PRODUCTS_DIR; }; 591A887C2B384E600059E40F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -106,6 +150,18 @@ 591A88852B384E610059E40F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 591A88882B384E610059E40F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 591A888A2B384E610059E40F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 592262232B61203000CA5A11 /* DetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailView.swift; sourceTree = ""; }; + 59503A492B741F1E0006CF35 /* Secret.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Secret.xcconfig; sourceTree = ""; }; + 59503A4D2B751B0B0006CF35 /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = ""; }; + 59503A512B751FCC0006CF35 /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = ""; }; + 59503A532B751FD50006CF35 /* SearchViewModelImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModelImpl.swift; sourceTree = ""; }; + 59503A552B756CCD0006CF35 /* SearchBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBarView.swift; sourceTree = ""; }; + 59503A572B758DCE0006CF35 /* SearchDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchDTO.swift; sourceTree = ""; }; + 59503A5B2B75927F0006CF35 /* SearchStoreResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchStoreResponse.swift; sourceTree = ""; }; + 59503A5D2B7596990006CF35 /* FetchSearchStoresUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchSearchStoresUseCase.swift; sourceTree = ""; }; + 59503A5F2B75970E0006CF35 /* FetchSearchStoresUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchSearchStoresUseCaseImpl.swift; sourceTree = ""; }; + 595EE2A42B693DE700CC01CE /* ErrorAlertMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorAlertMessage.swift; sourceTree = ""; }; + 596DDC4C2B6416AB00A4BBC4 /* SummaryViewContents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SummaryViewContents.swift; sourceTree = ""; }; 5977BE572B5524D500725C90 /* RefreshButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshButton.swift; sourceTree = ""; }; 5977BE5B2B5535A100725C90 /* FetchRefreshStoresUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchRefreshStoresUseCaseImpl.swift; sourceTree = ""; }; 5977BE5D2B5535C700725C90 /* FetchRefreshStoresUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchRefreshStoresUseCase.swift; sourceTree = ""; }; @@ -120,15 +176,27 @@ 5977BE992B59AC3300725C90 /* ImageRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRepository.swift; sourceTree = ""; }; 5977BE9B2B59AC8D00725C90 /* ImageRepositoryImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRepositoryImpl.swift; sourceTree = ""; }; 5977BE9D2B59ACE800725C90 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = ""; }; - 5986DCE82B390A8D005AE43B /* Secret.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Secret.xcconfig; sourceTree = ""; }; 598CC4D72B5D2E3C0043D064 /* FetchRefreshStoresUseCaseImplTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchRefreshStoresUseCaseImplTests.swift; sourceTree = ""; }; 598CC4DA2B5D344C0043D064 /* MockSuccessStoreRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSuccessStoreRepository.swift; sourceTree = ""; }; 598CC4DC2B5D44940043D064 /* MockFailStoreRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFailStoreRepository.swift; sourceTree = ""; }; + 59B886252B6A3A02005750EF /* StoreListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreListViewController.swift; sourceTree = ""; }; + 59B886282B6A3F1E005750EF /* StoreTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreTableViewCell.swift; sourceTree = ""; }; + 59B8862A2B6A3F7F005750EF /* UITableViewCell+Identifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableViewCell+Identifier.swift"; sourceTree = ""; }; + 59B886452B6A5CDC005750EF /* StoreListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreListViewModel.swift; sourceTree = ""; }; + 59B886472B6A5CE9005750EF /* StoreListViewModelImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreListViewModelImpl.swift; sourceTree = ""; }; + 59B886492B6A9CCB005750EF /* UIStackView+clear.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIStackView+clear.swift"; sourceTree = ""; }; + 59B8864F2B6AB9F7005750EF /* StoreTableViewCellContents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreTableViewCellContents.swift; sourceTree = ""; }; + 59B886592B6E3B40005750EF /* StoreInformationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreInformationViewController.swift; sourceTree = ""; }; + 59B8865B2B6E4B8B005750EF /* StoreInformationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreInformationViewModel.swift; sourceTree = ""; }; + 59B8865D2B6E4B98005750EF /* StoreInformationViewModelImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreInformationViewModelImpl.swift; sourceTree = ""; }; + 59B8865F2B6E7CF6005750EF /* UIViewController+Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Alert.swift"; sourceTree = ""; }; + 59B886612B6E8484005750EF /* UISheetPresentationController+Detent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISheetPresentationController+Detent.swift"; sourceTree = ""; }; + 59B886632B6EC816005750EF /* SummaryViewHeightCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SummaryViewHeightCase.swift; sourceTree = ""; }; 59C306A32B4D7EBA00862625 /* Marker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Marker.swift; sourceTree = ""; }; 59C306A52B4D966C00862625 /* CertificationType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificationType.swift; sourceTree = ""; }; 59C306A82B4FF9AF00862625 /* StoreRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreRepository.swift; sourceTree = ""; }; 59C306AC2B4FFAC700862625 /* StoreDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreDTO.swift; sourceTree = ""; }; - 59C306AF2B4FFE4400862625 /* StoreResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreResponse.swift; sourceTree = ""; }; + 59C306AF2B4FFE4400862625 /* RefreshStoreResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshStoreResponse.swift; sourceTree = ""; }; 59C306B12B50001F00862625 /* StoreRepositoryImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreRepositoryImpl.swift; sourceTree = ""; }; 59C306B32B50015500862625 /* APIResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIResponse.swift; sourceTree = ""; }; 59C306B52B50027300862625 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = ""; }; @@ -142,6 +210,7 @@ 59C306CE2B50399C00862625 /* RequestLocationDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestLocationDTO.swift; sourceTree = ""; }; 59C306D72B50650D00862625 /* Encodable+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Encodable+.swift"; sourceTree = ""; }; 59C306DB2B506F3D00862625 /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = ""; }; + 59EC537F2B69E5E2004DB2F9 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 59F478B02B59BB00002FEF9E /* ImageRepositoryError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRepositoryError.swift; sourceTree = ""; }; 59F478B22B59BDD6002FEF9E /* FetchImageUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchImageUseCaseImpl.swift; sourceTree = ""; }; 59F478B42B59BE0B002FEF9E /* FetchImageUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchImageUseCase.swift; sourceTree = ""; }; @@ -149,8 +218,10 @@ 59F478BC2B5AE180002FEF9E /* FetchStoresUseCaseImplTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchStoresUseCaseImplTests.swift; sourceTree = ""; }; 59F478BE2B5BEA08002FEF9E /* RequestLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestLocation.swift; sourceTree = ""; }; 59F478C02B5D0D8D002FEF9E /* ImageRepositoryImplTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRepositoryImplTests.swift; sourceTree = ""; }; - 5FF0FF2386EEB69182D6EA4C /* Pods-KCSUnitTest.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-KCSUnitTest.debug.xcconfig"; path = "Target Support Files/Pods-KCSUnitTest/Pods-KCSUnitTest.debug.xcconfig"; sourceTree = ""; }; - 9EA5C8EA72EA9E937C11400A /* Pods-KCS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-KCS.debug.xcconfig"; path = "Target Support Files/Pods-KCS/Pods-KCS.debug.xcconfig"; sourceTree = ""; }; + 5A3902FBE673069073F47D82 /* Pods_KCS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_KCS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 6E7A587B8D04F1EBD1715550 /* Pods-KCS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-KCS.release.xcconfig"; path = "Target Support Files/Pods-KCS/Pods-KCS.release.xcconfig"; sourceTree = ""; }; + 71A6818C1431365A23C873FB /* Pods_KCSUnitTest.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_KCSUnitTest.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 96F7AFBA49BAE4D780F6D753 /* Pods-KCS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-KCS.debug.xcconfig"; path = "Target Support Files/Pods-KCS/Pods-KCS.debug.xcconfig"; sourceTree = ""; }; A802D1F52B5277620091FDE7 /* CertificationLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificationLabel.swift; sourceTree = ""; }; A81EFBB22B5BC57800D0C0D7 /* OpenClosedContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenClosedContent.swift; sourceTree = ""; }; A81EFBB42B5D477600D0C0D7 /* UILabel+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILabel+.swift"; sourceTree = ""; }; @@ -164,22 +235,40 @@ A81EFBBE2B5D597400D0C0D7 /* Pretendard-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Pretendard-Bold.ttf"; sourceTree = ""; }; A81EFBBF2B5D597400D0C0D7 /* Pretendard-Light.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Pretendard-Light.ttf"; sourceTree = ""; }; A81EFBC92B5D5A2300D0C0D7 /* UIFont+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+.swift"; sourceTree = ""; }; + A821A3732B74B84700089B8F /* SplashViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashViewController.swift; sourceTree = ""; }; + A821A3752B74B9F900089B8F /* NetworkRepositoryImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkRepositoryImpl.swift; sourceTree = ""; }; + A821A3772B74BAA700089B8F /* NetworkRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkRepository.swift; sourceTree = ""; }; + A821A3792B74BBE200089B8F /* CheckNetworkStatusUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckNetworkStatusUseCaseImpl.swift; sourceTree = ""; }; + A821A37B2B74BC4B00089B8F /* CheckNetworkStatusUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckNetworkStatusUseCase.swift; sourceTree = ""; }; + A821A37F2B74BDA200089B8F /* SplashViewModelImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashViewModelImpl.swift; sourceTree = ""; }; + A821A3822B74C08600089B8F /* SplashViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashViewModel.swift; sourceTree = ""; }; + A83367B52B6F993F00E0A844 /* MoreStoreButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreStoreButton.swift; sourceTree = ""; }; + A83367B72B6FA0E700E0A844 /* FetchStores.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchStores.swift; sourceTree = ""; }; + A83367BA2B709C0200E0A844 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = ""; }; + A83367BC2B70A52900E0A844 /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = ""; }; + A83367BE2B7246E700E0A844 /* FirstOnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstOnboardingView.swift; sourceTree = ""; }; + A83367C02B726E2600E0A844 /* SecondOnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondOnboardingView.swift; sourceTree = ""; }; + A83367C22B72714B00E0A844 /* ThirdOnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdOnboardingView.swift; sourceTree = ""; }; + A83367C42B7271B900E0A844 /* FourthOnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FourthOnboardingView.swift; sourceTree = ""; }; + A83367C62B72725700E0A844 /* FifthOnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FifthOnboardingView.swift; sourceTree = ""; }; A89087032B4E7F3500767225 /* FilterButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterButton.swift; sourceTree = ""; }; A89087092B4EF00B00767225 /* SystemImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemImage.swift; sourceTree = ""; }; A890870C2B4EF91600767225 /* UIView+SetLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+SetLayer.swift"; sourceTree = ""; }; - A890870E2B4F836C00767225 /* StoreInformationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreInformationViewController.swift; sourceTree = ""; }; + A890870E2B4F836C00767225 /* SummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SummaryView.swift; sourceTree = ""; }; + A8A7E05A2B642EC900D015E5 /* DetailViewContents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailViewContents.swift; sourceTree = ""; }; + A8A7E05C2B64AF1200D015E5 /* StoreInformationViewConstraints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreInformationViewConstraints.swift; sourceTree = ""; }; + A8A7E05F2B64E62200D015E5 /* NMFMyPosition+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NMFMyPosition+.swift"; sourceTree = ""; }; + A8A7E0612B652F0D00D015E5 /* MarkerContents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkerContents.swift; sourceTree = ""; }; A8ACB7D72B57BE7D00540BD1 /* StoreRepositoryError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreRepositoryError.swift; sourceTree = ""; }; A8ACB7DA2B58B51A00540BD1 /* Date+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+.swift"; sourceTree = ""; }; A8ACB7DC2B58E3DE00540BD1 /* OpenClosedType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenClosedType.swift; sourceTree = ""; }; - A8ACB7DE2B594F4B00540BD1 /* StoreInformationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreInformationViewModel.swift; sourceTree = ""; }; - A8ACB7E12B594F7400540BD1 /* StoreInformationViewModelImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreInformationViewModelImpl.swift; sourceTree = ""; }; A8ACB7E72B595A2100540BD1 /* GetOpenClosedUseCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetOpenClosedUseCase.swift; sourceTree = ""; }; A8ACB7E92B595A3E00540BD1 /* GetOpenClosedUseCaseImpl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetOpenClosedUseCaseImpl.swift; sourceTree = ""; }; A8ACB7EC2B59647400540BD1 /* OpeningHourError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpeningHourError.swift; sourceTree = ""; }; A8ACB7EE2B5AEBB800540BD1 /* GetStoreInformationUseCaseImpl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetStoreInformationUseCaseImpl.swift; sourceTree = ""; }; A8ACB7F02B5AEBE300540BD1 /* GetStoreInformationUseCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetStoreInformationUseCase.swift; sourceTree = ""; }; - AA9EF30352C847A7C6DEC110 /* Pods-KCS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-KCS.release.xcconfig"; path = "Target Support Files/Pods-KCS/Pods-KCS.release.xcconfig"; sourceTree = ""; }; - E11E3144529848C9A0FC6F77 /* Pods_KCS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_KCS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A8AE4B1A2B62A60B00632355 /* OpeningHoursCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpeningHoursCellView.swift; sourceTree = ""; }; + D7B848B1C5D2F1B31C605818 /* Pods-KCSUnitTest.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-KCSUnitTest.debug.xcconfig"; path = "Target Support Files/Pods-KCSUnitTest/Pods-KCSUnitTest.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -187,7 +276,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 8FE699E5DAEEDFE5A53D5E82 /* Pods_KCS.framework in Frameworks */, + BBE1483137890D1D37D0E308 /* Pods_KCS.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -195,7 +284,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - F242B43374CDD61CC6F6A4D5 /* Pods_KCSUnitTest.framework in Frameworks */, + AE60727C9A543E3D0F3A0279 /* Pods_KCSUnitTest.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -205,13 +294,14 @@ 591A88702B384E600059E40F = { isa = PBXGroup; children = ( - 5986DCE82B390A8D005AE43B /* Secret.xcconfig */, + 59503A492B741F1E0006CF35 /* Secret.xcconfig */, 59053D0A2B3889A200D190CC /* .swiftlint.yml */, + 59EC537F2B69E5E2004DB2F9 /* GoogleService-Info.plist */, 591A887B2B384E600059E40F /* KCS */, 5977BE8B2B5966F900725C90 /* KCSUnitTest */, 591A887A2B384E600059E40F /* Products */, ED50AD730FA5F4D533F6DF5F /* Pods */, - CBA0F2F7C6731F98AFE4E86D /* Frameworks */, + E99F20FF9A30DCF9C51285A2 /* Frameworks */, ); sourceTree = ""; }; @@ -237,7 +327,41 @@ path = KCS; sourceTree = ""; }; - 5977BE592B55355D00725C90 /* UseCase */ = { + 59503A4B2B751AEE0006CF35 /* Search */ = { + isa = PBXGroup; + children = ( + 59503A4C2B751AFD0006CF35 /* View */, + 59503A4F2B751FB30006CF35 /* ViewModel */, + ); + path = Search; + sourceTree = ""; + }; + 59503A4C2B751AFD0006CF35 /* View */ = { + isa = PBXGroup; + children = ( + 59503A4D2B751B0B0006CF35 /* SearchViewController.swift */, + ); + path = View; + sourceTree = ""; + }; + 59503A4F2B751FB30006CF35 /* ViewModel */ = { + isa = PBXGroup; + children = ( + 59503A502B751FBE0006CF35 /* Protocol */, + 59503A532B751FD50006CF35 /* SearchViewModelImpl.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + 59503A502B751FBE0006CF35 /* Protocol */ = { + isa = PBXGroup; + children = ( + 59503A512B751FCC0006CF35 /* SearchViewModel.swift */, + ); + path = Protocol; + sourceTree = ""; + }; + 5977BE592B55355D00725C90 /* protocol */ = { isa = PBXGroup; children = ( 5977BE5D2B5535C700725C90 /* FetchRefreshStoresUseCase.swift */, @@ -245,19 +369,24 @@ A8ACB7F02B5AEBE300540BD1 /* GetStoreInformationUseCase.swift */, A8ACB7E72B595A2100540BD1 /* GetOpenClosedUseCase.swift */, 59F478B42B59BE0B002FEF9E /* FetchImageUseCase.swift */, + A821A37B2B74BC4B00089B8F /* CheckNetworkStatusUseCase.swift */, + 59503A5D2B7596990006CF35 /* FetchSearchStoresUseCase.swift */, ); - path = UseCase; + path = protocol; sourceTree = ""; }; 5977BE5A2B55356600725C90 /* UseCase */ = { isa = PBXGroup; children = ( + 5977BE592B55355D00725C90 /* protocol */, A8ACB7EB2B59644100540BD1 /* Error */, 5977BE5B2B5535A100725C90 /* FetchRefreshStoresUseCaseImpl.swift */, 5977BE972B5999E000725C90 /* FetchStoresUseCaseImpl.swift */, A8ACB7EE2B5AEBB800540BD1 /* GetStoreInformationUseCaseImpl.swift */, A8ACB7E92B595A3E00540BD1 /* GetOpenClosedUseCaseImpl.swift */, 59F478B22B59BDD6002FEF9E /* FetchImageUseCaseImpl.swift */, + A821A3792B74BBE200089B8F /* CheckNetworkStatusUseCaseImpl.swift */, + 59503A5F2B75970E0006CF35 /* FetchSearchStoresUseCaseImpl.swift */, ); path = UseCase; sourceTree = ""; @@ -268,7 +397,6 @@ A8ACB7E02B594F5F00540BD1 /* protocol */, 5977BE652B553BA800725C90 /* HomeViewModelImpl.swift */, 5977BE672B553C8300725C90 /* HomeDependency.swift */, - A8ACB7E12B594F7400540BD1 /* StoreInformationViewModelImpl.swift */, ); path = ViewModel; sourceTree = ""; @@ -313,6 +441,7 @@ A8ACB7CD2B54ED6400540BD1 /* Repository */, 59C306AE2B4FFE3700862625 /* Network */, 5977BE9D2B59ACE800725C90 /* ImageCache.swift */, + A83367BC2B70A52900E0A844 /* Storage.swift */, ); path = Data; sourceTree = ""; @@ -330,8 +459,13 @@ 5986DCDF2B3892EB005AE43B /* Presentation */ = { isa = PBXGroup; children = ( + 59503A4B2B751AEE0006CF35 /* Search */, A890870B2B4EF8F900767225 /* Extension */, 5986DCEA2B392996005AE43B /* Home */, + 59B886242B6A39E9005750EF /* StoreList */, + 59B886552B6E3A59005750EF /* StoreInformation */, + A83367B92B709BE900E0A844 /* OnBoarding */, + A821A3722B74B82600089B8F /* Splash */, ); path = Presentation; sourceTree = ""; @@ -357,11 +491,11 @@ isa = PBXGroup; children = ( 591A88802B384E600059E40F /* HomeViewController.swift */, - A890870E2B4F836C00767225 /* StoreInformationViewController.swift */, 59C306A32B4D7EBA00862625 /* Marker.swift */, A89087032B4E7F3500767225 /* FilterButton.swift */, - A802D1F52B5277620091FDE7 /* CertificationLabel.swift */, 5977BE572B5524D500725C90 /* RefreshButton.swift */, + A83367B52B6F993F00E0A844 /* MoreStoreButton.swift */, + 59503A552B756CCD0006CF35 /* SearchBarView.swift */, ); path = View; sourceTree = ""; @@ -375,11 +509,85 @@ path = MockRepository; sourceTree = ""; }; + 59B886242B6A39E9005750EF /* StoreList */ = { + isa = PBXGroup; + children = ( + 59B886272B6A3AF0005750EF /* View */, + 59B886432B6A5CB6005750EF /* ViewModel */, + ); + path = StoreList; + sourceTree = ""; + }; + 59B886272B6A3AF0005750EF /* View */ = { + isa = PBXGroup; + children = ( + 59B886252B6A3A02005750EF /* StoreListViewController.swift */, + 59B886282B6A3F1E005750EF /* StoreTableViewCell.swift */, + ); + path = View; + sourceTree = ""; + }; + 59B886432B6A5CB6005750EF /* ViewModel */ = { + isa = PBXGroup; + children = ( + 59B886442B6A5CB6005750EF /* Protocol */, + 59B886472B6A5CE9005750EF /* StoreListViewModelImpl.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + 59B886442B6A5CB6005750EF /* Protocol */ = { + isa = PBXGroup; + children = ( + 59B886452B6A5CDC005750EF /* StoreListViewModel.swift */, + ); + path = Protocol; + sourceTree = ""; + }; + 59B886552B6E3A59005750EF /* StoreInformation */ = { + isa = PBXGroup; + children = ( + 59B886572B6E3A8A005750EF /* View */, + 59B886562B6E3A7F005750EF /* ViewModel */, + ); + path = StoreInformation; + sourceTree = ""; + }; + 59B886562B6E3A7F005750EF /* ViewModel */ = { + isa = PBXGroup; + children = ( + 59B886582B6E3A8E005750EF /* Protocol */, + 59B8865D2B6E4B98005750EF /* StoreInformationViewModelImpl.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + 59B886572B6E3A8A005750EF /* View */ = { + isa = PBXGroup; + children = ( + 59B886592B6E3B40005750EF /* StoreInformationViewController.swift */, + A890870E2B4F836C00767225 /* SummaryView.swift */, + 592262232B61203000CA5A11 /* DetailView.swift */, + A802D1F52B5277620091FDE7 /* CertificationLabel.swift */, + A8AE4B1A2B62A60B00632355 /* OpeningHoursCellView.swift */, + ); + path = View; + sourceTree = ""; + }; + 59B886582B6E3A8E005750EF /* Protocol */ = { + isa = PBXGroup; + children = ( + 59B8865B2B6E4B8B005750EF /* StoreInformationViewModel.swift */, + ); + path = Protocol; + sourceTree = ""; + }; 59C306A72B4FF98600862625 /* Repository */ = { isa = PBXGroup; children = ( 59C306A82B4FF9AF00862625 /* StoreRepository.swift */, 5977BE992B59AC3300725C90 /* ImageRepository.swift */, + A821A3772B74BAA700089B8F /* NetworkRepository.swift */, ); path = Repository; sourceTree = ""; @@ -393,9 +601,17 @@ 59C306C82B501B9D00862625 /* RegularOpeningHours.swift */, 59C306A52B4D966C00862625 /* CertificationType.swift */, A8ACB7DC2B58E3DE00540BD1 /* OpenClosedType.swift */, + A81EFBB22B5BC57800D0C0D7 /* OpenClosedContent.swift */, 5977BE732B57FA7A00725C90 /* FilteredStores.swift */, 59F478BE2B5BEA08002FEF9E /* RequestLocation.swift */, - A81EFBB22B5BC57800D0C0D7 /* OpenClosedContent.swift */, + A8A7E05C2B64AF1200D015E5 /* StoreInformationViewConstraints.swift */, + 596DDC4C2B6416AB00A4BBC4 /* SummaryViewContents.swift */, + A8A7E05A2B642EC900D015E5 /* DetailViewContents.swift */, + A8A7E0612B652F0D00D015E5 /* MarkerContents.swift */, + 595EE2A42B693DE700CC01CE /* ErrorAlertMessage.swift */, + 59B8864F2B6AB9F7005750EF /* StoreTableViewCellContents.swift */, + 59B886632B6EC816005750EF /* SummaryViewHeightCase.swift */, + A83367B72B6FA0E700E0A844 /* FetchStores.swift */, ); path = Entity; sourceTree = ""; @@ -405,6 +621,7 @@ children = ( 59C306AC2B4FFAC700862625 /* StoreDTO.swift */, 59C306CE2B50399C00862625 /* RequestLocationDTO.swift */, + 59503A572B758DCE0006CF35 /* SearchDTO.swift */, ); path = DTO; sourceTree = ""; @@ -426,7 +643,8 @@ isa = PBXGroup; children = ( 59C306B82B50033A00862625 /* Protocol */, - 59C306AF2B4FFE4400862625 /* StoreResponse.swift */, + 59C306AF2B4FFE4400862625 /* RefreshStoreResponse.swift */, + 59503A5B2B75927F0006CF35 /* SearchStoreResponse.swift */, ); path = Response; sourceTree = ""; @@ -472,6 +690,53 @@ path = font; sourceTree = ""; }; + A821A3722B74B82600089B8F /* Splash */ = { + isa = PBXGroup; + children = ( + A821A37D2B74BD7B00089B8F /* View */, + A821A37E2B74BD8400089B8F /* ViewModel */, + ); + path = Splash; + sourceTree = ""; + }; + A821A37D2B74BD7B00089B8F /* View */ = { + isa = PBXGroup; + children = ( + A821A3732B74B84700089B8F /* SplashViewController.swift */, + ); + path = View; + sourceTree = ""; + }; + A821A37E2B74BD8400089B8F /* ViewModel */ = { + isa = PBXGroup; + children = ( + A821A3812B74C05500089B8F /* protocol */, + A821A37F2B74BDA200089B8F /* SplashViewModelImpl.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + A821A3812B74C05500089B8F /* protocol */ = { + isa = PBXGroup; + children = ( + A821A3822B74C08600089B8F /* SplashViewModel.swift */, + ); + path = protocol; + sourceTree = ""; + }; + A83367B92B709BE900E0A844 /* OnBoarding */ = { + isa = PBXGroup; + children = ( + A83367BA2B709C0200E0A844 /* OnboardingViewController.swift */, + A83367BE2B7246E700E0A844 /* FirstOnboardingView.swift */, + A83367C02B726E2600E0A844 /* SecondOnboardingView.swift */, + A83367C22B72714B00E0A844 /* ThirdOnboardingView.swift */, + A83367C42B7271B900E0A844 /* FourthOnboardingView.swift */, + A83367C62B72725700E0A844 /* FifthOnboardingView.swift */, + ); + path = OnBoarding; + sourceTree = ""; + }; A890870B2B4EF8F900767225 /* Extension */ = { isa = PBXGroup; children = ( @@ -479,6 +744,11 @@ A890870C2B4EF91600767225 /* UIView+SetLayer.swift */, A81EFBB42B5D477600D0C0D7 /* UILabel+.swift */, A81EFBC92B5D5A2300D0C0D7 /* UIFont+.swift */, + A8A7E05F2B64E62200D015E5 /* NMFMyPosition+.swift */, + 59B8862A2B6A3F7F005750EF /* UITableViewCell+Identifier.swift */, + 59B886492B6A9CCB005750EF /* UIStackView+clear.swift */, + 59B8865F2B6E7CF6005750EF /* UIViewController+Alert.swift */, + 59B886612B6E8484005750EF /* UISheetPresentationController+Detent.swift */, ); path = Extension; sourceTree = ""; @@ -486,7 +756,6 @@ A8ACB7CC2B54ED3800540BD1 /* Interface */ = { isa = PBXGroup; children = ( - 5977BE592B55355D00725C90 /* UseCase */, 59C306A72B4FF98600862625 /* Repository */, ); path = Interface; @@ -498,6 +767,7 @@ A8ACB7D62B57BE4E00540BD1 /* Error */, 59C306B12B50001F00862625 /* StoreRepositoryImpl.swift */, 5977BE9B2B59AC8D00725C90 /* ImageRepositoryImpl.swift */, + A821A3752B74B9F900089B8F /* NetworkRepositoryImpl.swift */, ); path = Repository; sourceTree = ""; @@ -515,7 +785,6 @@ isa = PBXGroup; children = ( 5977BE602B55374000725C90 /* HomeViewModel.swift */, - A8ACB7DE2B594F4B00540BD1 /* StoreInformationViewModel.swift */, ); path = protocol; sourceTree = ""; @@ -528,11 +797,11 @@ path = Error; sourceTree = ""; }; - CBA0F2F7C6731F98AFE4E86D /* Frameworks */ = { + E99F20FF9A30DCF9C51285A2 /* Frameworks */ = { isa = PBXGroup; children = ( - E11E3144529848C9A0FC6F77 /* Pods_KCS.framework */, - 0FF39E6207057AD78DB44730 /* Pods_KCSUnitTest.framework */, + 5A3902FBE673069073F47D82 /* Pods_KCS.framework */, + 71A6818C1431365A23C873FB /* Pods_KCSUnitTest.framework */, ); name = Frameworks; sourceTree = ""; @@ -540,10 +809,10 @@ ED50AD730FA5F4D533F6DF5F /* Pods */ = { isa = PBXGroup; children = ( - 9EA5C8EA72EA9E937C11400A /* Pods-KCS.debug.xcconfig */, - AA9EF30352C847A7C6DEC110 /* Pods-KCS.release.xcconfig */, - 5FF0FF2386EEB69182D6EA4C /* Pods-KCSUnitTest.debug.xcconfig */, - 4BD0CCD4DBD4E121C26925E6 /* Pods-KCSUnitTest.release.xcconfig */, + 96F7AFBA49BAE4D780F6D753 /* Pods-KCS.debug.xcconfig */, + 6E7A587B8D04F1EBD1715550 /* Pods-KCS.release.xcconfig */, + D7B848B1C5D2F1B31C605818 /* Pods-KCSUnitTest.debug.xcconfig */, + 2A59B3837A53AAB2D7A1E09C /* Pods-KCSUnitTest.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -555,12 +824,13 @@ isa = PBXNativeTarget; buildConfigurationList = 591A888D2B384E610059E40F /* Build configuration list for PBXNativeTarget "KCS" */; buildPhases = ( - CB55E62BB6C1E559A1B678AA /* [CP] Check Pods Manifest.lock */, + 1710AC2AA053B9D767B25C83 /* [CP] Check Pods Manifest.lock */, 591A88752B384E600059E40F /* Sources */, 591A88762B384E600059E40F /* Frameworks */, 591A88772B384E600059E40F /* Resources */, - 3BC1B94F5FD0808C2E71C0CF /* [CP] Embed Pods Frameworks */, - 591A88902B3884930059E40F /* Run Script */, + 591A88902B3884930059E40F /* SwiftLint Run Script */, + C9DF99EE5FF9433DE46D74ED /* [CP] Embed Pods Frameworks */, + 59B886232B69EE17005750EF /* Firebase Run Script */, ); buildRules = ( ); @@ -575,11 +845,11 @@ isa = PBXNativeTarget; buildConfigurationList = 5977BE922B5966F900725C90 /* Build configuration list for PBXNativeTarget "KCSUnitTest" */; buildPhases = ( - B40FA4C5FEA2CEF5D14076EF /* [CP] Check Pods Manifest.lock */, + A73C3D34B606A83DF89A57DD /* [CP] Check Pods Manifest.lock */, 5977BE862B5966F900725C90 /* Sources */, 5977BE872B5966F900725C90 /* Frameworks */, 5977BE882B5966F900725C90 /* Resources */, - BC4E38BB9B3B1281AC2576D1 /* [CP] Embed Pods Frameworks */, + 4A3D260580D318595657BED8 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -640,6 +910,7 @@ A81EFBC52B5D597400D0C0D7 /* Pretendard-SemiBold.ttf in Resources */, A81EFBC22B5D597400D0C0D7 /* Pretendard-Regular.ttf in Resources */, A81EFBC02B5D597400D0C0D7 /* Pretendard-Medium.ttf in Resources */, + 59503A4A2B741F1E0006CF35 /* Secret.xcconfig in Resources */, 59053D0B2B3889A200D190CC /* .swiftlint.yml in Resources */, 591A88892B384E610059E40F /* LaunchScreen.storyboard in Resources */, A81EFBC82B5D597400D0C0D7 /* Pretendard-Light.ttf in Resources */, @@ -647,6 +918,7 @@ 591A88862B384E610059E40F /* Assets.xcassets in Resources */, A81EFBC72B5D597400D0C0D7 /* Pretendard-Bold.ttf in Resources */, A81EFBC12B5D597400D0C0D7 /* Pretendard-Black.ttf in Resources */, + 59EC53802B69E5E2004DB2F9 /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -660,82 +932,89 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 3BC1B94F5FD0808C2E71C0CF /* [CP] Embed Pods Frameworks */ = { + 1710AC2AA053B9D767B25C83 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-KCS/Pods-KCS-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "[CP] Embed Pods Frameworks"; + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-KCS/Pods-KCS-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-KCS-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-KCS/Pods-KCS-frameworks.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 591A88902B3884930059E40F /* Run Script */ = { + 4A3D260580D318595657BED8 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-KCSUnitTest/Pods-KCSUnitTest-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); - name = "Run Script"; + name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - ); - outputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-KCSUnitTest/Pods-KCSUnitTest-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/SwiftLint/swiftlint\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-KCSUnitTest/Pods-KCSUnitTest-frameworks.sh\"\n"; + showEnvVarsInLog = 0; }; - B40FA4C5FEA2CEF5D14076EF /* [CP] Check Pods Manifest.lock */ = { + 591A88902B3884930059E40F /* SwiftLint Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", ); - name = "[CP] Check Pods Manifest.lock"; + name = "SwiftLint Run Script"; outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-KCSUnitTest-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; + shellScript = "\"${PODS_ROOT}/SwiftLint/swiftlint\"\n"; }; - BC4E38BB9B3B1281AC2576D1 /* [CP] Embed Pods Frameworks */ = { + 59B886232B69EE17005750EF /* Firebase Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-KCSUnitTest/Pods-KCSUnitTest-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "[CP] Embed Pods Frameworks"; + inputPaths = ( + "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}", + "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${PRODUCT_NAME}", + "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist", + "$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)", + "$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist", + ); + name = "Firebase Run Script"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-KCSUnitTest/Pods-KCSUnitTest-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-KCSUnitTest/Pods-KCSUnitTest-frameworks.sh\"\n"; - showEnvVarsInLog = 0; + shellScript = "\"${PODS_ROOT}/FirebaseCrashlytics/run\"\n"; }; - CB55E62BB6C1E559A1B678AA /* [CP] Check Pods Manifest.lock */ = { + A73C3D34B606A83DF89A57DD /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -750,13 +1029,30 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-KCS-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-KCSUnitTest-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + C9DF99EE5FF9433DE46D74ED /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-KCS/Pods-KCS-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-KCS/Pods-KCS-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-KCS/Pods-KCS-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -764,45 +1060,85 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A821A3762B74B9F900089B8F /* NetworkRepositoryImpl.swift in Sources */, + 59B886482B6A5CE9005750EF /* StoreListViewModelImpl.swift in Sources */, + A83367BD2B70A52900E0A844 /* Storage.swift in Sources */, A81EFBB52B5D477600D0C0D7 /* UILabel+.swift in Sources */, 59C306A62B4D966C00862625 /* CertificationType.swift in Sources */, - A890870F2B4F836C00767225 /* StoreInformationViewController.swift in Sources */, + A890870F2B4F836C00767225 /* SummaryView.swift in Sources */, A802D1F62B5277630091FDE7 /* CertificationLabel.swift in Sources */, + 59B886262B6A3A02005750EF /* StoreListViewController.swift in Sources */, + A83367C72B72725700E0A844 /* FifthOnboardingView.swift in Sources */, 5977BE612B55374000725C90 /* HomeViewModel.swift in Sources */, + 595EE2A52B693DE700CC01CE /* ErrorAlertMessage.swift in Sources */, + A821A37A2B74BBE200089B8F /* CheckNetworkStatusUseCaseImpl.swift in Sources */, + A8AE4B1B2B62A60B00632355 /* OpeningHoursCellView.swift in Sources */, A89087042B4E7F3500767225 /* FilterButton.swift in Sources */, 59F478B12B59BB00002FEF9E /* ImageRepositoryError.swift in Sources */, + A821A3832B74C08600089B8F /* SplashViewModel.swift in Sources */, + A821A3802B74BDA200089B8F /* SplashViewModelImpl.swift in Sources */, 59C306C92B501B9D00862625 /* RegularOpeningHours.swift in Sources */, + A83367B82B6FA0E700E0A844 /* FetchStores.swift in Sources */, 59F478B52B59BE0B002FEF9E /* FetchImageUseCase.swift in Sources */, 59C306BF2B50109100862625 /* Location.swift in Sources */, + A8A7E05B2B642EC900D015E5 /* DetailViewContents.swift in Sources */, 591A88812B384E600059E40F /* HomeViewController.swift in Sources */, 5977BE9C2B59AC8D00725C90 /* ImageRepositoryImpl.swift in Sources */, 59F478B32B59BDD6002FEF9E /* FetchImageUseCaseImpl.swift in Sources */, A8ACB7ED2B59647400540BD1 /* OpeningHourError.swift in Sources */, 5977BE742B57FA7A00725C90 /* FilteredStores.swift in Sources */, + A821A37C2B74BC4B00089B8F /* CheckNetworkStatusUseCase.swift in Sources */, + A8A7E05D2B64AF1300D015E5 /* StoreInformationViewConstraints.swift in Sources */, 5977BE5E2B5535C700725C90 /* FetchRefreshStoresUseCase.swift in Sources */, - 59C306B02B4FFE4400862625 /* StoreResponse.swift in Sources */, + 59503A4E2B751B0B0006CF35 /* SearchViewController.swift in Sources */, + A83367BB2B709C0200E0A844 /* OnboardingViewController.swift in Sources */, + 59503A542B751FD50006CF35 /* SearchViewModelImpl.swift in Sources */, + A83367B62B6F993F00E0A844 /* MoreStoreButton.swift in Sources */, + 59C306B02B4FFE4400862625 /* RefreshStoreResponse.swift in Sources */, + A8A7E0602B64E62200D015E5 /* NMFMyPosition+.swift in Sources */, A8ACB7EA2B595A3E00540BD1 /* GetOpenClosedUseCaseImpl.swift in Sources */, A890870A2B4EF00B00767225 /* SystemImage.swift in Sources */, 59C306CD2B5035B100862625 /* StoreAPI.swift in Sources */, + A821A3782B74BAA700089B8F /* NetworkRepository.swift in Sources */, A8ACB7DB2B58B51A00540BD1 /* Date+.swift in Sources */, A81EFBCA2B5D5A2300D0C0D7 /* UIFont+.swift in Sources */, + A83367C52B7271B900E0A844 /* FourthOnboardingView.swift in Sources */, 5977BE9E2B59ACE800725C90 /* ImageCache.swift in Sources */, + 59B886642B6EC816005750EF /* SummaryViewHeightCase.swift in Sources */, 59C306A42B4D7EBA00862625 /* Marker.swift in Sources */, + 59B8862B2B6A3F7F005750EF /* UITableViewCell+Identifier.swift in Sources */, + 59B886462B6A5CDC005750EF /* StoreListViewModel.swift in Sources */, + 59B8865E2B6E4B98005750EF /* StoreInformationViewModelImpl.swift in Sources */, 5977BE662B553BA800725C90 /* HomeViewModelImpl.swift in Sources */, + 59B886292B6A3F1E005750EF /* StoreTableViewCell.swift in Sources */, + 592262242B61203000CA5A11 /* DetailView.swift in Sources */, A81EFBB32B5BC57800D0C0D7 /* OpenClosedContent.swift in Sources */, 5977BE5C2B5535A100725C90 /* FetchRefreshStoresUseCaseImpl.swift in Sources */, - A8ACB7E22B594F7400540BD1 /* StoreInformationViewModelImpl.swift in Sources */, + A821A3742B74B84700089B8F /* SplashViewController.swift in Sources */, + 59503A602B75970E0006CF35 /* FetchSearchStoresUseCaseImpl.swift in Sources */, 59C306CF2B50399C00862625 /* RequestLocationDTO.swift in Sources */, + A83367C32B72714C00E0A844 /* ThirdOnboardingView.swift in Sources */, + 59503A562B756CCD0006CF35 /* SearchBarView.swift in Sources */, A890870D2B4EF91600767225 /* UIView+SetLayer.swift in Sources */, + 59503A522B751FCC0006CF35 /* SearchViewModel.swift in Sources */, A8ACB7D82B57BE7D00540BD1 /* StoreRepositoryError.swift in Sources */, + 59B8864A2B6A9CCB005750EF /* UIStackView+clear.swift in Sources */, 59C306C72B501B1E00862625 /* JSONContentsError.swift in Sources */, 59C306CB2B50357900862625 /* Router.swift in Sources */, A8ACB7E82B595A2100540BD1 /* GetOpenClosedUseCase.swift in Sources */, + A83367BF2B7246E700E0A844 /* FirstOnboardingView.swift in Sources */, + 59503A5E2B7596990006CF35 /* FetchSearchStoresUseCase.swift in Sources */, 59C306B62B50027300862625 /* Store.swift in Sources */, 59C306B42B50015500862625 /* APIResponse.swift in Sources */, + 59B8865A2B6E3B40005750EF /* StoreInformationViewController.swift in Sources */, + A83367C12B726E2600E0A844 /* SecondOnboardingView.swift in Sources */, + A8A7E0622B652F0D00D015E5 /* MarkerContents.swift in Sources */, + 59B8865C2B6E4B8B005750EF /* StoreInformationViewModel.swift in Sources */, 5977BE582B5524D500725C90 /* RefreshButton.swift in Sources */, 5977BE942B59738800725C90 /* FetchStoresUseCase.swift in Sources */, + 59B886502B6AB9F7005750EF /* StoreTableViewCellContents.swift in Sources */, 59F478BF2B5BEA08002FEF9E /* RequestLocation.swift in Sources */, + 596DDC4D2B6416AB00A4BBC4 /* SummaryViewContents.swift in Sources */, 59C306B22B50001F00862625 /* StoreRepositoryImpl.swift in Sources */, 5977BE9A2B59AC3300725C90 /* ImageRepository.swift in Sources */, 591A887D2B384E600059E40F /* AppDelegate.swift in Sources */, @@ -811,12 +1147,15 @@ 59C306AD2B4FFAC700862625 /* StoreDTO.swift in Sources */, 59C306A92B4FF9AF00862625 /* StoreRepository.swift in Sources */, 5977BE982B5999E000725C90 /* FetchStoresUseCaseImpl.swift in Sources */, - A8ACB7DF2B594F4B00540BD1 /* StoreInformationViewModel.swift in Sources */, A8ACB7F12B5AEBE300540BD1 /* GetStoreInformationUseCase.swift in Sources */, 591A887F2B384E600059E40F /* SceneDelegate.swift in Sources */, A8ACB7EF2B5AEBB900540BD1 /* GetStoreInformationUseCaseImpl.swift in Sources */, A8ACB7DD2B58E3DE00540BD1 /* OpenClosedType.swift in Sources */, + 59503A5C2B75927F0006CF35 /* SearchStoreResponse.swift in Sources */, + 59B886622B6E8484005750EF /* UISheetPresentationController+Detent.swift in Sources */, 59C306D82B50650D00862625 /* Encodable+.swift in Sources */, + 59503A582B758DCE0006CF35 /* SearchDTO.swift in Sources */, + 59B886602B6E7CF6005750EF /* UIViewController+Alert.swift in Sources */, 5977BE682B553C8300725C90 /* HomeDependency.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -859,7 +1198,7 @@ /* Begin XCBuildConfiguration section */ 591A888B2B384E610059E40F /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 5986DCE82B390A8D005AE43B /* Secret.xcconfig */; + baseConfigurationReference = 59503A492B741F1E0006CF35 /* Secret.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -915,6 +1254,7 @@ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; + OTHER_LDFLAGS = ""; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -923,6 +1263,7 @@ }; 591A888C2B384E610059E40F /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 59503A492B741F1E0006CF35 /* Secret.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -971,6 +1312,7 @@ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; + OTHER_LDFLAGS = ""; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; VALIDATE_PRODUCT = YES; @@ -979,30 +1321,35 @@ }; 591A888E2B384E610059E40F /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9EA5C8EA72EA9E937C11400A /* Pods-KCS.debug.xcconfig */; + baseConfigurationReference = 96F7AFBA49BAE4D780F6D753 /* Pods-KCS.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = MMM58CZBQF; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 7CQAR4CYZX; ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = KCS/Resource/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "๋‚˜์ธ๊ฐ€"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.lifestyle"; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "์‚ฌ์šฉ์ž์˜ ์œ„์น˜๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ฐ€๊ฒŒ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•˜๊ธฐ ์œ„ํ•ด ๊ถŒํ•œ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_UIUserInterfaceStyle = Light; IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 3.1; PRODUCT_BUNDLE_IDENTIFIER = com.kcs.nainga; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -1014,30 +1361,34 @@ }; 591A888F2B384E610059E40F /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = AA9EF30352C847A7C6DEC110 /* Pods-KCS.release.xcconfig */; + baseConfigurationReference = 6E7A587B8D04F1EBD1715550 /* Pods-KCS.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = MMM58CZBQF; + DEVELOPMENT_TEAM = 7CQAR4CYZX; ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = KCS/Resource/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "๋‚˜์ธ๊ฐ€"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.lifestyle"; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "์‚ฌ์šฉ์ž์˜ ์œ„์น˜๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ฐ€๊ฒŒ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•˜๊ธฐ ์œ„ํ•ด ๊ถŒํ•œ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_UIUserInterfaceStyle = Light; IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 3.1; PRODUCT_BUNDLE_IDENTIFIER = com.kcs.nainga; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -1049,16 +1400,17 @@ }; 5977BE902B5966F900725C90 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 5FF0FF2386EEB69182D6EA4C /* Pods-KCSUnitTest.debug.xcconfig */; + baseConfigurationReference = D7B848B1C5D2F1B31C605818 /* Pods-KCSUnitTest.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 7CQAR4CYZX; ENABLE_APP_SANDBOX = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = KoreaCertifiedStore.KCSUnitTest; @@ -1074,16 +1426,17 @@ }; 5977BE912B5966F900725C90 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 4BD0CCD4DBD4E121C26925E6 /* Pods-KCSUnitTest.release.xcconfig */; + baseConfigurationReference = 2A59B3837A53AAB2D7A1E09C /* Pods-KCSUnitTest.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 7CQAR4CYZX; ENABLE_APP_SANDBOX = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = KoreaCertifiedStore.KCSUnitTest; diff --git a/KCS/KCS.xcodeproj/xcshareddata/xcschemes/KCS.xcscheme b/KCS/KCS.xcodeproj/xcshareddata/xcschemes/KCS.xcscheme index 5ff40d72..34426967 100644 --- a/KCS/KCS.xcodeproj/xcshareddata/xcschemes/KCS.xcscheme +++ b/KCS/KCS.xcodeproj/xcshareddata/xcschemes/KCS.xcscheme @@ -1,6 +1,6 @@ Bool { - // Override point for customization after application launch. if let id = Bundle.main.object(forInfoDictionaryKey: "NMAP_CLIENT_ID") as? String { NMFAuthManager.shared().clientId = id } + FirebaseApp.configure() return true } diff --git a/KCS/KCS/Application/SceneDelegate.swift b/KCS/KCS/Application/SceneDelegate.swift index 97758015..34b07744 100644 --- a/KCS/KCS/Application/SceneDelegate.swift +++ b/KCS/KCS/Application/SceneDelegate.swift @@ -6,11 +6,12 @@ // import UIKit +import RxRelay class SceneDelegate: UIResponder, UIWindowSceneDelegate { - + var window: UIWindow? - + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = (scene as? UIWindowScene) else { return } @@ -22,11 +23,60 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { dependency: HomeDependency(), fetchRefreshStoresUseCase: FetchRefreshStoresUseCaseImpl(repository: repository), fetchStoresUseCase: FetchStoresUseCaseImpl(repository: repository), - getStoreInformationUseCase: GetStoreInformationUseCaseImpl(repository: repository) + getStoreInformationUseCase: GetStoreInformationUseCaseImpl(repository: repository), + fetchSearchStoresUseCase: FetchSearchStoresUseCaseImpl(repository: repository) + ) + let summaryViewHeightObserver = PublishRelay() + let listCellSelectedObserver = PublishRelay() + let storeInformationViewController = StoreInformationViewController( + summaryViewHeightObserver: summaryViewHeightObserver, + viewModel: StoreInformationViewModelImpl( + getOpenClosedUseCase: GetOpenClosedUseCaseImpl(), + fetchImageUseCase: FetchImageUseCaseImpl( + repository: ImageRepositoryImpl(cache: ImageCache()) + ) + ) + ) + let searchObserver = PublishRelay() + let homeViewController = HomeViewController( + viewModel: viewModel, + storeInformationViewController: storeInformationViewController, + storeListViewController: StoreListViewController( + viewModel: StoreListViewModelImpl( + fetchImageUseCase: FetchImageUseCaseImpl( + repository: ImageRepositoryImpl(cache: ImageCache()) + ) + ), + listCellSelectedObserver: listCellSelectedObserver + ), + summaryViewHeightObserver: summaryViewHeightObserver, + listCellSelectedObserver: listCellSelectedObserver, + searchViewController: SearchViewController( + viewModel: SearchViewModelImpl(), + searchObserver: searchObserver + ), + searchObserver: searchObserver + ) + + var rootViewController: UIViewController + + if Storage.isOnboarded() { + rootViewController = OnboardingViewController(homeViewController: homeViewController) + } else { + rootViewController = homeViewController + } + + let splashViewController = SplashViewController( + viewModel: SplashViewModelImpl( + checkNetworkStatusUseCase: CheckNetworkStatusUseCaseImpl( + repository: NetworkRepositoryImpl() + ) + ), rootViewController: rootViewController ) - window?.rootViewController = HomeViewController(viewModel: viewModel) + + window?.rootViewController = splashViewController window?.makeKeyAndVisible() } - + } diff --git a/KCS/KCS/Data/Network/DTO/SearchDTO.swift b/KCS/KCS/Data/Network/DTO/SearchDTO.swift new file mode 100644 index 00000000..ba24e420 --- /dev/null +++ b/KCS/KCS/Data/Network/DTO/SearchDTO.swift @@ -0,0 +1,16 @@ +// +// SearchDTO.swift +// KCS +// +// Created by ์กฐ์„ฑ๋ฏผ on 2/9/24. +// + +import Foundation + +struct SearchDTO: Encodable { + + let currLong: Double + let currLat: Double + let searchKeyword: String + +} diff --git a/KCS/KCS/Data/Network/DTO/StoreDTO.swift b/KCS/KCS/Data/Network/DTO/StoreDTO.swift index 090bf31c..de88ebe5 100644 --- a/KCS/KCS/Data/Network/DTO/StoreDTO.swift +++ b/KCS/KCS/Data/Network/DTO/StoreDTO.swift @@ -33,25 +33,19 @@ struct StoreDTO: Codable { } - func toEntity() -> Store { + func toEntity() throws -> Store { var certificationTypes: [CertificationType] = [] var openingHours: [RegularOpeningHours] = [] - do { - for name in certificationName { - guard let type = CertificationType(rawValue: name) else { - throw JSONContentsError.wrongCertificationType - } - certificationTypes.append(type) + for name in certificationName { + guard let type = CertificationType(rawValue: name) else { + throw JSONContentsError.wrongCertificationType } - - for hour in regularOpeningHours { - openingHours.append(try hour.toEntity()) - } - } catch let error as JSONContentsError { - print(error.errorDescription) - } catch let error { - print(error.localizedDescription) + certificationTypes.append(type) + } + + for hour in regularOpeningHours { + openingHours.append(try hour.toEntity()) } return Store( diff --git a/KCS/KCS/Data/Network/Error/NetworkError.swift b/KCS/KCS/Data/Network/Error/NetworkError.swift index 5771526b..540155ad 100644 --- a/KCS/KCS/Data/Network/Error/NetworkError.swift +++ b/KCS/KCS/Data/Network/Error/NetworkError.swift @@ -10,11 +10,14 @@ import Foundation enum NetworkError: Error, LocalizedError { case wrongURL + case wrongParameters var errorDescription: String { switch self { case .wrongURL: return "URL์ด ์ž˜๋ชป๋˜์—ˆ์Šต๋‹ˆ๋‹ค." + case .wrongParameters: + return "Parameters๊ฐ€ ์ž˜๋ชป๋˜์—ˆ์Šต๋‹ˆ๋‹ค." } } diff --git a/KCS/KCS/Data/Network/Extension/Encodable+.swift b/KCS/KCS/Data/Network/Extension/Encodable+.swift index 1724e12c..9add86b6 100644 --- a/KCS/KCS/Data/Network/Extension/Encodable+.swift +++ b/KCS/KCS/Data/Network/Extension/Encodable+.swift @@ -11,7 +11,10 @@ extension Encodable { func asDictionary() throws -> [String: Any] { let data = try JSONEncoder().encode(self) - guard let dictionary = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else { + guard let dictionary = try JSONSerialization.jsonObject( + with: data, + options: .allowFragments + ) as? [String: Any] else { throw JSONContentsError.dictionaryConvert } return dictionary diff --git a/KCS/KCS/Data/Network/Response/Protocol/APIResponse.swift b/KCS/KCS/Data/Network/Response/Protocol/APIResponse.swift index e3df41ec..bad20310 100644 --- a/KCS/KCS/Data/Network/Response/Protocol/APIResponse.swift +++ b/KCS/KCS/Data/Network/Response/Protocol/APIResponse.swift @@ -13,6 +13,6 @@ protocol APIResponse: Codable { var code: Int { get } var message: String { get } - var data: [ResponseType] { get } + var data: ResponseType { get } } diff --git a/KCS/KCS/Data/Network/Response/RefreshStoreResponse.swift b/KCS/KCS/Data/Network/Response/RefreshStoreResponse.swift new file mode 100644 index 00000000..f55e317e --- /dev/null +++ b/KCS/KCS/Data/Network/Response/RefreshStoreResponse.swift @@ -0,0 +1,18 @@ +// +// RefreshStoreResponse.swift +// KCS +// +// Created by ์กฐ์„ฑ๋ฏผ on 1/11/24. +// + +import Foundation + +struct RefreshStoreResponse: APIResponse { + + typealias ResponseType = [[StoreDTO]] + + let code: Int + let message: String + let data: ResponseType + +} diff --git a/KCS/KCS/Data/Network/Response/SearchStoreResponse.swift b/KCS/KCS/Data/Network/Response/SearchStoreResponse.swift new file mode 100644 index 00000000..44b330e1 --- /dev/null +++ b/KCS/KCS/Data/Network/Response/SearchStoreResponse.swift @@ -0,0 +1,18 @@ +// +// SearchStoreResponse.swift +// KCS +// +// Created by ์กฐ์„ฑ๋ฏผ on 2/9/24. +// + +import Foundation + +struct SearchStoreResponse: APIResponse { + + typealias ResponseType = [StoreDTO] + + let code: Int + let message: String + let data: ResponseType + +} diff --git a/KCS/KCS/Data/Network/Response/StoreResponse.swift b/KCS/KCS/Data/Network/Response/StoreResponse.swift deleted file mode 100644 index 38016877..00000000 --- a/KCS/KCS/Data/Network/Response/StoreResponse.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// StoreResponse.swift -// KCS -// -// Created by ์กฐ์„ฑ๋ฏผ on 1/11/24. -// - -import Foundation - -struct StoreResponse: APIResponse { - - typealias ResponseType = StoreDTO - - let code: Int - let message: String - let data: [ResponseType] - -} diff --git a/KCS/KCS/Data/Network/Router.swift b/KCS/KCS/Data/Network/Router.swift index 3932d727..d8af9c27 100644 --- a/KCS/KCS/Data/Network/Router.swift +++ b/KCS/KCS/Data/Network/Router.swift @@ -9,7 +9,7 @@ import Alamofire protocol Router { - var baseURL: String { get } + var baseURL: String? { get } var path: String { get } var method: HTTPMethod { get } var headers: [String: String] { get } diff --git a/KCS/KCS/Data/Network/StoreAPI.swift b/KCS/KCS/Data/Network/StoreAPI.swift index 20437f87..58a368cd 100644 --- a/KCS/KCS/Data/Network/StoreAPI.swift +++ b/KCS/KCS/Data/Network/StoreAPI.swift @@ -12,42 +12,42 @@ enum StoreAPI { case getStores(location: RequestLocationDTO) case getImage(url: String) + case getSearchStores(searchDTO: SearchDTO) } extension StoreAPI: Router, URLRequestConvertible { - - public var baseURL: String { + + var baseURL: String? { switch self { - case .getStores: - do { - return try getURL(type: .develop) - } catch { - print(error.localizedDescription) - return "" - } + case .getStores, .getSearchStores: + return getURL(type: .develop) case .getImage(let url): return url } } - public var path: String { + var path: String { switch self { - case .getStores, .getImage: + case .getStores: + return "/v2/storecertification/byLocation" + case .getImage: return "" + case .getSearchStores: + return "/v1/storecertification/byLocationAndKeyword" } } - public var method: HTTPMethod { + var method: HTTPMethod { switch self { - case .getStores, .getImage: + case .getStores, .getImage, .getSearchStores: return .get } } - public var headers: [String: String] { + var headers: [String: String] { switch self { - case .getStores: + case .getStores, .getSearchStores: return [ "Content-Type": "application/json" ] @@ -56,33 +56,35 @@ extension StoreAPI: Router, URLRequestConvertible { } } - public var parameters: [String: Any]? { + var parameters: [String: Any]? { do { switch self { case let .getStores(location): return try location.asDictionary() case .getImage: - return nil + return [:] + case let .getSearchStores(searchDTO): + return try searchDTO.asDictionary() } - } catch let error { - print(error.localizedDescription) + } catch { return nil } } /// ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ณด๋‚ด์•ผํ•  ๊ฒƒ์ด ์žˆ๋‹ค๋ฉด, URLEncoding.default /// ๋ฐ”๋””์— ๋‹ด์•„์„œ ๋ณด๋‚ด์•ผํ•  ๊ฒƒ์ด ์žˆ๋‹ค๋ฉด, JSONEncoding.default - public var encoding: ParameterEncoding? { + var encoding: ParameterEncoding? { switch self { - case .getStores: + case .getStores, .getSearchStores: return URLEncoding.default case .getImage: return nil } } - public func asURLRequest() throws -> URLRequest { - guard let url = URL(string: baseURL + path) else { + func asURLRequest() throws -> URLRequest { + guard let base = baseURL, + let url = URL(string: base + path) else { throw NetworkError.wrongURL } var request = URLRequest(url: url) @@ -91,7 +93,11 @@ extension StoreAPI: Router, URLRequestConvertible { request.headers = HTTPHeaders(headers) if let encoding = encoding { - return try encoding.encode(request, with: parameters) + if let parameters = parameters { + return try encoding.encode(request, with: parameters) + } else { + throw NetworkError.wrongParameters + } } return request @@ -108,13 +114,13 @@ private extension StoreAPI { } - func getURL(type: URLType) throws -> String { + func getURL(type: URLType) -> String? { switch type { case .develop: - guard let url = Bundle.main.object(forInfoDictionaryKey: "DEV_SERVER_URL") as? String else { throw NetworkError.wrongURL } + guard let url = Bundle.main.object(forInfoDictionaryKey: "DEV_SERVER_URL") as? String else { return nil } return url case .product: - guard let url = Bundle.main.object(forInfoDictionaryKey: "PROD_SERVER_URL") as? String else { throw NetworkError.wrongURL } + guard let url = Bundle.main.object(forInfoDictionaryKey: "PROD_SERVER_URL") as? String else { return nil } return url } } diff --git a/KCS/KCS/Data/Repository/Error/StoreRepositoryError.swift b/KCS/KCS/Data/Repository/Error/StoreRepositoryError.swift index dd9b7ef0..6d384274 100644 --- a/KCS/KCS/Data/Repository/Error/StoreRepositoryError.swift +++ b/KCS/KCS/Data/Repository/Error/StoreRepositoryError.swift @@ -10,11 +10,14 @@ import Foundation enum StoreRepositoryError: Error, LocalizedError { case wrongStoreId + case wrongStoreIndex var errorDescription: String { switch self { case .wrongStoreId: return "๊ฐ€๊ฒŒ Id๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค." + case .wrongStoreIndex: + return "๊ฐ€๊ฒŒ Index๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€์•Š์Šต๋‹ˆ๋‹ค." } } } diff --git a/KCS/KCS/Data/Repository/ImageRepositoryImpl.swift b/KCS/KCS/Data/Repository/ImageRepositoryImpl.swift index 8ad86fb4..449f6327 100644 --- a/KCS/KCS/Data/Repository/ImageRepositoryImpl.swift +++ b/KCS/KCS/Data/Repository/ImageRepositoryImpl.swift @@ -28,7 +28,7 @@ struct ImageRepositoryImpl: ImageRepository { .response(completionHandler: { response in switch response.result { case .success(let result): - if let resultData = result { + if let resultData = result, String(data: resultData, encoding: .utf8) == nil { cache.setImageData(resultData as NSData, for: imageURL as NSURL) observer.onNext(resultData) } else { diff --git a/KCS/KCS/Data/Repository/NetworkRepositoryImpl.swift b/KCS/KCS/Data/Repository/NetworkRepositoryImpl.swift new file mode 100644 index 00000000..57cd87e5 --- /dev/null +++ b/KCS/KCS/Data/Repository/NetworkRepositoryImpl.swift @@ -0,0 +1,41 @@ +// +// NetworkRepositoryImpl.swift +// KCS +// +// Created by ๊น€์˜ํ˜„ on 2/8/24. +// + +import Foundation +import SystemConfiguration + +class NetworkRepositoryImpl: NetworkRepository { + + func checkDeviceNetworkStatus() -> Bool { + var zeroAddress = sockaddr_in( + sin_len: 0, + sin_family: 0, + sin_port: 0, + sin_addr: in_addr(s_addr: 0), + sin_zero: (0, 0, 0, 0, 0, 0, 0, 0) + ) + zeroAddress.sin_len = UInt8(MemoryLayout.size(ofValue: zeroAddress)) + zeroAddress.sin_family = sa_family_t(AF_INET) + + let defaultRouteReachability = withUnsafePointer(to: &zeroAddress) { + $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {zeroSockAddress in + SCNetworkReachabilityCreateWithAddress(nil, zeroSockAddress) + } + } + + var flags: SCNetworkReachabilityFlags = SCNetworkReachabilityFlags(rawValue: 0) + if SCNetworkReachabilityGetFlags(defaultRouteReachability!, &flags) == false { + return false + } + + let isReachable = (flags.rawValue & UInt32(kSCNetworkFlagsReachable)) != 0 + let needsConnection = (flags.rawValue & UInt32(kSCNetworkFlagsConnectionRequired)) != 0 + + return isReachable && !needsConnection + } + +} diff --git a/KCS/KCS/Data/Repository/StoreRepositoryImpl.swift b/KCS/KCS/Data/Repository/StoreRepositoryImpl.swift index cb0ca25d..b2b2da67 100644 --- a/KCS/KCS/Data/Repository/StoreRepositoryImpl.swift +++ b/KCS/KCS/Data/Repository/StoreRepositoryImpl.swift @@ -17,9 +17,10 @@ final class StoreRepositoryImpl: StoreRepository { } func fetchRefreshStores( - requestLocation: RequestLocation - ) -> Observable<[Store]> { - return Observable<[Store]>.create { observer -> Disposable in + requestLocation: RequestLocation, + isEntire: Bool + ) -> Observable { + return Observable.create { observer -> Disposable in AF.request(StoreAPI.getStores(location: RequestLocationDTO( nwLong: requestLocation.northWest.longitude, nwLat: requestLocation.northWest.latitude, @@ -30,23 +31,57 @@ final class StoreRepositoryImpl: StoreRepository { neLong: requestLocation.northEast.longitude, neLat: requestLocation.northEast.latitude ))) - .responseDecodable(of: StoreResponse.self) { [weak self] response in - switch response.result { - case .success(let result): - let resultStores = result.data.map { $0.toEntity() } - self?.stores = resultStores - observer.onNext(resultStores) - case .failure(let error): + .responseDecodable(of: RefreshStoreResponse.self) { [weak self] response in + do { + switch response.result { + case .success(let result): + let resultStores = try result.data.map { try $0.map { try $0.toEntity() } } + self?.stores = resultStores.flatMap({ $0 }) + if isEntire { + observer.onNext(FetchStores( + fetchCountContent: FetchCountContent(), + stores: resultStores.flatMap { $0 } + )) + } else if let firstIndexStore = resultStores.first { + observer.onNext(FetchStores( + fetchCountContent: FetchCountContent(maxFetchCount: resultStores.count), + stores: firstIndexStore + )) + } else { + observer.onNext(FetchStores( + fetchCountContent: FetchCountContent(), + stores: [] + )) + } + case .failure(let error): + if let underlyingError = error.underlyingError as? NSError { + switch underlyingError.code { + case URLError.notConnectedToInternet.rawValue: + observer.onError(ErrorAlertMessage.internet) + default: + observer.onError(ErrorAlertMessage.server) + } + } + } + } catch { observer.onError(error) } } - return Disposables.create() } } - func fetchStores() -> [Store] { - return stores + func fetchStores(count: Int) -> [Store] { + if stores.isEmpty { return [] } + var fetchResult: [Store] = [] + var storeCount = count * 15 + if storeCount > stores.count { + storeCount = stores.count + } + for index in 0.. Observable<[Store]> { + return Observable<[Store]>.create { observer -> Disposable in + AF.request(StoreAPI.getSearchStores(searchDTO: SearchDTO( + currLong: location.longitude, + currLat: location.latitude, + searchKeyword: keyword + ))) + .responseDecodable(of: SearchStoreResponse.self) { [weak self] response in + do { + switch response.result { + case .success(let result): + let resultStores = try result.data.map { try $0.toEntity() } + self?.stores = resultStores + observer.onNext(resultStores) + case .failure(let error): + if let underlyingError = error.underlyingError as? NSError { + switch underlyingError.code { + case URLError.notConnectedToInternet.rawValue: + observer.onError(ErrorAlertMessage.internet) + default: + observer.onError(ErrorAlertMessage.server) + } + } + } + } catch { + observer.onError(error) + } + } + return Disposables.create() + } + } + } diff --git a/KCS/KCS/Data/Storage.swift b/KCS/KCS/Data/Storage.swift new file mode 100644 index 00000000..39935b1e --- /dev/null +++ b/KCS/KCS/Data/Storage.swift @@ -0,0 +1,21 @@ +// +// Storage.swift +// KCS +// +// Created by ๊น€์˜ํ˜„ on 2/5/24. +// + +import Foundation + +final class Storage { + + static func isOnboarded() -> Bool { + let defaults = UserDefaults.standard + if defaults.object(forKey: "executeOnboarding") == nil { + return true + } else { + return false + } + } + +} diff --git a/KCS/KCS/Domain/Entity/Day.swift b/KCS/KCS/Domain/Entity/Day.swift index 0d38eedf..2342b750 100644 --- a/KCS/KCS/Domain/Entity/Day.swift +++ b/KCS/KCS/Domain/Entity/Day.swift @@ -7,7 +7,7 @@ import Foundation -enum Day: String { +enum Day: String, CaseIterable { case sunday = "SUN" case monday = "MON" @@ -36,4 +36,22 @@ enum Day: String { } } + var description: String { + switch self { + case .sunday: + return "์ผ" + case .monday: + return "์›”" + case .tuesday: + return "ํ™”" + case .wednesday: + return "์ˆ˜" + case .thursday: + return "๋ชฉ" + case .friday: + return "๊ธˆ" + case .saturday: + return "ํ† " + } + } } diff --git a/KCS/KCS/Domain/Entity/DetailViewContents.swift b/KCS/KCS/Domain/Entity/DetailViewContents.swift new file mode 100644 index 00000000..e34aa652 --- /dev/null +++ b/KCS/KCS/Domain/Entity/DetailViewContents.swift @@ -0,0 +1,20 @@ +// +// DetailViewContents.swift +// KCS +// +// Created by ๊น€์˜ํ˜„ on 1/27/24. +// + +import Foundation + +struct DetailViewContents { + + let storeTitle: String + let category: String? + let certificationTypes: [CertificationType] + let address: String + let phoneNumber: String + let openClosedContent: OpenClosedContent + let detailOpeningHour: [DetailOpeningHour] + +} diff --git a/KCS/KCS/Domain/Entity/ErrorAlertMessage.swift b/KCS/KCS/Domain/Entity/ErrorAlertMessage.swift new file mode 100644 index 00000000..5e9537f8 --- /dev/null +++ b/KCS/KCS/Domain/Entity/ErrorAlertMessage.swift @@ -0,0 +1,27 @@ +// +// ErrorAlertMessage.swift +// KCS +// +// Created by ์กฐ์„ฑ๋ฏผ on 1/30/24. +// + +import Foundation + +enum ErrorAlertMessage: LocalizedError { + + case server + case internet + case client + + var errorDescription: String? { + switch self { + case .server: + return "์„œ๋ฒ„์™€์˜ ํ†ต์‹ ์ด ์›ํ™œํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค" + case .internet: + return "์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”" + case .client: + return "์•Œ ์ˆ˜ ์—†๋Š” ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค" + } + } + +} diff --git a/KCS/KCS/Domain/Entity/FetchStores.swift b/KCS/KCS/Domain/Entity/FetchStores.swift new file mode 100644 index 00000000..dabf216a --- /dev/null +++ b/KCS/KCS/Domain/Entity/FetchStores.swift @@ -0,0 +1,22 @@ +// +// FetchStores.swift +// KCS +// +// Created by ๊น€์˜ํ˜„ on 2/4/24. +// + +import Foundation + +struct FetchStores { + + let fetchCountContent: FetchCountContent + let stores: [Store] + +} + +struct FetchCountContent { + + var maxFetchCount: Int = 1 + var fetchCount: Int = 1 + +} diff --git a/KCS/KCS/Domain/Entity/MarkerContents.swift b/KCS/KCS/Domain/Entity/MarkerContents.swift new file mode 100644 index 00000000..ee760caa --- /dev/null +++ b/KCS/KCS/Domain/Entity/MarkerContents.swift @@ -0,0 +1,17 @@ +// +// MarkerContents.swift +// KCS +// +// Created by ๊น€์˜ํ˜„ on 1/27/24. +// + +import Foundation + +struct MarkerContents { + + let tag: Int + let location: Location + let deselectImageName: String + let selectImageName: String + +} diff --git a/KCS/KCS/Domain/Entity/OpenClosedContent.swift b/KCS/KCS/Domain/Entity/OpenClosedContent.swift index 8ae331da..470faeab 100644 --- a/KCS/KCS/Domain/Entity/OpenClosedContent.swift +++ b/KCS/KCS/Domain/Entity/OpenClosedContent.swift @@ -10,6 +10,20 @@ import Foundation struct OpenClosedContent { let openClosedType: OpenClosedType - let openingHour: String + let nextOpeningHour: String + +} + +struct DetailOpeningHour { + + let weekDay: Day + let openingHour: OpeningHour + +} + +struct OpeningHour { + + let openingHour: String? + let breakTime: String? } diff --git a/KCS/KCS/Domain/Entity/OpenClosedType.swift b/KCS/KCS/Domain/Entity/OpenClosedType.swift index 893c3896..b5a3326c 100644 --- a/KCS/KCS/Domain/Entity/OpenClosedType.swift +++ b/KCS/KCS/Domain/Entity/OpenClosedType.swift @@ -12,8 +12,23 @@ enum OpenClosedType: String { case open = "์˜์—… ์ค‘" case closed = "์˜์—… ์ข…๋ฃŒ" case breakTime = "๋ธŒ๋ ˆ์ดํฌ ํƒ€์ž„" - case dayOff = "ํœด๋ฌด์ผ" - case none = "" + case dayOff, none = "" + case alwaysOpen = "24์‹œ๊ฐ„ ์˜์—…" + + var description: String { + switch self { + case .open, .alwaysOpen: + return OpenClosedType.open.rawValue + case .closed: + return OpenClosedType.closed.rawValue + case .breakTime: + return OpenClosedType.breakTime.rawValue + case .dayOff: + return "ํœด๋ฌด์ผ" + case .none: + return "" + } + } } diff --git a/KCS/KCS/Domain/Entity/StoreInformationViewConstraints.swift b/KCS/KCS/Domain/Entity/StoreInformationViewConstraints.swift new file mode 100644 index 00000000..ec235069 --- /dev/null +++ b/KCS/KCS/Domain/Entity/StoreInformationViewConstraints.swift @@ -0,0 +1,22 @@ +// +// StoreInformationViewConstraints.swift +// KCS +// +// Created by ๊น€์˜ํ˜„ on 1/27/24. +// + +import Foundation + +struct StoreInformationViewConstraints { + + let heightConstraint: CGFloat + let bottomConstraint: CGFloat + let animated: Bool + + init(heightConstraint: CGFloat, bottomConstraint: CGFloat, animated: Bool = false) { + self.heightConstraint = heightConstraint + self.bottomConstraint = bottomConstraint + self.animated = animated + } + +} diff --git a/KCS/KCS/Domain/Entity/StoreTableViewCellContents.swift b/KCS/KCS/Domain/Entity/StoreTableViewCellContents.swift new file mode 100644 index 00000000..5ddd170f --- /dev/null +++ b/KCS/KCS/Domain/Entity/StoreTableViewCellContents.swift @@ -0,0 +1,17 @@ +// +// StoreTableViewCellContents.swift +// KCS +// +// Created by ์กฐ์„ฑ๋ฏผ on 2/1/24. +// + +import Foundation + +struct StoreTableViewCellContents: Hashable { + + let storeTitle: String + let category: String? + let certificationTypes: [CertificationType] + let thumbnailImageData: Data? + +} diff --git a/KCS/KCS/Domain/Entity/SummaryViewContents.swift b/KCS/KCS/Domain/Entity/SummaryViewContents.swift new file mode 100644 index 00000000..1ddb51ff --- /dev/null +++ b/KCS/KCS/Domain/Entity/SummaryViewContents.swift @@ -0,0 +1,17 @@ +// +// SummaryViewContents.swift +// KCS +// +// Created by ์กฐ์„ฑ๋ฏผ on 1/27/24. +// + +import Foundation + +struct SummaryViewContents { + + let storeTitle: String + let category: String? + let certificationTypes: [CertificationType] + let openClosedContent: OpenClosedContent + +} diff --git a/KCS/KCS/Domain/Entity/SummaryViewHeightCase.swift b/KCS/KCS/Domain/Entity/SummaryViewHeightCase.swift new file mode 100644 index 00000000..8354fec2 --- /dev/null +++ b/KCS/KCS/Domain/Entity/SummaryViewHeightCase.swift @@ -0,0 +1,15 @@ +// +// SummaryViewHeightCase.swift +// KCS +// +// Created by ์กฐ์„ฑ๋ฏผ on 2/4/24. +// + +import Foundation + +enum SummaryViewHeightCase { + + case small + case large + +} diff --git a/KCS/KCS/Domain/Interface/Repository/NetworkRepository.swift b/KCS/KCS/Domain/Interface/Repository/NetworkRepository.swift new file mode 100644 index 00000000..26aedd6c --- /dev/null +++ b/KCS/KCS/Domain/Interface/Repository/NetworkRepository.swift @@ -0,0 +1,14 @@ +// +// NetworkRepository.swift +// KCS +// +// Created by ๊น€์˜ํ˜„ on 2/8/24. +// + +import Foundation + +protocol NetworkRepository { + + func checkDeviceNetworkStatus() -> Bool + +} diff --git a/KCS/KCS/Domain/Interface/Repository/StoreRepository.swift b/KCS/KCS/Domain/Interface/Repository/StoreRepository.swift index 3d548781..9a9f8fc4 100644 --- a/KCS/KCS/Domain/Interface/Repository/StoreRepository.swift +++ b/KCS/KCS/Domain/Interface/Repository/StoreRepository.swift @@ -10,13 +10,19 @@ import RxSwift protocol StoreRepository { func fetchRefreshStores( - requestLocation: RequestLocation - ) -> Observable<[Store]> + requestLocation: RequestLocation, + isEntire: Bool + ) -> Observable - func fetchStores() -> [Store] + func fetchStores(count: Int) -> [Store] func getStoreInformation( tag: UInt ) throws -> Store + func fetchSearchStores( + location: Location, + keyword: String + ) -> Observable<[Store]> + } diff --git a/KCS/KCS/Domain/UseCase/CheckNetworkStatusUseCaseImpl.swift b/KCS/KCS/Domain/UseCase/CheckNetworkStatusUseCaseImpl.swift new file mode 100644 index 00000000..bb19e7ea --- /dev/null +++ b/KCS/KCS/Domain/UseCase/CheckNetworkStatusUseCaseImpl.swift @@ -0,0 +1,18 @@ +// +// CheckNetworkStatusUseCaseImpl.swift +// KCS +// +// Created by ๊น€์˜ํ˜„ on 2/8/24. +// + +import Foundation + +struct CheckNetworkStatusUseCaseImpl: CheckNetworkStatusUseCase { + + let repository: NetworkRepository + + func execute() -> Bool { + return repository.checkDeviceNetworkStatus() + } + +} diff --git a/KCS/KCS/Domain/UseCase/FetchRefreshStoresUseCaseImpl.swift b/KCS/KCS/Domain/UseCase/FetchRefreshStoresUseCaseImpl.swift index ae37e183..dbc26fa7 100644 --- a/KCS/KCS/Domain/UseCase/FetchRefreshStoresUseCaseImpl.swift +++ b/KCS/KCS/Domain/UseCase/FetchRefreshStoresUseCaseImpl.swift @@ -12,85 +12,10 @@ struct FetchRefreshStoresUseCaseImpl: FetchRefreshStoresUseCase { let repository: StoreRepository func execute( - requestLocation: RequestLocation - ) -> Observable<[Store]> { - let newLocation = parallelTranslate(requestLocation: requestLocation) - return repository.fetchRefreshStores(requestLocation: newLocation) - } - -} - -private extension FetchRefreshStoresUseCaseImpl { - - func parallelTranslate (requestLocation: RequestLocation) -> RequestLocation { - let distance1 = sqrt( - pow(requestLocation.northWest.longitude - requestLocation.southWest.longitude, 2) - + pow(requestLocation.northWest.latitude - requestLocation.southWest.latitude, 2) - ) - let distance2 = sqrt( - pow(requestLocation.northWest.longitude - requestLocation.northEast.longitude, 2) + - pow(requestLocation.northWest.latitude - requestLocation.northEast.latitude, 2) - ) - - let center = Location( - longitude: (requestLocation.northWest.longitude + requestLocation.southEast.longitude) / 2.0, - latitude: (requestLocation.northWest.latitude + requestLocation.southEast.latitude) / 2.0 - ) - - var newLocation: RequestLocation - if distance1 > 0.07 { - newLocation = translateHeightLocations( - loc1: requestLocation.northWest, - loc2: requestLocation.northEast, - center: center - ) - if distance2 > 0.07 { - newLocation = translateHeightLocations( - loc1: newLocation.northEast, - loc2: newLocation.southEast, - center: center - ) - } - return newLocation - } - - return requestLocation - } - - func translateHeightLocations(loc1: Location, loc2: Location, center: Location) -> RequestLocation { - if loc1.latitude == loc2.latitude { - return RequestLocation( - northWest: Location(longitude: loc1.longitude, latitude: center.latitude + 0.035), - southWest: Location(longitude: loc1.longitude, latitude: center.latitude - 0.035), - southEast: Location(longitude: loc2.longitude, latitude: center.latitude - 0.035), - northEast: Location(longitude: loc2.longitude, latitude: center.latitude + 0.035) - ) - } else if loc1.longitude == loc2.longitude { - return RequestLocation( - northWest: Location(longitude: center.longitude + 0.035, latitude: loc1.latitude), - southWest: Location(longitude: center.longitude - 0.035, latitude: loc1.latitude), - southEast: Location(longitude: center.longitude - 0.035, latitude: loc2.latitude), - northEast: Location(longitude: center.longitude + 0.035, latitude: loc2.latitude) - ) - } - - let slope = (loc2.latitude - loc1.latitude) / (loc2.longitude - loc1.longitude) - let constant1 = 0.035 * sqrt(pow(slope, 2) + 1) - slope * center.longitude + center.latitude - let constant2 = (-0.035) * sqrt(pow(slope, 2) + 1) - slope * center.longitude + center.latitude - - return RequestLocation( - northWest: getNewLocation(location: loc1, slope: slope, constant: constant1), - southWest: getNewLocation(location: loc1, slope: slope, constant: constant2), - southEast: getNewLocation(location: loc2, slope: slope, constant: constant2), - northEast: getNewLocation(location: loc2, slope: slope, constant: constant1) - ) - } - - func getNewLocation(location: Location, slope: Double, constant: Double) -> Location { - return Location( - longitude: (location.latitude + (location.longitude / slope) - constant) / (slope + 1 / slope), - latitude: (slope * location.latitude + location.longitude + constant / slope) / (slope + 1 / slope) - ) + requestLocation: RequestLocation, + isEntire: Bool + ) -> Observable { + return repository.fetchRefreshStores(requestLocation: requestLocation, isEntire: isEntire) } } diff --git a/KCS/KCS/Domain/UseCase/FetchSearchStoresUseCaseImpl.swift b/KCS/KCS/Domain/UseCase/FetchSearchStoresUseCaseImpl.swift new file mode 100644 index 00000000..0465e0a0 --- /dev/null +++ b/KCS/KCS/Domain/UseCase/FetchSearchStoresUseCaseImpl.swift @@ -0,0 +1,18 @@ +// +// FetchSearchStoresUseCaseImpl.swift +// KCS +// +// Created by ์กฐ์„ฑ๋ฏผ on 2/9/24. +// + +import RxSwift + +struct FetchSearchStoresUseCaseImpl: FetchSearchStoresUseCase { + + var repository: StoreRepository + + func execute(location: Location, keyword: String) -> Observable<[Store]> { + return repository.fetchSearchStores(location: location, keyword: keyword) + } + +} diff --git a/KCS/KCS/Domain/UseCase/FetchStoresUseCaseImpl.swift b/KCS/KCS/Domain/UseCase/FetchStoresUseCaseImpl.swift index 0fc4561e..8635894f 100644 --- a/KCS/KCS/Domain/UseCase/FetchStoresUseCaseImpl.swift +++ b/KCS/KCS/Domain/UseCase/FetchStoresUseCaseImpl.swift @@ -11,8 +11,8 @@ struct FetchStoresUseCaseImpl: FetchStoresUseCase { let repository: StoreRepository - func execute() -> [Store] { - return repository.fetchStores() + func execute(fetchCount: Int) -> [Store] { + return repository.fetchStores(count: fetchCount) } } diff --git a/KCS/KCS/Domain/UseCase/GetOpenClosedUseCaseImpl.swift b/KCS/KCS/Domain/UseCase/GetOpenClosedUseCaseImpl.swift index 3b6cb124..335f5816 100644 --- a/KCS/KCS/Domain/UseCase/GetOpenClosedUseCaseImpl.swift +++ b/KCS/KCS/Domain/UseCase/GetOpenClosedUseCaseImpl.swift @@ -11,38 +11,43 @@ struct GetOpenClosedUseCaseImpl: GetOpenClosedUseCase { func execute( openingHours: [RegularOpeningHours] - ) -> OpenClosedContent { - return getOpenClosedContent(openingHour: openingHours) + ) throws -> OpenClosedContent { + return try getOpenClosedContent(openingHour: openingHours) } } private extension GetOpenClosedUseCaseImpl { - func getOpenClosedContent(openingHour: [RegularOpeningHours]) -> OpenClosedContent { - let nowOpenClosedType = getOpenClosedType(openingHour: openingHour) - lazy var openingHourString: String = { + func getOpenClosedContent(openingHour: [RegularOpeningHours]) throws -> OpenClosedContent { + let nowOpenClosedType = try getOpenClosedType(openingHour: openingHour) + let openingHourString: String = try { switch nowOpenClosedType { case .none, .dayOff: return OpenClosedType.none.rawValue + case .alwaysOpen: + return OpenClosedType.alwaysOpen.rawValue case .breakTime, .closed: - return getOpenClosedString(openingHour: openingHour, openClosedType: .open) + return try getOpenClosedString(openingHour: openingHour, openClosedType: .open) case .open: - return getOpenClosedString(openingHour: openingHour, openClosedType: .close) + return try getOpenClosedString(openingHour: openingHour, openClosedType: .close) } }() - return OpenClosedContent(openClosedType: nowOpenClosedType, openingHour: openingHourString) + return OpenClosedContent(openClosedType: nowOpenClosedType, nextOpeningHour: openingHourString) } } private extension GetOpenClosedUseCaseImpl { - func getOpenClosedType(openingHour: [RegularOpeningHours]) -> OpenClosedType { + func getOpenClosedType(openingHour: [RegularOpeningHours]) throws -> OpenClosedType { if openingHour.isEmpty { return OpenClosedType.none } - let openCloseTime = getOpenClosedTimeArray(openingHours: openingHour) + if !openingHour.filter({ $0.open == $0.close }).isEmpty { + return OpenClosedType.alwaysOpen + } + let openCloseTime = try getOpenClosedTimeArray(openingHours: openingHour) if openCloseTime.isEmpty { return OpenClosedType.dayOff } @@ -66,8 +71,8 @@ private extension GetOpenClosedUseCaseImpl { return OpenClosedType.dayOff } - func getOpenClosedString(openingHour: [RegularOpeningHours], openClosedType: OpenClose) -> String { - let openCloseTime = getOpenClosedTimeArray(openingHours: openingHour) + func getOpenClosedString(openingHour: [RegularOpeningHours], openClosedType: OpenClose) throws -> String { + let openCloseTime = try getOpenClosedTimeArray(openingHours: openingHour) var nextTime = openCloseTime.filter({ $0 > Date().toSecond() }) switch openClosedType { case .open: @@ -92,7 +97,7 @@ private extension GetOpenClosedUseCaseImpl { } } - func getOpenClosedTimeArray(openingHours: [RegularOpeningHours]) -> [Int] { + func getOpenClosedTimeArray(openingHours: [RegularOpeningHours]) throws -> [Int] { var openCloseTime: [Int] = [] let todayOpenHours = filteredOpeningHours(openingHours: openingHours, day: 0) if todayOpenHours.isEmpty { @@ -101,9 +106,9 @@ private extension GetOpenClosedUseCaseImpl { let yesterdayOpenHours = filteredOpeningHours(openingHours: openingHours, day: -1) let tomorrowOpenHours = filteredOpeningHours(openingHours: openingHours, day: 1) - openCloseTime.append(contentsOf: appendYesterdayClosedHour(openingHours: yesterdayOpenHours)) - openCloseTime.append(contentsOf: appendTodayOpenClosedHour(openingHours: todayOpenHours)) - openCloseTime.append(contentsOf: appendTomorrowOpenHour(openingHours: tomorrowOpenHours)) + openCloseTime.append(contentsOf: try appendYesterdayClosedHour(openingHours: yesterdayOpenHours)) + openCloseTime.append(contentsOf: try appendTodayOpenClosedHour(openingHours: todayOpenHours)) + openCloseTime.append(contentsOf: try appendTomorrowOpenHour(openingHours: tomorrowOpenHours)) return openCloseTime } @@ -115,49 +120,34 @@ private extension GetOpenClosedUseCaseImpl { } } - func appendYesterdayClosedHour(openingHours: [RegularOpeningHours]) -> [Int] { + func appendYesterdayClosedHour(openingHours: [RegularOpeningHours]) throws -> [Int] { if let businessHour = openingHours.last?.close { - if let time = catchHourError(businessHour: businessHour, openClose: .close) { - return [time - 86400] - } + return [try catchHourError(businessHour: businessHour, openClose: .close) - 86400] } return [.zero] } - func appendTodayOpenClosedHour(openingHours: [RegularOpeningHours]) -> [Int] { + func appendTodayOpenClosedHour(openingHours: [RegularOpeningHours]) throws -> [Int] { var openCloseTime: [Int] = [] - openingHours.forEach { businessHour in - if let openHour = catchHourError(businessHour: businessHour.open, openClose: .open), - let closeHour = catchHourError(businessHour: businessHour.close, openClose: .close) { - openCloseTime.append(openHour) - openCloseTime.append(closeHour) - } + try openingHours.forEach { businessHour in + openCloseTime.append(try catchHourError(businessHour: businessHour.open, openClose: .open)) + openCloseTime.append(try catchHourError(businessHour: businessHour.close, openClose: .close)) } return openCloseTime } - func appendTomorrowOpenHour(openingHours: [RegularOpeningHours]) -> [Int] { + func appendTomorrowOpenHour(openingHours: [RegularOpeningHours]) throws -> [Int] { if let businessHour = openingHours.first?.open { - if let time = catchHourError(businessHour: businessHour, openClose: .open) { - return [time + 86400] - } + return [try catchHourError(businessHour: businessHour, openClose: .open) + 86400] } return [86400 * 2] } - func catchHourError(businessHour: BusinessHour, openClose: OpenClose) -> Int? { - do { - return try toSecond(businessHour: businessHour, openClose: openClose) - } catch let error as OpeningHourError { - print(error.description) - } catch { - print(error) - } - - return nil + func catchHourError(businessHour: BusinessHour, openClose: OpenClose) throws -> Int { + return try toSecond(businessHour: businessHour, openClose: openClose) } func toSecond(businessHour: BusinessHour, openClose: OpenClose) throws -> Int { diff --git a/KCS/KCS/Domain/UseCase/protocol/CheckNetworkStatusUseCase.swift b/KCS/KCS/Domain/UseCase/protocol/CheckNetworkStatusUseCase.swift new file mode 100644 index 00000000..b146bf3d --- /dev/null +++ b/KCS/KCS/Domain/UseCase/protocol/CheckNetworkStatusUseCase.swift @@ -0,0 +1,18 @@ +// +// CheckNetworkStatusUseCase.swift +// KCS +// +// Created by ๊น€์˜ํ˜„ on 2/8/24. +// + +import Foundation + +protocol CheckNetworkStatusUseCase { + + var repository: NetworkRepository { get } + + init(repository: NetworkRepository) + + func execute() -> Bool + +} diff --git a/KCS/KCS/Domain/Interface/UseCase/FetchImageUseCase.swift b/KCS/KCS/Domain/UseCase/protocol/FetchImageUseCase.swift similarity index 100% rename from KCS/KCS/Domain/Interface/UseCase/FetchImageUseCase.swift rename to KCS/KCS/Domain/UseCase/protocol/FetchImageUseCase.swift diff --git a/KCS/KCS/Domain/Interface/UseCase/FetchRefreshStoresUseCase.swift b/KCS/KCS/Domain/UseCase/protocol/FetchRefreshStoresUseCase.swift similarity index 72% rename from KCS/KCS/Domain/Interface/UseCase/FetchRefreshStoresUseCase.swift rename to KCS/KCS/Domain/UseCase/protocol/FetchRefreshStoresUseCase.swift index be4e4fd4..29c2b212 100644 --- a/KCS/KCS/Domain/Interface/UseCase/FetchRefreshStoresUseCase.swift +++ b/KCS/KCS/Domain/UseCase/protocol/FetchRefreshStoresUseCase.swift @@ -14,7 +14,8 @@ protocol FetchRefreshStoresUseCase { init(repository: StoreRepository) func execute( - requestLocation: RequestLocation - ) -> Observable<[Store]> + requestLocation: RequestLocation, + isEntire: Bool + ) -> Observable } diff --git a/KCS/KCS/Domain/UseCase/protocol/FetchSearchStoresUseCase.swift b/KCS/KCS/Domain/UseCase/protocol/FetchSearchStoresUseCase.swift new file mode 100644 index 00000000..00307b2f --- /dev/null +++ b/KCS/KCS/Domain/UseCase/protocol/FetchSearchStoresUseCase.swift @@ -0,0 +1,21 @@ +// +// FetchSearchStoresUseCase.swift +// KCS +// +// Created by ์กฐ์„ฑ๋ฏผ on 2/9/24. +// + +import RxSwift + +protocol FetchSearchStoresUseCase { + + var repository: StoreRepository { get } + + init(repository: StoreRepository) + + func execute( + location: Location, + keyword: String + ) -> Observable<[Store]> + +} diff --git a/KCS/KCS/Domain/Interface/UseCase/FetchStoresUseCase.swift b/KCS/KCS/Domain/UseCase/protocol/FetchStoresUseCase.swift similarity index 83% rename from KCS/KCS/Domain/Interface/UseCase/FetchStoresUseCase.swift rename to KCS/KCS/Domain/UseCase/protocol/FetchStoresUseCase.swift index 94508e4d..d66125fa 100644 --- a/KCS/KCS/Domain/Interface/UseCase/FetchStoresUseCase.swift +++ b/KCS/KCS/Domain/UseCase/protocol/FetchStoresUseCase.swift @@ -13,6 +13,6 @@ protocol FetchStoresUseCase { init(repository: StoreRepository) - func execute() -> [Store] + func execute(fetchCount: Int) -> [Store] } diff --git a/KCS/KCS/Domain/Interface/UseCase/GetOpenClosedUseCase.swift b/KCS/KCS/Domain/UseCase/protocol/GetOpenClosedUseCase.swift similarity index 86% rename from KCS/KCS/Domain/Interface/UseCase/GetOpenClosedUseCase.swift rename to KCS/KCS/Domain/UseCase/protocol/GetOpenClosedUseCase.swift index 8839be20..b988b34e 100644 --- a/KCS/KCS/Domain/Interface/UseCase/GetOpenClosedUseCase.swift +++ b/KCS/KCS/Domain/UseCase/protocol/GetOpenClosedUseCase.swift @@ -11,6 +11,6 @@ protocol GetOpenClosedUseCase { func execute( openingHours: [RegularOpeningHours] - ) -> OpenClosedContent + ) throws -> OpenClosedContent } diff --git a/KCS/KCS/Domain/Interface/UseCase/GetStoreInformationUseCase.swift b/KCS/KCS/Domain/UseCase/protocol/GetStoreInformationUseCase.swift similarity index 100% rename from KCS/KCS/Domain/Interface/UseCase/GetStoreInformationUseCase.swift rename to KCS/KCS/Domain/UseCase/protocol/GetStoreInformationUseCase.swift diff --git a/KCS/KCS/Presentation/Extension/NMFMyPosition+.swift b/KCS/KCS/Presentation/Extension/NMFMyPosition+.swift new file mode 100644 index 00000000..c7a13449 --- /dev/null +++ b/KCS/KCS/Presentation/Extension/NMFMyPosition+.swift @@ -0,0 +1,24 @@ +// +// NMFMyPosition+.swift +// KCS +// +// Created by ๊น€์˜ํ˜„ on 1/27/24. +// + +import Foundation +import NMapsMap + +extension NMFMyPositionMode { + + func getImageName() -> String? { + switch self { + case .direction: + return "LocationButtonNormal" + case .compass, .normal: + return "LocationButtonCompass" + default: + return nil + } + } + +} diff --git a/KCS/KCS/Presentation/Extension/UISheetPresentationController+Detent.swift b/KCS/KCS/Presentation/Extension/UISheetPresentationController+Detent.swift new file mode 100644 index 00000000..aea391d8 --- /dev/null +++ b/KCS/KCS/Presentation/Extension/UISheetPresentationController+Detent.swift @@ -0,0 +1,36 @@ +// +// UISheetPresentationController+Detent.swift +// KCS +// +// Created by ์กฐ์„ฑ๋ฏผ on 2/3/24. +// + +import UIKit + +extension UISheetPresentationController.Detent.Identifier { + + static let smallSummaryDetentIdentifier = UISheetPresentationController.Detent.Identifier("SmallSummaryDetent") + static let largeSummaryDetentIdentifier = UISheetPresentationController.Detent.Identifier("LargeSummaryDetent") + static let detailDetentIdentifier = UISheetPresentationController.Detent.Identifier("DetailDetent") + static let smallStoreListViewDetentIdentifier = UISheetPresentationController.Detent.Identifier("SmallListDetent") + static let largeStoreListViewDetentIdentifier = UISheetPresentationController.Detent.Identifier.large + +} + +extension UISheetPresentationController.Detent { + + static let smallSummaryViewDetent = custom(identifier: .smallSummaryDetentIdentifier) { _ in + return 230 - 21 + } + static let largeSummaryViewDetent = custom(identifier: .largeSummaryDetentIdentifier) { _ in + return 253 - 21 + } + static let detailViewDetent = custom(identifier: .detailDetentIdentifier) { _ in + return 616 - 21 + } + static let smallStoreListViewDetent = custom(identifier: .smallStoreListViewDetentIdentifier) { _ in + return 40 + } + static let largeStoreListViewDetent = large() + +} diff --git a/KCS/KCS/Presentation/Extension/UIStackView+clear.swift b/KCS/KCS/Presentation/Extension/UIStackView+clear.swift new file mode 100644 index 00000000..ecd1fac7 --- /dev/null +++ b/KCS/KCS/Presentation/Extension/UIStackView+clear.swift @@ -0,0 +1,20 @@ +// +// UIStackView+clear.swift +// KCS +// +// Created by ์กฐ์„ฑ๋ฏผ on 2/1/24. +// + +import UIKit + +extension UIStackView { + + func clear() { + let subviews = arrangedSubviews + arrangedSubviews.forEach { + removeArrangedSubview($0) + } + subviews.forEach { $0.removeFromSuperview() } + } + +} diff --git a/KCS/KCS/Presentation/Extension/UITableViewCell+Identifier.swift b/KCS/KCS/Presentation/Extension/UITableViewCell+Identifier.swift new file mode 100644 index 00000000..868afd19 --- /dev/null +++ b/KCS/KCS/Presentation/Extension/UITableViewCell+Identifier.swift @@ -0,0 +1,16 @@ +// +// UITableViewCell+Identifier.swift +// KCS +// +// Created by ์กฐ์„ฑ๋ฏผ on 1/31/24. +// + +import UIKit + +extension UITableViewCell { + + static var identifier: String { + return String(describing: self) + } + +} diff --git a/KCS/KCS/Presentation/Extension/UIViewController+Alert.swift b/KCS/KCS/Presentation/Extension/UIViewController+Alert.swift new file mode 100644 index 00000000..dd116dc5 --- /dev/null +++ b/KCS/KCS/Presentation/Extension/UIViewController+Alert.swift @@ -0,0 +1,86 @@ +// +// UIViewController+Alert.swift +// KCS +// +// Created by ์กฐ์„ฑ๋ฏผ on 2/3/24. +// + +import UIKit + +extension UIViewController { + + func presentErrorAlert(error: ErrorAlertMessage) { + let alertController = UIAlertController(title: nil, message: error.errorDescription, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: "ํ™•์ธ", style: .default)) + if let presentController = presentedViewController { + presentController.presentErrorAlert(error: error) + } else if !(self is UIAlertController) { + present(alertController, animated: true) + } + } + + func presentLocationAlert() { + let requestLocationServiceAlert = UIAlertController( + title: "์œ„์น˜ ์ •๋ณด ์ด์šฉ", + message: "์œ„์น˜ ์„œ๋น„์Šค๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.\n๋””๋ฐ”์ด์Šค์˜ '์„ค์ • > ๊ฐœ์ธ์ •๋ณด ๋ณดํ˜ธ'์—์„œ ์œ„์น˜ ์„œ๋น„์Šค๋ฅผ ์ผœ์ฃผ์„ธ์š”.", + preferredStyle: .alert + ) + let goSetting = UIAlertAction(title: "์„ค์ •์œผ๋กœ ์ด๋™", style: .destructive) { _ in + if let appSetting = URL(string: UIApplication.openSettingsURLString) { + UIApplication.shared.open(appSetting) + } + } + let cancel = UIAlertAction(title: "์ทจ์†Œ", style: .cancel) + + requestLocationServiceAlert.addAction(cancel) + requestLocationServiceAlert.addAction(goSetting) + + if let presentController = presentedViewController { + presentController.presentLocationAlert() + } else { + present(requestLocationServiceAlert, animated: true) + } + } + + func showToast(message: String) { + let toastLabel = UILabel() + toastLabel.translatesAutoresizingMaskIntoConstraints = false + toastLabel.backgroundColor = .black.withAlphaComponent(0.6) + toastLabel.textColor = .white + toastLabel.font = .pretendard(size: 14, weight: .medium) + toastLabel.textAlignment = .center + toastLabel.text = message + toastLabel.alpha = 0 + toastLabel.setLayerCorner(cornerRadius: 12) + toastLabel.clipsToBounds = true + view.addSubview(toastLabel) + NSLayoutConstraint.activate([ + toastLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + toastLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor), + toastLabel.widthAnchor.constraint(equalToConstant: 150), + toastLabel.heightAnchor.constraint(equalToConstant: 30) + ]) + + UIView.animate( + withDuration: 0.4, + delay: 0, + options: .curveEaseIn, + animations: { + toastLabel.alpha = 1.0 + }, + completion: { _ in + UIView.animate( + withDuration: 0.8, + delay: 1.4, + options: .curveEaseOut, + animations: { + toastLabel.alpha = 0.0 + }, completion: { _ in + toastLabel.removeFromSuperview() + } + ) + } + ) + } + +} diff --git a/KCS/KCS/Presentation/Home/View/HomeViewController.swift b/KCS/KCS/Presentation/Home/View/HomeViewController.swift index a907458a..e7211567 100644 --- a/KCS/KCS/Presentation/Home/View/HomeViewController.swift +++ b/KCS/KCS/Presentation/Home/View/HomeViewController.swift @@ -10,11 +10,10 @@ import NMapsMap import CoreLocation import RxSwift import RxCocoa +import RxGesture final class HomeViewController: UIViewController { - private let disposeBag = DisposeBag() - private lazy var goodPriceFilterButton: FilterButton = { let type = CertificationType.goodPrice let button = FilterButton(type: type) @@ -52,6 +51,22 @@ final class HomeViewController: UIViewController { return stack }() + private lazy var searchView: SearchBarView = { + let view = SearchBarView() + view.translatesAutoresizingMaskIntoConstraints = false + view.rx + .tapGesture() + .when(.ended) + .subscribe(onNext: { [weak self] _ in + guard let self = self else { return } + searchViewController.setSearchKeyword(keyword: view.getSearchKeyword()) + presentSearchViewController() + }) + .disposed(by: disposeBag) + + return view + }() + private lazy var locationManager: CLLocationManager = { let locationManager = CLLocationManager() locationManager.delegate = self @@ -65,18 +80,12 @@ final class HomeViewController: UIViewController { button.rx.tap .bind { [weak self] _ in guard let self = self else { return } - - checkLocationService() - switch mapView.mapView.positionMode { - case .direction: - button.setImage(UIImage.locationButtonCompass, for: .normal) - mapView.mapView.positionMode = .compass - case .compass, .normal: - button.setImage(UIImage.locationButtonNormal, for: .normal) - mapView.mapView.positionMode = .direction - default: - break - } + viewModel.action( + input: .locationButtonTapped( + locationAuthorizationStatus: locationManager.authorizationStatus, + positionMode: mapView.mapView.positionMode + ) + ) } .disposed(by: self.disposeBag) button.setImage(UIImage.locationButtonNone, for: .normal) @@ -84,103 +93,152 @@ final class HomeViewController: UIViewController { return button }() - private var markers: [Marker] = [] - private var clickedMarker: Marker? + private lazy var compassView: NMFCompassView = { + let compass = NMFCompassView() + compass.translatesAutoresizingMaskIntoConstraints = false + compass.mapView = mapView.mapView + + return compass + }() private lazy var mapView: NMFNaverMapView = { let map = NMFNaverMapView() map.translatesAutoresizingMaskIntoConstraints = false - map.showZoomControls = false map.showCompass = false + map.showZoomControls = false map.showScaleBar = false map.showIndoorLevelPicker = false map.showLocationButton = false map.mapView.logoAlign = .rightBottom + map.mapView.logoMargin = UIEdgeInsets(top: 0, left: 0, bottom: 55, right: 0) map.mapView.touchDelegate = self map.mapView.addCameraDelegate(delegate: self) return map }() - - private lazy var locationBottomConstraint = locationButton.bottomAnchor.constraint( - equalTo: mapView.safeAreaLayoutGuide.bottomAnchor, - constant: -16 - ) - private lazy var refreshBottomConstraint = refreshButton.bottomAnchor.constraint( - equalTo: mapView.safeAreaLayoutGuide.bottomAnchor, - constant: -17 - ) - - private let requestLocationServiceAlert: UIAlertController = { - let alertController = UIAlertController( - title: "์œ„์น˜ ์ •๋ณด ์ด์šฉ", - message: "์œ„์น˜ ์„œ๋น„์Šค๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.\n๋””๋ฐ”์ด์Šค์˜ '์„ค์ • > ๊ฐœ์ธ์ •๋ณด ๋ณดํ˜ธ'์—์„œ ์œ„์น˜ ์„œ๋น„์Šค๋ฅผ ์ผœ์ฃผ์„ธ์š”.", - preferredStyle: .alert - ) - let goSetting = UIAlertAction(title: "์„ค์ •์œผ๋กœ ์ด๋™", style: .destructive) { _ in - if let appSetting = URL(string: UIApplication.openSettingsURLString) { - UIApplication.shared.open(appSetting) - } - } - let cancel = UIAlertAction(title: "์ทจ์†Œ", style: .cancel) - - alertController.addAction(cancel) - alertController.addAction(goSetting) - - return alertController - }() private lazy var refreshButton: RefreshButton = { let button = RefreshButton() button.translatesAutoresizingMaskIntoConstraints = false - button.isHidden = true button.rx.tap - .bind { [weak self] _ in - guard let self = self else { return } - let northWestPoint = mapView.mapView.projection.latlng(from: CGPoint(x: 0, y: 0)) - let southWestPoint = mapView.mapView.projection.latlng(from: CGPoint(x: 0, y: view.frame.height)) - let southEastPoint = mapView.mapView.projection.latlng(from: CGPoint(x: view.frame.width, y: view.frame.height)) - let northEastPoint = mapView.mapView.projection.latlng(from: CGPoint(x: view.frame.width, y: 0)) + .debounce(.milliseconds(10), scheduler: MainScheduler()) + .map { [weak self] _ -> RequestLocation? in + guard let self = self else { return nil } + button.animationFire() + + return makeRequestLocation(projection: mapView.mapView.projection) + } + .observe(on: ConcurrentDispatchQueueScheduler(queue: DispatchQueue.global())) + .bind { [weak self] requestLocation in + guard let self = self, let location = requestLocation else { return } viewModel.action( input: .refresh( - requestLocation: RequestLocation( - northWest: Location( - longitude: northWestPoint.lng, - latitude: northWestPoint.lat - ), - southWest: Location( - longitude: southWestPoint.lng, - latitude: southWestPoint.lat - ), - southEast: Location( - longitude: southEastPoint.lng, - latitude: southEastPoint.lat - ), - northEast: Location( - longitude: northEastPoint.lng, - latitude: northEastPoint.lat - ) - ), - filters: getActivatedTypes() + requestLocation: location ) ) - refreshButton.isHidden = true } - .disposed(by: self.disposeBag) + .disposed(by: disposeBag) + + return button + }() + + private lazy var moreStoreButton: MoreStoreButton = { + let button = MoreStoreButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.isHidden = true + button.rx.tap + .debounce(.milliseconds(100), scheduler: MainScheduler()) + .bind { [weak self] in + self?.viewModel.action( + input: .moreStoreButtonTapped + ) + } + .disposed(by: disposeBag) + + return button + }() + + // TODO: BackButton configuration ์ˆ˜์ • ํ•„์š” + private lazy var backStoreListButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.backgroundColor = .blue + button.setTitle("๋’ค๋กœ๊ฐ€๊ธฐ", for: .normal) + button.isHidden = true + button.rx.tap + .debounce(.milliseconds(100), scheduler: MainScheduler()) + .bind { [weak self] in + self?.storeInformationViewDismiss() + if let sheet = self?.storeListViewController.sheetPresentationController { + sheet.animateChanges { + sheet.selectedDetentIdentifier = .largeStoreListViewDetentIdentifier + } + } + } + .disposed(by: disposeBag) return button }() - private var activatedFilter: [CertificationType] = [] + private lazy var dimView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .clear + view.isUserInteractionEnabled = false + view.alpha = 0.4 + + view.rx.tapGesture() + .when(.ended) + .subscribe(onNext: { [weak self] _ in + self?.viewModel.action( + input: .dimViewTapGestureEnded + ) + }) + .disposed(by: disposeBag) + + return view + }() - private var storeInformationViewController: StoreInformationViewController? + private lazy var refreshButtonBottomConstraint = refreshButton.bottomAnchor.constraint( + equalTo: mapView.bottomAnchor, constant: -90 + ) - private let dismissObserver = PublishRelay() + private lazy var moreStoreButtonBottomConstraint = moreStoreButton.bottomAnchor.constraint( + equalTo: mapView.bottomAnchor, constant: -90 + ) - private let viewModel: HomeViewModel + private lazy var locationButtonBottomConstraint = locationButton.bottomAnchor.constraint( + equalTo: mapView.bottomAnchor, constant: -90 + ) - init(viewModel: HomeViewModel) { + private let searchViewController: SearchViewController + private let searchObserver: PublishRelay + private let disposeBag = DisposeBag() + private var markers: [Marker] = [] + private let storeInformationViewController: StoreInformationViewController + private var clickedMarker: Marker? + private let storeListViewController: StoreListViewController + private let viewModel: HomeViewModel + private let summaryViewHeightObserver: PublishRelay + private let listCellSelectedObserver: PublishRelay + + init( + viewModel: HomeViewModel, + storeInformationViewController: StoreInformationViewController, + storeListViewController: StoreListViewController, + summaryViewHeightObserver: PublishRelay, + listCellSelectedObserver: PublishRelay, + searchViewController: SearchViewController, + searchObserver: PublishRelay + ) { self.viewModel = viewModel + self.storeInformationViewController = storeInformationViewController + self.storeListViewController = storeListViewController + self.summaryViewHeightObserver = summaryViewHeightObserver + self.listCellSelectedObserver = listCellSelectedObserver + self.searchViewController = searchViewController + self.searchObserver = searchObserver + super.init(nibName: nil, bundle: nil) } @@ -193,34 +251,193 @@ final class HomeViewController: UIViewController { addUIComponents() configureConstraints() - checkUserCurrentLocationAuthorization() bind() + setup() + refresh() } } private extension HomeViewController { + func setup() { + unDimmedView() + viewModel.action( + input: .checkLocationAuthorization( + status: locationManager.authorizationStatus + ) + ) + navigationController?.isNavigationBarHidden = true + } + func bind() { - viewModel.refreshOutput + bindFetchStores() + bindApplyFilters() + bindSetMarker() + bindLocationButton() + bindLocationAuthorization() + bindStoreInformationView() + bindErrorAlert() + bindListCellSelected() + bindSearch() + } + + func bindFetchStores() { + viewModel.refreshDoneOutput + .bind { [weak self] isEntire in + self?.refreshButton.animationInvalidate() + self?.refreshButton.isHidden = true + self?.moreStoreButton.isHidden = isEntire + self?.moreStoreButton.isEnabled = true + self?.mapView.mapView.positionMode = .normal + self?.locationButton.setImage(UIImage.locationButtonNone, for: .normal) + } + .disposed(by: disposeBag) + + viewModel.fetchCountOutput + .bind { [weak self] fetchCount in + self?.moreStoreButton.setFetchCount(fetchCount: fetchCount) + } + .disposed(by: disposeBag) + + viewModel.noMoreStoresOutput + .bind { [weak self] in + self?.moreStoreButton.isEnabled = false + } + .disposed(by: disposeBag) + } + + func bindApplyFilters() { + viewModel.filteredStoresOutput + .debounce(.milliseconds(100), scheduler: MainScheduler()) .bind { [weak self] filteredStores in guard let self = self else { return } self.markers.forEach { $0.mapView = nil } + self.markers = [] + var stores: [Store] = [] filteredStores.forEach { filteredStore in - filteredStore.stores.forEach { - let location = $0.location.toMapLocation() - self.setMarker(marker: Marker(certificationType: filteredStore.type, position: location), tag: UInt($0.id)) + filteredStore.stores.forEach { [weak self] store in + self?.viewModel.action( + input: .setMarker( + store: store, + certificationType: filteredStore.type + ) + ) + stores.append(store) } } - storeInformationViewController?.dismiss(animated: true) + storeInformationViewDismiss() + storeListViewController.updateList(stores: stores) + if stores.isEmpty { + showToast(message: "๊ฐ€๊ฒŒ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.") + } + } + .disposed(by: disposeBag) + } + + func bindSetMarker() { + viewModel.setMarkerOutput + .bind { [weak self] content in + guard let selectImage = UIImage(named: content.selectImageName), + let deselectImage = UIImage(named: content.deselectImageName) else { return } + let marker = Marker(position: content.location.toMapLocation(), selectImage: selectImage, deselectImage: deselectImage) + marker.tag = UInt(content.tag) + marker.mapView = self?.mapView.mapView + self?.markerTouchHandler(marker: marker) + self?.markers.append(marker) + } + .disposed(by: disposeBag) + } + + func bindLocationButton() { + viewModel.locationButtonOutput + .bind { [weak self] positionMode in + guard let imageName = positionMode.getImageName() else { return } + self?.locationButton.setImage(UIImage(named: imageName), for: .normal) + self?.mapView.mapView.positionMode = positionMode } .disposed(by: disposeBag) + viewModel.locationButtonImageNameOutput + .bind { [weak self] imageName in + self?.locationButton.setImage(UIImage(named: imageName), for: .normal) + } + .disposed(by: disposeBag) + } + + func bindStoreInformationView() { viewModel.getStoreInformationOutput .bind { [weak self] store in + self?.storeInformationViewController.setUIContents(store: store) + self?.changeButtonsConstraints(delay: false) + } + .disposed(by: disposeBag) + + viewModel.dimViewTapGestureEndedOutput + .bind { [weak self] _ in + self?.storeInformationViewController.changeToSummary() + self?.unDimmedView() + } + .disposed(by: disposeBag) + + summaryViewHeightObserver.bind { [weak self] heightCase in + guard let self = self else { return } + if let sheet = storeInformationViewController.sheetPresentationController { + sheet.animateChanges { + switch heightCase { + case .small: + sheet.detents = [.smallSummaryViewDetent, .detailViewDetent] + sheet.selectedDetentIdentifier = .smallSummaryDetentIdentifier + case .large: + sheet.detents = [.largeSummaryViewDetent, .detailViewDetent] + sheet.selectedDetentIdentifier = .largeSummaryDetentIdentifier + } + } + sheet.delegate = self + sheet.prefersGrabberVisible = true + sheet.preferredCornerRadius = 15 + sheet.largestUndimmedDetentIdentifier = .detailDetentIdentifier + } + storeInformationViewController.changeToSummary() + if !(presentedViewController is StoreInformationViewController) { + dismiss(animated: true) + changeButtonsConstraints(delay: true) + present(storeInformationViewController, animated: true) + } + } + .disposed(by: disposeBag) + } + + func bindLocationAuthorization() { + viewModel.locationAuthorizationStatusDeniedOutput + .bind { [weak self] _ in guard let self = self else { return } - presentStoreView() - storeInformationViewController?.setUIContents(store: store) + presentLocationAlert() + } + .disposed(by: disposeBag) + + viewModel.locationStatusNotDeterminedOutput + .bind { [weak self] _ in + self?.locationManager.requestWhenInUseAuthorization() + self?.locationButton.setImage(UIImage.locationButtonNone, for: .normal) + } + .disposed(by: disposeBag) + + viewModel.locationStatusAuthorizedWhenInUse + .debounce(.milliseconds(10), scheduler: MainScheduler()) + .bind { [weak self] _ in + guard let self = self else { return } + guard let location = locationManager.location else { return } + let cameraUpdate = NMFCameraUpdate( + scrollTo: NMGLatLng( + lat: location.coordinate.latitude, + lng: location.coordinate.longitude + ) + ) + cameraUpdate.animation = .none + mapView.mapView.moveCamera(cameraUpdate) + mapView.mapView.positionMode = .direction + refresh() } .disposed(by: disposeBag) } @@ -229,32 +446,109 @@ private extension HomeViewController { button.rx.tap .scan(false) { [weak self] (lastState, _) in guard let self = self else { return lastState } - if lastState { - guard let lastIndex = activatedFilter.lastIndex(of: type) else { return lastState } - activatedFilter.remove(at: lastIndex) - } else { - activatedFilter.append(type) - } - viewModel.action(input: .fetchFilteredStores(filters: getActivatedTypes())) + viewModel.action( + input: .filterButtonTapped(activatedFilter: type) + ) return !lastState } .bind(to: button.rx.isSelected) .disposed(by: disposeBag) } - func setMarker(marker: Marker, tag: UInt) { - marker.tag = tag - marker.mapView = mapView.mapView - markerTouchHandler(marker: marker) - markers.append(marker) + func bindErrorAlert() { + viewModel.errorAlertOutput + .debounce(.milliseconds(100), scheduler: MainScheduler()) + .bind { [weak self] error in + self?.presentErrorAlert(error: error) + } + .disposed(by: disposeBag) } - func getActivatedTypes() -> [CertificationType] { - if activatedFilter.isEmpty { - return [.safe, .exemplary, .goodPrice] - } + func bindListCellSelected() { + listCellSelectedObserver + .debounce(.milliseconds(10), scheduler: MainScheduler()) + .bind { [weak self] index in + guard let self = self else { return } + if markers.indices ~= index { + let targetMarker = markers[index] + + let cameraUpdate = NMFCameraUpdate( + position: NMFCameraPosition(targetMarker.position.toLatLng(), zoom: 15) + ) + cameraUpdate.animation = .easeIn + mapView.mapView.moveCamera(cameraUpdate) + + viewModel.action( + input: .markerTapped(tag: targetMarker.tag) + ) + targetMarker.select() + clickedMarker = targetMarker + + setBackStoreListButton(row: index) + } else { + presentErrorAlert(error: .client) + } + } + .disposed(by: disposeBag) + } + + func bindSearch() { + searchObserver + .bind { [weak self] keyword in + guard let center = self?.view.center else { return } + let centerPosition = Location( + longitude: Double(center.x), + latitude: Double(center.y) + ) + self?.viewModel.action(input: .search(location: centerPosition, keyword: keyword)) + } + .disposed(by: disposeBag) - return activatedFilter + viewModel.searchStoresOutput + .bind { [weak self] stores in + guard let self = self else { return } + resetFilters() + storeInformationViewDismiss() + setSearchStoresMarker(stores: stores) + + mapView.mapView.moveCamera(NMFCameraUpdate(heading: 0)) + let cameraUpdate = NMFCameraUpdate( + fit: NMGLatLngBounds(latLngs: markers.map({ $0.position })), + padding: 30 + ) + cameraUpdate.animation = .easeIn + cameraUpdate.animationDuration = 0.5 + mapView.mapView.moveCamera(cameraUpdate) + mapView.mapView.positionMode = .normal + } + .disposed(by: disposeBag) + + viewModel.searchOneStoreOutput + .bind { [weak self] store in + guard let self = self else { return } + resetFilters() + setSearchStoresMarker(stores: [store]) + + mapView.mapView.moveCamera(NMFCameraUpdate(heading: 0)) + + let cameraUpdate = NMFCameraUpdate( + position: NMFCameraPosition(store.location.toMapLocation(), zoom: 15) + ) + cameraUpdate.animation = .easeIn + cameraUpdate.animationDuration = 0.5 + mapView.mapView.moveCamera(cameraUpdate) + mapView.mapView.positionMode = .normal + + guard let marker = markers.first(where: { $0.tag == store.id}) else { return } + if let clickedMarker = clickedMarker { + if clickedMarker == marker { return } + storeInformationViewDismiss(changeMarker: true) + } + storeInformationViewController.setUIContents(store: store) + marker.select() + clickedMarker = marker + } + .disposed(by: disposeBag) } } @@ -263,97 +557,96 @@ private extension HomeViewController { func markerTouchHandler(marker: Marker) { marker.touchHandler = { [weak self] (_: NMFOverlay) -> Bool in + if let clickedMarker = self?.clickedMarker { if clickedMarker == marker { return true } - if clickedMarker.isSelected { - self?.storeInformationViewController?.dismiss(animated: true) { [weak self] in - self?.markerSelected(marker: marker) - } - } else { - self?.markerSelected(marker: marker) - } - } else { - self?.markerSelected(marker: marker) + self?.storeInformationViewDismiss(changeMarker: true) } + self?.viewModel.action( + input: .markerTapped(tag: marker.tag) + ) + marker.select() + self?.clickedMarker = marker + return true } } - func markerSelected(marker: Marker) { - marker.isSelected.toggle() - if marker.isSelected { - viewModel.action(input: .markerTapped(tag: marker.tag)) + func storeInformationViewDismiss(changeMarker: Bool = false) { + backStoreListButton.isHidden = true + clickedMarker?.deselect() + clickedMarker = nil + if !changeMarker { + storeInformationViewController.dismiss(animated: true) + presentStoreListView() } - clickedMarker = marker } - func markerClicked(height: CGFloat) { - mapView.mapView.logoMargin = UIEdgeInsets(top: 0, left: 0, bottom: height, right: 0) - locationBottomConstraint.constant = -height - refreshBottomConstraint.constant = -height - UIView.animate(withDuration: 0.3) { - self.view.layoutIfNeeded() + func dimmedView() { + dimView.isUserInteractionEnabled = true + UIView.animate(withDuration: 0.3) { [weak self] in + self?.dimView.backgroundColor = .black } } - func presentStoreView() { - let storeViewModel = StoreInformationViewModelImpl( - getOpenClosedUseCase: GetOpenClosedUseCaseImpl(), - fetchImageUseCase: FetchImageUseCaseImpl(repository: ImageRepositoryImpl()) - ) - let contentHeightObserver = PublishRelay() - storeInformationViewController = StoreInformationViewController( - viewModel: storeViewModel, - contentHeightObserver: contentHeightObserver, - dismissObserver: dismissObserver - ) - storeInformationViewController?.transitioningDelegate = self - - if let viewController = storeInformationViewController { - contentHeightObserver - .bind { [weak self] contentHeight in - guard let self = self else { return } - let bottomSafeArea: CGFloat = 34 - let height = contentHeight - bottomSafeArea - if let sheet = viewController.sheetPresentationController { - let detentIdentifier = UISheetPresentationController.Detent.Identifier("detent") - let detent = UISheetPresentationController.Detent.custom(identifier: detentIdentifier) { _ in - return height - } - sheet.detents = [detent] - sheet.largestUndimmedDetentIdentifier = detentIdentifier - sheet.preferredCornerRadius = 15 - } - markerClicked(height: contentHeight - bottomSafeArea + 16) - } - .disposed(by: disposeBag) - present(viewController, animated: true) + func unDimmedView() { + dimView.isUserInteractionEnabled = false + UIView.animate(withDuration: 0.3) { [weak self] in + self?.dimView.backgroundColor = .clear + } + if let sheet = storeInformationViewController.sheetPresentationController { + sheet.animateChanges { + sheet.selectedDetentIdentifier = .smallSummaryDetentIdentifier + } } } -} - -private extension HomeViewController { + func makeRequestLocation(projection: NMFProjection) -> RequestLocation { + let northWestPoint = projection.latlng(from: CGPoint(x: 0, y: 0)) + let southWestPoint = projection.latlng(from: CGPoint(x: 0, y: view.frame.height)) + let southEastPoint = projection.latlng(from: CGPoint(x: view.frame.width, y: view.frame.height)) + let northEastPoint = projection.latlng(from: CGPoint(x: view.frame.width, y: 0)) + + return RequestLocation( + northWest: Location( + longitude: northWestPoint.lng, + latitude: northWestPoint.lat + ), + southWest: Location( + longitude: southWestPoint.lng, + latitude: southWestPoint.lat + ), + southEast: Location( + longitude: southEastPoint.lng, + latitude: southEastPoint.lat + ), + northEast: Location( + longitude: northEastPoint.lng, + latitude: northEastPoint.lat + ) + ) + } - func checkUserCurrentLocationAuthorization() { - switch locationManager.authorizationStatus { - case .notDetermined: - locationManager.requestWhenInUseAuthorization() - locationButton.setImage(UIImage.locationButtonNone, for: .normal) - case .authorizedWhenInUse: - guard let location = locationManager.location else { return } - let cameraUpdate = NMFCameraUpdate(scrollTo: NMGLatLng(lat: location.coordinate.latitude, lng: location.coordinate.longitude)) - cameraUpdate.animation = .none - mapView.mapView.moveCamera(cameraUpdate) - default: - break - } + func refresh() { + refreshButton.animationFire() + viewModel.action( + input: .refresh( + requestLocation: makeRequestLocation(projection: mapView.mapView.projection) + ) + ) + } + + func setBackStoreListButton(row: Int) { + storeListViewController.scrollToPreviousCell(indexPath: IndexPath(row: row, section: 0)) + backStoreListButton.isHidden = false } - func checkLocationService() { - if locationManager.authorizationStatus == .denied { - present(requestLocationServiceAlert, animated: true) + func presentSearchViewController() { + if let presentedViewController = presentedViewController { + let navigationController = UINavigationController(rootViewController: searchViewController) + navigationController.modalPresentationStyle = .fullScreen + presentedViewController.present(navigationController, animated: false) } } @@ -365,7 +658,12 @@ private extension HomeViewController { view.addSubview(mapView) mapView.addSubview(locationButton) mapView.addSubview(filterButtonStackView) + mapView.addSubview(searchView) + mapView.addSubview(compassView) mapView.addSubview(refreshButton) + mapView.addSubview(moreStoreButton) + mapView.addSubview(backStoreListButton) + mapView.addSubview(dimView) } func configureConstraints() { @@ -376,11 +674,18 @@ private extension HomeViewController { mapView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0) ]) + NSLayoutConstraint.activate([ + dimView.leadingAnchor.constraint(equalTo: mapView.leadingAnchor, constant: 0), + dimView.trailingAnchor.constraint(equalTo: mapView.trailingAnchor, constant: 0), + dimView.bottomAnchor.constraint(equalTo: mapView.bottomAnchor, constant: 0), + dimView.topAnchor.constraint(equalTo: mapView.topAnchor, constant: 0) + ]) + NSLayoutConstraint.activate([ locationButton.leadingAnchor.constraint(equalTo: mapView.safeAreaLayoutGuide.leadingAnchor, constant: 16), locationButton.widthAnchor.constraint(equalToConstant: 48), locationButton.heightAnchor.constraint(equalToConstant: 48), - locationBottomConstraint + locationButtonBottomConstraint ]) NSLayoutConstraint.activate([ @@ -388,18 +693,91 @@ private extension HomeViewController { filterButtonStackView.topAnchor.constraint(equalTo: mapView.safeAreaLayoutGuide.topAnchor, constant: 8) ]) + NSLayoutConstraint.activate([ + searchView.centerXAnchor.constraint(equalTo: mapView.safeAreaLayoutGuide.centerXAnchor), + searchView.topAnchor.constraint(equalTo: filterButtonStackView.bottomAnchor, constant: 10), + searchView.widthAnchor.constraint(equalToConstant: 150), + searchView.heightAnchor.constraint(equalToConstant: 30) + ]) + + NSLayoutConstraint.activate([ + compassView.leadingAnchor.constraint(equalTo: mapView.safeAreaLayoutGuide.leadingAnchor, constant: 16), + compassView.topAnchor.constraint(equalTo: filterButtonStackView.bottomAnchor, constant: 16) + ]) + NSLayoutConstraint.activate([ refreshButton.centerXAnchor.constraint(equalTo: mapView.centerXAnchor), - refreshBottomConstraint + refreshButton.widthAnchor.constraint(equalToConstant: 110), + refreshButton.heightAnchor.constraint(equalToConstant: 35), + refreshButtonBottomConstraint + ]) + + NSLayoutConstraint.activate([ + moreStoreButton.centerXAnchor.constraint(equalTo: mapView.centerXAnchor), + moreStoreButton.widthAnchor.constraint(equalToConstant: 97), + moreStoreButtonBottomConstraint + ]) + + // TODO: BackButton AutoLayout ์ˆ˜์ • ํ•„์š” + NSLayoutConstraint.activate([ + backStoreListButton.trailingAnchor.constraint(equalTo: mapView.trailingAnchor, constant: -20), + backStoreListButton.bottomAnchor.constraint(equalTo: mapView.bottomAnchor, constant: -290), + backStoreListButton.widthAnchor.constraint(equalToConstant: 80), + backStoreListButton.heightAnchor.constraint(equalToConstant: 35) ]) } + func changeButtonsConstraints(delay: Bool) { + guard let controller = storeInformationViewController.sheetPresentationController else { return } + if controller.detents.contains(.smallSummaryViewDetent) { + refreshButtonBottomConstraint.constant = -260 + locationButtonBottomConstraint.constant = -260 + moreStoreButtonBottomConstraint.constant = -260 + mapView.mapView.logoMargin.bottom = 225 + } else { + refreshButtonBottomConstraint.constant = -283 + locationButtonBottomConstraint.constant = -283 + moreStoreButtonBottomConstraint.constant = -283 + mapView.mapView.logoMargin.bottom = 248 + } + UIView.animate(withDuration: 0.3, delay: delay ? 0.5 : 0) { + self.view.layoutIfNeeded() + } + } + + func resetFilters() { + safeFilterButton.isSelected = false + exemplaryFilterButton.isSelected = false + goodPriceFilterButton.isSelected = false + viewModel.action(input: .resetFilters) + } + + func setSearchStoresMarker(stores: [Store]) { + markers.forEach({ $0.mapView = nil }) + markers = [] + stores.forEach { [weak self] store in + guard let certificationType = store.certificationTypes.last else { return } + self?.viewModel.action(input: .setMarker( + store: store, + certificationType: certificationType + )) + } + storeListViewController.updateList(stores: stores) + if stores.isEmpty { + showToast(message: "๊ฐ€๊ฒŒ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.") + } + } + } extension HomeViewController: CLLocationManagerDelegate { func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { - checkUserCurrentLocationAuthorization() + viewModel.action( + input: .checkLocationAuthorization( + status: locationManager.authorizationStatus + ) + ) } } @@ -411,41 +789,35 @@ extension HomeViewController: NMFMapViewCameraDelegate { locationButton.setImage(UIImage.locationButtonNone, for: .normal) } refreshButton.isHidden = false + moreStoreButton.isHidden = true } func mapView(_ mapView: NMFMapView, cameraDidChangeByReason reason: Int, animated: Bool) { if reason == NMFMapChangedByDeveloper { - mapView.positionMode = .direction - locationButton.setImage(UIImage.locationButtonNormal, for: .normal) - - let northWestPoint = mapView.projection.latlng(from: CGPoint(x: 0, y: 0)) - let southWestPoint = mapView.projection.latlng(from: CGPoint(x: 0, y: view.frame.height)) - let southEastPoint = mapView.projection.latlng(from: CGPoint(x: view.frame.width, y: view.frame.height)) - let northEastPoint = mapView.projection.latlng(from: CGPoint(x: view.frame.width, y: 0)) - viewModel.action( - input: .refresh( - requestLocation: RequestLocation( - northWest: Location( - longitude: northWestPoint.lng, - latitude: northWestPoint.lat - ), - southWest: Location( - longitude: southWestPoint.lng, - latitude: southWestPoint.lat - ), - southEast: Location( - longitude: southEastPoint.lng, - latitude: southEastPoint.lat - ), - northEast: Location( - longitude: northEastPoint.lng, - latitude: northEastPoint.lat - ) - ), - filters: getActivatedTypes() - ) + viewModel.action(input: + .checkLocationAuthorizationWhenCameraDidChange( + status: locationManager.authorizationStatus + ) ) - refreshButton.isHidden = true + } + } + + func presentStoreListView() { + if !(presentedViewController is StoreListViewController) { + if let sheet = storeListViewController.sheetPresentationController { + sheet.detents = [.smallStoreListViewDetent, .largeStoreListViewDetent] + sheet.largestUndimmedDetentIdentifier = .smallStoreListViewDetentIdentifier + sheet.prefersGrabberVisible = true + sheet.preferredCornerRadius = 15 + } + refreshButtonBottomConstraint.constant = -90 + locationButtonBottomConstraint.constant = -90 + moreStoreButtonBottomConstraint.constant = -90 + mapView.mapView.logoMargin.bottom = 55 + UIView.animate(withDuration: 0.5) { + self.view.layoutIfNeeded() + } + present(storeListViewController, animated: true) } } @@ -454,28 +826,28 @@ extension HomeViewController: NMFMapViewCameraDelegate { extension HomeViewController: NMFMapViewTouchDelegate { func mapView(_ mapView: NMFMapView, didTapMap latlng: NMGLatLng, point: CGPoint) { - storeInformationViewController?.dismiss(animated: true) + storeInformationViewDismiss() } } -extension HomeViewController: UIViewControllerTransitioningDelegate { - - func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { - dismissObserver - .bind { [weak self] in - guard let self = self else { return } - clickedMarker?.isSelected = false - mapView.mapView.logoMargin = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) - locationBottomConstraint.constant = -16 - refreshBottomConstraint.constant = -17 - UIView.animate(withDuration: 0.3) { - self.view.layoutIfNeeded() - } +extension HomeViewController: UISheetPresentationControllerDelegate { + + func sheetPresentationControllerDidChangeSelectedDetentIdentifier( + _ sheetPresentationController: UISheetPresentationController + ) { + if let identifier = sheetPresentationController.selectedDetentIdentifier { + switch identifier { + case .smallSummaryDetentIdentifier, .largeSummaryDetentIdentifier: + storeInformationViewController.changeToSummary() + unDimmedView() + case .detailDetentIdentifier: + storeInformationViewController.changeToDetail() + dimmedView() + default: + break } - .disposed(by: disposeBag) - - return nil + } } } diff --git a/KCS/KCS/Presentation/Home/View/Marker.swift b/KCS/KCS/Presentation/Home/View/Marker.swift index b1389666..8301e5e5 100644 --- a/KCS/KCS/Presentation/Home/View/Marker.swift +++ b/KCS/KCS/Presentation/Home/View/Marker.swift @@ -11,51 +11,34 @@ import RxSwift final class Marker: NMFMarker { - var isSelected: Bool = false { - didSet { - setUI(type: certificationType) - } - } - private lazy var unselectedGlobalZIndex: Int = self.globalZIndex - private let certificationType: CertificationType + private var unselectedGlobalZIndex: Int? + private let selectImage: NMFOverlayImage + private let deselectImage: NMFOverlayImage - init(certificationType: CertificationType, position: NMGLatLng? = nil) { - self.certificationType = certificationType + init(position: NMGLatLng? = nil, selectImage: UIImage, deselectImage: UIImage) { + self.selectImage = NMFOverlayImage(image: selectImage) + self.deselectImage = NMFOverlayImage(image: deselectImage) super.init() + self.unselectedGlobalZIndex = globalZIndex if let position = position { self.position = position } - setUI(type: certificationType) + self.iconImage = self.deselectImage } } -private extension Marker { +extension Marker { - func setUI(type: CertificationType) { - var icon = NMFOverlayImage() - if isSelected { - switch type { - case .goodPrice: - icon = NMFOverlayImage(image: UIImage.markerGoodPriceSelected) - case .exemplary: - icon = NMFOverlayImage(image: UIImage.markerExemplarySelected) - case .safe: - icon = NMFOverlayImage(image: UIImage.markerSafeSelected) - } - self.globalZIndex = 250000 - } else { - switch type { - case .goodPrice: - icon = NMFOverlayImage(image: UIImage.markerGoodPriceNormal) - case .exemplary: - icon = NMFOverlayImage(image: UIImage.markerExemplaryNormal) - case .safe: - icon = NMFOverlayImage(image: UIImage.markerSafeNormal) - } - self.globalZIndex = unselectedGlobalZIndex - } - self.iconImage = icon + func select() { + self.iconImage = selectImage + self.globalZIndex = 250000 + } + + func deselect() { + self.iconImage = deselectImage + guard let zIndex = unselectedGlobalZIndex else { return } + self.globalZIndex = zIndex } } diff --git a/KCS/KCS/Presentation/Home/View/MoreStoreButton.swift b/KCS/KCS/Presentation/Home/View/MoreStoreButton.swift new file mode 100644 index 00000000..8fc3c5ca --- /dev/null +++ b/KCS/KCS/Presentation/Home/View/MoreStoreButton.swift @@ -0,0 +1,51 @@ +// +// MoreStoreButton.swift +// KCS +// +// Created by ๊น€์˜ํ˜„ on 2/4/24. +// + +import UIKit + +final class MoreStoreButton: UIButton { + + override var isEnabled: Bool { + didSet { + if isEnabled { + configuration?.baseForegroundColor = .black + } else { + configuration?.baseForegroundColor = .grayLabel + } + } + } + + init() { + super.init(frame: .zero) + setUI() + setLayerShadow(shadowOffset: CGSize(width: 0, height: 2)) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setFetchCount(fetchCount: FetchCountContent) { + var titleAttribute = AttributedString(String(format: "๊ฒฐ๊ณผ ๋”๋ณด๊ธฐ %d/%d", fetchCount.fetchCount, fetchCount.maxFetchCount)) + titleAttribute.font = UIFont.pretendard(size: 10, weight: .medium) + configuration?.attributedTitle = titleAttribute + } + +} + +private extension MoreStoreButton { + + func setUI() { + var config = UIButton.Configuration.filled() + config.background.backgroundColor = .white + config.baseForegroundColor = .black + config.cornerStyle = .capsule + config.contentInsets = NSDirectionalEdgeInsets(top: 11, leading: 10, bottom: 11, trailing: 10) + configuration = config + } + +} diff --git a/KCS/KCS/Presentation/Home/View/RefreshButton.swift b/KCS/KCS/Presentation/Home/View/RefreshButton.swift index 7f6d5901..834b7fad 100644 --- a/KCS/KCS/Presentation/Home/View/RefreshButton.swift +++ b/KCS/KCS/Presentation/Home/View/RefreshButton.swift @@ -9,6 +9,15 @@ import UIKit final class RefreshButton: UIButton { + private var animationTimer: Timer? + + private let originalTitle: AttributedString = { + var titleAttribute = AttributedString("ํ˜„ ์ง€๋„์—์„œ ๊ฒ€์ƒ‰") + titleAttribute.font = UIFont.pretendard(size: 10, weight: .medium) + + return titleAttribute + }() + init() { super.init(frame: .zero) setUI() @@ -19,16 +28,44 @@ final class RefreshButton: UIButton { fatalError("init(coder:) has not been implemented") } + func animationFire() { + isUserInteractionEnabled = false + var imageIndex = 0 + var config = configuration + config?.attributedTitle = nil + animationTimer?.invalidate() + animationTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in + guard let self = self else { return } + let images = [ + UIImage.refreshAnimation1, + UIImage.refreshAnimation2, + UIImage.refreshAnimation3, + UIImage.refreshAnimation4, + UIImage.refreshAnimation5 + ] + config?.image = images[imageIndex] + self.configuration = config + + imageIndex = (imageIndex + 1) % 5 + } + } + + func animationInvalidate() { + isUserInteractionEnabled = true + isHidden = true + animationTimer?.invalidate() + + configuration?.attributedTitle = originalTitle + configuration?.image = SystemImage.refresh?.withTintColor(.primary3, renderingMode: .alwaysOriginal) + } + } private extension RefreshButton { func setUI() { - var titleAttribute = AttributedString("ํ˜„ ์ง€๋„์—์„œ ๊ฒ€์ƒ‰") - titleAttribute.font = UIFont.pretendard(size: 10, weight: .medium) - var config = UIButton.Configuration.filled() - config.attributedTitle = titleAttribute + config.attributedTitle = originalTitle config.baseBackgroundColor = .white config.baseForegroundColor = .black config.image = SystemImage.refresh?.withTintColor(.primary3, renderingMode: .alwaysOriginal) diff --git a/KCS/KCS/Presentation/Home/View/SearchBarView.swift b/KCS/KCS/Presentation/Home/View/SearchBarView.swift new file mode 100644 index 00000000..d6ec6c3c --- /dev/null +++ b/KCS/KCS/Presentation/Home/View/SearchBarView.swift @@ -0,0 +1,58 @@ +// +// SearchBarView.swift +// KCS +// +// Created by ์กฐ์„ฑ๋ฏผ on 2/9/24. +// + +import UIKit + +final class SearchBarView: UIView { + + private let searchingKeywordLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + + return label + }() + + func setSearchKeyword(keyword: String) { + searchingKeywordLabel.text = keyword + } + + func getSearchKeyword() -> String? { + return searchingKeywordLabel.text + } + + init() { + super.init(frame: .zero) + + addUIComponents() + configureConstraints() + setup() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +private extension SearchBarView { + + func setup() { + backgroundColor = .white + } + + func addUIComponents() { + addSubview(searchingKeywordLabel) + } + + func configureConstraints() { + NSLayoutConstraint.activate([ + searchingKeywordLabel.centerYAnchor.constraint(equalTo: centerYAnchor), + searchingKeywordLabel.centerXAnchor.constraint(equalTo: centerXAnchor) + ]) + } + +} diff --git a/KCS/KCS/Presentation/Home/ViewModel/HomeDependency.swift b/KCS/KCS/Presentation/Home/ViewModel/HomeDependency.swift index 6b09372c..c3f24d9f 100644 --- a/KCS/KCS/Presentation/Home/ViewModel/HomeDependency.swift +++ b/KCS/KCS/Presentation/Home/ViewModel/HomeDependency.swift @@ -5,8 +5,17 @@ // Created by ์กฐ์„ฑ๋ฏผ on 1/15/24. // -import Foundation +import RxSwift struct HomeDependency { + let disposeBag = DisposeBag() + var activatedFilter: [CertificationType] = [] + var fetchCount: Int = 1 + var maxFetchCount: Int = 1 + + mutating func resetFetchCount() { + fetchCount = 1 + } + } diff --git a/KCS/KCS/Presentation/Home/ViewModel/HomeViewModelImpl.swift b/KCS/KCS/Presentation/Home/ViewModel/HomeViewModelImpl.swift index fc819bc5..3f4a6a57 100644 --- a/KCS/KCS/Presentation/Home/ViewModel/HomeViewModelImpl.swift +++ b/KCS/KCS/Presentation/Home/ViewModel/HomeViewModelImpl.swift @@ -7,47 +7,72 @@ import RxRelay import RxSwift +import CoreLocation +import NMapsMap final class HomeViewModelImpl: HomeViewModel { let fetchRefreshStoresUseCase: FetchRefreshStoresUseCase let fetchStoresUseCase: FetchStoresUseCase let getStoreInformationUseCase: GetStoreInformationUseCase + let fetchSearchStoresUseCase: FetchSearchStoresUseCase - private let disposeBag = DisposeBag() + let getStoreInformationOutput = PublishRelay() + let refreshDoneOutput = PublishRelay() + let locationButtonOutput = PublishRelay() + let locationButtonImageNameOutput = PublishRelay() + let setMarkerOutput = PublishRelay() + let locationAuthorizationStatusDeniedOutput = PublishRelay() + let locationStatusNotDeterminedOutput = PublishRelay() + let locationStatusAuthorizedWhenInUse = PublishRelay() + let errorAlertOutput = PublishRelay() + let filteredStoresOutput = PublishRelay<[FilteredStores]>() + let fetchCountOutput = PublishRelay() + let noMoreStoresOutput = PublishRelay() + let dimViewTapGestureEndedOutput = PublishRelay() + let searchStoresOutput = PublishRelay<[Store]>() + let searchOneStoreOutput = PublishRelay() - var getStoreInformationOutput = PublishRelay() - var refreshOutput = PublishRelay<[FilteredStores]>() - - let dependency: HomeDependency + var dependency: HomeDependency init( dependency: HomeDependency, fetchRefreshStoresUseCase: FetchRefreshStoresUseCase, fetchStoresUseCase: FetchStoresUseCase, - getStoreInformationUseCase: GetStoreInformationUseCase + getStoreInformationUseCase: GetStoreInformationUseCase, + fetchSearchStoresUseCase: FetchSearchStoresUseCase ) { self.dependency = dependency self.fetchRefreshStoresUseCase = fetchRefreshStoresUseCase self.fetchStoresUseCase = fetchStoresUseCase self.getStoreInformationUseCase = getStoreInformationUseCase + self.fetchSearchStoresUseCase = fetchSearchStoresUseCase } func action(input: HomeViewModelInputCase) { - do { - switch input { - case .refresh(let requestLocation, let filters): - refresh( - requestLocation: requestLocation, - filters: filters - ) - case .fetchFilteredStores(let filters): - fetchFilteredStores(filters: filters) - case .markerTapped(let tag): - try markerTapped(tag: tag) - } - } catch { - print(error.localizedDescription) + switch input { + case .refresh(let requestLocation, let isEntire): + refresh(requestLocation: requestLocation, isEntire: isEntire) + case .moreStoreButtonTapped: + moreStoreButtonTapped() + case .filterButtonTapped(let filter): + filterButtonTapped(filter: filter) + case .markerTapped(let tag): + markerTapped(tag: tag) + case .locationButtonTapped(let locationAuthorizationStatus, let positionMode): + locationButtonTapped(locationAuthorizationStatus: locationAuthorizationStatus, positionMode: positionMode) + case .dimViewTapGestureEnded: + dimViewTapGestureEnded() + case .setMarker(let store, let certificationType): + setMarker(store: store, certificationType: certificationType) + case .checkLocationAuthorization(let status): + checkLocationAuthorization(status: status) + case .checkLocationAuthorizationWhenCameraDidChange(let status): + checkLocationAuthorizationWhenCameraDidChange(status: status) + case .search(let location, let keyword): + search(location: location, keyword: keyword) + case .resetFilters: + resetFilters() } } @@ -57,24 +82,65 @@ private extension HomeViewModelImpl { func refresh( requestLocation: RequestLocation, - filters: [CertificationType] = [.goodPrice, .exemplary, .safe] + isEntire: Bool ) { fetchRefreshStoresUseCase.execute( - requestLocation: requestLocation + requestLocation: requestLocation, + isEntire: isEntire ) .subscribe( - onNext: { [weak self] stores in - self?.applyFilters(stores: stores, filters: filters) + onNext: { [weak self] refreshContent in + guard let self = self else { return } + dependency.resetFetchCount() + dependency.maxFetchCount = refreshContent.fetchCountContent.maxFetchCount + applyFilters(stores: refreshContent.stores, filters: getActivatedTypes()) + fetchCountOutput.accept(FetchCountContent(maxFetchCount: dependency.maxFetchCount)) + refreshDoneOutput.accept(isEntire) + checkLastFetch() }, - onError: { error in - print(error.localizedDescription) + onError: { [weak self] error in + if error is StoreRepositoryError { + self?.errorAlertOutput.accept(.client) + } else { + guard let error = error as? ErrorAlertMessage else { return } + self?.errorAlertOutput.accept(error) + self?.refreshDoneOutput.accept(true) + } } ) - .disposed(by: disposeBag) + .disposed(by: dependency.disposeBag) + } + + func moreStoreButtonTapped() { + if dependency.fetchCount < dependency.maxFetchCount { + dependency.fetchCount += 1 + applyFilters(stores: fetchStoresUseCase.execute(fetchCount: dependency.fetchCount), filters: getActivatedTypes()) + fetchCountOutput.accept(FetchCountContent(maxFetchCount: dependency.maxFetchCount, fetchCount: dependency.fetchCount)) + } + checkLastFetch() + } + + func checkLastFetch() { + if dependency.fetchCount == dependency.maxFetchCount { + noMoreStoresOutput.accept(()) + } + } + + func filterButtonTapped(filter: CertificationType) { + if let lastIndex = dependency.activatedFilter.lastIndex(of: filter) { + dependency.activatedFilter.remove(at: lastIndex) + } else { + dependency.activatedFilter.append(filter) + } + applyFilters(stores: fetchStoresUseCase.execute(fetchCount: dependency.fetchCount), filters: getActivatedTypes()) } - func fetchFilteredStores(filters: [CertificationType]) { - applyFilters(stores: fetchStoresUseCase.execute(), filters: filters) + func getActivatedTypes() -> [CertificationType] { + if dependency.activatedFilter.isEmpty { + return [.safe, .exemplary, .goodPrice] + } + + return dependency.activatedFilter } func applyFilters(stores: [Store], filters: [CertificationType]) { @@ -109,13 +175,109 @@ private extension HomeViewModelImpl { } } } - refreshOutput.accept([goodPriceStores, exemplaryStores, safeStores]) + filteredStoresOutput.accept([goodPriceStores, exemplaryStores, safeStores]) } - func markerTapped(tag: UInt) throws { - getStoreInformationOutput.accept( - try getStoreInformationUseCase.execute(tag: tag) - ) + func markerTapped(tag: UInt) { + do { + getStoreInformationOutput.accept( + try getStoreInformationUseCase.execute(tag: tag) + ) + } catch { + errorAlertOutput.accept(.client) + } + } + + func setMarker(store: Store, certificationType: CertificationType) { + switch certificationType { + case .goodPrice: + setMarkerOutput.accept( + MarkerContents( + tag: store.id, + location: store.location, + deselectImageName: "MarkerGoodPriceNormal", + selectImageName: "MarkerGoodPriceSelected" + ) + ) + case .exemplary: + setMarkerOutput.accept( + MarkerContents( + tag: store.id, + location: store.location, + deselectImageName: "MarkerExemplaryNormal", + selectImageName: "MarkerExemplarySelected" + ) + ) + case .safe: + setMarkerOutput.accept( + MarkerContents( + tag: store.id, + location: store.location, + deselectImageName: "MarkerSafeNormal", + selectImageName: "MarkerSafeSelected" + ) + ) + } + } + + func locationButtonTapped(locationAuthorizationStatus: CLAuthorizationStatus, positionMode: NMFMyPositionMode) { + if locationAuthorizationStatus == .denied { + locationAuthorizationStatusDeniedOutput.accept(()) + } + switch positionMode { + case .direction: + locationButtonOutput.accept(.compass) + case .compass, .normal: + locationButtonOutput.accept(.direction) + default: + break + } + } + + func dimViewTapGestureEnded() { + dimViewTapGestureEndedOutput.accept(()) + } + + func checkLocationAuthorization(status: CLAuthorizationStatus) { + switch status { + case .notDetermined: + locationStatusNotDeterminedOutput.accept(()) + case .authorizedWhenInUse: + locationStatusAuthorizedWhenInUse.accept(()) + default: + break + } + } + + func checkLocationAuthorizationWhenCameraDidChange(status: CLAuthorizationStatus) { + switch status { + case .denied, .restricted, .notDetermined: + locationButtonImageNameOutput.accept("LocationButtonNone") + case .authorizedWhenInUse: + locationButtonImageNameOutput.accept("LocationButtonNormal") + default: + break + } + } + + func search(location: Location, keyword: String) { + fetchSearchStoresUseCase.execute(location: location, keyword: keyword) + .subscribe(onNext: { [weak self] stores in + guard let self = self else { return } + dependency.resetFetchCount() + dependency.maxFetchCount = 1 + if stores.count == 1 { + guard let oneStore = stores.first else { return } + searchOneStoreOutput.accept(oneStore) + } else { + searchStoresOutput.accept(stores) + } + }) + .disposed(by: dependency.disposeBag) + } + + func resetFilters() { + dependency.activatedFilter = [] } } diff --git a/KCS/KCS/Presentation/Home/ViewModel/StoreInformationViewModelImpl.swift b/KCS/KCS/Presentation/Home/ViewModel/StoreInformationViewModelImpl.swift deleted file mode 100644 index 73758c9f..00000000 --- a/KCS/KCS/Presentation/Home/ViewModel/StoreInformationViewModelImpl.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// StoreInformationViewModelImpl.swift -// KCS -// -// Created by ๊น€์˜ํ˜„ on 1/18/24. -// - -import RxSwift -import RxRelay - -final class StoreInformationViewModelImpl: StoreInformationViewModel { - - private let disposeBag = DisposeBag() - - let getOpenClosedUseCase: GetOpenClosedUseCase - let fetchImageUseCase: FetchImageUseCase - - var openClosedOutput = PublishRelay() - var thumbnailImageOutput = PublishRelay() - - init(getOpenClosedUseCase: GetOpenClosedUseCase, fetchImageUseCase: FetchImageUseCase) { - self.getOpenClosedUseCase = getOpenClosedUseCase - self.fetchImageUseCase = fetchImageUseCase - } - - func action(input: StoreInformationViewInputCase) { - switch input { - case .setInformationView(let openingHour, let url): - setOpenClosed(openingHour: openingHour) - if let url = url { - fetchThumbnailImage(url: url) - } - } - } - -} - -private extension StoreInformationViewModelImpl { - - func setOpenClosed( - openingHour: [RegularOpeningHours] - ) { - openClosedOutput.accept(getOpenClosedUseCase.execute(openingHours: openingHour)) - } - - func fetchThumbnailImage(url: String) { - fetchImageUseCase.execute(url: url) - .subscribe( - onNext: { [weak self] imageData in - self?.thumbnailImageOutput.accept(imageData) - }, - onError: { error in - print(error.localizedDescription) - } - ) - .disposed(by: disposeBag) - } - -} diff --git a/KCS/KCS/Presentation/Home/ViewModel/protocol/HomeViewModel.swift b/KCS/KCS/Presentation/Home/ViewModel/protocol/HomeViewModel.swift index 9d647d41..f3421517 100644 --- a/KCS/KCS/Presentation/Home/ViewModel/protocol/HomeViewModel.swift +++ b/KCS/KCS/Presentation/Home/ViewModel/protocol/HomeViewModel.swift @@ -6,6 +6,7 @@ // import RxCocoa +import NMapsMap protocol HomeViewModel: HomeViewModelInput, HomeViewModelOutput { @@ -14,28 +15,31 @@ protocol HomeViewModel: HomeViewModelInput, HomeViewModelOutput { var fetchRefreshStoresUseCase: FetchRefreshStoresUseCase { get } var fetchStoresUseCase: FetchStoresUseCase { get } var getStoreInformationUseCase: GetStoreInformationUseCase { get } + var fetchSearchStoresUseCase: FetchSearchStoresUseCase { get } init( dependency: HomeDependency, fetchRefreshStoresUseCase: FetchRefreshStoresUseCase, fetchStoresUseCase: FetchStoresUseCase, - getStoreInformationUseCase: GetStoreInformationUseCase + getStoreInformationUseCase: GetStoreInformationUseCase, + fetchSearchStoresUseCase: FetchSearchStoresUseCase ) } enum HomeViewModelInputCase { - case refresh( - requestLocation: RequestLocation, - filters: [CertificationType] - ) - case fetchFilteredStores( - filters: [CertificationType] - ) - case markerTapped( - tag: UInt - ) + case refresh(requestLocation: RequestLocation, isEntire: Bool = false) + case moreStoreButtonTapped + case filterButtonTapped(activatedFilter: CertificationType) + case markerTapped(tag: UInt) + case locationButtonTapped(locationAuthorizationStatus: CLAuthorizationStatus, positionMode: NMFMyPositionMode) + case dimViewTapGestureEnded + case setMarker(store: Store, certificationType: CertificationType) + case checkLocationAuthorization(status: CLAuthorizationStatus) + case checkLocationAuthorizationWhenCameraDidChange(status: CLAuthorizationStatus) + case search(location: Location, keyword: String) + case resetFilters } @@ -48,6 +52,19 @@ protocol HomeViewModelInput { protocol HomeViewModelOutput { var getStoreInformationOutput: PublishRelay { get } - var refreshOutput: PublishRelay<[FilteredStores]> { get } + var refreshDoneOutput: PublishRelay { get } + var filteredStoresOutput: PublishRelay<[FilteredStores]> { get } + var locationButtonOutput: PublishRelay { get } + var locationButtonImageNameOutput: PublishRelay { get } + var setMarkerOutput: PublishRelay { get } + var locationAuthorizationStatusDeniedOutput: PublishRelay { get } + var locationStatusNotDeterminedOutput: PublishRelay { get } + var locationStatusAuthorizedWhenInUse: PublishRelay { get } + var errorAlertOutput: PublishRelay { get } + var fetchCountOutput: PublishRelay { get } + var noMoreStoresOutput: PublishRelay { get } + var dimViewTapGestureEndedOutput: PublishRelay { get } + var searchStoresOutput: PublishRelay<[Store]> { get } + var searchOneStoreOutput: PublishRelay { get } } diff --git a/KCS/KCS/Presentation/OnBoarding/FifthOnboardingView.swift b/KCS/KCS/Presentation/OnBoarding/FifthOnboardingView.swift new file mode 100644 index 00000000..72f6e57d --- /dev/null +++ b/KCS/KCS/Presentation/OnBoarding/FifthOnboardingView.swift @@ -0,0 +1,82 @@ +// +// FifthOnboardingView.swift +// KCS +// +// Created by ๊น€์˜ํ˜„ on 2/6/24. +// + +import UIKit + +final class FifthOnboardingView: UIView { + + private let topLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = "๋‚˜์ธ๊ฐ€ ์‹œ์ž‘ํ•˜๊ธฐ" + label.font = UIFont.pretendard(size: 24, weight: .bold) + label.textColor = .primary1 + label.textAlignment = .center + + return label + }() + + private let centerImageView: UIImageView = { + let imageView = UIImageView(image: UIImage.onboarding5) + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .scaleAspectFit + + return imageView + }() + + private let bottomLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = "์ด์   ์ •๋ง ๋‚˜์ธ๊ฐ€์™€ ํ•จ๊ป˜ ํ•  ์‹œ๊ฐ„์ด์—์š”!" + label.font = UIFont.pretendard(size: 19, weight: .medium) + label.textColor = .black + label.textAlignment = .center + + return label + }() + + init() { + super.init(frame: .zero) + + translatesAutoresizingMaskIntoConstraints = false + addUIComponents() + configureConstraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +private extension FifthOnboardingView { + + func addUIComponents() { + addSubview(topLabel) + addSubview(centerImageView) + addSubview(bottomLabel) + } + + func configureConstraints() { + NSLayoutConstraint.activate([ + topLabel.centerXAnchor.constraint(equalTo: centerXAnchor), + topLabel.topAnchor.constraint(equalTo: topAnchor) + ]) + + NSLayoutConstraint.activate([ + centerImageView.centerXAnchor.constraint(equalTo: centerXAnchor), + centerImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 83), + centerImageView.topAnchor.constraint(equalTo: topLabel.bottomAnchor, constant: 73) + ]) + + NSLayoutConstraint.activate([ + bottomLabel.centerXAnchor.constraint(equalTo: centerXAnchor), + bottomLabel.topAnchor.constraint(equalTo: centerImageView.bottomAnchor, constant: 93) + ]) + } + +} diff --git a/KCS/KCS/Presentation/OnBoarding/FirstOnboardingView.swift b/KCS/KCS/Presentation/OnBoarding/FirstOnboardingView.swift new file mode 100644 index 00000000..a8e1e479 --- /dev/null +++ b/KCS/KCS/Presentation/OnBoarding/FirstOnboardingView.swift @@ -0,0 +1,84 @@ +// +// FirstOnboardingView.swift +// KCS +// +// Created by ๊น€์˜ํ˜„ on 2/6/24. +// + +import UIKit + +final class FirstOnboardingView: UIView { + + private let topLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = "์ธ์ฆ์ œ ๋ณ„ ๊ฐ€๊ฒŒ ์œ„์น˜๋ฅผ\n ์ง€๋„๋กœ ํ•œ ๋ˆˆ์— ์•Œ์•„๋ด์š”!" + label.font = UIFont.pretendard(size: 24, weight: .bold) + label.textColor = .primary1 + label.textAlignment = .center + label.numberOfLines = 2 + + return label + }() + + private let centerImageView: UIImageView = { + let imageView = UIImageView(image: UIImage.onboarding1) + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .scaleAspectFit + + return imageView + }() + + private let bottomLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = "์—ฌ๊ธฐ์„œ ์ž ๊น,\n ์ธ์ฆ์ œ์— ๋Œ€ํ•ด ์„ค๋ช…ํ•ด๋“œ๋ฆด๊ฒŒ์š”" + label.font = UIFont.pretendard(size: 19, weight: .medium) + label.textColor = .black + label.textAlignment = .center + label.numberOfLines = 2 + + return label + }() + + init() { + super.init(frame: .zero) + + translatesAutoresizingMaskIntoConstraints = false + addUIComponents() + configureConstraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +private extension FirstOnboardingView { + + func addUIComponents() { + addSubview(topLabel) + addSubview(centerImageView) + addSubview(bottomLabel) + } + + func configureConstraints() { + NSLayoutConstraint.activate([ + topLabel.centerXAnchor.constraint(equalTo: centerXAnchor), + topLabel.topAnchor.constraint(equalTo: topAnchor) + ]) + + NSLayoutConstraint.activate([ + centerImageView.centerXAnchor.constraint(equalTo: centerXAnchor), + centerImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 83), + centerImageView.topAnchor.constraint(equalTo: topLabel.bottomAnchor, constant: 79) + ]) + + NSLayoutConstraint.activate([ + bottomLabel.centerXAnchor.constraint(equalTo: centerXAnchor), + bottomLabel.topAnchor.constraint(equalTo: centerImageView.bottomAnchor, constant: 52) + ]) + } + +} diff --git a/KCS/KCS/Presentation/OnBoarding/FourthOnboardingView.swift b/KCS/KCS/Presentation/OnBoarding/FourthOnboardingView.swift new file mode 100644 index 00000000..5b76e667 --- /dev/null +++ b/KCS/KCS/Presentation/OnBoarding/FourthOnboardingView.swift @@ -0,0 +1,91 @@ +// +// FourthOnboardingView.swift +// KCS +// +// Created by ๊น€์˜ํ˜„ on 2/6/24. +// + +import UIKit + +final class FourthOnboardingView: UIView { + + private let topLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = "์•ˆ์‹ฌ์‹๋‹น์ด๋ž€?" + label.font = UIFont.pretendard(size: 24, weight: .bold) + label.textColor = .primary1 + label.textAlignment = .center + + return label + }() + + private let centerImageView: UIImageView = { + let imageView = UIImageView(image: UIImage.onboarding4) + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .scaleAspectFit + + return imageView + }() + + private let bottomLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = UIFont.pretendard(size: 19, weight: .medium) + label.textColor = .black + label.textAlignment = .center + label.numberOfLines = 5 + + let text = "๊ฐ์—ผ๋ณ‘์— ์ทจ์•ฝํ•œ ์‹์‚ฌ๋ฌธํ™” ๊ฐœ์„ ์„ ์œ„ํ•ด\n๋œ์–ด๋จน๊ธฐ, ์œ„์ƒ์  ์ˆ˜์ €๊ด€๋ฆฌ, ์ข…์‚ฌ์ž ๋งˆ์Šคํฌ\n์ฐฉ์šฉ ๋ฐ ์ƒํ™œ ๋ฐฉ์—ญ์„ ์ค€์ˆ˜ํ•˜๋Š” ๊ณณ์œผ๋กœ\n์†Œ์žฌ์ง€ ์ง€์ž์ฒด์˜ ์ธ์ฆ์„\n๋ฐ›์€ ์Œ์‹์ ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค." + let attributeString = NSMutableAttributedString(string: text) + attributeString.addAttribute( + .font, + value: UIFont.pretendard(size: 19, weight: .heavy), + range: (text as NSString).range(of: "๋œ์–ด๋จน๊ธฐ, ์œ„์ƒ์  ์ˆ˜์ €๊ด€๋ฆฌ, ์ข…์‚ฌ์ž ๋งˆ์Šคํฌ\n์ฐฉ์šฉ ๋ฐ ์ƒํ™œ ๋ฐฉ์—ญ์„ ์ค€์ˆ˜ํ•˜๋Š” ๊ณณ") + ) + label.attributedText = attributeString + + return label + }() + + init() { + super.init(frame: .zero) + + translatesAutoresizingMaskIntoConstraints = false + addUIComponents() + configureConstraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +private extension FourthOnboardingView { + + func addUIComponents() { + addSubview(topLabel) + addSubview(centerImageView) + addSubview(bottomLabel) + } + + func configureConstraints() { + NSLayoutConstraint.activate([ + topLabel.centerXAnchor.constraint(equalTo: centerXAnchor), + topLabel.topAnchor.constraint(equalTo: topAnchor) + ]) + + NSLayoutConstraint.activate([ + centerImageView.centerXAnchor.constraint(equalTo: centerXAnchor), + centerImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 83), + centerImageView.topAnchor.constraint(equalTo: topLabel.bottomAnchor, constant: 60) + ]) + + NSLayoutConstraint.activate([ + bottomLabel.centerXAnchor.constraint(equalTo: centerXAnchor), + bottomLabel.topAnchor.constraint(equalTo: centerImageView.bottomAnchor, constant: 89) + ]) + } + +} diff --git a/KCS/KCS/Presentation/OnBoarding/OnboardingViewController.swift b/KCS/KCS/Presentation/OnBoarding/OnboardingViewController.swift new file mode 100644 index 00000000..0f81d3e5 --- /dev/null +++ b/KCS/KCS/Presentation/OnBoarding/OnboardingViewController.swift @@ -0,0 +1,176 @@ +// +// OnboardingViewController.swift +// KCS +// +// Created by ๊น€์˜ํ˜„ on 2/5/24. +// + +import UIKit +import RxSwift +import RxCocoa + +final class OnboardingViewController: UIViewController { + + private let disposeBag = DisposeBag() + + private let onboardingViews: [UIView] = [ + FirstOnboardingView(), + SecondOnboardingView(), + ThirdOnboardingView(), + FourthOnboardingView(), + FifthOnboardingView() + ] + + private lazy var onboardingScrollView: UIScrollView = { + let scrollView = UIScrollView() + scrollView.translatesAutoresizingMaskIntoConstraints = false + scrollView.contentSize = CGSize( + width: UIScreen.main.bounds.width * CGFloat(onboardingViews.count), + height: scrollView.bounds.height + ) + scrollView.isPagingEnabled = true + scrollView.showsHorizontalScrollIndicator = false + scrollView.showsVerticalScrollIndicator = false + scrollView.bounces = false + scrollView.delegate = self + + onboardingViews.forEach { view in + scrollView.addSubview(view) + } + + return scrollView + }() + + private lazy var pageControl: UIPageControl = { + let pageControl = UIPageControl() + pageControl.translatesAutoresizingMaskIntoConstraints = false + pageControl.pageIndicatorTintColor = .swipeBar + pageControl.currentPageIndicatorTintColor = .primary1 + pageControl.numberOfPages = onboardingViews.count + pageControl.currentPage = 0 + pageControl.isUserInteractionEnabled = false + + return pageControl + }() + + private lazy var startButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitle("์‹œ์ž‘ํ•˜๊ธฐ", for: .normal) + button.titleLabel?.font = UIFont.pretendard(size: 16, weight: .medium) + button.titleLabel?.textColor = .white + button.setLayerCorner(cornerRadius: 6) + button.backgroundColor = .primary1 + button.isHidden = true + button.rx.tap + .debounce(.milliseconds(100), scheduler: MainScheduler()) + .bind { [weak self] in + guard let self = self else { return } + UserDefaults.standard.set(false, forKey: "executeOnboarding") + homeViewController.modalPresentationStyle = UIModalPresentationStyle.fullScreen + present(homeViewController, animated: true) + } + .disposed(by: disposeBag) + + return button + }() + + private let homeViewController: HomeViewController + + init(homeViewController: HomeViewController) { + self.homeViewController = homeViewController + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + setup() + addUIComponents() + configureConstraints() + } + +} + +private extension OnboardingViewController { + + func setup() { + view.backgroundColor = .white + } + + func addUIComponents() { + view.addSubview(onboardingScrollView) + view.addSubview(pageControl) + view.addSubview(startButton) + } + + func configureConstraints() { + NSLayoutConstraint.activate([ + onboardingScrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 53), + onboardingScrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + onboardingScrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + onboardingScrollView.bottomAnchor.constraint(equalTo: pageControl.topAnchor, constant: 50) + ]) + + NSLayoutConstraint.activate([ + pageControl.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -99), + pageControl.centerXAnchor.constraint(equalTo: view.centerXAnchor) + ]) + + NSLayoutConstraint.activate([ + startButton.topAnchor.constraint(equalTo: pageControl.bottomAnchor, constant: 10), + startButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), + startButton.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16), + startButton.heightAnchor.constraint(equalToConstant: 50) + ]) + + NSLayoutConstraint.activate([ + onboardingViews[0].leadingAnchor.constraint(equalTo: onboardingScrollView.leadingAnchor), + onboardingViews[0].bottomAnchor.constraint(equalTo: onboardingScrollView.bottomAnchor), + onboardingViews[0].widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width) + ]) + + NSLayoutConstraint.activate([ + onboardingViews[1].leadingAnchor.constraint(equalTo: onboardingViews[0].trailingAnchor), + onboardingViews[1].bottomAnchor.constraint(equalTo: onboardingScrollView.bottomAnchor), + onboardingViews[1].widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width) + ]) + + NSLayoutConstraint.activate([ + onboardingViews[2].leadingAnchor.constraint(equalTo: onboardingViews[1].trailingAnchor), + onboardingViews[2].bottomAnchor.constraint(equalTo: onboardingScrollView.bottomAnchor), + onboardingViews[2].widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width) + ]) + + NSLayoutConstraint.activate([ + onboardingViews[3].leadingAnchor.constraint(equalTo: onboardingViews[2].trailingAnchor), + onboardingViews[3].bottomAnchor.constraint(equalTo: onboardingScrollView.bottomAnchor), + onboardingViews[3].widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width) + ]) + + NSLayoutConstraint.activate([ + onboardingViews[4].leadingAnchor.constraint(equalTo: onboardingViews[3].trailingAnchor), + onboardingViews[4].bottomAnchor.constraint(equalTo: onboardingScrollView.bottomAnchor), + onboardingViews[4].widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width) + ]) + } + +} + +extension OnboardingViewController: UIScrollViewDelegate { + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + pageControl.currentPage = Int(round(scrollView.contentOffset.x / scrollView.frame.size.width)) + + if pageControl.currentPage == onboardingViews.count - 1 { + startButton.isHidden = false + } else { + startButton.isHidden = true + } + } + +} diff --git a/KCS/KCS/Presentation/OnBoarding/SecondOnboardingView.swift b/KCS/KCS/Presentation/OnBoarding/SecondOnboardingView.swift new file mode 100644 index 00000000..e6d03642 --- /dev/null +++ b/KCS/KCS/Presentation/OnBoarding/SecondOnboardingView.swift @@ -0,0 +1,96 @@ +// +// SecondOnboardingView.swift +// KCS +// +// Created by ๊น€์˜ํ˜„ on 2/6/24. +// + +import UIKit + +final class SecondOnboardingView: UIView { + + private let topLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = "์ฐฉํ•œ ๊ฐ€๊ฒฉ ์—…์†Œ๋ž€?" + label.font = UIFont.pretendard(size: 24, weight: .bold) + label.textColor = .primary1 + label.textAlignment = .center + + return label + }() + + private let centerImageView: UIImageView = { + let imageView = UIImageView(image: UIImage.onboarding2) + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .scaleAspectFit + + return imageView + }() + + private let bottomLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = UIFont.pretendard(size: 19, weight: .medium) + label.textColor = .black + label.textAlignment = .center + label.numberOfLines = 4 + + let text = "2011๋…„๋ถ€ํ„ฐ ๋ฌผ๊ฐ€์•ˆ์ •์„ ์œ„ํ•ด\n๊ฐ€๊ฒฉ์ด ์ €๋ ดํ•˜์ง€๋งŒ ์–‘์งˆ์˜ ์„œ๋น„์Šค๋ฅผ\n์ œ๊ณตํ•˜๋Š” ๊ณณ์„ ์ •๋ถ€๊ฐ€ ์ง€์ •ํ•œ\n์šฐ๋ฆฌ ๋™๋„ค์˜ ์ข‹์€ ์—…์†Œ์ž…๋‹ˆ๋‹ค." + let attributeString = NSMutableAttributedString(string: text) + attributeString.addAttribute( + .font, + value: UIFont.pretendard(size: 19, weight: .heavy), + range: (text as NSString).range(of: "๊ฐ€๊ฒฉ์ด ์ €๋ ด") + ) + attributeString.addAttribute( + .font, + value: UIFont.pretendard(size: 19, weight: .heavy), + range: (text as NSString).range(of: "์–‘์งˆ์˜ ์„œ๋น„์Šค") + ) + label.attributedText = attributeString + + return label + }() + + init() { + super.init(frame: .zero) + + translatesAutoresizingMaskIntoConstraints = false + addUIComponents() + configureConstraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +private extension SecondOnboardingView { + + func addUIComponents() { + addSubview(topLabel) + addSubview(centerImageView) + addSubview(bottomLabel) + } + + func configureConstraints() { + NSLayoutConstraint.activate([ + topLabel.centerXAnchor.constraint(equalTo: centerXAnchor), + topLabel.topAnchor.constraint(equalTo: topAnchor) + ]) + + NSLayoutConstraint.activate([ + centerImageView.centerXAnchor.constraint(equalTo: centerXAnchor), + centerImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 83), + centerImageView.topAnchor.constraint(equalTo: topLabel.bottomAnchor, constant: 60) + ]) + + NSLayoutConstraint.activate([ + bottomLabel.centerXAnchor.constraint(equalTo: centerXAnchor), + bottomLabel.topAnchor.constraint(equalTo: centerImageView.bottomAnchor, constant: 89) + ]) + } + +} diff --git a/KCS/KCS/Presentation/OnBoarding/ThirdOnboardingView.swift b/KCS/KCS/Presentation/OnBoarding/ThirdOnboardingView.swift new file mode 100644 index 00000000..ca607033 --- /dev/null +++ b/KCS/KCS/Presentation/OnBoarding/ThirdOnboardingView.swift @@ -0,0 +1,91 @@ +// +// ThirdOnBoardingView.swift +// KCS +// +// Created by ๊น€์˜ํ˜„ on 2/6/24. +// + +import UIKit + +final class ThirdOnboardingView: UIView { + + private let topLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = "๋ชจ๋ฒ” ์Œ์‹์ ์ด๋ž€?" + label.font = UIFont.pretendard(size: 24, weight: .bold) + label.textColor = .primary1 + label.textAlignment = .center + + return label + }() + + private let centerImageView: UIImageView = { + let imageView = UIImageView(image: UIImage.onboarding3) + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .scaleAspectFit + + return imageView + }() + + private let bottomLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = UIFont.pretendard(size: 19, weight: .medium) + label.textColor = .black + label.textAlignment = .center + label.numberOfLines = 5 + + let text = "์‹ํ’ˆ์œ„์ƒ๋ฒ•์— ๊ทผ๊ฑฐํ•˜์—ฌ\n์œ„์ƒ๊ด€๋ฆฌ ์ƒํƒœ ๋“ฑ์ด ์šฐ์ˆ˜ํ•œ ์—…์†Œ๋ฅผ\n๋ชจ๋ฒ”์—…์†Œ๋กœ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.\n์„œ๋น„์Šค ์ˆ˜์ค€ ํ–ฅ์ƒ๊ณผ ์œ„์ƒ์  ๊ฐœ์„ ์„ ๋„๋ชจํ•˜๊ธฐ\n์œ„ํ•ด ์šด์˜๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค." + let attributeString = NSMutableAttributedString(string: text) + attributeString.addAttribute( + .font, + value: UIFont.pretendard(size: 19, weight: .heavy), + range: (text as NSString).range(of: "์œ„์ƒ๊ด€๋ฆฌ ์ƒํƒœ ๋“ฑ์ด ์šฐ์ˆ˜ํ•œ ์—…์†Œ") + ) + label.attributedText = attributeString + + return label + }() + + init() { + super.init(frame: .zero) + + translatesAutoresizingMaskIntoConstraints = false + addUIComponents() + configureConstraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +private extension ThirdOnboardingView { + + func addUIComponents() { + addSubview(topLabel) + addSubview(centerImageView) + addSubview(bottomLabel) + } + + func configureConstraints() { + NSLayoutConstraint.activate([ + topLabel.centerXAnchor.constraint(equalTo: centerXAnchor), + topLabel.topAnchor.constraint(equalTo: topAnchor) + ]) + + NSLayoutConstraint.activate([ + centerImageView.centerXAnchor.constraint(equalTo: centerXAnchor), + centerImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 83), + centerImageView.topAnchor.constraint(equalTo: topLabel.bottomAnchor, constant: 60) + ]) + + NSLayoutConstraint.activate([ + bottomLabel.centerXAnchor.constraint(equalTo: centerXAnchor), + bottomLabel.topAnchor.constraint(equalTo: centerImageView.bottomAnchor, constant: 67) + ]) + } + +} diff --git a/KCS/KCS/Presentation/Search/View/SearchViewController.swift b/KCS/KCS/Presentation/Search/View/SearchViewController.swift new file mode 100644 index 00000000..32704fc0 --- /dev/null +++ b/KCS/KCS/Presentation/Search/View/SearchViewController.swift @@ -0,0 +1,177 @@ +// +// SearchViewController.swift +// KCS +// +// Created by ์กฐ์„ฑ๋ฏผ on 2/8/24. +// + +import UIKit +import RxSwift +import RxRelay + +final class SearchViewController: UIViewController { + + private let disposeBag = DisposeBag() + + private lazy var backButton: UIBarButtonItem = { + let button = UIBarButtonItem(image: SystemImage.back, style: .plain, target: nil, action: nil) + button.tintColor = .primary3 + button.rx.tap + .bind { [weak self] _ in + self?.navigationController?.dismiss(animated: false) + } + .disposed(by: disposeBag) + + return button + }() + + private lazy var searchController: UISearchController = { + let searchController = UISearchController(searchResultsController: nil) + searchController.searchBar.placeholder = "๊ฒ€์ƒ‰์–ด๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”" + searchController.hidesNavigationBarDuringPresentation = false + searchController.searchResultsUpdater = self + searchController.obscuresBackgroundDuringPresentation = false + searchController.automaticallyShowsCancelButton = false + searchController.searchBar.delegate = self + + navigationItem.searchController = searchController + navigationItem.hidesSearchBarWhenScrolling = false + + return searchController + }() + + private lazy var searchTableView: UITableView = { + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.delegate = self + tableView.register(UITableViewCell.self, forCellReuseIdentifier: UITableViewCell.identifier) + tableView.backgroundColor = .white + // TODO: ๋””์ž์ธ์— ๋งž์ถฐ์•ผ ํ•จ + tableView.rowHeight = 50 + + return tableView + }() + + enum Section { + case keyword + } + + private lazy var dataSource: UITableViewDiffableDataSource = { + return UITableViewDiffableDataSource( + tableView: searchTableView + ) { (tableView, indexPath, keyword) in + let cell = tableView.dequeueReusableCell( + withIdentifier: UITableViewCell.identifier, + for: indexPath + ) + cell.selectionStyle = .none + var configuration = cell.defaultContentConfiguration() + configuration.text = keyword + cell.contentConfiguration = configuration + + return cell + } + }() + + private let searchObserver: PublishRelay + private let viewModel: SearchViewModel + + init(viewModel: SearchViewModel, searchObserver: PublishRelay) { + self.viewModel = viewModel + self.searchObserver = searchObserver + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + addUIComponents() + configureConstraints() + bind() + setup() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + searchController.searchBar.becomeFirstResponder() + } + + func setSearchKeyword(keyword: String?) { + searchController.searchBar.searchTextField.text = keyword + } + +} + +private extension SearchViewController { + + func setup() { + view.backgroundColor = .white + searchController.isActive = true + } + + func addUIComponents() { + view.addSubview(searchTableView) + navigationItem.setLeftBarButton(backButton, animated: true) + } + + func configureConstraints() { + NSLayoutConstraint.activate([ + searchTableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + searchTableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + searchTableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + searchTableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) + ]) + } + + func bind() { + viewModel.generateDataOutput + .bind { [weak self] data in + self?.generateData(data: data) + } + .disposed(by: disposeBag) + } + +} + +private extension SearchViewController { + + func generateData(data: [String]) { + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.keyword]) + snapshot.appendItems(data) + dataSource.apply(snapshot, animatingDifferences: false) + } +} + +extension SearchViewController: UISearchResultsUpdating, UISearchBarDelegate { + + func updateSearchResults(for searchController: UISearchController) { + guard let text = searchController.searchBar.text else { return } + viewModel.action(input: .textChanged(text: text)) + } + + func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { + guard let text = searchBar.text else { return } + + searchObserver.accept(text) + navigationController?.dismiss(animated: false) + } + +} + +extension SearchViewController: UITableViewDelegate { + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let text = dataSource.itemIdentifier(for: indexPath) else { return } + self.dismiss(animated: true) + searchObserver.accept(text) + navigationController?.dismiss(animated: false) + } + +} diff --git a/KCS/KCS/Presentation/Search/ViewModel/Protocol/SearchViewModel.swift b/KCS/KCS/Presentation/Search/ViewModel/Protocol/SearchViewModel.swift new file mode 100644 index 00000000..d3226d34 --- /dev/null +++ b/KCS/KCS/Presentation/Search/ViewModel/Protocol/SearchViewModel.swift @@ -0,0 +1,30 @@ +// +// SearchViewModel.swift +// KCS +// +// Created by ์กฐ์„ฑ๋ฏผ on 2/8/24. +// + +import RxRelay + +protocol SearchViewModel: SearchViewModelInput, SearchViewModelOutput { + +} + +protocol SearchViewModelInput { + + func action(input: SearchViewModelInputCase) + +} + +enum SearchViewModelInputCase { + + case textChanged(text: String) + +} + +protocol SearchViewModelOutput { + + var generateDataOutput: PublishRelay<[String]> { get } + +} diff --git a/KCS/KCS/Presentation/Search/ViewModel/SearchViewModelImpl.swift b/KCS/KCS/Presentation/Search/ViewModel/SearchViewModelImpl.swift new file mode 100644 index 00000000..07cc1900 --- /dev/null +++ b/KCS/KCS/Presentation/Search/ViewModel/SearchViewModelImpl.swift @@ -0,0 +1,33 @@ +// +// SearchViewModelImpl.swift +// KCS +// +// Created by ์กฐ์„ฑ๋ฏผ on 2/8/24. +// + +import RxRelay + +final class SearchViewModelImpl: SearchViewModel { + + var generateDataOutput = PublishRelay<[String]>() + + func action(input: SearchViewModelInputCase) { + switch input { + case .textChanged(let text): + textChanged(text: text) + } + } + +} + +private extension SearchViewModelImpl { + + func textChanged(text: String) { + if text.isEmpty { + // TODO: recentHistory usecase ์‹คํ–‰(debounce) ํ›„ generateDataOutput.accept([]) + } else { + // TODO: autoCompletion usecase ์‹คํ–‰(debounce) ํ›„ generateDataOutput.accept([]) + } + } + +} diff --git a/KCS/KCS/Presentation/Splash/View/SplashViewController.swift b/KCS/KCS/Presentation/Splash/View/SplashViewController.swift new file mode 100644 index 00000000..fe742596 --- /dev/null +++ b/KCS/KCS/Presentation/Splash/View/SplashViewController.swift @@ -0,0 +1,106 @@ +// +// SplashViewController.swift +// KCS +// +// Created by ๊น€์˜ํ˜„ on 2/8/24. +// + +import RxSwift +import RxRelay + +final class SplashViewController: UIViewController { + + let logoImageView: UIImageView = { + let imageView = UIImageView(image: UIImage.kcsLogo) + imageView.translatesAutoresizingMaskIntoConstraints = false + + return imageView + }() + + let disposeBag = DisposeBag() + let viewModel: SplashViewModel + let rootViewController: UIViewController + + init(viewModel: SplashViewModel, rootViewController: UIViewController) { + self.viewModel = viewModel + self.rootViewController = rootViewController + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + addUIComponents() + configureConstraints() + bind() + setup() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + viewModel.input(action: .checkNetworkInput) + } + +} + +private extension SplashViewController { + + func setup() { + view.backgroundColor = .white + } + + func bind() { + viewModel.networkEnableOutput + .bind { [weak self] in + guard let self = self else { return } + rootViewController.modalPresentationStyle = .fullScreen + present(rootViewController, animated: true) + } + .disposed(by: disposeBag) + + viewModel.networkDisableOutput + .bind { [weak self] in + self?.presentNetworkAlert() + } + .disposed(by: disposeBag) + } + + func presentNetworkAlert() { + let alertController = UIAlertController( + title: "๋„คํŠธ์›Œํฌ ์ƒํƒœ ํ™•์ธ", + message: "๋„คํŠธ์›Œํฌ๊ฐ€ ๋ถˆ์•ˆ์ • ํ•ฉ๋‹ˆ๋‹ค.", + preferredStyle: .alert + ) + let alertAction = UIAlertAction( + title: "๋‹ค์‹œ ์‹œ๋„", + style: .default + ) { [weak self] _ in + self?.viewModel.input(action: .checkNetworkInput) + } + alertController.addAction(alertAction) + present(alertController, animated: true) + } + +} + +private extension SplashViewController { + + func addUIComponents() { + view.addSubview(logoImageView) + } + + func configureConstraints() { + NSLayoutConstraint.activate([ + logoImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + logoImageView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + logoImageView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 100), + logoImageView.heightAnchor.constraint(equalTo: logoImageView.widthAnchor, multiplier: 94/156.45) + ]) + } + +} diff --git a/KCS/KCS/Presentation/Splash/ViewModel/SplashViewModelImpl.swift b/KCS/KCS/Presentation/Splash/ViewModel/SplashViewModelImpl.swift new file mode 100644 index 00000000..a53cf751 --- /dev/null +++ b/KCS/KCS/Presentation/Splash/ViewModel/SplashViewModelImpl.swift @@ -0,0 +1,40 @@ +// +// SplashViewModelImpl.swift +// KCS +// +// Created by ๊น€์˜ํ˜„ on 2/8/24. +// + +import RxRelay + +final class SplashViewModelImpl: SplashViewModel { + + let checkNetworkStatusUseCase: CheckNetworkStatusUseCase + + let networkEnableOutput = PublishRelay() + let networkDisableOutput = PublishRelay() + + init(checkNetworkStatusUseCase: CheckNetworkStatusUseCase) { + self.checkNetworkStatusUseCase = checkNetworkStatusUseCase + } + + func input(action: SplashViewModelInputCase) { + switch action { + case .checkNetworkInput: + checkNetworkInput() + } + } + +} + +private extension SplashViewModelImpl { + + func checkNetworkInput() { + if checkNetworkStatusUseCase.execute() { + networkEnableOutput.accept(()) + } else { + networkDisableOutput.accept(()) + } + } + +} diff --git a/KCS/KCS/Presentation/Splash/ViewModel/protocol/SplashViewModel.swift b/KCS/KCS/Presentation/Splash/ViewModel/protocol/SplashViewModel.swift new file mode 100644 index 00000000..66681f7b --- /dev/null +++ b/KCS/KCS/Presentation/Splash/ViewModel/protocol/SplashViewModel.swift @@ -0,0 +1,33 @@ +// +// SplashViewModel.swift +// KCS +// +// Created by ๊น€์˜ํ˜„ on 2/8/24. +// + +import RxRelay + +protocol SplashViewModel: SplashViewModelInput, SplashViewModelOutput { + + var checkNetworkStatusUseCase: CheckNetworkStatusUseCase { get } + +} + +protocol SplashViewModelInput { + + func input(action: SplashViewModelInputCase) + +} + +enum SplashViewModelInputCase { + + case checkNetworkInput + +} + +protocol SplashViewModelOutput { + + var networkEnableOutput: PublishRelay { get } + var networkDisableOutput: PublishRelay { get } + +} diff --git a/KCS/KCS/Presentation/Home/View/CertificationLabel.swift b/KCS/KCS/Presentation/StoreInformation/View/CertificationLabel.swift similarity index 96% rename from KCS/KCS/Presentation/Home/View/CertificationLabel.swift rename to KCS/KCS/Presentation/StoreInformation/View/CertificationLabel.swift index af6231ef..b9bc1a79 100644 --- a/KCS/KCS/Presentation/Home/View/CertificationLabel.swift +++ b/KCS/KCS/Presentation/StoreInformation/View/CertificationLabel.swift @@ -15,7 +15,7 @@ final class CertificationLabel: UIView { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = UIFont.pretendard(size: 9, weight: .medium) - label.textColor = UIColor.certificationLabelText + label.textColor = UIColor.grayLabel label.text = certificationType.description return label diff --git a/KCS/KCS/Presentation/StoreInformation/View/DetailView.swift b/KCS/KCS/Presentation/StoreInformation/View/DetailView.swift new file mode 100644 index 00000000..6c12c65b --- /dev/null +++ b/KCS/KCS/Presentation/StoreInformation/View/DetailView.swift @@ -0,0 +1,331 @@ +// +// DetailView.swift +// KCS +// +// Created by ์กฐ์„ฑ๋ฏผ on 1/24/24. +// + +import UIKit +import RxSwift +import RxRelay + +final class DetailView: UIView { + + private lazy var storeTitle: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = UIFont.pretendard(size: 22, weight: .bold) + label.textColor = UIColor.primary1 + label.numberOfLines = 2 + + return label + }() + + private lazy var category: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = UIFont.pretendard(size: 13, weight: .regular) + label.textColor = UIColor.grayLabel + + return label + }() + + private lazy var certificationStackView: UIStackView = { + let stackView = UIStackView() + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .horizontal + stackView.spacing = 4 + stackView.distribution = .fillProportionally + + return stackView + }() + + private let divideView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = UIColor.divideView + + return view + }() + + private let storeImageView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.setLayerCorner(cornerRadius: 6) + imageView.clipsToBounds = true + imageView.image = UIImage.basicStore + imageView.contentMode = .scaleAspectFill + + return imageView + }() + + private let clockIcon: UIImageView = { + let imageView = UIImageView(image: UIImage.clockIcon) + imageView.translatesAutoresizingMaskIntoConstraints = false + + return imageView + }() + + private lazy var storeOpenClosed: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = UIFont.pretendard(size: 13, weight: .regular) + label.textColor = .black + + return label + }() + + private lazy var openingHour: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = UIFont.pretendard(size: 12, weight: .regular) + label.textColor = UIColor.grayLabel + + return label + }() + + private let openingHoursStackView: UIStackView = { + let stackView = UIStackView() + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .vertical + stackView.spacing = 5 + stackView.alignment = .leading + stackView.distribution = .equalSpacing + + return stackView + }() + + private let phoneIcon: UIImageView = { + let imageView = UIImageView(image: UIImage.phoneIcon) + imageView.translatesAutoresizingMaskIntoConstraints = false + + return imageView + }() + + private let phoneNumber: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = UIFont.pretendard(size: 13, weight: .regular) + label.textColor = .black + + return label + }() + + private let addressIcon: UIImageView = { + let imageView = UIImageView(image: UIImage.addressIcon) + imageView.translatesAutoresizingMaskIntoConstraints = false + + return imageView + }() + + private let address: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = UIFont.pretendard(size: 13, weight: .regular) + label.textColor = .black + label.numberOfLines = 0 + + return label + }() + + private lazy var addressConstraint = address.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16) + private lazy var phoneNumberConstraint = phoneNumber.topAnchor.constraint(equalTo: openingHoursStackView.bottomAnchor, constant: 20) + + init() { + super.init(frame: .zero) + + setBackgroundColor() + addUIComponents() + configureConstraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +private extension DetailView { + + func setBackgroundColor() { + backgroundColor = .white + } + + func addUIComponents() { + addSubview(storeTitle) + addSubview(category) + addSubview(certificationStackView) + addSubview(divideView) + addSubview(storeImageView) + addSubview(clockIcon) + addSubview(storeOpenClosed) + addSubview(openingHour) + addSubview(openingHoursStackView) + addSubview(phoneIcon) + addSubview(phoneNumber) + addSubview(addressIcon) + addSubview(address) + } + + func configureConstraints() { + storeRepresentConstraints() + openingHourConstraints() + phoneConstraints() + addressConstraints() + } + + func storeRepresentConstraints() { + NSLayoutConstraint.activate([ + storeTitle.topAnchor.constraint(equalTo: topAnchor, constant: 27), + storeTitle.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), + storeTitle.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16) + ]) + + NSLayoutConstraint.activate([ + category.topAnchor.constraint(equalTo: storeTitle.bottomAnchor, constant: 4), + category.leadingAnchor.constraint(equalTo: storeTitle.leadingAnchor) + ]) + + NSLayoutConstraint.activate([ + certificationStackView.topAnchor.constraint(equalTo: category.bottomAnchor, constant: 9), + certificationStackView.leadingAnchor.constraint(equalTo: storeTitle.leadingAnchor) + ]) + + NSLayoutConstraint.activate([ + divideView.topAnchor.constraint(equalTo: certificationStackView.bottomAnchor, constant: 20), + divideView.leadingAnchor.constraint(equalTo: leadingAnchor), + divideView.trailingAnchor.constraint(equalTo: trailingAnchor), + divideView.heightAnchor.constraint(equalToConstant: 6) + ]) + + NSLayoutConstraint.activate([ + storeImageView.topAnchor.constraint(equalTo: divideView.bottomAnchor, constant: 16), + storeImageView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16), + storeImageView.widthAnchor.constraint(equalToConstant: 150), + storeImageView.heightAnchor.constraint(equalToConstant: 150) + ]) + } + + func openingHourConstraints() { + NSLayoutConstraint.activate([ + clockIcon.centerYAnchor.constraint(equalTo: storeOpenClosed.centerYAnchor), + clockIcon.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), + clockIcon.heightAnchor.constraint(equalToConstant: 14), + clockIcon.widthAnchor.constraint(equalToConstant: 14) + ]) + + NSLayoutConstraint.activate([ + storeOpenClosed.topAnchor.constraint(equalTo: divideView.bottomAnchor, constant: 16), + storeOpenClosed.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 40) + ]) + + NSLayoutConstraint.activate([ + openingHour.bottomAnchor.constraint(equalTo: storeOpenClosed.bottomAnchor), + openingHour.leadingAnchor.constraint(equalTo: storeOpenClosed.trailingAnchor, constant: 8) + ]) + + NSLayoutConstraint.activate([ + openingHoursStackView.topAnchor.constraint(equalTo: storeOpenClosed.bottomAnchor, constant: 8), + openingHoursStackView.leadingAnchor.constraint(equalTo: storeOpenClosed.leadingAnchor) + ]) + } + + func phoneConstraints() { + NSLayoutConstraint.activate([ + phoneIcon.centerYAnchor.constraint(equalTo: phoneNumber.centerYAnchor), + phoneIcon.leadingAnchor.constraint(equalTo: clockIcon.leadingAnchor), + phoneIcon.heightAnchor.constraint(equalToConstant: 14), + phoneIcon.widthAnchor.constraint(equalToConstant: 13) + ]) + + NSLayoutConstraint.activate([ + phoneNumber.leadingAnchor.constraint(equalTo: phoneIcon.trailingAnchor, constant: 11), + phoneNumberConstraint + ]) + } + + func addressConstraints() { + NSLayoutConstraint.activate([ + addressIcon.topAnchor.constraint(equalTo: address.topAnchor), + addressIcon.leadingAnchor.constraint(equalTo: clockIcon.leadingAnchor), + addressIcon.heightAnchor.constraint(equalToConstant: 16), + addressIcon.widthAnchor.constraint(equalToConstant: 11) + ]) + + NSLayoutConstraint.activate([ + address.topAnchor.constraint(equalTo: phoneNumber.bottomAnchor, constant: 20), + address.leadingAnchor.constraint(equalTo: addressIcon.trailingAnchor, constant: 13), + addressConstraint + ]) + } + + func setOpeningHourText(openClosedContent: OpenClosedContent) { + if openClosedContent.openClosedType == .none { + storeOpenClosed.text = "์˜์—…์‹œ๊ฐ„ ์ •๋ณด ์—†์Œ" + storeOpenClosed.textColor = .black + openingHour.text = openClosedContent.openClosedType.rawValue + addressConstraint.constant = -174 + phoneNumberConstraint.constant = 20 - 11 + } else { + storeOpenClosed.text = openClosedContent.openClosedType.description + storeOpenClosed.textColor = UIColor.goodPrice + openingHour.text = openClosedContent.nextOpeningHour + addressConstraint.constant = -16 + phoneNumberConstraint.constant = 20 + } + } + +} + +extension DetailView { + + func setUIContents(contents: DetailViewContents) { + storeTitle.text = contents.storeTitle + category.text = contents.category + contents.certificationTypes + .map({ + CertificationLabel(certificationType: $0) + }) + .forEach { [weak self] in + self?.certificationStackView.addArrangedSubview($0) + } + address.text = contents.address + phoneNumber.text = contents.phoneNumber + setOpeningHourText(openClosedContent: contents.openClosedContent) + + var detailOpeningHours = contents.detailOpeningHour + if detailOpeningHours.isEmpty { return } + let today = detailOpeningHours.removeFirst() + openingHoursStackView.addArrangedSubview( + OpeningHoursCellView( + weekday: today.weekDay, + openingHour: today.openingHour, + isToday: true + ) + ) + detailOpeningHours.forEach { [weak self] detailOpeningHour in + self?.openingHoursStackView.addArrangedSubview( + OpeningHoursCellView( + weekday: detailOpeningHour.weekDay, + openingHour: detailOpeningHour.openingHour + ) + ) + } + } + + func setThumbnailImage(imageData: Data) { + storeImageView.image = UIImage(data: imageData) + } + + func resetUIContents() { + storeTitle.text = nil + category.text = nil + address.text = nil + phoneNumber.text = nil + storeOpenClosed.text = nil + openingHour.text = nil + storeImageView.image = UIImage.basicStore + certificationStackView.clear() + openingHoursStackView.clear() + } +} diff --git a/KCS/KCS/Presentation/StoreInformation/View/OpeningHoursCellView.swift b/KCS/KCS/Presentation/StoreInformation/View/OpeningHoursCellView.swift new file mode 100644 index 00000000..47100d0a --- /dev/null +++ b/KCS/KCS/Presentation/StoreInformation/View/OpeningHoursCellView.swift @@ -0,0 +1,100 @@ +// +// OpeningHoursCellView.swift +// KCS +// +// Created by ๊น€์˜ํ˜„ on 1/25/24. +// + +import UIKit + +final class OpeningHoursCellView: UIView { + + private let weekday: Day + private let openingHour: OpeningHour + private let isToday: Bool + + private lazy var weekdayLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = weekday.description + label.textColor = .black + if isToday { + label.font = UIFont.pretendard(size: 13, weight: .medium) + } else { + label.font = UIFont.pretendard(size: 13, weight: .regular) + } + + return label + }() + + private lazy var openingHourLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = openingHour.openingHour + label.textColor = .black + if isToday { + label.font = UIFont.pretendard(size: 12, weight: .medium) + } else { + label.font = UIFont.pretendard(size: 12, weight: .regular) + } + + return label + }() + + private lazy var breakTimeLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = openingHour.breakTime + label.textColor = .black + if isToday { + label.font = UIFont.pretendard(size: 12, weight: .medium) + } else { + label.font = UIFont.pretendard(size: 12, weight: .regular) + } + + return label + }() + + init(weekday: Day, openingHour: OpeningHour, isToday: Bool = false) { + self.weekday = weekday + self.openingHour = openingHour + self.isToday = isToday + super.init(frame: .zero) + + addUIComponents() + configureConstraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +private extension OpeningHoursCellView { + + func addUIComponents() { + addSubview(weekdayLabel) + addSubview(openingHourLabel) + addSubview(breakTimeLabel) + } + + func configureConstraints() { + NSLayoutConstraint.activate([ + weekdayLabel.leadingAnchor.constraint(equalTo: leadingAnchor), + weekdayLabel.topAnchor.constraint(equalTo: topAnchor) + ]) + + NSLayoutConstraint.activate([ + openingHourLabel.bottomAnchor.constraint(equalTo: weekdayLabel.bottomAnchor), + openingHourLabel.leadingAnchor.constraint(equalTo: weekdayLabel.trailingAnchor, constant: 5) + ]) + + NSLayoutConstraint.activate([ + breakTimeLabel.topAnchor.constraint(equalTo: openingHourLabel.bottomAnchor), + breakTimeLabel.leadingAnchor.constraint(equalTo: openingHourLabel.leadingAnchor), + breakTimeLabel.bottomAnchor.constraint(equalTo: bottomAnchor) + ]) + } + +} diff --git a/KCS/KCS/Presentation/StoreInformation/View/StoreInformationViewController.swift b/KCS/KCS/Presentation/StoreInformation/View/StoreInformationViewController.swift new file mode 100644 index 00000000..98c06aea --- /dev/null +++ b/KCS/KCS/Presentation/StoreInformation/View/StoreInformationViewController.swift @@ -0,0 +1,153 @@ +// +// StoreInformationViewController.swift +// KCS +// +// Created by ์กฐ์„ฑ๋ฏผ on 2/3/24. +// + +import UIKit +import RxRelay +import RxSwift + +final class StoreInformationViewController: UIViewController { + + private let disposeBag = DisposeBag() + + private lazy var summaryView: SummaryView = { + let view = SummaryView( + summaryViewHeightObserver: summaryViewHeightObserver + ) + view.translatesAutoresizingMaskIntoConstraints = false + + return view + }() + + private let summaryViewHeightObserver: PublishRelay + + private lazy var detailView: DetailView = { + let view = DetailView() + view.translatesAutoresizingMaskIntoConstraints = false + + return view + }() + + private let viewModel: StoreInformationViewModel + + init( + summaryViewHeightObserver: PublishRelay, + viewModel: StoreInformationViewModel + ) { + self.summaryViewHeightObserver = summaryViewHeightObserver + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + bind() + } + + override func viewDidLoad() { + super.viewDidLoad() + + addUIComponents() + configureConstraints() + setup() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +extension StoreInformationViewController { + + func setup() { + view.backgroundColor = .white + isModalInPresentation = true + } + + func bind() { + viewModel.errorAlertOutput + .debounce(.milliseconds(100), scheduler: MainScheduler()) + .bind { [weak self] error in + self?.presentErrorAlert(error: error) + } + .disposed(by: disposeBag) + + viewModel.thumbnailImageOutput + .bind { [weak self] imageData in + self?.summaryView.setThumbnailImage(imageData: imageData) + self?.detailView.setThumbnailImage(imageData: imageData) + } + .disposed(by: disposeBag) + + viewModel.summaryCallButtonOutput + .debounce(.milliseconds(100), scheduler: MainScheduler()) + .bind { [weak self] phoneNumber in + self?.summaryView.setCallButton(phoneNumber: phoneNumber) + } + .disposed(by: disposeBag) + + viewModel.setSummaryUIContentsOutput + .bind { [weak self] contents in + self?.summaryView.setUIContents(contents: contents) + } + .disposed(by: disposeBag) + + viewModel.setDetailUIContentsOutput + .bind { [weak self] contents in + self?.detailView.setUIContents(contents: contents) + } + .disposed(by: disposeBag) + } + + func setUIContents(store: Store) { + summaryView.resetUIContents() + detailView.resetUIContents() + viewModel.action(input: .setUIContents(store: store)) + } + + func changeToSummary() { + summaryView.isUserInteractionEnabled = true + detailView.isUserInteractionEnabled = false + UIView.animate(withDuration: 0.3) { [weak self] in + self?.summaryView.alpha = 1 + self?.detailView.alpha = 0 + } + } + + func changeToDetail() { + summaryView.isUserInteractionEnabled = false + detailView.isUserInteractionEnabled = true + UIView.animate(withDuration: 0.3) { [weak self] in + self?.summaryView.alpha = 0 + self?.detailView.alpha = 1 + } + } + +} + +private extension StoreInformationViewController { + + func addUIComponents() { + view.addSubview(summaryView) + view.addSubview(detailView) + } + + func configureConstraints() { + + NSLayoutConstraint.activate([ + summaryView.topAnchor.constraint(equalTo: view.topAnchor), + summaryView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + summaryView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + summaryView.trailingAnchor.constraint(equalTo: view.trailingAnchor) + ]) + + NSLayoutConstraint.activate([ + detailView.topAnchor.constraint(equalTo: view.topAnchor), + detailView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + detailView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + detailView.trailingAnchor.constraint(equalTo: view.trailingAnchor) + ]) + + } + +} diff --git a/KCS/KCS/Presentation/Home/View/StoreInformationViewController.swift b/KCS/KCS/Presentation/StoreInformation/View/SummaryView.swift similarity index 52% rename from KCS/KCS/Presentation/Home/View/StoreInformationViewController.swift rename to KCS/KCS/Presentation/StoreInformation/View/SummaryView.swift index 61e3e2f2..e64766eb 100644 --- a/KCS/KCS/Presentation/Home/View/StoreInformationViewController.swift +++ b/KCS/KCS/Presentation/StoreInformation/View/SummaryView.swift @@ -1,5 +1,5 @@ // -// StoreInformationViewController.swift +// SummaryView.swift // KCS // // Created by ๊น€์˜ํ˜„ on 1/11/24. @@ -9,15 +9,13 @@ import UIKit import RxSwift import RxCocoa -final class StoreInformationViewController: UIViewController { - - private let disposeBag = DisposeBag() +final class SummaryView: UIView { private lazy var storeTitle: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = UIFont.pretendard(size: 22, weight: .bold) - label.textColor = UIColor.primary2 + label.textColor = UIColor.primary1 label.numberOfLines = 2 return label @@ -37,7 +35,7 @@ final class StoreInformationViewController: UIViewController { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = UIFont.pretendard(size: 13, weight: .regular) - label.textColor = UIColor.kcsGray + label.textColor = UIColor.grayLabel return label }() @@ -55,7 +53,7 @@ final class StoreInformationViewController: UIViewController { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = UIFont.pretendard(size: 13, weight: .regular) - label.textColor = UIColor.kcsGray + label.textColor = UIColor.grayLabel return label }() @@ -66,6 +64,7 @@ final class StoreInformationViewController: UIViewController { imageView.setLayerCorner(cornerRadius: 6) imageView.clipsToBounds = true imageView.image = UIImage.basicStore + imageView.contentMode = .scaleAspectFill return imageView }() @@ -74,6 +73,7 @@ final class StoreInformationViewController: UIViewController { var config = UIButton.Configuration.gray() config.image = SystemImage.phone config.cornerStyle = .capsule + config.baseForegroundColor = .primary3 let button = UIButton() button.translatesAutoresizingMaskIntoConstraints = false @@ -82,78 +82,46 @@ final class StoreInformationViewController: UIViewController { return button }() - private let dismissIndicatorView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = UIColor.swipeBar - view.layer.cornerRadius = 2 - - return view - }() + private var callDisposable: Disposable? - private let viewModel: StoreInformationViewModel - private let contentHeightObserver: PublishRelay - private let dismissObserver: PublishRelay + private let summaryViewHeightObserver: PublishRelay - init(viewModel: StoreInformationViewModel, contentHeightObserver: PublishRelay, dismissObserver: PublishRelay) { - self.viewModel = viewModel - self.contentHeightObserver = contentHeightObserver - self.dismissObserver = dismissObserver - super.init(nibName: nil, bundle: nil) + init(summaryViewHeightObserver: PublishRelay) { + self.summaryViewHeightObserver = summaryViewHeightObserver + super.init(frame: .zero) setBackgroundColor() addUIComponents() configureConstraints() - bind() - } - - override func viewDidDisappear(_ animated: Bool) { - dismissObserver.accept(()) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + } -private extension StoreInformationViewController { - - func bind() { - viewModel.thumbnailImageOutput - .subscribe(onNext: { [weak self] data in - self?.storeImageView.image = UIImage(data: data) - }) - .disposed(by: disposeBag) - - viewModel.openClosedOutput - .bind { [weak self] openClosedContent in - self?.storeOpenClosed.text = openClosedContent.openClosedType.rawValue - self?.openingHour.text = openClosedContent.openingHour - } - .disposed(by: disposeBag) - - } +private extension SummaryView { func setBackgroundColor() { - view.backgroundColor = .white + backgroundColor = .white } func addUIComponents() { - view.addSubview(storeTitle) - view.addSubview(certificationStackView) - view.addSubview(category) - view.addSubview(storeOpenClosed) - view.addSubview(openingHour) - view.addSubview(storeImageView) - view.addSubview(storeCallButton) - view.addSubview(dismissIndicatorView) + addSubview(storeTitle) + addSubview(certificationStackView) + addSubview(category) + addSubview(storeOpenClosed) + addSubview(openingHour) + addSubview(storeImageView) + addSubview(storeCallButton) } func configureConstraints() { NSLayoutConstraint.activate([ - storeTitle.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 27), - storeTitle.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16), - storeTitle.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -156) + storeTitle.topAnchor.constraint(equalTo: topAnchor, constant: 27), + storeTitle.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), + storeTitle.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -156) ]) NSLayoutConstraint.activate([ @@ -184,69 +152,65 @@ private extension StoreInformationViewController { ]) NSLayoutConstraint.activate([ - storeImageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 27), - storeImageView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16), + storeImageView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 27), + storeImageView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -16), storeImageView.widthAnchor.constraint(equalToConstant: 132), storeImageView.heightAnchor.constraint(equalToConstant: 132) ]) - - NSLayoutConstraint.activate([ - dismissIndicatorView.topAnchor.constraint(equalTo: view.topAnchor, constant: 8), - dismissIndicatorView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - dismissIndicatorView.widthAnchor.constraint(equalToConstant: 35), - dismissIndicatorView.heightAnchor.constraint(equalToConstant: 4) - ]) + + } + + func callButtonTapped(phoneNum: String) { + if let url = URL(string: "tel://" + "\(phoneNum.filter { $0.isNumber })") { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } } } -extension StoreInformationViewController { +extension SummaryView { - func setUIContents(store: Store) { - storeTitle.text = store.title - category.text = store.category - removeStackView() - store.certificationTypes + func setUIContents(contents: SummaryViewContents) { + storeTitle.text = contents.storeTitle + if storeTitle.numberOfVisibleLines == 1 { + summaryViewHeightObserver.accept(.small) + } else { + summaryViewHeightObserver.accept(.large) + } + storeOpenClosed.text = contents.openClosedContent.openClosedType.description + openingHour.text = contents.openClosedContent.nextOpeningHour + category.text = contents.category + contents.certificationTypes .map({ CertificationLabel(certificationType: $0) }) - .forEach { - certificationStackView.addArrangedSubview($0) + .forEach { [weak self] in + self?.certificationStackView.addArrangedSubview($0) } - if let phoneNum = store.phoneNumber { - storeCallButton.rx.tap - .bind { [weak self] _ in - self?.callButtonTapped(phoneNum: phoneNum) - } - .disposed(by: disposeBag) - } - viewModel.action(input: .setInformationView( - openingHour: store.openingHour, - url: store.localPhotos.first) - ) - if storeTitle.numberOfVisibleLines > 1 { - contentHeightObserver.accept(253) - } else { - contentHeightObserver.accept(230) - } } -} - -private extension StoreInformationViewController { + func setThumbnailImage(imageData: Data) { + storeImageView.image = UIImage(data: imageData) + } - func removeStackView() { - let subviews = certificationStackView.arrangedSubviews - certificationStackView.arrangedSubviews.forEach { - certificationStackView.removeArrangedSubview($0) - } - subviews.forEach { $0.removeFromSuperview() } + func setCallButton(phoneNumber: String) { + storeCallButton.isHidden = false + callDisposable = storeCallButton.rx.tap + .debounce(.milliseconds(100), scheduler: MainScheduler()) + .bind { [weak self] _ in + self?.callButtonTapped(phoneNum: phoneNumber) + } } - func callButtonTapped(phoneNum: String) { - if let url = URL(string: "tel://" + "\(phoneNum.filter { $0.isNumber })") { - UIApplication.shared.open(url, options: [:], completionHandler: nil) - } + func resetUIContents() { + storeTitle.text = nil + category.text = nil + storeOpenClosed.text = nil + openingHour.text = nil + certificationStackView.clear() + callDisposable?.dispose() + storeCallButton.isHidden = true + storeImageView.image = .basicStore } } diff --git a/KCS/KCS/Presentation/Home/ViewModel/protocol/StoreInformationViewModel.swift b/KCS/KCS/Presentation/StoreInformation/ViewModel/Protocol/StoreInformationViewModel.swift similarity index 51% rename from KCS/KCS/Presentation/Home/ViewModel/protocol/StoreInformationViewModel.swift rename to KCS/KCS/Presentation/StoreInformation/ViewModel/Protocol/StoreInformationViewModel.swift index 19bb156f..a226b0eb 100644 --- a/KCS/KCS/Presentation/Home/ViewModel/protocol/StoreInformationViewModel.swift +++ b/KCS/KCS/Presentation/StoreInformation/ViewModel/Protocol/StoreInformationViewModel.swift @@ -2,10 +2,9 @@ // StoreInformationViewModel.swift // KCS // -// Created by ๊น€์˜ํ˜„ on 1/18/24. +// Created by ์กฐ์„ฑ๋ฏผ on 2/3/24. // -import RxSwift import RxRelay protocol StoreInformationViewModel: StoreInformationViewModelInput, StoreInformationViewModelOutput { @@ -15,24 +14,24 @@ protocol StoreInformationViewModel: StoreInformationViewModelInput, StoreInforma } -enum StoreInformationViewInputCase { +enum StoreInformationViewModelInputCase { - case setInformationView( - openingHour: [RegularOpeningHours], - url: String? - ) + case setUIContents(store: Store) } protocol StoreInformationViewModelInput { - func action(input: StoreInformationViewInputCase) + func action(input: StoreInformationViewModelInputCase) } protocol StoreInformationViewModelOutput { - var openClosedOutput: PublishRelay { get } + var setDetailUIContentsOutput: PublishRelay { get } + var setSummaryUIContentsOutput: PublishRelay { get } var thumbnailImageOutput: PublishRelay { get } + var summaryCallButtonOutput: PublishRelay { get } + var errorAlertOutput: PublishRelay { get } } diff --git a/KCS/KCS/Presentation/StoreInformation/ViewModel/StoreInformationViewModelImpl.swift b/KCS/KCS/Presentation/StoreInformation/ViewModel/StoreInformationViewModelImpl.swift new file mode 100644 index 00000000..359ac21f --- /dev/null +++ b/KCS/KCS/Presentation/StoreInformation/ViewModel/StoreInformationViewModelImpl.swift @@ -0,0 +1,183 @@ +// +// StoreInformationViewModelImpl.swift +// KCS +// +// Created by ์กฐ์„ฑ๋ฏผ on 2/3/24. +// + +import RxRelay +import RxSwift + +final class StoreInformationViewModelImpl: StoreInformationViewModel { + + private let disposeBag = DisposeBag() + + let getOpenClosedUseCase: GetOpenClosedUseCase + let fetchImageUseCase: FetchImageUseCase + + let setDetailUIContentsOutput = PublishRelay() + let setSummaryUIContentsOutput = PublishRelay() + let thumbnailImageOutput = PublishRelay() + let summaryCallButtonOutput = PublishRelay() + let errorAlertOutput = PublishRelay() + + init(getOpenClosedUseCase: GetOpenClosedUseCase, fetchImageUseCase: FetchImageUseCase) { + self.getOpenClosedUseCase = getOpenClosedUseCase + self.fetchImageUseCase = fetchImageUseCase + } + + func action(input: StoreInformationViewModelInputCase) { + switch input { + case .setUIContents(let store): + setUIContents(store: store) + } + } + +} + +private extension StoreInformationViewModelImpl { + + func setUIContents(store: Store) { + fetchThumbnailImage(localPhotos: store.localPhotos) + + if let phoneNumber = store.phoneNumber { + summaryCallButtonOutput.accept(phoneNumber) + } + + do { + let openClosedContent = try getOpenClosedUseCase.execute(openingHours: store.openingHour) + setSummaryUIContentsOutput.accept( + SummaryViewContents( + storeTitle: store.title, + category: store.category, + certificationTypes: store.certificationTypes, + openClosedContent: openClosedContent + ) + ) + setDetailUIContentsOutput.accept( + DetailViewContents( + storeTitle: store.title, + category: store.category, + certificationTypes: store.certificationTypes, + address: store.address, + phoneNumber: store.phoneNumber ?? "์ „ํ™”๋ฒˆํ˜ธ ์ •๋ณด ์—†์Œ", + openClosedContent: openClosedContent, + detailOpeningHour: detailOpeningHour(openingHours: store.openingHour) + ) + ) + } catch { + errorAlertOutput.accept(.client) + } + } + + func fetchThumbnailImage(localPhotos: [String]) { + guard let url = localPhotos.first else { return } + fetchImageUseCase.execute(url: url) + .subscribe( + onNext: { [weak self] imageData in + self?.thumbnailImageOutput.accept(imageData) + }, + onError: { [weak self] _ in + self?.errorAlertOutput.accept(.server) + } + ) + .disposed(by: disposeBag) + } + +} + +private extension StoreInformationViewModelImpl { + + func detailOpeningHour(openingHours: [RegularOpeningHours]) -> [DetailOpeningHour] { + if openingHours.isEmpty { return [] } + var detailOpeningHourArray: [DetailOpeningHour] = [] + + let today = Date().weekDay + for idx in today.. OpeningHour { + if openingHours.isEmpty { + return OpeningHour( + openingHour: OpenClosedType.dayOff.rawValue, + breakTime: nil + ) + } + + if openingHours.count == 1 { + if let openingHour = openingHours.first { + if openingHour.open == openingHour.close { + return OpeningHour( + openingHour: OpenClosedType.alwaysOpen.rawValue, + breakTime: nil + ) + } else { + return OpeningHour( + openingHour: openingHourToString( + open: openingHour.open, + close: openingHour.close + ), + breakTime: nil + ) + } + } + } else { + if let firstOpeningHour = openingHours.first, + let lastOpeningHour = openingHours.last { + if firstOpeningHour.open == lastOpeningHour.close { + return OpeningHour( + openingHour: openingHourToString( + open: lastOpeningHour.open, + close: firstOpeningHour.close + ), + breakTime: nil + ) + } else { + return OpeningHour( + openingHour: openingHourToString( + open: firstOpeningHour.open, + close: lastOpeningHour.close + ), + breakTime: openingHourToString( + open: firstOpeningHour.close, + close: lastOpeningHour.open, + isBreakTime: true + ) + ) + } + } + } + + return OpeningHour(openingHour: nil, breakTime: nil) + } + + func openingHourToString(open: BusinessHour, close: BusinessHour, isBreakTime: Bool = false) -> String { + var format = "%02d:%02d - %02d:%02d" + if isBreakTime { + format = "%02d:%02d - %02d:%02d ๋ธŒ๋ ˆ์ดํฌ ํƒ€์ž„" + } + + return String( + format: format, + open.hour, + open.minute, + close.hour, + close.minute + ) + } + +} diff --git a/KCS/KCS/Presentation/StoreList/View/StoreListViewController.swift b/KCS/KCS/Presentation/StoreList/View/StoreListViewController.swift new file mode 100644 index 00000000..921b129a --- /dev/null +++ b/KCS/KCS/Presentation/StoreList/View/StoreListViewController.swift @@ -0,0 +1,154 @@ +// +// StoreListViewController.swift +// KCS +// +// Created by ์กฐ์„ฑ๋ฏผ on 1/31/24. +// + +import UIKit +import RxSwift +import RxRelay + +final class StoreListViewController: UIViewController { + + private let listCellSelectedObserver: PublishRelay + + private let disposeBag = DisposeBag() + + private let titleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = "๊ฐ€๊ฒŒ ๋ชจ์•„๋ณด๊ธฐ" + label.font = UIFont.pretendard(size: 16, weight: .medium) + label.textColor = .black + + return label + }() + + private let divideView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = UIColor.lightGray + + return view + }() + + private let storeTableView: UITableView = { + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.rowHeight = 109 + tableView.register(StoreTableViewCell.self, forCellReuseIdentifier: StoreTableViewCell.identifier) + tableView.backgroundColor = .white + + return tableView + }() + + enum Section { + case store + } + + private lazy var dataSource: UITableViewDiffableDataSource = { + return UITableViewDiffableDataSource( + tableView: storeTableView + ) { (tableView, indexPath, storeContents) in + guard let cell = tableView.dequeueReusableCell( + withIdentifier: StoreTableViewCell.identifier, + for: indexPath + ) as? StoreTableViewCell else { + return StoreTableViewCell() + } + cell.setUIContents(storeContents: storeContents) + cell.selectionStyle = .none + + return cell + } + }() + + private let viewModel: StoreListViewModel + + init(viewModel: StoreListViewModel, listCellSelectedObserver: PublishRelay) { + self.viewModel = viewModel + self.listCellSelectedObserver = listCellSelectedObserver + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + addUIComponents() + configureConstraints() + bind() + setup() + } + + func updateList(stores: [Store]) { + viewModel.action(input: .updateList(stores: stores)) + } + + func scrollToPreviousCell(indexPath: IndexPath) { + storeTableView.scrollToRow(at: indexPath, at: .top, animated: false) + } + +} + +private extension StoreListViewController { + + func setup() { + isModalInPresentation = true + storeTableView.delegate = self + view.backgroundColor = .white + } + + func addUIComponents() { + view.addSubview(storeTableView) + view.addSubview(titleLabel) + view.addSubview(divideView) + } + + func configureConstraints() { + NSLayoutConstraint.activate([ + titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 27), + titleLabel.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor) + ]) + + NSLayoutConstraint.activate([ + divideView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 27), + divideView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + divideView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + divideView.heightAnchor.constraint(equalToConstant: 0.5) + ]) + + NSLayoutConstraint.activate([ + storeTableView.topAnchor.constraint(equalTo: divideView.bottomAnchor), + storeTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + storeTableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + storeTableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor) + ]) + } + + func bind() { + viewModel.updateListOutput + .bind { [weak self] contentsArray in + guard let self = self else { return } + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.store]) + snapshot.appendItems(contentsArray, toSection: Section.store) + dataSource.apply(snapshot) + } + .disposed(by: disposeBag) + } + +} + +extension StoreListViewController: UITableViewDelegate { + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + listCellSelectedObserver.accept(indexPath.row) + } + +} diff --git a/KCS/KCS/Presentation/StoreList/View/StoreTableViewCell.swift b/KCS/KCS/Presentation/StoreList/View/StoreTableViewCell.swift new file mode 100644 index 00000000..07052557 --- /dev/null +++ b/KCS/KCS/Presentation/StoreList/View/StoreTableViewCell.swift @@ -0,0 +1,124 @@ +// +// StoreTableViewCell.swift +// KCS +// +// Created by ์กฐ์„ฑ๋ฏผ on 1/31/24. +// + +import UIKit +import RxSwift + +final class StoreTableViewCell: UITableViewCell { + + private lazy var storeTitle: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = UIFont.pretendard(size: 20, weight: .bold) + label.textColor = UIColor.primary1 + label.numberOfLines = 1 + + return label + }() + + private lazy var certificationStackView: UIStackView = { + let stack = UIStackView() + stack.translatesAutoresizingMaskIntoConstraints = false + stack.axis = .horizontal + stack.spacing = 4 + stack.distribution = .fillProportionally + + return stack + }() + + private lazy var category: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = UIFont.pretendard(size: 11, weight: .regular) + label.textColor = UIColor.grayLabel + + return label + }() + + private let storeImageView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.setLayerCorner(cornerRadius: 4) + imageView.clipsToBounds = true + imageView.image = UIImage.basicStore + imageView.contentMode = .scaleAspectFill + + return imageView + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + self.backgroundColor = .white + + addUIContents() + configureConstraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + storeTitle.text = nil + certificationStackView.clear() + category.text = nil + storeImageView.image = .basicStore + } + + func setUIContents(storeContents: StoreTableViewCellContents) { + storeTitle.text = storeContents.storeTitle + category.text = storeContents.category + storeContents.certificationTypes.map({ + CertificationLabel(certificationType: $0) + }) + .forEach { [weak self] in + self?.certificationStackView.addArrangedSubview($0) + } + guard let thumbnailImageData = storeContents.thumbnailImageData, + let thumbnailImage = UIImage(data: thumbnailImageData) else { + return + } + storeImageView.image = thumbnailImage + } + +} + +private extension StoreTableViewCell { + + func addUIContents() { + contentView.addSubview(storeTitle) + contentView.addSubview(certificationStackView) + contentView.addSubview(category) + contentView.addSubview(storeImageView) + } + + func configureConstraints() { + NSLayoutConstraint.activate([ + storeImageView.centerYAnchor.constraint(equalTo: centerYAnchor), + storeImageView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -16), + storeImageView.widthAnchor.constraint(equalToConstant: 77), + storeImageView.heightAnchor.constraint(equalToConstant: 77) + ]) + + NSLayoutConstraint.activate([ + storeTitle.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16), + storeTitle.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), + storeTitle.trailingAnchor.constraint(equalTo: storeImageView.leadingAnchor, constant: -8) + ]) + + NSLayoutConstraint.activate([ + category.topAnchor.constraint(equalTo: storeTitle.bottomAnchor, constant: 8), + category.leadingAnchor.constraint(equalTo: storeTitle.leadingAnchor) + ]) + + NSLayoutConstraint.activate([ + certificationStackView.topAnchor.constraint(equalTo: category.bottomAnchor, constant: 11), + certificationStackView.leadingAnchor.constraint(equalTo: storeTitle.leadingAnchor) + ]) + } + +} diff --git a/KCS/KCS/Presentation/StoreList/ViewModel/Protocol/StoreListViewModel.swift b/KCS/KCS/Presentation/StoreList/ViewModel/Protocol/StoreListViewModel.swift new file mode 100644 index 00000000..8e072c2a --- /dev/null +++ b/KCS/KCS/Presentation/StoreList/ViewModel/Protocol/StoreListViewModel.swift @@ -0,0 +1,32 @@ +// +// StoreListViewModel.swift +// KCS +// +// Created by ์กฐ์„ฑ๋ฏผ on 1/31/24. +// + +import RxRelay + +protocol StoreListViewModel: StoreListViewModelInput, StoreListViewModelOutput { + + var fetchImageUseCase: FetchImageUseCase { get } + +} + +protocol StoreListViewModelInput { + + func action(input: StoreListViewModelInputCase) + +} + +enum StoreListViewModelInputCase { + + case updateList(stores: [Store]) + +} + +protocol StoreListViewModelOutput { + + var updateListOutput: BehaviorRelay<[StoreTableViewCellContents]> { get } + +} diff --git a/KCS/KCS/Presentation/StoreList/ViewModel/StoreListViewModelImpl.swift b/KCS/KCS/Presentation/StoreList/ViewModel/StoreListViewModelImpl.swift new file mode 100644 index 00000000..ecb0d8ba --- /dev/null +++ b/KCS/KCS/Presentation/StoreList/ViewModel/StoreListViewModelImpl.swift @@ -0,0 +1,64 @@ +// +// StoreListViewModelImpl.swift +// KCS +// +// Created by ์กฐ์„ฑ๋ฏผ on 1/31/24. +// + +import RxRelay +import RxSwift + +final class StoreListViewModelImpl: StoreListViewModel { + + var fetchImageUseCase: FetchImageUseCase + + var updateListOutput = BehaviorRelay<[StoreTableViewCellContents]>(value: []) + + private let disposeBag = DisposeBag() + + init(fetchImageUseCase: FetchImageUseCase) { + self.fetchImageUseCase = fetchImageUseCase + } + + func action(input: StoreListViewModelInputCase) { + switch input { + case .updateList(let stores): + updateList(stores: stores) + } + } + +} + +private extension StoreListViewModelImpl { + + func updateList(stores: [Store]) { + if stores.isEmpty { + updateListOutput.accept([]) + } else { + Observable.zip(stores.map({ [weak self] store in + guard let self = self, + let url = store.localPhotos.first else { return Observable.just(nil) } + + return fetchImageUseCase.execute(url: url) + .flatMap { Observable.just($0) } + })) + .bind { [weak self] imageDataArray in + var storeContentsArray: [StoreTableViewCellContents] = [] + for index in stores.indices { + let store = stores[index] + storeContentsArray.append( + StoreTableViewCellContents( + storeTitle: store.title, + category: store.category, + certificationTypes: store.certificationTypes, + thumbnailImageData: imageDataArray[index] + ) + ) + } + self?.updateListOutput.accept(storeContentsArray) + } + .disposed(by: disposeBag) + } + } + +} diff --git a/KCS/KCS/Resource/Assets.xcassets/AddressIcon.imageset/Contents.json b/KCS/KCS/Resource/Assets.xcassets/AddressIcon.imageset/Contents.json new file mode 100644 index 00000000..79e25443 --- /dev/null +++ b/KCS/KCS/Resource/Assets.xcassets/AddressIcon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "address.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "address@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "address@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/KCS/KCS/Resource/Assets.xcassets/AddressIcon.imageset/address.png b/KCS/KCS/Resource/Assets.xcassets/AddressIcon.imageset/address.png new file mode 100644 index 00000000..75b00346 Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/AddressIcon.imageset/address.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/AddressIcon.imageset/address@2x.png b/KCS/KCS/Resource/Assets.xcassets/AddressIcon.imageset/address@2x.png new file mode 100644 index 00000000..17bd617a Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/AddressIcon.imageset/address@2x.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/AddressIcon.imageset/address@3x.png b/KCS/KCS/Resource/Assets.xcassets/AddressIcon.imageset/address@3x.png new file mode 100644 index 00000000..401efa03 Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/AddressIcon.imageset/address@3x.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/ClockIcon.imageset/Contents.json b/KCS/KCS/Resource/Assets.xcassets/ClockIcon.imageset/Contents.json new file mode 100644 index 00000000..f2feb3d4 --- /dev/null +++ b/KCS/KCS/Resource/Assets.xcassets/ClockIcon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "clockIcon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "clockIcon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "clockIcon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/KCS/KCS/Resource/Assets.xcassets/ClockIcon.imageset/clockIcon.png b/KCS/KCS/Resource/Assets.xcassets/ClockIcon.imageset/clockIcon.png new file mode 100644 index 00000000..af40e74f Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/ClockIcon.imageset/clockIcon.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/ClockIcon.imageset/clockIcon@2x.png b/KCS/KCS/Resource/Assets.xcassets/ClockIcon.imageset/clockIcon@2x.png new file mode 100644 index 00000000..fb3277d5 Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/ClockIcon.imageset/clockIcon@2x.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/ClockIcon.imageset/clockIcon@3x.png b/KCS/KCS/Resource/Assets.xcassets/ClockIcon.imageset/clockIcon@3x.png new file mode 100644 index 00000000..2f62283b Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/ClockIcon.imageset/clockIcon@3x.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/CertificationLabelText.colorset/Contents.json b/KCS/KCS/Resource/Assets.xcassets/DivideView.colorset/Contents.json similarity index 71% rename from KCS/KCS/Resource/Assets.xcassets/CertificationLabelText.colorset/Contents.json rename to KCS/KCS/Resource/Assets.xcassets/DivideView.colorset/Contents.json index 4cf137f1..d9474651 100644 --- a/KCS/KCS/Resource/Assets.xcassets/CertificationLabelText.colorset/Contents.json +++ b/KCS/KCS/Resource/Assets.xcassets/DivideView.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.561", - "green" : "0.561", - "red" : "0.561" + "blue" : "0.914", + "green" : "0.914", + "red" : "0.914" } }, "idiom" : "universal" @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.561", - "green" : "0.561", - "red" : "0.561" + "blue" : "0.914", + "green" : "0.914", + "red" : "0.914" } }, "idiom" : "universal" @@ -34,5 +34,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "localizable" : true } } diff --git a/KCS/KCS/Resource/Assets.xcassets/KCSGray.colorset/Contents.json b/KCS/KCS/Resource/Assets.xcassets/GrayLabel.colorset/Contents.json similarity index 100% rename from KCS/KCS/Resource/Assets.xcassets/KCSGray.colorset/Contents.json rename to KCS/KCS/Resource/Assets.xcassets/GrayLabel.colorset/Contents.json diff --git a/KCS/KCS/Resource/Assets.xcassets/Onboarding1.imageset/Contents.json b/KCS/KCS/Resource/Assets.xcassets/Onboarding1.imageset/Contents.json new file mode 100644 index 00000000..741952d6 --- /dev/null +++ b/KCS/KCS/Resource/Assets.xcassets/Onboarding1.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Onboarding1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Onboarding1@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Onboarding1@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/KCS/KCS/Resource/Assets.xcassets/Onboarding1.imageset/Onboarding1.png b/KCS/KCS/Resource/Assets.xcassets/Onboarding1.imageset/Onboarding1.png new file mode 100644 index 00000000..2f6aa77a Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/Onboarding1.imageset/Onboarding1.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/Onboarding1.imageset/Onboarding1@2x.png b/KCS/KCS/Resource/Assets.xcassets/Onboarding1.imageset/Onboarding1@2x.png new file mode 100644 index 00000000..c2245506 Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/Onboarding1.imageset/Onboarding1@2x.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/Onboarding1.imageset/Onboarding1@3x.png b/KCS/KCS/Resource/Assets.xcassets/Onboarding1.imageset/Onboarding1@3x.png new file mode 100644 index 00000000..1896807e Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/Onboarding1.imageset/Onboarding1@3x.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/Onboarding2.imageset/Contents.json b/KCS/KCS/Resource/Assets.xcassets/Onboarding2.imageset/Contents.json new file mode 100644 index 00000000..69d09f0e --- /dev/null +++ b/KCS/KCS/Resource/Assets.xcassets/Onboarding2.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Onboarding2.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Onboarding2@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Onboarding2@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/KCS/KCS/Resource/Assets.xcassets/Onboarding2.imageset/Onboarding2.png b/KCS/KCS/Resource/Assets.xcassets/Onboarding2.imageset/Onboarding2.png new file mode 100644 index 00000000..9ed6076d Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/Onboarding2.imageset/Onboarding2.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/Onboarding2.imageset/Onboarding2@2x.png b/KCS/KCS/Resource/Assets.xcassets/Onboarding2.imageset/Onboarding2@2x.png new file mode 100644 index 00000000..3da9265a Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/Onboarding2.imageset/Onboarding2@2x.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/Onboarding2.imageset/Onboarding2@3x.png b/KCS/KCS/Resource/Assets.xcassets/Onboarding2.imageset/Onboarding2@3x.png new file mode 100644 index 00000000..e2d5ae52 Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/Onboarding2.imageset/Onboarding2@3x.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/Onboarding3.imageset/Contents.json b/KCS/KCS/Resource/Assets.xcassets/Onboarding3.imageset/Contents.json new file mode 100644 index 00000000..43eba381 --- /dev/null +++ b/KCS/KCS/Resource/Assets.xcassets/Onboarding3.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Onboarding3.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Onboarding3@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Onboarding3@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/KCS/KCS/Resource/Assets.xcassets/Onboarding3.imageset/Onboarding3.png b/KCS/KCS/Resource/Assets.xcassets/Onboarding3.imageset/Onboarding3.png new file mode 100644 index 00000000..670a860b Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/Onboarding3.imageset/Onboarding3.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/Onboarding3.imageset/Onboarding3@2x.png b/KCS/KCS/Resource/Assets.xcassets/Onboarding3.imageset/Onboarding3@2x.png new file mode 100644 index 00000000..68972c8e Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/Onboarding3.imageset/Onboarding3@2x.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/Onboarding3.imageset/Onboarding3@3x.png b/KCS/KCS/Resource/Assets.xcassets/Onboarding3.imageset/Onboarding3@3x.png new file mode 100644 index 00000000..33ebbc5d Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/Onboarding3.imageset/Onboarding3@3x.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/Onboarding4.imageset/Contents.json b/KCS/KCS/Resource/Assets.xcassets/Onboarding4.imageset/Contents.json new file mode 100644 index 00000000..6664a0ab --- /dev/null +++ b/KCS/KCS/Resource/Assets.xcassets/Onboarding4.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Onboarding4.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Onboarding4@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Onboarding4@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/KCS/KCS/Resource/Assets.xcassets/Onboarding4.imageset/Onboarding4.png b/KCS/KCS/Resource/Assets.xcassets/Onboarding4.imageset/Onboarding4.png new file mode 100644 index 00000000..b1b762c7 Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/Onboarding4.imageset/Onboarding4.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/Onboarding4.imageset/Onboarding4@2x.png b/KCS/KCS/Resource/Assets.xcassets/Onboarding4.imageset/Onboarding4@2x.png new file mode 100644 index 00000000..babca338 Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/Onboarding4.imageset/Onboarding4@2x.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/Onboarding4.imageset/Onboarding4@3x.png b/KCS/KCS/Resource/Assets.xcassets/Onboarding4.imageset/Onboarding4@3x.png new file mode 100644 index 00000000..ce7a220b Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/Onboarding4.imageset/Onboarding4@3x.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/Onboarding5.imageset/Contents.json b/KCS/KCS/Resource/Assets.xcassets/Onboarding5.imageset/Contents.json new file mode 100644 index 00000000..15e08ab7 --- /dev/null +++ b/KCS/KCS/Resource/Assets.xcassets/Onboarding5.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Onboarding5.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Onboarding5@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Onboarding5@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/KCS/KCS/Resource/Assets.xcassets/Onboarding5.imageset/Onboarding5.png b/KCS/KCS/Resource/Assets.xcassets/Onboarding5.imageset/Onboarding5.png new file mode 100644 index 00000000..20e4a756 Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/Onboarding5.imageset/Onboarding5.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/Onboarding5.imageset/Onboarding5@2x.png b/KCS/KCS/Resource/Assets.xcassets/Onboarding5.imageset/Onboarding5@2x.png new file mode 100644 index 00000000..d53ec963 Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/Onboarding5.imageset/Onboarding5@2x.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/Onboarding5.imageset/Onboarding5@3x.png b/KCS/KCS/Resource/Assets.xcassets/Onboarding5.imageset/Onboarding5@3x.png new file mode 100644 index 00000000..c6c4f2eb Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/Onboarding5.imageset/Onboarding5@3x.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/PhoneIcon.imageset/Contents.json b/KCS/KCS/Resource/Assets.xcassets/PhoneIcon.imageset/Contents.json new file mode 100644 index 00000000..72725f21 --- /dev/null +++ b/KCS/KCS/Resource/Assets.xcassets/PhoneIcon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "phoneIcon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "phoneIcon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "phoneIcon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/KCS/KCS/Resource/Assets.xcassets/PhoneIcon.imageset/phoneIcon.png b/KCS/KCS/Resource/Assets.xcassets/PhoneIcon.imageset/phoneIcon.png new file mode 100644 index 00000000..00f74007 Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/PhoneIcon.imageset/phoneIcon.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/PhoneIcon.imageset/phoneIcon@2x.png b/KCS/KCS/Resource/Assets.xcassets/PhoneIcon.imageset/phoneIcon@2x.png new file mode 100644 index 00000000..ae0ff954 Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/PhoneIcon.imageset/phoneIcon@2x.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/PhoneIcon.imageset/phoneIcon@3x.png b/KCS/KCS/Resource/Assets.xcassets/PhoneIcon.imageset/phoneIcon@3x.png new file mode 100644 index 00000000..0a0a4db8 Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/PhoneIcon.imageset/phoneIcon@3x.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation1.imageset/Contents.json b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation1.imageset/Contents.json new file mode 100644 index 00000000..b07a6eec --- /dev/null +++ b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation1.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "RefreshAnimation1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "RefreshAnimation1@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "RefreshAnimation1@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation1.imageset/RefreshAnimation1.png b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation1.imageset/RefreshAnimation1.png new file mode 100644 index 00000000..0c15a584 Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation1.imageset/RefreshAnimation1.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation1.imageset/RefreshAnimation1@2x.png b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation1.imageset/RefreshAnimation1@2x.png new file mode 100644 index 00000000..f1c8f7cf Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation1.imageset/RefreshAnimation1@2x.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation1.imageset/RefreshAnimation1@3x.png b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation1.imageset/RefreshAnimation1@3x.png new file mode 100644 index 00000000..0c755e95 Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation1.imageset/RefreshAnimation1@3x.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation2.imageset/Contents.json b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation2.imageset/Contents.json new file mode 100644 index 00000000..ecb9f9b1 --- /dev/null +++ b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation2.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "RefreshAnimation2.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "RefreshAnimation2@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "RefreshAnimation2@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation2.imageset/RefreshAnimation2.png b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation2.imageset/RefreshAnimation2.png new file mode 100644 index 00000000..a76f2795 Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation2.imageset/RefreshAnimation2.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation2.imageset/RefreshAnimation2@2x.png b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation2.imageset/RefreshAnimation2@2x.png new file mode 100644 index 00000000..76734aaa Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation2.imageset/RefreshAnimation2@2x.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation2.imageset/RefreshAnimation2@3x.png b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation2.imageset/RefreshAnimation2@3x.png new file mode 100644 index 00000000..14d510a0 Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation2.imageset/RefreshAnimation2@3x.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation3.imageset/Contents.json b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation3.imageset/Contents.json new file mode 100644 index 00000000..2c682edf --- /dev/null +++ b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation3.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "RefreshAnimation3.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "RefreshAnimation3@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "RefreshAnimation3@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation3.imageset/RefreshAnimation3.png b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation3.imageset/RefreshAnimation3.png new file mode 100644 index 00000000..f6cfa8ea Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation3.imageset/RefreshAnimation3.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation3.imageset/RefreshAnimation3@2x.png b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation3.imageset/RefreshAnimation3@2x.png new file mode 100644 index 00000000..88e7dbba Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation3.imageset/RefreshAnimation3@2x.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation3.imageset/RefreshAnimation3@3x.png b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation3.imageset/RefreshAnimation3@3x.png new file mode 100644 index 00000000..93c8d993 Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation3.imageset/RefreshAnimation3@3x.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation4.imageset/Contents.json b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation4.imageset/Contents.json new file mode 100644 index 00000000..0a752b3c --- /dev/null +++ b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation4.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "RefreshAnimation4.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "RefreshAnimation4@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "RefreshAnimation4@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation4.imageset/RefreshAnimation4.png b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation4.imageset/RefreshAnimation4.png new file mode 100644 index 00000000..804ffd72 Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation4.imageset/RefreshAnimation4.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation4.imageset/RefreshAnimation4@2x.png b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation4.imageset/RefreshAnimation4@2x.png new file mode 100644 index 00000000..b0a5bd20 Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation4.imageset/RefreshAnimation4@2x.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation4.imageset/RefreshAnimation4@3x.png b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation4.imageset/RefreshAnimation4@3x.png new file mode 100644 index 00000000..241ca863 Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation4.imageset/RefreshAnimation4@3x.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation5.imageset/Contents.json b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation5.imageset/Contents.json new file mode 100644 index 00000000..2cb91e66 --- /dev/null +++ b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation5.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "RefreshAnimation5.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "RefreshAnimation5@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "RefreshAnimation5@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation5.imageset/RefreshAnimation5.png b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation5.imageset/RefreshAnimation5.png new file mode 100644 index 00000000..e4f44387 Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation5.imageset/RefreshAnimation5.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation5.imageset/RefreshAnimation5@2x.png b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation5.imageset/RefreshAnimation5@2x.png new file mode 100644 index 00000000..4b7a7f10 Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation5.imageset/RefreshAnimation5@2x.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation5.imageset/RefreshAnimation5@3x.png b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation5.imageset/RefreshAnimation5@3x.png new file mode 100644 index 00000000..265ce968 Binary files /dev/null and b/KCS/KCS/Resource/Assets.xcassets/RefreshAnimation5.imageset/RefreshAnimation5@3x.png differ diff --git a/KCS/KCS/Resource/Assets.xcassets/SwipeBar.colorset/Contents.json b/KCS/KCS/Resource/Assets.xcassets/SwipeBar.colorset/Contents.json index 4120543f..9e90a5f5 100644 --- a/KCS/KCS/Resource/Assets.xcassets/SwipeBar.colorset/Contents.json +++ b/KCS/KCS/Resource/Assets.xcassets/SwipeBar.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.780", - "green" : "0.780", - "red" : "0.780" + "blue" : "0.851", + "green" : "0.851", + "red" : "0.851" } }, "idiom" : "universal" @@ -34,5 +34,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "localizable" : true } } diff --git a/KCS/KCS/Resource/Info.plist b/KCS/KCS/Resource/Info.plist index 389f5f20..f35b2cd5 100644 --- a/KCS/KCS/Resource/Info.plist +++ b/KCS/KCS/Resource/Info.plist @@ -4,6 +4,8 @@ DEV_SERVER_URL $(DEV_SERVER_URL) + ITSAppUsesNonExemptEncryption + NMAP_CLIENT_ID $(NMAP_CLIENT_ID) NSAppTransportSecurity diff --git a/KCS/KCS/Util/SystemImage.swift b/KCS/KCS/Util/SystemImage.swift index 34c88b87..8bc9255a 100644 --- a/KCS/KCS/Util/SystemImage.swift +++ b/KCS/KCS/Util/SystemImage.swift @@ -12,5 +12,6 @@ enum SystemImage { static let circle = UIImage(systemName: "circle.fill") static let phone = UIImage(systemName: "phone.fill") static let refresh = UIImage(systemName: "arrow.clockwise") + static let back = UIImage(systemName: "arrow.backward") } diff --git a/KCS/Podfile b/KCS/Podfile index 8e0e1bc0..ff491dcd 100644 --- a/KCS/Podfile +++ b/KCS/Podfile @@ -10,9 +10,12 @@ target 'KCS' do pod 'RxSwift', '6.6.0' pod 'RxCocoa', '6.6.0' + pod 'RxGesture' pod 'Alamofire' pod 'SwiftLint' pod 'NMapsMap' + pod 'Firebase/Analytics' + pod 'Firebase/Crashlytics' post_install do |installer| installer.pods_project.targets.each do |target| diff --git a/KCS/Podfile.lock b/KCS/Podfile.lock index 79c58045..4bb108a1 100644 --- a/KCS/Podfile.lock +++ b/KCS/Podfile.lock @@ -1,13 +1,124 @@ PODS: - Alamofire (5.8.1) + - Firebase/Analytics (10.20.0): + - Firebase/Core + - Firebase/Core (10.20.0): + - Firebase/CoreOnly + - FirebaseAnalytics (~> 10.20.0) + - Firebase/CoreOnly (10.20.0): + - FirebaseCore (= 10.20.0) + - Firebase/Crashlytics (10.20.0): + - Firebase/CoreOnly + - FirebaseCrashlytics (~> 10.20.0) + - FirebaseAnalytics (10.20.0): + - FirebaseAnalytics/AdIdSupport (= 10.20.0) + - FirebaseCore (~> 10.0) + - FirebaseInstallations (~> 10.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.11) + - GoogleUtilities/MethodSwizzler (~> 7.11) + - GoogleUtilities/Network (~> 7.11) + - "GoogleUtilities/NSData+zlib (~> 7.11)" + - nanopb (< 2.30910.0, >= 2.30908.0) + - FirebaseAnalytics/AdIdSupport (10.20.0): + - FirebaseCore (~> 10.0) + - FirebaseInstallations (~> 10.0) + - GoogleAppMeasurement (= 10.20.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.11) + - GoogleUtilities/MethodSwizzler (~> 7.11) + - GoogleUtilities/Network (~> 7.11) + - "GoogleUtilities/NSData+zlib (~> 7.11)" + - nanopb (< 2.30910.0, >= 2.30908.0) + - FirebaseCore (10.20.0): + - FirebaseCoreInternal (~> 10.0) + - GoogleUtilities/Environment (~> 7.12) + - GoogleUtilities/Logger (~> 7.12) + - FirebaseCoreExtension (10.20.0): + - FirebaseCore (~> 10.0) + - FirebaseCoreInternal (10.20.0): + - "GoogleUtilities/NSData+zlib (~> 7.8)" + - FirebaseCrashlytics (10.20.0): + - FirebaseCore (~> 10.5) + - FirebaseInstallations (~> 10.0) + - FirebaseSessions (~> 10.5) + - GoogleDataTransport (~> 9.2) + - GoogleUtilities/Environment (~> 7.8) + - nanopb (< 2.30910.0, >= 2.30908.0) + - PromisesObjC (~> 2.1) + - FirebaseInstallations (10.20.0): + - FirebaseCore (~> 10.0) + - GoogleUtilities/Environment (~> 7.8) + - GoogleUtilities/UserDefaults (~> 7.8) + - PromisesObjC (~> 2.1) + - FirebaseSessions (10.20.0): + - FirebaseCore (~> 10.5) + - FirebaseCoreExtension (~> 10.0) + - FirebaseInstallations (~> 10.0) + - GoogleDataTransport (~> 9.2) + - GoogleUtilities/Environment (~> 7.10) + - nanopb (< 2.30910.0, >= 2.30908.0) + - PromisesSwift (~> 2.1) + - GoogleAppMeasurement (10.20.0): + - GoogleAppMeasurement/AdIdSupport (= 10.20.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.11) + - GoogleUtilities/MethodSwizzler (~> 7.11) + - GoogleUtilities/Network (~> 7.11) + - "GoogleUtilities/NSData+zlib (~> 7.11)" + - nanopb (< 2.30910.0, >= 2.30908.0) + - GoogleAppMeasurement/AdIdSupport (10.20.0): + - GoogleAppMeasurement/WithoutAdIdSupport (= 10.20.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.11) + - GoogleUtilities/MethodSwizzler (~> 7.11) + - GoogleUtilities/Network (~> 7.11) + - "GoogleUtilities/NSData+zlib (~> 7.11)" + - nanopb (< 2.30910.0, >= 2.30908.0) + - GoogleAppMeasurement/WithoutAdIdSupport (10.20.0): + - GoogleUtilities/AppDelegateSwizzler (~> 7.11) + - GoogleUtilities/MethodSwizzler (~> 7.11) + - GoogleUtilities/Network (~> 7.11) + - "GoogleUtilities/NSData+zlib (~> 7.11)" + - nanopb (< 2.30910.0, >= 2.30908.0) + - GoogleDataTransport (9.3.0): + - GoogleUtilities/Environment (~> 7.7) + - nanopb (< 2.30910.0, >= 2.30908.0) + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/AppDelegateSwizzler (7.12.0): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Environment (7.12.0): + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/Logger (7.12.0): + - GoogleUtilities/Environment + - GoogleUtilities/MethodSwizzler (7.12.0): + - GoogleUtilities/Logger + - GoogleUtilities/Network (7.12.0): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (7.12.0)" + - GoogleUtilities/Reachability (7.12.0): + - GoogleUtilities/Logger + - GoogleUtilities/UserDefaults (7.12.0): + - GoogleUtilities/Logger + - nanopb (2.30909.1): + - nanopb/decode (= 2.30909.1) + - nanopb/encode (= 2.30909.1) + - nanopb/decode (2.30909.1) + - nanopb/encode (2.30909.1) - NMapsGeometry (1.0.1) - NMapsMap (3.17.0): - NMapsGeometry + - PromisesObjC (2.3.1) + - PromisesSwift (2.3.1): + - PromisesObjC (= 2.3.1) - RxBlocking (6.6.0): - RxSwift (= 6.6.0) - RxCocoa (6.6.0): - RxRelay (= 6.6.0) - RxSwift (= 6.6.0) + - RxGesture (4.0.4): + - RxCocoa (~> 6.0) + - RxSwift (~> 6.0) - RxRelay (6.6.0): - RxSwift (= 6.6.0) - RxSwift (6.6.0) @@ -17,9 +128,12 @@ PODS: DEPENDENCIES: - Alamofire + - Firebase/Analytics + - Firebase/Crashlytics - NMapsMap - RxBlocking - RxCocoa (= 6.6.0) + - RxGesture - RxSwift (= 6.6.0) - RxTest - SwiftLint @@ -27,10 +141,25 @@ DEPENDENCIES: SPEC REPOS: trunk: - Alamofire + - Firebase + - FirebaseAnalytics + - FirebaseCore + - FirebaseCoreExtension + - FirebaseCoreInternal + - FirebaseCrashlytics + - FirebaseInstallations + - FirebaseSessions + - GoogleAppMeasurement + - GoogleDataTransport + - GoogleUtilities + - nanopb - NMapsGeometry - NMapsMap + - PromisesObjC + - PromisesSwift - RxBlocking - RxCocoa + - RxGesture - RxRelay - RxSwift - RxTest @@ -38,15 +167,30 @@ SPEC REPOS: SPEC CHECKSUMS: Alamofire: 3ca42e259043ee0dc5c0cdd76c4bc568b8e42af7 + Firebase: 10c8cb12fb7ad2ae0c09ffc86cd9c1ab392a0031 + FirebaseAnalytics: a2731bf3670747ce8f65368b118d18aa8e368246 + FirebaseCore: 28045c1560a2600d284b9c45a904fe322dc890b6 + FirebaseCoreExtension: 0659f035b88c5a7a15a9763c48c2e6ca8c0a2977 + FirebaseCoreInternal: efeeb171ac02d623bdaefe121539939821e10811 + FirebaseCrashlytics: 81530595edb6d99f1918f723a6c33766a24a4c86 + FirebaseInstallations: 558b1da7d65afeb996fd5c814332f013234ece4e + FirebaseSessions: 2f348975f6d1c139231c180e12194161da2e0cd6 + GoogleAppMeasurement: bb3c564c3efb933136af0e94899e0a46167466a8 + GoogleDataTransport: 57c22343ab29bc686febbf7cbb13bad167c2d8fe + GoogleUtilities: 0759d1a57ebb953965c2dfe0ba4c82e95ccc2e34 + nanopb: d4d75c12cd1316f4a64e3c6963f879ecd4b5e0d5 NMapsGeometry: 53c573ead66466681cf123f99f698dc8071a4b83 NMapsMap: a5b909a31b6f3d27a670f6eb2ddc913c38975474 + PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 + PromisesSwift: 28dca69a9c40779916ac2d6985a0192a5cb4a265 RxBlocking: fbd1f8501443024f686e556f36dac79b0d5f4d7c RxCocoa: 44a80de90e25b739b5aeaae3c8c371a32e3343cc + RxGesture: f3efb47ed2d26a8082f7b660d4a59970e275a7f8 RxRelay: 45eaa5db8ee4fb50e5ebd57deec0159e97fa51e6 RxSwift: a4b44f7d24599f674deebd1818eab82e58410632 RxTest: a23f26bb53a5e146a0a69db4f0fa0b69001ce7f4 SwiftLint: c1de071d9d08c8aba837545f6254315bc900e211 -PODFILE CHECKSUM: 0f88675320ac9cb6ad8c84bdece01c7160cd9d79 +PODFILE CHECKSUM: a5566a18764ef703abc0265b00efa39f0938cc7c -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.2