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