diff --git a/Docs/Detail&SoldOut.gif b/Docs/Detail&SoldOut.gif new file mode 100644 index 000000000..b712b6794 Binary files /dev/null and b/Docs/Detail&SoldOut.gif differ diff --git a/Docs/MainView.gif b/Docs/MainView.gif new file mode 100644 index 000000000..8ec370927 Binary files /dev/null and b/Docs/MainView.gif differ diff --git a/Docs/Order.gif b/Docs/Order.gif new file mode 100644 index 000000000..cc1f7ab73 Binary files /dev/null and b/Docs/Order.gif differ diff --git a/Docs/Toast.gif b/Docs/Toast.gif new file mode 100644 index 000000000..afca1dfc2 Binary files /dev/null and b/Docs/Toast.gif differ diff --git a/README.md b/README.md index 39a151d3f..be239841d 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,46 @@ Team 10 - Bongf - K +## μ‹€ν–‰ ν™”λ©΄ + +### Main View + + + +πŸ’β€β™‚οΈ 메인 ν™”λ©΄μ—μ„œ **λ©”μΈμš”λ¦¬**, **κ΅­λ¬Όμš”λ¦¬**, **λ°‘λ°˜μ°¬** 으둜 λΆ„λ₯˜ 된 μŒμ‹λ“€μ„ 상, ν•˜λ‘œ 슀크둀 ν•˜μ—¬ 탐색 ν•˜μ‹€ 수 μžˆμŠ΅λ‹ˆλ‹€. + +πŸ’β€β™‚οΈ 각 λ°˜μ°¬λ“€μ˜ 이름과 μš©λŸ‰, κ°„λ‹¨ν•œ μ„€λͺ…, μ •κ°€, 할인 가격, 할인 정보λ₯Ό ν™•μΈν•˜μ‹€ 수 μžˆμŠ΅λ‹ˆλ‹€. + + + +### Detail View & Sold Out + +

+ +πŸ’β€β™‚οΈ 메인 ν™”λ©΄μ—μ„œ μ›ν•˜λŠ” λ°˜μ°¬μ„ ν΄λ¦­ν•˜μ‹œλ©΄ ν•΄λ‹Ή λ°˜μ°¬μ— λŒ€ν•œ μƒμ„Έν•œ 정보λ₯Ό μ–»μœΌμ‹€ 수 μžˆμŠ΅λ‹ˆλ‹€. + +πŸ’β€β™‚οΈ ν•΄λ‹Ή 반찬의 μž¬κ³ κ°€ μ—†μœΌλ©΄ μ•„μ‰½μ§€λ§Œ `μ£Όλ¬Έ ν•˜κΈ°` λ²„νŠΌμ€ `μΌμ‹œ ν’ˆμ ˆ`둜 λ°”λ€Œλ©° λ²„νŠΌμ΄ λΉ„ν™œμ„±ν™” λ˜μ–΄ μ£Όλ¬Έν•˜μ‹€ 수 μ—†μŠ΅λ‹ˆλ‹€. + + + +### Order + +

+ +πŸ’β€β™‚οΈ μ›ν•˜μ‹œλŠ” λ°˜μ°¬μ— λŒ€ν•œ μž¬κ³ κ°€ 있으면 `μ£Όλ¬Έν•˜κΈ°` λ²„νŠΌμ΄ ν™œμ„±ν™” λ˜μ–΄μžˆμœΌλ©° μ£Όλ¬Έ 진행이 κ°€λŠ₯ν•©λ‹ˆλ‹€. + +πŸ’β€β™‚οΈ ν•˜μ§€λ§Œ ν˜„μž¬ μž¬κ³ λ³΄λ‹€ λ§Žμ€ μˆ˜λŸ‰μ„ μ£Όλ¬Έν•˜λ € ν• μ‹œμ—λŠ” `Alert` 둜 μž¬κ³ κ°€ 뢀쑱함을 μ•Œλ¦½λ‹ˆλ‹€. + +πŸ’β€β™‚οΈ 재고 μ•ˆμ—μ„œ 주문을 ν•˜μ‹€ κ²½μš°μ—λŠ” μ„±κ³΅μ μœΌλ‘œ μ£Όλ¬Έ μ ‘μˆ˜κ°€ μ™„λ£Œλ˜λ©° `주문이 μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€` λΌλŠ” μ•Œλ¦Όκ³Ό ν•¨κ»˜ Main ν™”λ©΄μœΌλ‘œ μ΄λ™ν•˜κ²Œ λ©λ‹ˆλ‹€. + + + +### Toast + +

+ +πŸ’β€β™‚οΈ 메인 ν™”λ©΄μ—μ„œ **λ©”μΈμš”λ¦¬**, **κ΅­λ¬Όμš”λ¦¬**, **λ°‘λ°˜μ°¬** μ½”λ„ˆμ˜ 헀더λ₯Ό νƒ­ν•˜κ²Œ 되면 ν™”λ©΄ 쀑앙 ν•˜λ‹¨μ— Toast둜 ν•΄λ‹Ή μ½”λ„ˆμ˜ λ“±λ‘λœ 반찬 μ’…λ₯˜μ˜ 개수λ₯Ό μ•Œ 수 μžˆμŠ΅λ‹ˆλ‹€. + ## 브랜치 μ „λž΅ 폴더λ₯Ό iOS와 BE λ‘κ°œλ‘œ λ‚˜λˆ„μ–΄μ„œ κ΄€λ¦¬ν•©λ‹ˆλ‹€. diff --git a/iOS/SideDish/Pods/Pods.xcodeproj/xcuserdata/jibook.xcuserdatad/xcschemes/xcschememanagement.plist b/iOS/SideDish/Pods/Pods.xcodeproj/xcuserdata/jibook.xcuserdatad/xcschemes/xcschememanagement.plist index 1d2b04313..cce3a9137 100644 --- a/iOS/SideDish/Pods/Pods.xcodeproj/xcuserdata/jibook.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/iOS/SideDish/Pods/Pods.xcodeproj/xcuserdata/jibook.xcuserdatad/xcschemes/xcschememanagement.plist @@ -11,6 +11,11 @@ orderHint 0 + Kingfisher.xcscheme_^#shared#^_ + + orderHint + 5 + Pods-SideDish-SideDishUITests.xcscheme isShown diff --git a/iOS/SideDish/Pods/Pods.xcodeproj/xcuserdata/shim.xcuserdatad/xcschemes/xcschememanagement.plist b/iOS/SideDish/Pods/Pods.xcodeproj/xcuserdata/shim.xcuserdatad/xcschemes/xcschememanagement.plist index 08dd6efed..6bebd9c0f 100644 --- a/iOS/SideDish/Pods/Pods.xcodeproj/xcuserdata/shim.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/iOS/SideDish/Pods/Pods.xcodeproj/xcuserdata/shim.xcuserdatad/xcschemes/xcschememanagement.plist @@ -16,7 +16,7 @@ isShown orderHint - 1 + 2 Pods-SideDish-SideDishUITests.xcscheme @@ -30,7 +30,7 @@ isShown orderHint - 2 + 1 Pods-SideDishTests.xcscheme diff --git a/iOS/SideDish/SideDish.xcodeproj/project.pbxproj b/iOS/SideDish/SideDish.xcodeproj/project.pbxproj index 82e775dc2..4054e4659 100644 --- a/iOS/SideDish/SideDish.xcodeproj/project.pbxproj +++ b/iOS/SideDish/SideDish.xcodeproj/project.pbxproj @@ -12,6 +12,17 @@ 5C69859173AA8099B0701A54 /* Pods_SideDishTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D7433C6120AE2C2B5B290532 /* Pods_SideDishTests.framework */; }; D4A2C9AC263273C10019AFB6 /* BadgeLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4A2C9AB263273C10019AFB6 /* BadgeLabel.swift */; }; D4A2CA37263417B60019AFB6 /* ImageView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4A2CA36263417B60019AFB6 /* ImageView+Extension.swift */; }; + D4A2CABA2637A6890019AFB6 /* EndPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4A2CAB92637A6890019AFB6 /* EndPoint.swift */; }; + D4A2CAC32637A8970019AFB6 /* BanchanListRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4A2CAC22637A8970019AFB6 /* BanchanListRepository.swift */; }; + D4A2CACC2637AFCA0019AFB6 /* BanchanSceneFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4A2CACB2637AFCA0019AFB6 /* BanchanSceneFlowCoordinator.swift */; }; + D4A2CAD32637B1250019AFB6 /* BanchanSceneDIContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4A2CAD22637B1250019AFB6 /* BanchanSceneDIContainer.swift */; }; + D4A2CAD92637B25B0019AFB6 /* AppDIContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4A2CAD82637B25B0019AFB6 /* AppDIContainer.swift */; }; + D4A2CADE2637B3D90019AFB6 /* AppFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4A2CADD2637B3D90019AFB6 /* AppFlowCoordinator.swift */; }; + D4A2CAF42637D7B10019AFB6 /* BanchanDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4A2CAF32637D7B10019AFB6 /* BanchanDetailViewModel.swift */; }; + D4A2CAF92637D7E70019AFB6 /* BanchanDetailDTO+Mapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4A2CAF82637D7E70019AFB6 /* BanchanDetailDTO+Mapping.swift */; }; + D4A2CAFE2637D8210019AFB6 /* BanchanDetailRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4A2CAFD2637D8210019AFB6 /* BanchanDetailRepository.swift */; }; + D4A2CB062637D8EF0019AFB6 /* FetchBanchanDetailUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4A2CB052637D8EF0019AFB6 /* FetchBanchanDetailUseCase.swift */; }; + D4A2CB2526392FFC0019AFB6 /* OrderBanchanUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4A2CB2426392FFC0019AFB6 /* OrderBanchanUseCase.swift */; }; D4BFBAD3262E989000D68297 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4BFBAD2262E989000D68297 /* AppDelegate.swift */; }; D4BFBAD5262E989000D68297 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4BFBAD4262E989000D68297 /* SceneDelegate.swift */; }; D4BFBAD7262E989000D68297 /* BanchanListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4BFBAD6262E989000D68297 /* BanchanListViewController.swift */; }; @@ -29,10 +40,12 @@ D4BFBB3C262EC8DB00D68297 /* BanchanCustomCellHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4BFBB3A262EC8DB00D68297 /* BanchanCustomCellHeader.swift */; }; D4BFBB3D262EC8DB00D68297 /* BanchanCustomCellHeader.xib in Resources */ = {isa = PBXBuildFile; fileRef = D4BFBB3B262EC8DB00D68297 /* BanchanCustomCellHeader.xib */; }; FFA10EB1262FC6CD00D584B6 /* BanchanCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFA10EB0262FC6CD00D584B6 /* BanchanCollectionView.swift */; }; + FFB6F9D3263A569F00BDB164 /* CoreDataStorage.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = FFB6F9D1263A569F00BDB164 /* CoreDataStorage.xcdatamodeld */; }; + FFB6F9D5263A574500BDB164 /* CoreDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB6F9D4263A574500BDB164 /* CoreDataManager.swift */; }; + FFB6F9D7263A576600BDB164 /* BanchanListStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB6F9D6263A576600BDB164 /* BanchanListStorage.swift */; }; FFEF70F926310FB400189376 /* NetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFEF70F826310FB400189376 /* NetworkService.swift */; }; FFEF70FF2631104B00189376 /* BanchanListDTO+Mapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFEF70FE2631104B00189376 /* BanchanListDTO+Mapping.swift */; }; FFEF71042631107200189376 /* FetchBanchanListUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFEF71032631107200189376 /* FetchBanchanListUseCase.swift */; }; - FFEF71092631128900189376 /* FetchImageUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFEF71082631128900189376 /* FetchImageUseCase.swift */; }; FFEF7123263119C500189376 /* BanchanListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFEF7122263119C500189376 /* BanchanListViewModel.swift */; }; /* End PBXBuildFile section */ @@ -63,6 +76,17 @@ 8C36659865215502B6A787B2 /* Pods_SideDish.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SideDish.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D4A2C9AB263273C10019AFB6 /* BadgeLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BadgeLabel.swift; sourceTree = ""; }; D4A2CA36263417B60019AFB6 /* ImageView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ImageView+Extension.swift"; sourceTree = ""; }; + D4A2CAB92637A6890019AFB6 /* EndPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndPoint.swift; sourceTree = ""; }; + D4A2CAC22637A8970019AFB6 /* BanchanListRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BanchanListRepository.swift; sourceTree = ""; }; + D4A2CACB2637AFCA0019AFB6 /* BanchanSceneFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BanchanSceneFlowCoordinator.swift; sourceTree = ""; }; + D4A2CAD22637B1250019AFB6 /* BanchanSceneDIContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BanchanSceneDIContainer.swift; sourceTree = ""; }; + D4A2CAD82637B25B0019AFB6 /* AppDIContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDIContainer.swift; sourceTree = ""; }; + D4A2CADD2637B3D90019AFB6 /* AppFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppFlowCoordinator.swift; sourceTree = ""; }; + D4A2CAF32637D7B10019AFB6 /* BanchanDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BanchanDetailViewModel.swift; path = SideDish/Data/Repositories/BanchanDetailViewModel.swift; sourceTree = SOURCE_ROOT; }; + D4A2CAF82637D7E70019AFB6 /* BanchanDetailDTO+Mapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BanchanDetailDTO+Mapping.swift"; sourceTree = ""; }; + D4A2CAFD2637D8210019AFB6 /* BanchanDetailRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BanchanDetailRepository.swift; sourceTree = ""; }; + D4A2CB052637D8EF0019AFB6 /* FetchBanchanDetailUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchBanchanDetailUseCase.swift; sourceTree = ""; }; + D4A2CB2426392FFC0019AFB6 /* OrderBanchanUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderBanchanUseCase.swift; sourceTree = ""; }; D4BFBACF262E989000D68297 /* SideDish.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SideDish.app; sourceTree = BUILT_PRODUCTS_DIR; }; D4BFBAD2262E989000D68297 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; D4BFBAD4262E989000D68297 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -88,10 +112,12 @@ D7433C6120AE2C2B5B290532 /* Pods_SideDishTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SideDishTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E5121CE39396F4ECB1937C15 /* Pods-SideDish-SideDishUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SideDish-SideDishUITests.debug.xcconfig"; path = "Target Support Files/Pods-SideDish-SideDishUITests/Pods-SideDish-SideDishUITests.debug.xcconfig"; sourceTree = ""; }; FFA10EB0262FC6CD00D584B6 /* BanchanCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BanchanCollectionView.swift; sourceTree = ""; }; + FFB6F9D2263A569F00BDB164 /* CoreDataStorage.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = CoreDataStorage.xcdatamodel; sourceTree = ""; }; + FFB6F9D4263A574500BDB164 /* CoreDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataManager.swift; sourceTree = ""; }; + FFB6F9D6263A576600BDB164 /* BanchanListStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BanchanListStorage.swift; sourceTree = ""; }; FFEF70F826310FB400189376 /* NetworkService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkService.swift; sourceTree = ""; }; FFEF70FE2631104B00189376 /* BanchanListDTO+Mapping.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BanchanListDTO+Mapping.swift"; sourceTree = ""; }; FFEF71032631107200189376 /* FetchBanchanListUseCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchBanchanListUseCase.swift; sourceTree = ""; }; - FFEF71082631128900189376 /* FetchImageUseCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchImageUseCase.swift; sourceTree = ""; }; FFEF7122263119C500189376 /* BanchanListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BanchanListViewModel.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -155,6 +181,43 @@ path = Utills; sourceTree = ""; }; + D4A2CAC12637A8810019AFB6 /* Repositories */ = { + isa = PBXGroup; + children = ( + D4A2CAC22637A8970019AFB6 /* BanchanListRepository.swift */, + D4A2CAFD2637D8210019AFB6 /* BanchanDetailRepository.swift */, + ); + path = Repositories; + sourceTree = ""; + }; + D4A2CACA2637AFB20019AFB6 /* Flow */ = { + isa = PBXGroup; + children = ( + D4A2CACB2637AFCA0019AFB6 /* BanchanSceneFlowCoordinator.swift */, + ); + path = Flow; + sourceTree = ""; + }; + D4A2CAD02637B0EB0019AFB6 /* Application */ = { + isa = PBXGroup; + children = ( + D4A2CAD12637B10B0019AFB6 /* DIContainer */, + D4BFBAD2262E989000D68297 /* AppDelegate.swift */, + D4BFBAD4262E989000D68297 /* SceneDelegate.swift */, + D4A2CADD2637B3D90019AFB6 /* AppFlowCoordinator.swift */, + ); + path = Application; + sourceTree = ""; + }; + D4A2CAD12637B10B0019AFB6 /* DIContainer */ = { + isa = PBXGroup; + children = ( + D4A2CAD22637B1250019AFB6 /* BanchanSceneDIContainer.swift */, + D4A2CAD82637B25B0019AFB6 /* AppDIContainer.swift */, + ); + path = DIContainer; + sourceTree = ""; + }; D4BFBAC6262E989000D68297 = { isa = PBXGroup; children = ( @@ -180,6 +243,7 @@ D4BFBAD1262E989000D68297 /* SideDish */ = { isa = PBXGroup; children = ( + D4A2CAD02637B0EB0019AFB6 /* Application */, D4BFBB1B262EA76300D68297 /* Data */, D4BFBB1A262EA75D00D68297 /* Domain */, D4BFBB19262EA74E00D68297 /* Presentaion */, @@ -211,6 +275,7 @@ D4BFBB19262EA74E00D68297 /* Presentaion */ = { isa = PBXGroup; children = ( + D4A2CACA2637AFB20019AFB6 /* Flow */, D4A2C9AA263273A60019AFB6 /* Utills */, D4BFBB1F262EA7AC00D68297 /* DetailBanchan */, D4BFBB1E262EA7A100D68297 /* BanchanList */, @@ -230,6 +295,7 @@ D4BFBB1B262EA76300D68297 /* Data */ = { isa = PBXGroup; children = ( + D4A2CAC12637A8810019AFB6 /* Repositories */, D4BFBB27262EA80B00D68297 /* PersistentStorages */, D4BFBB26262EA7F500D68297 /* Network */, ); @@ -287,6 +353,7 @@ D4BFBB23262EA7C700D68297 /* ViewModel */ = { isa = PBXGroup; children = ( + D4A2CAF32637D7B10019AFB6 /* BanchanDetailViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -304,7 +371,8 @@ isa = PBXGroup; children = ( FFEF71032631107200189376 /* FetchBanchanListUseCase.swift */, - FFEF71082631128900189376 /* FetchImageUseCase.swift */, + D4A2CB052637D8EF0019AFB6 /* FetchBanchanDetailUseCase.swift */, + D4A2CB2426392FFC0019AFB6 /* OrderBanchanUseCase.swift */, ); path = UseCases; sourceTree = ""; @@ -313,6 +381,7 @@ isa = PBXGroup; children = ( FFEF70F826310FB400189376 /* NetworkService.swift */, + D4A2CAB92637A6890019AFB6 /* EndPoint.swift */, FFEF70FD2631103C00189376 /* DataMapping */, ); path = Network; @@ -321,25 +390,36 @@ D4BFBB27262EA80B00D68297 /* PersistentStorages */ = { isa = PBXGroup; children = ( + FFB6F9D6263A576600BDB164 /* BanchanListStorage.swift */, + FFB6F9D0263A569300BDB164 /* CoreDataStorage */, ); - path = PersistentStorages; + name = PersistentStorages; + path = ../PersistentStorages; sourceTree = ""; }; D4BFBB28262EA83E00D68297 /* SupportingFiles */ = { isa = PBXGroup; children = ( - D4BFBAD2262E989000D68297 /* AppDelegate.swift */, - D4BFBAD4262E989000D68297 /* SceneDelegate.swift */, D4BFBADB262E989300D68297 /* Assets.xcassets */, D4BFBADD262E989300D68297 /* LaunchScreen.storyboard */, ); path = SupportingFiles; sourceTree = ""; }; + FFB6F9D0263A569300BDB164 /* CoreDataStorage */ = { + isa = PBXGroup; + children = ( + FFB6F9D4263A574500BDB164 /* CoreDataManager.swift */, + FFB6F9D1263A569F00BDB164 /* CoreDataStorage.xcdatamodeld */, + ); + path = CoreDataStorage; + sourceTree = ""; + }; FFEF70FD2631103C00189376 /* DataMapping */ = { isa = PBXGroup; children = ( FFEF70FE2631104B00189376 /* BanchanListDTO+Mapping.swift */, + D4A2CAF82637D7E70019AFB6 /* BanchanDetailDTO+Mapping.swift */, ); path = DataMapping; sourceTree = ""; @@ -585,21 +665,34 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D4A2CB062637D8EF0019AFB6 /* FetchBanchanDetailUseCase.swift in Sources */, FFEF70F926310FB400189376 /* NetworkService.swift in Sources */, D4BFBAD7262E989000D68297 /* BanchanListViewController.swift in Sources */, D4BFBAD3262E989000D68297 /* AppDelegate.swift in Sources */, + D4A2CAF42637D7B10019AFB6 /* BanchanDetailViewModel.swift in Sources */, + D4A2CAC32637A8970019AFB6 /* BanchanListRepository.swift in Sources */, + FFB6F9D5263A574500BDB164 /* CoreDataManager.swift in Sources */, + D4A2CB2526392FFC0019AFB6 /* OrderBanchanUseCase.swift in Sources */, + D4A2CAF92637D7E70019AFB6 /* BanchanDetailDTO+Mapping.swift in Sources */, D4BFBAD5262E989000D68297 /* SceneDelegate.swift in Sources */, FFA10EB1262FC6CD00D584B6 /* BanchanCollectionView.swift in Sources */, + D4A2CABA2637A6890019AFB6 /* EndPoint.swift in Sources */, + FFB6F9D7263A576600BDB164 /* BanchanListStorage.swift in Sources */, D4BFBB0A262E9A8200D68297 /* BanchanDetailViewController.swift in Sources */, D4BFBB2F262EAD1C00D68297 /* BanchanDetail.swift in Sources */, + FFB6F9D3263A569F00BDB164 /* CoreDataStorage.xcdatamodeld in Sources */, FFEF71042631107200189376 /* FetchBanchanListUseCase.swift in Sources */, FFEF70FF2631104B00189376 /* BanchanListDTO+Mapping.swift in Sources */, D4BFBB35262EB89800D68297 /* BanchanCustomCell.swift in Sources */, D4A2CA37263417B60019AFB6 /* ImageView+Extension.swift in Sources */, D4BFBB2A262EA93700D68297 /* Banchan.swift in Sources */, - FFEF71092631128900189376 /* FetchImageUseCase.swift in Sources */, + D4A2CAD92637B25B0019AFB6 /* AppDIContainer.swift in Sources */, + D4A2CACC2637AFCA0019AFB6 /* BanchanSceneFlowCoordinator.swift in Sources */, D4A2C9AC263273C10019AFB6 /* BadgeLabel.swift in Sources */, + D4A2CAFE2637D8210019AFB6 /* BanchanDetailRepository.swift in Sources */, + D4A2CAD32637B1250019AFB6 /* BanchanSceneDIContainer.swift in Sources */, FFEF7123263119C500189376 /* BanchanListViewModel.swift in Sources */, + D4A2CADE2637B3D90019AFB6 /* AppFlowCoordinator.swift in Sources */, D4BFBB3C262EC8DB00D68297 /* BanchanCustomCellHeader.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -939,6 +1032,19 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCVersionGroup section */ + FFB6F9D1263A569F00BDB164 /* CoreDataStorage.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + FFB6F9D2263A569F00BDB164 /* CoreDataStorage.xcdatamodel */, + ); + currentVersion = FFB6F9D2263A569F00BDB164 /* CoreDataStorage.xcdatamodel */; + path = CoreDataStorage.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; +/* End XCVersionGroup section */ }; rootObject = D4BFBAC7262E989000D68297 /* Project object */; } diff --git a/iOS/SideDish/SideDish.xcodeproj/xcuserdata/jibook.xcuserdatad/xcschemes/xcschememanagement.plist b/iOS/SideDish/SideDish.xcodeproj/xcuserdata/jibook.xcuserdatad/xcschemes/xcschememanagement.plist index 03b6a757e..6a2e741db 100644 --- a/iOS/SideDish/SideDish.xcodeproj/xcuserdata/jibook.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/iOS/SideDish/SideDish.xcodeproj/xcuserdata/jibook.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ SideDish.xcscheme_^#shared#^_ orderHint - 5 + 6 diff --git a/iOS/SideDish/SideDish.xcworkspace/xcuserdata/jibook.xcuserdatad/UserInterfaceState.xcuserstate b/iOS/SideDish/SideDish.xcworkspace/xcuserdata/jibook.xcuserdatad/UserInterfaceState.xcuserstate index 14146f759..33f3ede3e 100644 Binary files a/iOS/SideDish/SideDish.xcworkspace/xcuserdata/jibook.xcuserdatad/UserInterfaceState.xcuserstate and b/iOS/SideDish/SideDish.xcworkspace/xcuserdata/jibook.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/iOS/SideDish/SideDish.xcworkspace/xcuserdata/jibook.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/iOS/SideDish/SideDish.xcworkspace/xcuserdata/jibook.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 000000000..2850884d4 --- /dev/null +++ b/iOS/SideDish/SideDish.xcworkspace/xcuserdata/jibook.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,6 @@ + + + diff --git a/iOS/SideDish/SideDish.xcworkspace/xcuserdata/shim.xcuserdatad/UserInterfaceState.xcuserstate b/iOS/SideDish/SideDish.xcworkspace/xcuserdata/shim.xcuserdatad/UserInterfaceState.xcuserstate index bbd758f88..4be10ab4d 100644 Binary files a/iOS/SideDish/SideDish.xcworkspace/xcuserdata/shim.xcuserdatad/UserInterfaceState.xcuserstate and b/iOS/SideDish/SideDish.xcworkspace/xcuserdata/shim.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/iOS/SideDish/SideDish/SupportingFiles/AppDelegate.swift b/iOS/SideDish/SideDish/Application/AppDelegate.swift similarity index 99% rename from iOS/SideDish/SideDish/SupportingFiles/AppDelegate.swift rename to iOS/SideDish/SideDish/Application/AppDelegate.swift index 6d4fad651..f81d70bf4 100644 --- a/iOS/SideDish/SideDish/SupportingFiles/AppDelegate.swift +++ b/iOS/SideDish/SideDish/Application/AppDelegate.swift @@ -7,6 +7,7 @@ import UIKit + @main class AppDelegate: UIResponder, UIApplicationDelegate { diff --git a/iOS/SideDish/SideDish/Application/AppFlowCoordinator.swift b/iOS/SideDish/SideDish/Application/AppFlowCoordinator.swift new file mode 100644 index 000000000..94059ab30 --- /dev/null +++ b/iOS/SideDish/SideDish/Application/AppFlowCoordinator.swift @@ -0,0 +1,24 @@ +// +// AppFlowCoordinator.swift +// SideDish +// +// Created by μ‹¬μ˜λ―Ό on 2021/04/27. +// + +import UIKit + +class AppFlowCoordinator { + private var navigationController: UINavigationController + private let appDIContainer: AppDIContainer + + init(navigationController: UINavigationController, appDIContainer: AppDIContainer) { + self.navigationController = navigationController + self.appDIContainer = appDIContainer + } + + func start() { + let banchanSceneDIContainer = appDIContainer.makeBanchanSceneDIContainer() + let flow = banchanSceneDIContainer.makeBanchanSceneFlowCoordinator(navigationController: navigationController) + flow.start() + } +} diff --git a/iOS/SideDish/SideDish/Application/DIContainer/AppDIContainer.swift b/iOS/SideDish/SideDish/Application/DIContainer/AppDIContainer.swift new file mode 100644 index 000000000..eb0624374 --- /dev/null +++ b/iOS/SideDish/SideDish/Application/DIContainer/AppDIContainer.swift @@ -0,0 +1,17 @@ +// +// AppDIContainer.swift +// SideDish +// +// Created by μ‹¬μ˜λ―Ό on 2021/04/27. +// + +import Foundation + +class AppDIContainer { + lazy var apiNetworkService = DefaultNetworkSerivce() + + func makeBanchanSceneDIContainer() -> BanchanSceneDIContainer { + let dependencies = BanchanSceneDIContainer.Dependencies.init(apiNetworkService: apiNetworkService) + return BanchanSceneDIContainer(dependencies: dependencies) + } +} diff --git a/iOS/SideDish/SideDish/Application/DIContainer/BanchanSceneDIContainer.swift b/iOS/SideDish/SideDish/Application/DIContainer/BanchanSceneDIContainer.swift new file mode 100644 index 000000000..07e6a2e15 --- /dev/null +++ b/iOS/SideDish/SideDish/Application/DIContainer/BanchanSceneDIContainer.swift @@ -0,0 +1,73 @@ +// +// BanchanSceneDIContainer.swift +// SideDish +// +// Created by μ‹¬μ˜λ―Ό on 2021/04/27. +// + +import UIKit + +class BanchanSceneDIContainer: BanchanSceneFlowCoordinatorDependencies { + + struct Dependencies { + let apiNetworkService: NetworkService + } + + private let dependencies: Dependencies + + init(dependencies: Dependencies) { + self.dependencies = dependencies + } + + // MARK: - Persistent Storages + private func makeBanchanListStroage() -> CoreDataBanchanListStorage { + return CoreDataBanchanListStorage() + } + + // MARK: - Repositories + private func makeBanchanListRepository() -> BanchanListRepository { + return DefaultBanchanListRepository(network: dependencies.apiNetworkService, storage: makeBanchanListStroage()) + } + + // MARK: - Use Cases + private func makeFetchBanchanListUseCase() -> FetchBanchanListUseCase{ + return DefaultFetchBanchanListUseCase(banchanListRepository: makeBanchanListRepository()) + } + + // MARK: - ViewModel + private func makeBanchanListViewModel(action: BanchanListViewModelAction) -> BanchanListViewModel { + return BanchanListViewModel(fetchBanchanListUseCase: makeFetchBanchanListUseCase(), action: action) + } + + // MARK: - ViewController + internal func makeBanchanListViewController(action: BanchanListViewModelAction) -> BanchanListViewController { + return BanchanListViewController.create(with: makeBanchanListViewModel(action: action)) + } + + // MARK: - Flow Coordinator + func makeBanchanSceneFlowCoordinator(navigationController: UINavigationController) -> BanchanSceneFlowCoordinator { + return BanchanSceneFlowCoordinator(navigationController: navigationController, dependencies: self) + } +} + +extension BanchanSceneDIContainer { + private func makeBanchanDetailRepository() -> BanchanDetailRepository { + return DefaultBanchanDetailRepository(network: dependencies.apiNetworkService) + } + + // MARK: - Use Cases + private func makeFetchBanchanDetailUseCase() -> FetchBanchanDetailUseCase{ + return DefaultFetchBanchanDetailUseCase(banchanDetailRepository: makeBanchanDetailRepository()) + } + + // MARK: - ViewModel + private func makeBanchanDetailViewModel(hash: Int, action: BanchanDetailViewModelAction) -> BanchanDetailViewModel { + return BanchanDetailViewModel(hash: hash, fetchBanchanDetailUseCase: makeFetchBanchanDetailUseCase(), action: action) + } + + // MARK: - ViewController + internal func makeBanchanDetailViewController(hash: Int, action: BanchanDetailViewModelAction) -> BanchanDetailViewController { + return BanchanDetailViewController.create(with: makeBanchanDetailViewModel(hash: hash, action: action)) + } +} + diff --git a/iOS/SideDish/SideDish/Application/SceneDelegate.swift b/iOS/SideDish/SideDish/Application/SceneDelegate.swift new file mode 100644 index 000000000..a80c06257 --- /dev/null +++ b/iOS/SideDish/SideDish/Application/SceneDelegate.swift @@ -0,0 +1,26 @@ +// +// SceneDelegate.swift +// SideDish +// +// Created by μ‹¬μ˜λ―Ό on 2021/04/20. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + private let appDIContainer = AppDIContainer() + private var flowCoordinator: AppFlowCoordinator? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let windowScene = (scene as? UIWindowScene) else { return } + window = UIWindow(windowScene: windowScene) + let navigationController = UINavigationController() + window?.rootViewController = navigationController + flowCoordinator = AppFlowCoordinator(navigationController: navigationController, appDIContainer: appDIContainer) + flowCoordinator?.start() + window?.makeKeyAndVisible() + } +} + diff --git a/iOS/SideDish/SideDish/Base.lproj/Main.storyboard b/iOS/SideDish/SideDish/Base.lproj/Main.storyboard index 36ba0373c..839e44bad 100644 --- a/iOS/SideDish/SideDish/Base.lproj/Main.storyboard +++ b/iOS/SideDish/SideDish/Base.lproj/Main.storyboard @@ -1,8 +1,9 @@ - - + + - + + @@ -12,7 +13,7 @@ - + @@ -60,27 +61,370 @@ - - + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + - - + + - + + + + + + + + + + + + diff --git a/iOS/SideDish/SideDish/Data/Network/DataMapping/BanchanDetailDTO+Mapping.swift b/iOS/SideDish/SideDish/Data/Network/DataMapping/BanchanDetailDTO+Mapping.swift new file mode 100644 index 000000000..40ba9ae6a --- /dev/null +++ b/iOS/SideDish/SideDish/Data/Network/DataMapping/BanchanDetailDTO+Mapping.swift @@ -0,0 +1,39 @@ +// +// BanchanDetailDTO+Mapping.swift +// SideDish +// +// Created by μ‹¬μ˜λ―Ό on 2021/04/27. +// + +import Foundation + +struct BanchanDetailDTO: Decodable { + private let hash: Int + private let data: BanchanDetailItemDTO + +} + +extension BanchanDetailDTO { + struct BanchanDetailItemDTO: Decodable { + private let topImage: String + private let thumbImage: [String] + private let detailSection: [String] + private let productDescription: String + private let title: String + private let deliveryInfo: String + private let deliveryFee: String + private let point: String + private let nPrice: String? + private let sPrice: String + private let badges: [String] + private let inStock: Bool + + func toDomain() -> BanchanDetail { + return .init(topImage: topImage, thumbImages: thumbImage, detailSection: detailSection, productDescription: productDescription, title: title, deliveryInfo: deliveryInfo, deliveryFee: deliveryFee, point: point, nPrice: nPrice, sPrice: sPrice, badges: badges, inStock: inStock) + } + } + + func toDomain() -> BanchanDetail { + return data.toDomain() + } +} diff --git a/iOS/SideDish/SideDish/Data/Network/DataMapping/BanchanListDTO+Mapping.swift b/iOS/SideDish/SideDish/Data/Network/DataMapping/BanchanListDTO+Mapping.swift index 2ef49c0d8..7bcd73b82 100644 --- a/iOS/SideDish/SideDish/Data/Network/DataMapping/BanchanListDTO+Mapping.swift +++ b/iOS/SideDish/SideDish/Data/Network/DataMapping/BanchanListDTO+Mapping.swift @@ -6,6 +6,7 @@ // import Foundation +import CoreData struct BanchanListDTO: Decodable { private let body: [BanchanListItemDTO] @@ -21,10 +22,25 @@ extension BanchanListDTO { private let title: String private var nPrice: String? private let sPrice: String - private let badge: [String]? + private let badges: [String]? func toDomain() -> Banchan { - return .init(detailHash: detailHash, image: image, alt: alt, title: title, description: description, nPrice: nPrice, sPrice: sPrice, badges: badge, deliveryType: deliveryType) + return .init(detailHash: detailHash, image: image, alt: alt, title: title, description: description, nPrice: nPrice, sPrice: sPrice, badges: badges, deliveryType: deliveryType) + } + + func toEntity(with context: NSManagedObjectContext) -> BanchanEntity { + let entity = BanchanEntity.init(context: context) + entity.detailHash = Int16(detailHash) + entity.image = image + entity.alt = alt + entity.deliveryType = deliveryType + entity.banchanDescription = description + entity.title = title + entity.nPrice = nPrice + entity.sPrice = sPrice + entity.badges = badges + + return entity } } @@ -34,4 +50,17 @@ extension BanchanListDTO { } return banchans } + + func toEntity(with context: NSManagedObjectContext) -> [BanchanEntity] { + let entities = body.map { banchanListItemDTO in + banchanListItemDTO.toEntity(with: context) + } + return entities + } +} + +extension BanchanEntity { + func toDomain() -> Banchan { + return Banchan.init(detailHash: Int(detailHash), image: image!, alt: alt!, title: title!, description: banchanDescription!, nPrice: nPrice, sPrice: sPrice!, badges: badges, deliveryType: deliveryType!) + } } diff --git a/iOS/SideDish/SideDish/Data/Network/EndPoint.swift b/iOS/SideDish/SideDish/Data/Network/EndPoint.swift new file mode 100644 index 000000000..099c7e56f --- /dev/null +++ b/iOS/SideDish/SideDish/Data/Network/EndPoint.swift @@ -0,0 +1,90 @@ +// +// EndPoint.swift +// SideDish +// +// Created by μ‹¬μ˜λ―Ό on 2021/04/27. +// + +import Foundation + +public protocol Requestable { + var baseURL: String { get } + var path: String { get } + var httpMethod: HttpMethod { get } + func url() -> URL? +} + +public struct BanchanAPIEndpoint: Requestable { + + public let baseURL = "http://ec2-54-180-115-20.ap-northeast-2.compute.amazonaws.com:8080/" + public let path: String + public let httpMethod: HttpMethod + + init(path: String, httpMethod: HttpMethod) { + self.path = path + self.httpMethod = httpMethod + } + + public func url() -> URL? { + return URL(string: baseURL + path) + } +} + +public struct BanchanDetailAPIEndpoint: Requestable { + + public let baseURL = "http://ec2-54-180-115-20.ap-northeast-2.compute.amazonaws.com:8080/detail/" + public let path: String + public let httpMethod: HttpMethod + + init(path: String, httpMethod: HttpMethod) { + self.path = path + self.httpMethod = httpMethod + } + + public func url() -> URL? { + return URL(string: baseURL + path) + } +} + +public struct OrderBanchanAPIEndPoint: Requestable { + public let baseURL = "http://ec2-54-180-115-20.ap-northeast-2.compute.amazonaws.com:8080/detail/" + public let path: String + public let httpMethod: HttpMethod + + init(path: String, httpMethod: HttpMethod) { + self.path = path + self.httpMethod = httpMethod + } + + public func url() -> URL? { + return URL(string: baseURL + path + "/order") + } +} + + +public enum Section: Int, CaseIterable { + case main = 0 + case soup, side + + func path() -> String { + switch self { + case .main: + return "main" + case .soup: + return "soup" + case .side: + return "side" + } + } + + func description() -> String { + switch self { + case .main: + return "λͺ¨λ‘κ°€ μ’‹μ•„ν•˜λŠ” λ“ λ“ ν•œ λ©”μΈμš”λ¦¬" + case .soup: + return "정성이 λ‹΄κΈ΄ 뜨끈뜨끈 κ΅­λ¬Όμš”λ¦¬" + case .side: + return "식탁을 ν’μ„±ν•˜κ²Œ ν•˜λŠ” μ •κ°ˆν•œ λ°‘λ°˜μ°¬" + } + } +} diff --git a/iOS/SideDish/SideDish/Data/Network/NetworkService.swift b/iOS/SideDish/SideDish/Data/Network/NetworkService.swift index 4001745a2..6bda1cf8d 100644 --- a/iOS/SideDish/SideDish/Data/Network/NetworkService.swift +++ b/iOS/SideDish/SideDish/Data/Network/NetworkService.swift @@ -6,32 +6,33 @@ // import Foundation -import Alamofire import Combine -protocol NetworkRequest { - func request(with url: String, dataType: T.Type, httpMethod: HTTPMethod) +protocol NetworkService { + func request(with endPoint: Requestable, dataType: T.Type) -> AnyPublisher + func post (with endPoint: Requestable, data: T) + -> AnyPublisher func decode(data: Data, dataType: T.Type) -> AnyPublisher + func encode(anyData: T) -> Result } -class NetworkSerivce: NetworkRequest { +public class DefaultNetworkSerivce: NetworkService { - static let shared = NetworkSerivce() - - private init() { + init() { } - func request(with url: String, dataType: T.Type, httpMethod: HTTPMethod) + func request(with endPoint: Requestable, dataType: T.Type) -> AnyPublisher { - guard let url = URL(string: url) else { + + guard let url = endPoint.url() else { let error = NetworkError.network(description: "Couldn't create URL") return Fail(error: error).eraseToAnyPublisher() } var request = URLRequest(url: url) request.httpBody = nil - request.httpMethod = httpMethod.rawValue + request.httpMethod = endPoint.httpMethod.rawValue return URLSession.shared.dataTaskPublisher(for: request) .mapError { error in @@ -43,27 +44,72 @@ class NetworkSerivce: NetworkRequest { .eraseToAnyPublisher() } + func post (with endPoint: Requestable, data: T) + -> AnyPublisher { + guard let url = endPoint.url() else { + let error = NetworkError.network(description: "Couldn't create URL") + return Fail(error: error).eraseToAnyPublisher() + } + + let result = encode(anyData: data) + let json: Data + switch result { + case .failure(let error): + print(error) + return Fail(error: error).eraseToAnyPublisher() + case .success(let data): + json = data + } + + var request = URLRequest(url: url) + request.httpBody = json + request.httpMethod = endPoint.httpMethod.rawValue + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + if json != nil { request.setValue(String(json.count), forHTTPHeaderField: "Content-Length") } + + return URLSession.shared.dataTaskPublisher(for: request) + .tryMap { data, response -> Int in + guard let httpResponse = response as? HTTPURLResponse else { + throw NetworkError.network(description: "Couldn't create URL") + } + return httpResponse.statusCode + } + .mapError { error in + NetworkError.network(description: error.localizedDescription) + } + .eraseToAnyPublisher() + } + func decode(data: Data, dataType: T.Type) -> AnyPublisher { let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase - // JustλŠ” 였직 ν•˜λ‚˜μ˜ κ°’λ§Œμ„ 좜λ ₯ν•˜κ³  λλ‚˜κ²Œλ˜λŠ” κ°€μž₯ λ‹¨μˆœν•œ ν˜•νƒœμ˜ Publisher - // Combine Framework μ—μ„œ λΉŒνŠΈμΈν˜•νƒœλ‘œ μ œκ³΅ν•˜λŠ” Publisher - // 인자둜 λ°›λŠ” κ°’μ˜ νƒ€μž…μ„ Output νƒ€μž…μœΌλ‘œ μ‹€νŒ¨νƒ€μž…μ€ 항상 Never둜 리턴 return Just(data) .decode(type: T.self, decoder: decoder) - // 기본적으둜 mapκ³Ό 같은역할. errorκ°€ λ°œμƒν•˜λ©΄ ν•΄λ‹Ή error λ₯Ό λ‚΄κ°€ μ›ν•˜λŠ” error νƒ€μž…μœΌλ‘œ λ°”κΏ”μ€€λ‹€. .mapError { error in .parsing(description: error.localizedDescription) } .eraseToAnyPublisher() - // Operationμ—μ„œ 데이터λ₯Ό μ²˜λ¦¬ν•  땐 Operation μƒν˜Έ κ°„ μ—λŸ¬ μ²˜λ¦¬λ‚˜ ν˜Ήμ€ 슀트림 μ œμ–΄λ₯Ό μœ„ν•΄μ„œ - // 데이터 ν˜•μ‹μ„ μ•Œμ•„μ•Ό ν•˜μ§€λ§Œ Subscriber μ—κ²Œ 전달될 땐 ν•„μš”κ°€ μ—†λ‹€. - // λ”°λΌμ„œ μ΅œμ’…μ μΈ ν˜•νƒœλ‘œ 데이터λ₯Ό 전달할 땐 eraseToAnyPublisher() λ₯Ό μ‚¬μš©ν•˜κ²Œ λœλ‹€. + } + + func encode(anyData: T) -> Result { + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + + guard let data = try? encoder.encode(anyData) else { + return Result.failure(NetworkError.encoding(description: "encoding Failure")) + } + return Result.success(data) } } enum NetworkError: Error { + case encoding(description: String) case parsing(description: String) case network(description: String) } + +public enum HttpMethod: String { + case get = "GET" + case post = "POST" +} diff --git a/iOS/SideDish/SideDish/Data/Repositories/BanchanDetailRepository.swift b/iOS/SideDish/SideDish/Data/Repositories/BanchanDetailRepository.swift new file mode 100644 index 000000000..8d483db77 --- /dev/null +++ b/iOS/SideDish/SideDish/Data/Repositories/BanchanDetailRepository.swift @@ -0,0 +1,38 @@ +// +// BanchanDetailRepository.swift +// SideDish +// +// Created by μ‹¬μ˜λ―Ό on 2021/04/27. +// + +import Combine + +protocol BanchanDetailRepository { + func fetchBanchanDetail(hash: Int, completion: @escaping (BanchanDetail?) -> Void) +} + +class DefaultBanchanDetailRepository: BanchanDetailRepository { + private let network: NetworkService + private var subscriptions = Set() + + init(network: NetworkService) { + self.network = network + } + + func fetchBanchanDetail(hash: Int, completion: @escaping (BanchanDetail?) -> Void) { + let endPoint = BanchanDetailAPIEndpoint(path: "\(hash)", httpMethod: .get) + + network.request(with: endPoint, dataType: BanchanDetailDTO.self) + .sink(receiveCompletion: {result in + switch result { + case .failure(let error): + print(error) + case .finished: + break + } + }, receiveValue: { banchanDetailDTO in + completion(banchanDetailDTO.toDomain()) + }) + .store(in: &subscriptions) + } +} diff --git a/iOS/SideDish/SideDish/Data/Repositories/BanchanDetailViewModel.swift b/iOS/SideDish/SideDish/Data/Repositories/BanchanDetailViewModel.swift new file mode 100644 index 000000000..690f47e2f --- /dev/null +++ b/iOS/SideDish/SideDish/Data/Repositories/BanchanDetailViewModel.swift @@ -0,0 +1,69 @@ +// +// BanchanDetailViewModel.swift +// SideDish +// +// Created by μ‹¬μ˜λ―Ό on 2021/04/27. +// + +import UIKit +import Combine + +struct BanchanDetailViewModelAction { + let popBanchanDetail: () -> Void +} +enum AlertMessage: String { + case success = "주문이 μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€." + case failure = "μž¬κ³ κ°€ λΆ€μ‘±ν•©λ‹ˆλ‹€." +} + + +class BanchanDetailViewModel { + + private var fetchBanchanDetailUseCase: FetchBanchanDetailUseCase + private var orderBanchanDetailUseCase: OrderBanchanUseCase + private var action: BanchanDetailViewModelAction + @Published var banchanDetail: BanchanDetail! + private var hash: Int + + init(hash: Int, fetchBanchanDetailUseCase: FetchBanchanDetailUseCase, action: BanchanDetailViewModelAction) { + self.hash = hash + self.fetchBanchanDetailUseCase = fetchBanchanDetailUseCase + self.orderBanchanDetailUseCase = DefaultOrderBanchanUseCase(network: DefaultNetworkSerivce()) + self.action = action + fetchBanchanDetail() + } + + func fetchBanchanDetail() { + fetchBanchanDetailUseCase.execute(hash: hash) { (banchanDetail) in + self.banchanDetail = banchanDetail + } + } + + func didTappedOrderButton(quantity: Int, completion: @escaping (Int?, NetworkError?) -> Void) { + orderBanchanDetailUseCase.execute(hash: hash, quantity: quantity) { (result, error) in + completion(result,error) + } + } + + func popBanchanDetail() { + action.popBanchanDetail() + } + + func makeAlert(message: AlertMessage) -> UIAlertController { + let alert = UIAlertController(title: "", message: "", preferredStyle: .alert) + + switch message { + case .failure: + alert.message = message.rawValue + let okAction = UIAlertAction(title: "OK", style: .cancel, handler: nil) + alert.addAction(okAction) + case .success: + alert.message = message.rawValue + let okAction = UIAlertAction(title: "OK", style: .cancel, handler: { _ in + self.popBanchanDetail() + }) + alert.addAction(okAction) + } + return alert + } +} diff --git a/iOS/SideDish/SideDish/Data/Repositories/BanchanListRepository.swift b/iOS/SideDish/SideDish/Data/Repositories/BanchanListRepository.swift new file mode 100644 index 000000000..e1bdd704b --- /dev/null +++ b/iOS/SideDish/SideDish/Data/Repositories/BanchanListRepository.swift @@ -0,0 +1,49 @@ +// +// BanchanListRepository.swift +// SideDish +// +// Created by μ‹¬μ˜λ―Ό on 2021/04/27. +// + +import CoreData +import Combine + +protocol BanchanListRepository { + func fetchBanchanList(section: Section, completion: @escaping ([Banchan]?) -> Void) +} + +class DefaultBanchanListRepository: BanchanListRepository { + private let network: NetworkService + private let banchanListStorage: CoreDataBanchanListStorage + private var subscriptions = Set() + + init(network: NetworkService, storage: CoreDataBanchanListStorage) { + self.network = network + self.banchanListStorage = storage + } + + func fetchBanchanList(section: Section, completion: @escaping ([Banchan]?) -> Void) { + banchanListStorage.fetch(section: section) { result in + switch result { + case .success(let banchans): + completion(banchans) + case .failure(_): + print("coredata Failure") + let endPoint = BanchanAPIEndpoint(path: section.path(), httpMethod: .get) + self.network.request(with: endPoint, dataType: BanchanListDTO.self) + .sink(receiveCompletion: { result in + switch result { + case .failure(let error): + print(error) + case .finished: + break + } + }, receiveValue: { banchanListDTO in + self.banchanListStorage.save(section: section, requestDTO: banchanListDTO) + completion(banchanListDTO.toDomain()) + }) + .store(in: &self.subscriptions) + } + } + } +} diff --git a/iOS/SideDish/SideDish/Domain/Entities/BanchanDetail.swift b/iOS/SideDish/SideDish/Domain/Entities/BanchanDetail.swift index 183bbc4c0..751620363 100644 --- a/iOS/SideDish/SideDish/Domain/Entities/BanchanDetail.swift +++ b/iOS/SideDish/SideDish/Domain/Entities/BanchanDetail.swift @@ -8,15 +8,16 @@ import Foundation struct BanchanDetail { - private var topImage: URL - private var thumbImages: [URL] - private var detailImage: [URL] - private var description: String - private var title: String - private var deliveryInfo: String - private var deliveryFee: String - private var point: Int - private var netPrice: Int - private var salePrice: Int? - private var badges: [PriceType] + private (set) var topImage: String + private (set) var thumbImages: [String] + private (set) var detailSection: [String] + private (set) var productDescription: String + private (set) var title: String + private (set) var deliveryInfo: String + private (set) var deliveryFee: String + private (set) var point: String + private (set) var nPrice: String? + private (set) var sPrice: String + private (set) var badges: [String] + private (set) var inStock: Bool } diff --git a/iOS/SideDish/SideDish/Domain/UseCases/FetchBanchanDetailUseCase.swift b/iOS/SideDish/SideDish/Domain/UseCases/FetchBanchanDetailUseCase.swift new file mode 100644 index 000000000..577c4ff80 --- /dev/null +++ b/iOS/SideDish/SideDish/Domain/UseCases/FetchBanchanDetailUseCase.swift @@ -0,0 +1,27 @@ +// +// File.swift +// SideDish +// +// Created by μ‹¬μ˜λ―Ό on 2021/04/27. +// + +import Combine + +protocol FetchBanchanDetailUseCase { + func execute(hash: Int, completion: @escaping (BanchanDetail?)->Void) +} + +class DefaultFetchBanchanDetailUseCase: FetchBanchanDetailUseCase { + + private let banchanDetailRepository: BanchanDetailRepository + + init(banchanDetailRepository: BanchanDetailRepository) { + self.banchanDetailRepository = banchanDetailRepository + } + + func execute(hash: Int, completion: @escaping (BanchanDetail?)->Void) { + banchanDetailRepository.fetchBanchanDetail(hash: hash) { (banchanDetail) in + completion(banchanDetail) + } + } +} diff --git a/iOS/SideDish/SideDish/Domain/UseCases/FetchBanchanListUseCase.swift b/iOS/SideDish/SideDish/Domain/UseCases/FetchBanchanListUseCase.swift index 5ce90a430..cab24715f 100644 --- a/iOS/SideDish/SideDish/Domain/UseCases/FetchBanchanListUseCase.swift +++ b/iOS/SideDish/SideDish/Domain/UseCases/FetchBanchanListUseCase.swift @@ -5,27 +5,23 @@ // Created by 지뢁 on 2021/04/21. // -import Foundation import Combine -struct FetchBanchanListUseCase { - private var subscriptions = Set() - - mutating func fetchBanchanList(network: NetworkRequest, baseURL: String, section: String, completion: @escaping ([Banchan]?) -> Void) { - let url = baseURL+section +protocol FetchBanchanListUseCase { + func execute(section: Section, completion: @escaping ([Banchan]?)->Void) +} - network.request(with: url, dataType: BanchanListDTO.self, httpMethod: .get) - .receive(on: DispatchQueue.main) - .sink(receiveCompletion: { result in - switch result { - case .failure(let error): - print(error) - case .finished: - break - } - }, receiveValue: { banchanListDTO in - completion(banchanListDTO.toDomain()) - }) - .store(in: &subscriptions) +class DefaultFetchBanchanListUseCase: FetchBanchanListUseCase { + + private let banchanListRepository: BanchanListRepository + + init(banchanListRepository: BanchanListRepository) { + self.banchanListRepository = banchanListRepository + } + + func execute(section: Section, completion: @escaping ([Banchan]?)->Void) { + banchanListRepository.fetchBanchanList(section: section) { (banchans) in + completion(banchans) + } } } diff --git a/iOS/SideDish/SideDish/Domain/UseCases/FetchImageUseCase.swift b/iOS/SideDish/SideDish/Domain/UseCases/FetchImageUseCase.swift deleted file mode 100644 index c4e5e03a4..000000000 --- a/iOS/SideDish/SideDish/Domain/UseCases/FetchImageUseCase.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// FetchImageUseCase.swift -// AlamofirePratice -// -// Created by 지뢁 on 2021/04/21. -// - -import Foundation - -struct FetchImageUseCase { -// static func fetch(network: NetworkRequest, imgURL: String, completion: @escaping (Data?) -> Void) { -// network.request(url: imgURL, httpMethod: .get) { dataDummy in -// let data = dataDummy.data -// completion(data) -// } -// } -} diff --git a/iOS/SideDish/SideDish/Domain/UseCases/OrderBanchanUseCase.swift b/iOS/SideDish/SideDish/Domain/UseCases/OrderBanchanUseCase.swift new file mode 100644 index 000000000..d467d48b9 --- /dev/null +++ b/iOS/SideDish/SideDish/Domain/UseCases/OrderBanchanUseCase.swift @@ -0,0 +1,44 @@ +// +// OrderBanchanUseCase.swift +// SideDish +// +// Created by μ‹¬μ˜λ―Ό on 2021/04/28. +// + +import Combine +import UIKit + +protocol OrderBanchanUseCase { + func execute(hash: Int, quantity: Int, completion: @escaping (Int?, NetworkError?) -> Void) +} + +class DefaultOrderBanchanUseCase: OrderBanchanUseCase { + + private var network: NetworkService + private var subscriptions = Set() + init(network: NetworkService) { + self.network = network + } + + func execute(hash: Int, quantity: Int, completion: @escaping (Int?, NetworkError?) -> Void) { + let endPoint = OrderBanchanAPIEndPoint(path: "\(hash)", httpMethod: .post) + let data = body(quantity: quantity) + network.post(with: endPoint, data: data) + .sink(receiveCompletion: { result in + switch result { + case .failure(let error): + completion(nil, error) + case .finished: + break + } + }, receiveValue: { Result in + completion(Result, nil) + }) + .store(in: &subscriptions) + } +} + + +struct body: Encodable { + var quantity: Int +} diff --git a/iOS/SideDish/SideDish/PersistentStorages/BanchanListStorage.swift b/iOS/SideDish/SideDish/PersistentStorages/BanchanListStorage.swift new file mode 100644 index 000000000..ef96bdabd --- /dev/null +++ b/iOS/SideDish/SideDish/PersistentStorages/BanchanListStorage.swift @@ -0,0 +1,95 @@ +// +// BanchanListStorage.swift +// SideDish +// +// Created by 지뢁 on 2021/04/29. +// + +import Foundation +import CoreData + +enum CoreDataError: Error { + case fetError +} + +class CoreDataBanchanListStorage { + private let coreDataManager: CoreDataManager + + init(manager: CoreDataManager = CoreDataManager.shared) { + self.coreDataManager = manager + setSectionEntity() + } + + func setSectionEntity() { + let requset: NSFetchRequest = BanchanSectionEntity.fetchRequest() + + coreDataManager.performBackgroundTask { context in + do { + let sectionEntities = try context.fetch(requset) + if sectionEntities.count >= 3 { return } + else { + try Section.allCases.forEach { section in + let banchanSectionEntity = BanchanSectionEntity.init(context: context) + try context.save() + } + } + } catch { + print("error in sectionEntity") + } + } + } + func save(section: Section, requestDTO: BanchanListDTO) { + let request: NSFetchRequest = BanchanSectionEntity.fetchRequest() + + coreDataManager.performBackgroundTask { context in + do{ + let entities = requestDTO.toEntity(with: context) + let sectionEntity = try context.fetch(request) + + entities.forEach { entity in + sectionEntity[section.rawValue].addToEntities(entity) + } + try context.save() + } catch { + fatalError() + } + } + } + + func fetch(section: Section, handler: @escaping (Result<[Banchan],CoreDataError>) -> Void) { + coreDataManager.performBackgroundTask { context in + do { + let fetchResult: NSFetchRequest = BanchanSectionEntity.fetchRequest() + let requestEntity = try context.fetch(fetchResult) + + var sectionEntity: BanchanSectionEntity + let index = section.rawValue + + if requestEntity[section.rawValue].entities?.array.count == 0 { + handler(.failure(.fetError)) + return + } + sectionEntity = requestEntity[index] + let entities = sectionEntity.entities?.array as! [BanchanEntity] + let newEntities = entities.map { entity in + entity.toDomain() + } + handler(.success(newEntities)) + } catch { + handler(.failure(.fetError)) + } + } + } + + func deleteAll(request: NSFetchRequest) { + coreDataManager.performBackgroundTask { context in + let request: NSFetchRequest = T.fetchRequest() + let delete = NSBatchDeleteRequest(fetchRequest: request) + do { + try context.execute(delete) + } catch { + print(error.localizedDescription) + } + } + } +} diff --git a/iOS/SideDish/SideDish/PersistentStorages/CoreDataStorage/CoreDataManager.swift b/iOS/SideDish/SideDish/PersistentStorages/CoreDataStorage/CoreDataManager.swift new file mode 100644 index 000000000..ac64dbdc7 --- /dev/null +++ b/iOS/SideDish/SideDish/PersistentStorages/CoreDataStorage/CoreDataManager.swift @@ -0,0 +1,43 @@ +// +// CoreDataManager.swift +// SideDish +// +// Created by 지뢁 on 2021/04/29. +// + +import Foundation +import CoreData + +class CoreDataManager { + static let shared = CoreDataManager() + + private init() {} + + private lazy var persistentContainer: NSPersistentContainer = { + let container = NSPersistentContainer(name: "CoreDataStorage") + container.loadPersistentStores { _, error in + if let error = error as NSError? { + // TODO: - Log to Crashlytics + assertionFailure("CoreDataStorage Unresolved error \(error), \(error.userInfo)") + } + } + return container + }() + + // MARK: - Core Data Saving support + func saveContext() { + let context = persistentContainer.viewContext + if context.hasChanges { + do { + try context.save() + } catch { + // TODO: - Log to Crashlytics + assertionFailure("CoreDataStorage Unresolved error \(error), \((error as NSError).userInfo)") + } + } + } + + func performBackgroundTask(_ block: @escaping (NSManagedObjectContext) -> Void) { + persistentContainer.performBackgroundTask(block) + } +} diff --git a/iOS/SideDish/SideDish/PersistentStorages/CoreDataStorage/CoreDataStorage.xcdatamodeld/CoreDataStorage.xcdatamodel/contents b/iOS/SideDish/SideDish/PersistentStorages/CoreDataStorage/CoreDataStorage.xcdatamodeld/CoreDataStorage.xcdatamodel/contents new file mode 100644 index 000000000..3da5f495a --- /dev/null +++ b/iOS/SideDish/SideDish/PersistentStorages/CoreDataStorage/CoreDataStorage.xcdatamodeld/CoreDataStorage.xcdatamodel/contents @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/iOS/SideDish/SideDish/Presentaion/BanchanList/View/BanchanListViewController.swift b/iOS/SideDish/SideDish/Presentaion/BanchanList/View/BanchanListViewController.swift index 54f4cfda5..d02962673 100644 --- a/iOS/SideDish/SideDish/Presentaion/BanchanList/View/BanchanListViewController.swift +++ b/iOS/SideDish/SideDish/Presentaion/BanchanList/View/BanchanListViewController.swift @@ -13,13 +13,24 @@ class BanchanListViewController: UIViewController { @IBOutlet weak var banchanCollectionView: BanchanCollectionView! - private var viewModel = BanchanListViewModel() + static let storyboardName = "Main" + static let storyboardID = "BanchanListViewController" + + private var viewModel: BanchanListViewModel! private lazy var dataSource = configureDataSource() private var subscriptions = Set() typealias DataSource = UICollectionViewDiffableDataSource typealias Snapshot = NSDiffableDataSourceSnapshot - typealias Section = BanchanListViewModel.Section + + static func create(with viewModel: BanchanListViewModel) -> BanchanListViewController { + let storyboard = UIStoryboard(name: storyboardName, bundle: Bundle.main) + guard let vc = storyboard.instantiateViewController(identifier: storyboardID) as? BanchanListViewController else { + return BanchanListViewController() + } + vc.viewModel = viewModel + return vc + } override func viewDidLoad() { super.viewDidLoad() @@ -99,6 +110,10 @@ extension BanchanListViewController: UICollectionViewDelegate, UICollectionViewD let cellSize = CGSize(width: 343, height: 130) return cellSize } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + viewModel.didSelectedItem(indexPath: indexPath) + } } // MARK: - UITapGestureRecongnizer Custom Class diff --git a/iOS/SideDish/SideDish/Presentaion/BanchanList/ViewModel/BanchanListViewModel.swift b/iOS/SideDish/SideDish/Presentaion/BanchanList/ViewModel/BanchanListViewModel.swift index ccbcf827e..8bcd1731c 100644 --- a/iOS/SideDish/SideDish/Presentaion/BanchanList/ViewModel/BanchanListViewModel.swift +++ b/iOS/SideDish/SideDish/Presentaion/BanchanList/ViewModel/BanchanListViewModel.swift @@ -8,57 +8,49 @@ import Foundation import Combine +struct BanchanListViewModelAction { + let showBanchanDetails: ((Banchan) -> Void) +} + class BanchanListViewModel { - enum Section: Int, CaseIterable{ - case main = 0 - case soup, side - - func description() -> String { - switch self { - case .main: - return "λͺ¨λ‘κ°€ μ’‹μ•„ν•˜λŠ” λ“ λ“ ν•œ λ©”μΈμš”λ¦¬" - case .soup: - return "정성이 λ‹΄κΈ΄ 뜨끈뜨끈 κ΅­λ¬Όμš”λ¦¬" - case .side: - return "식탁을 ν’μ„±ν•˜κ²Œ ν•˜λŠ” μ •κ°ˆν•œ λ°‘λ°˜μ°¬" - } - } - } - @Published var menu: Dictionary - private let baseURL = "http://ec2-54-180-115-20.ap-northeast-2.compute.amazonaws.com:8080/" + @Published var menu: [[Banchan]] private var fetchBanchanListUseCase: FetchBanchanListUseCase - private var network = NetworkSerivce.shared + private var action: BanchanListViewModelAction - init() { - self.menu = [:] - self.fetchBanchanListUseCase = FetchBanchanListUseCase.init() + init(fetchBanchanListUseCase: FetchBanchanListUseCase, action: BanchanListViewModelAction) { + self.fetchBanchanListUseCase = fetchBanchanListUseCase + self.menu = [] + self.action = action + configureMenu() fetchMenu() } func count(section: Section) -> Int { - return menu[section]?.count ?? 0 + return menu[section.rawValue].count } func fetchMenu() { - fetchBanchanListUseCase.fetchBanchanList(network: network, baseURL: baseURL, section: "main", completion: { banchans in - guard let banchans = banchans else { return } - self.menu[.main] = banchans - }) - - fetchBanchanListUseCase.fetchBanchanList(network: network, baseURL: baseURL, section: "soup", completion: { banchans in - guard let banchans = banchans else { return } - self.menu[.soup] = banchans - }) - - fetchBanchanListUseCase.fetchBanchanList(network: network, baseURL: baseURL, section: "side", completion: { banchans in - guard let banchans = banchans else { return } - self.menu[.side] = banchans + Section.allCases.forEach { (section) in + fetchBanchanListUseCase.execute(section: section) { (banchans) in + self.menu[section.rawValue] = banchans ?? [] + } + } + } + + func configureMenu() { + Section.allCases.forEach({ _ in + menu.append([]) }) } + func didSelectedItem(indexPath: IndexPath) { + let banchan = menu[indexPath.section][indexPath.row] + action.showBanchanDetails(banchan) + } + func getBanchans(section: Section) -> [Banchan]? { - return menu[section] + return menu[section.rawValue] } } diff --git a/iOS/SideDish/SideDish/Presentaion/DetailBanchan/VIew/BanchanDetailViewController.swift b/iOS/SideDish/SideDish/Presentaion/DetailBanchan/VIew/BanchanDetailViewController.swift index 913c64588..e95105417 100644 --- a/iOS/SideDish/SideDish/Presentaion/DetailBanchan/VIew/BanchanDetailViewController.swift +++ b/iOS/SideDish/SideDish/Presentaion/DetailBanchan/VIew/BanchanDetailViewController.swift @@ -6,10 +6,202 @@ // import UIKit +import Combine class BanchanDetailViewController: UIViewController { - + + @IBOutlet weak var mainScrollView: UIScrollView! + @IBOutlet weak var imageScrollView: UIScrollView! + @IBOutlet var imageViews: [UIImageView]! + + @IBOutlet weak var detailDescriptionStackView: UIStackView! + @IBOutlet weak var titleLabel: UILabel! + @IBOutlet weak var descriptionLabel: UILabel! + @IBOutlet weak var sPriceLabel: UILabel! + @IBOutlet weak var nPriceLabel: UILabel! + @IBOutlet weak var badgeStackView: UIStackView! + @IBOutlet weak var pointLabel: UILabel! + @IBOutlet weak var deliveryInfoLabel: UILabel! + @IBOutlet weak var deliveryFeeLabel: UILabel! + @IBOutlet weak var orderCountLabel: UILabel! + @IBOutlet weak var plusButton: UIButton! + @IBOutlet weak var minusButton: UIButton! + @IBOutlet weak var totalPriceLabel: UILabel! + + @IBOutlet weak var orderButton: UIButton! + private var quantity: Int = 1 + private var subscriptions = Set() + private var viewModel: BanchanDetailViewModel! + + static let storyboardName = "Main" + static let storyboardID = "BanchanDetailViewController" + + static func create(with viewModel: BanchanDetailViewModel) -> BanchanDetailViewController { + let storyboard = UIStoryboard(name: storyboardName, bundle: Bundle.main) + guard let vc = storyboard.instantiateViewController(identifier: storyboardID) as? BanchanDetailViewController else { + return BanchanDetailViewController() + } + vc.viewModel = viewModel + return vc + } + + override func viewDidLoad() { super.viewDidLoad() + bind() + } + + private func bind() { + viewModel.$banchanDetail + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: { (result) in + switch result { + case .failure(let error): + print(error) + case .finished: + break + } + }, receiveValue: { (banchanDetail) in + self.configure(banchanDetail: banchanDetail) + }) + .store(in: &subscriptions) + } + + @IBAction func buttonTouched(_ sender: UIButton) { + + if sender == plusButton { + quantity += 1 + orderCountLabel.text = "\(quantity)" + + } else { + if quantity != 1 { + quantity -= 1 + orderCountLabel.text = "\(quantity)" + } + } + + totalPriceLabel.text = decimalWon(value: viewModel.banchanDetail.sPrice) + + } + + func decimalWon(value: String) -> String { + let price = Int(value.replacingOccurrences(of: ",", with: "").replacingOccurrences(of: "원", with: "")) ?? 0 + let totalPrice = price * quantity + + let numberFormatter = NumberFormatter() + numberFormatter.numberStyle = .decimal + let result = numberFormatter.string(from: NSNumber(value: totalPrice))! + "원" + + return result + } + + @IBAction func orderButtonTouched(_ sender: UIButton) { + viewModel.didTappedOrderButton(quantity: quantity) { result, error in + var alert: UIAlertController! + + switch result { + case 200: + DispatchQueue.main.async { + alert = self.viewModel.makeAlert(message: .success) + self.present(alert, animated: true, completion: nil) + } + default: + DispatchQueue.main.async { + alert = self.viewModel.makeAlert(message: .failure) + self.present(alert, animated: true, completion: nil) + } + } + } + } + + private func configure(banchanDetail: BanchanDetail?) { + + guard let banchanDetail = banchanDetail else { + return + } + self.title = viewModel.banchanDetail.title + self.titleLabel.text = banchanDetail.title + self.descriptionLabel.text = banchanDetail.productDescription + self.sPriceLabel.text = banchanDetail.sPrice + setNPrice(text: banchanDetail.nPrice) + setBadges(badges: banchanDetail.badges) + self.pointLabel.text = banchanDetail.point + self.deliveryInfoLabel.text = banchanDetail.deliveryInfo + self.totalPriceLabel.text = banchanDetail.sPrice + setDeliveryFee(text: "\(banchanDetail.deliveryFee) (40,000원 이상 ꡬ맀 μ‹œ 무료)") + configureThumbImage(images: banchanDetail.thumbImages) + configureDescriptionImage(images: banchanDetail.detailSection) + configureOrderButton(stock: banchanDetail.inStock) + } + + private func setNPrice(text: String?) { + guard let text = text else { + nPriceLabel.isHidden = true + return + } + + let strikeThroughAttributeStyle = [NSAttributedString.Key.strikethroughStyle: NSNumber(value: NSUnderlineStyle.single.rawValue)] + nPriceLabel.attributedText = NSAttributedString(string: text, attributes: strikeThroughAttributeStyle) + } + + private func setDeliveryFee(text: String) { + let font = UIFont.boldSystemFont(ofSize: 14) + + let attributedStr = NSMutableAttributedString(string: text) + + attributedStr.addAttribute(.font, value: font, range: (text as NSString).range(of: "(40,000원 이상 ꡬ맀 μ‹œ 무료)")) + attributedStr.addAttribute(.foregroundColor, value: UIColor.black, range: (text as NSString).range(of: "40,000원 이상 ꡬ맀 μ‹œ 무료")) + deliveryFeeLabel.attributedText = attributedStr + } + + private func setBadges(badges: [String]?) { + badgeStackView.arrangedSubviews.forEach { + $0.removeFromSuperview() + } + + guard let badges = badges else { + badgeStackView.isHidden = true + return + } + badges.forEach({ (badge) in + let badgeLabel = BadgeLabel() + badgeLabel.configure(text: badge) + badgeStackView.addArrangedSubview(badgeLabel) + }) + } + + private func configureThumbImage(images: [String]) { + imageViews.removeAll() + for i in 0.. BanchanListViewController + func makeBanchanDetailViewController(hash: Int, action: BanchanDetailViewModelAction) -> BanchanDetailViewController +} + +class BanchanSceneFlowCoordinator { + + private weak var navigationController: UINavigationController? + private var banchanListVC: BanchanListViewController? + private let dependencies: BanchanSceneFlowCoordinatorDependencies + + init(navigationController: UINavigationController, dependencies: BanchanSceneFlowCoordinatorDependencies) { + self.navigationController = navigationController + self.dependencies = dependencies + } + + func start() { + let action = BanchanListViewModelAction(showBanchanDetails: showBanchanDetails(banchan:)) + let vc = dependencies.makeBanchanListViewController(action: action) + self.navigationController?.pushViewController(vc, animated: true) + self.banchanListVC = vc + } + + func showBanchanDetails(banchan: Banchan) { + let action = BanchanDetailViewModelAction(popBanchanDetail: popBanchanDetail) + let vc = dependencies.makeBanchanDetailViewController(hash: banchan.detailHash, action: action) + self.navigationController?.pushViewController(vc, animated: true) + } + + func popBanchanDetail() { + self.navigationController?.popViewController(animated: true) + } + +} diff --git a/iOS/SideDish/SideDish/SupportingFiles/SceneDelegate.swift b/iOS/SideDish/SideDish/SupportingFiles/SceneDelegate.swift deleted file mode 100644 index 420f341dc..000000000 --- a/iOS/SideDish/SideDish/SupportingFiles/SceneDelegate.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// SceneDelegate.swift -// SideDish -// -// Created by μ‹¬μ˜λ―Ό on 2021/04/20. -// - -import UIKit - -class SceneDelegate: UIResponder, UIWindowSceneDelegate { - - var window: UIWindow? - - - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - guard let _ = (scene as? UIWindowScene) else { return } - } -} -