├── .gitignore ├── DaechelinGuide ├── DaechelinGuide.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── DaechelinGuide │ ├── Resources │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ └── appstore.png │ │ │ ├── Contents.json │ │ │ └── appstore.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── appstore.png │ │ ├── Contents.json │ │ ├── Food │ │ │ ├── Contents.json │ │ │ ├── burger.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── burger.svg │ │ │ ├── ramen.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── ramen.svg │ │ │ └── taco.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── taco.svg │ │ ├── Icon │ │ │ ├── Contents.json │ │ │ ├── arrow_down.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── arrow-down.png │ │ │ ├── arrow_left.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── arrow-left.png │ │ │ ├── arrow_right.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── arrow-right.png │ │ │ ├── cat.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── cat.png │ │ │ ├── crown.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── crown.svg │ │ │ ├── elephant.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── elephant.png │ │ │ ├── navigation_arrow.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── arrow-left.png │ │ │ ├── rabbit.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── rabbit.png │ │ │ ├── ranking.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── ranking.svg │ │ │ ├── review.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── review.svg │ │ │ ├── setting.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── setting-2.svg │ │ │ ├── setting_arrow.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── arrow-right.png │ │ │ ├── snake.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── snake.png │ │ │ ├── star_empty.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── Star.svg │ │ │ ├── star_filled.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── Icon.png │ │ │ └── tiger.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── tiger.png │ │ └── Logo │ │ │ ├── Contents.json │ │ │ ├── logo.imageset │ │ │ ├── Contents.json │ │ │ └── logo.svg │ │ │ └── splash_logo.imageset │ │ │ ├── Contents.json │ │ │ └── splash_logo.svg │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── Info.plist │ └── ko.lproj │ │ ├── LaunchScreen.storyboard │ │ └── LaunchScreen.strings │ └── Sources │ ├── Delegate │ ├── AppDelegate.swift │ └── SceneDelegate.swift │ ├── Network │ ├── Configuration │ │ └── Constants.swift │ ├── Provider │ │ ├── MenuProvider.swift │ │ ├── ProviderWrapper.swift │ │ ├── RankingProvider.swift │ │ └── RatingProvider.swift │ ├── Request │ │ └── RatingRequest.swift │ ├── Response │ │ ├── MenuDetailResponse.swift │ │ ├── MenuResponse.swift │ │ ├── RankingResponse.swift │ │ └── RatingResponse.swift │ └── Service │ │ ├── MenuService.swift │ │ ├── RankingService.swift │ │ └── RatingService.swift │ ├── Presentation │ ├── Base │ │ ├── BaseView.swift │ │ └── BaseViewController.swift │ └── Scenes │ │ ├── Home │ │ ├── HomeReactor.swift │ │ ├── HomeViewController.swift │ │ └── MenuContainer.swift │ │ ├── MenuInfo │ │ ├── CommentCell.swift │ │ ├── MenuInfoReactor.swift │ │ └── MenuInfoViewController.swift │ │ ├── Ranking │ │ ├── RankingCell.swift │ │ ├── RankingReactor.swift │ │ └── RankingViewController.swift │ │ ├── Review │ │ ├── ReviewReactor.swift │ │ └── ReviewViewController.swift │ │ └── Setting │ │ ├── SettingReactor.swift │ │ └── SettingViewController.swift │ └── Shared │ ├── Assets │ ├── Color │ │ └── Color.swift │ └── Icon │ │ ├── Icon.swift │ │ └── UIImage+Ext.swift │ ├── Component │ ├── FadingView.swift │ └── ScaledButton.swift │ ├── Enum │ └── MealType.swift │ └── Extension │ ├── Foundation │ ├── Date+Ext.swift │ └── String+Ext.swift │ └── UIKit │ ├── UIColor+Ext.swift │ ├── UILabel+Ext.swift │ ├── UINavigationController+Ext.swift │ ├── UIStackView+Ext.swift │ └── UIView+Ext.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | .DS_Store 8 | 9 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 10 | *.xcscmblueprint 11 | *.xccheckout 12 | 13 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 14 | build/ 15 | DerivedData/ 16 | *.moved-aside 17 | *.pbxuser 18 | !default.pbxuser 19 | *.mode1v3 20 | !default.mode1v3 21 | *.mode2v3 22 | !default.mode2v3 23 | *.perspectivev3 24 | !default.perspectivev3 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | 29 | ## App packaging 30 | *.ipa 31 | *.dSYM.zip 32 | *.dSYM 33 | 34 | ## Playgrounds 35 | timeline.xctimeline 36 | playground.xcworkspace 37 | 38 | # Swift Package Manager 39 | # 40 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 41 | # Packages/ 42 | # Package.pins 43 | # Package.resolved 44 | # *.xcodeproj 45 | # 46 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 47 | # hence it is not needed unless you have added a package configuration file to your project 48 | # .swiftpm 49 | 50 | .build/ 51 | 52 | # CocoaPods 53 | # 54 | # We recommend against adding the Pods directory to your .gitignore. However 55 | # you should judge for yourself, the pros and cons are mentioned at: 56 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 57 | # 58 | # Pods/ 59 | # 60 | # Add this line if you want to avoid checking in source code from the Xcode workspace 61 | # *.xcworkspace 62 | 63 | # Carthage 64 | # 65 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 66 | # Carthage/Checkouts 67 | 68 | Carthage/Build/ 69 | 70 | # Accio dependency management 71 | Dependencies/ 72 | .accio/ 73 | 74 | # fastlane 75 | # 76 | # It is recommended to not store the screenshots in the git repo. 77 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 78 | # For more information about the recommended setup visit: 79 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 80 | 81 | fastlane/report.xml 82 | fastlane/Preview.html 83 | fastlane/screenshots/**/*.png 84 | fastlane/test_output 85 | 86 | # Code Injection 87 | # 88 | # After new code Injection tools there's a generated folder /iOSInjectionProject 89 | # https://github.com/johnno1962/injectionforxcode 90 | 91 | iOSInjectionProject/ 92 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "f8c3118037093b18f8e56c1d513b06f28e1041d31f3c1072e472d7a389edde20", 3 | "pins" : [ 4 | { 5 | "identity" : "alamofire", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/Alamofire/Alamofire.git", 8 | "state" : { 9 | "revision" : "f455c2975872ccd2d9c81594c658af65716e9b9a", 10 | "version" : "5.9.1" 11 | } 12 | }, 13 | { 14 | "identity" : "cosmos", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/evgenyneu/Cosmos", 17 | "state" : { 18 | "revision" : "40ba10aaf175bf50abefd0e518bd3b40862af3b1", 19 | "version" : "25.0.1" 20 | } 21 | }, 22 | { 23 | "identity" : "moya", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/Moya/Moya", 26 | "state" : { 27 | "revision" : "c263811c1f3dbf002be9bd83107f7cdc38992b26", 28 | "version" : "15.0.3" 29 | } 30 | }, 31 | { 32 | "identity" : "reactiveswift", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/ReactiveCocoa/ReactiveSwift.git", 35 | "state" : { 36 | "revision" : "c43bae3dac73fdd3cb906bd5a1914686ca71ed3c", 37 | "version" : "6.7.0" 38 | } 39 | }, 40 | { 41 | "identity" : "reactorkit", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/ReactorKit/ReactorKit.git", 44 | "state" : { 45 | "revision" : "8fa33f09c6f6621a2aa536d739956d53b84dd139", 46 | "version" : "3.2.0" 47 | } 48 | }, 49 | { 50 | "identity" : "rxgesture", 51 | "kind" : "remoteSourceControl", 52 | "location" : "https://github.com/RxSwiftCommunity/RxGesture", 53 | "state" : { 54 | "revision" : "1b137c576b4aaaab949235752278956697c9e4a0", 55 | "version" : "4.0.4" 56 | } 57 | }, 58 | { 59 | "identity" : "rxswift", 60 | "kind" : "remoteSourceControl", 61 | "location" : "https://github.com/ReactiveX/RxSwift.git", 62 | "state" : { 63 | "revision" : "b06a8c8596e4c3e8e7788e08e720e3248563ce6a", 64 | "version" : "6.7.1" 65 | } 66 | }, 67 | { 68 | "identity" : "snapkit", 69 | "kind" : "remoteSourceControl", 70 | "location" : "https://github.com/SnapKit/SnapKit", 71 | "state" : { 72 | "revision" : "2842e6e84e82eb9a8dac0100ca90d9444b0307f4", 73 | "version" : "5.7.1" 74 | } 75 | }, 76 | { 77 | "identity" : "then", 78 | "kind" : "remoteSourceControl", 79 | "location" : "https://github.com/devxoul/Then", 80 | "state" : { 81 | "revision" : "d41ef523faef0f911369f79c0b96815d9dbb6d7a", 82 | "version" : "3.0.0" 83 | } 84 | }, 85 | { 86 | "identity" : "weakmaptable", 87 | "kind" : "remoteSourceControl", 88 | "location" : "https://github.com/ReactorKit/WeakMapTable.git", 89 | "state" : { 90 | "revision" : "cb05d64cef2bbf51e85c53adee937df46540a74e", 91 | "version" : "1.2.1" 92 | } 93 | } 94 | ], 95 | "version" : 3 96 | } 97 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | }, 6 | { 7 | "appearances" : [ 8 | { 9 | "appearance" : "luminosity", 10 | "value" : "dark" 11 | } 12 | ], 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/AppIcon/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "appstore.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/AppIcon/AppIcon.appiconset/appstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daechelin-Guide/daechelin-guide-ios/69a80d63a09e34ab8b265f1639a8ef230d765fb0/DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/AppIcon/AppIcon.appiconset/appstore.png -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/AppIcon/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/AppIcon/appstore.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "appstore.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/AppIcon/appstore.imageset/appstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daechelin-Guide/daechelin-guide-ios/69a80d63a09e34ab8b265f1639a8ef230d765fb0/DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/AppIcon/appstore.imageset/appstore.png -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Food/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Food/burger.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "burger.svg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "original" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Food/ramen.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ramen.svg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "original" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Food/taco.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "taco.svg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "original" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/arrow_down.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "arrow-down.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "preserves-vector-representation" : true, 23 | "template-rendering-intent" : "template" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/arrow_down.imageset/arrow-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daechelin-Guide/daechelin-guide-ios/69a80d63a09e34ab8b265f1639a8ef230d765fb0/DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/arrow_down.imageset/arrow-down.png -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/arrow_left.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "arrow-left.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "preserves-vector-representation" : true, 23 | "template-rendering-intent" : "template" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/arrow_left.imageset/arrow-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daechelin-Guide/daechelin-guide-ios/69a80d63a09e34ab8b265f1639a8ef230d765fb0/DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/arrow_left.imageset/arrow-left.png -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/arrow_right.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "arrow-right.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "preserves-vector-representation" : true, 23 | "template-rendering-intent" : "template" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/arrow_right.imageset/arrow-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daechelin-Guide/daechelin-guide-ios/69a80d63a09e34ab8b265f1639a8ef230d765fb0/DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/arrow_right.imageset/arrow-right.png -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/cat.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "cat.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/cat.imageset/cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daechelin-Guide/daechelin-guide-ios/69a80d63a09e34ab8b265f1639a8ef230d765fb0/DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/cat.imageset/cat.png -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/crown.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "crown.svg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "preserves-vector-representation" : true, 23 | "template-rendering-intent" : "template" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/crown.imageset/crown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/elephant.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "elephant.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/elephant.imageset/elephant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daechelin-Guide/daechelin-guide-ios/69a80d63a09e34ab8b265f1639a8ef230d765fb0/DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/elephant.imageset/elephant.png -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/navigation_arrow.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "arrow-left.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "preserves-vector-representation" : true, 23 | "template-rendering-intent" : "template" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/navigation_arrow.imageset/arrow-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daechelin-Guide/daechelin-guide-ios/69a80d63a09e34ab8b265f1639a8ef230d765fb0/DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/navigation_arrow.imageset/arrow-left.png -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/rabbit.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "rabbit.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/rabbit.imageset/rabbit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daechelin-Guide/daechelin-guide-ios/69a80d63a09e34ab8b265f1639a8ef230d765fb0/DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/rabbit.imageset/rabbit.png -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/ranking.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ranking.svg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "preserves-vector-representation" : true, 23 | "template-rendering-intent" : "template" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/ranking.imageset/ranking.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/review.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "review.svg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "original" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/review.imageset/review.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/setting.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "setting-2.svg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "preserves-vector-representation" : true, 23 | "template-rendering-intent" : "template" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/setting.imageset/setting-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/setting_arrow.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "arrow-right.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "preserves-vector-representation" : true, 23 | "template-rendering-intent" : "template" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/setting_arrow.imageset/arrow-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daechelin-Guide/daechelin-guide-ios/69a80d63a09e34ab8b265f1639a8ef230d765fb0/DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/setting_arrow.imageset/arrow-right.png -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/snake.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "snake.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/snake.imageset/snake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daechelin-Guide/daechelin-guide-ios/69a80d63a09e34ab8b265f1639a8ef230d765fb0/DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/snake.imageset/snake.png -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/star_empty.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Star.svg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/star_empty.imageset/Star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/star_filled.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Icon.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/star_filled.imageset/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daechelin-Guide/daechelin-guide-ios/69a80d63a09e34ab8b265f1639a8ef230d765fb0/DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/star_filled.imageset/Icon.png -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/tiger.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "tiger.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/tiger.imageset/tiger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daechelin-Guide/daechelin-guide-ios/69a80d63a09e34ab8b265f1639a8ef230d765fb0/DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Icon/tiger.imageset/tiger.png -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Logo/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Logo/logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "logo.svg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Logo/logo.imageset/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Logo/splash_logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "splash_logo.svg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Assets.xcassets/Logo/splash_logo.imageset/splash_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ITSAppUsesNonExemptEncryption 6 | 7 | NSAppTransportSecurity 8 | 9 | NSAllowsArbitraryLoads 10 | 11 | 12 | UIApplicationSceneManifest 13 | 14 | UIApplicationSupportsMultipleScenes 15 | 16 | UISceneConfigurations 17 | 18 | UIWindowSceneSessionRoleApplication 19 | 20 | 21 | UISceneConfigurationName 22 | Default Configuration 23 | UISceneDelegateClassName 24 | $(PRODUCT_MODULE_NAME).SceneDelegate 25 | 26 | 27 | 28 | 29 | URL 30 | http://54.180.155.53:8080 31 | 32 | 33 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/ko.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Resources/ko.lproj/LaunchScreen.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Delegate/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 4/30/24. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | // MARK: UISceneSession Lifecycle 21 | 22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 23 | // Called when a new scene session is being created. 24 | // Use this method to select a configuration to create the new scene with. 25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 26 | } 27 | 28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 29 | // Called when the user discards a scene session. 30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 32 | } 33 | 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Delegate/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 4/30/24. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 15 | guard let windowScene = (scene as? UIWindowScene) else { return } 16 | 17 | window = UIWindow(windowScene: windowScene) 18 | let vc = HomeViewController(reactor: HomeReactor()) 19 | let root = UINavigationController(rootViewController: vc) 20 | 21 | window?.rootViewController = root 22 | window?.makeKeyAndVisible() 23 | } 24 | 25 | func sceneDidDisconnect(_ scene: UIScene) { } 26 | 27 | func sceneDidBecomeActive(_ scene: UIScene) { } 28 | 29 | func sceneWillResignActive(_ scene: UIScene) { } 30 | 31 | func sceneWillEnterForeground(_ scene: UIScene) { } 32 | 33 | func sceneDidEnterBackground(_ scene: UIScene) { } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Network/Configuration/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 4/30/24. 6 | // 7 | 8 | import Foundation 9 | 10 | public let apiUrl: String = { 11 | let plist = NSDictionary(contentsOfFile: Bundle.main.path(forResource: "Info", ofType: "plist")!) 12 | return (plist?.object(forKey: "URL") as? String)! 13 | }() 14 | 15 | public let appVersion: String = { 16 | return (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String)! 17 | }() 18 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Network/Provider/MenuProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuProvider.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/4/24. 6 | // 7 | 8 | import Foundation 9 | import Moya 10 | import RxSwift 11 | 12 | final class MenuProvider { 13 | static let shared = MenuProvider() 14 | 15 | private let wrapper = ProviderWrapper() 16 | 17 | func getMenu(_ date: String) -> Observable> { 18 | return Observable.create { observer in 19 | self.wrapper.daechelinRequest( 20 | target: .getMenu(date), 21 | instance: MenuResponse.self 22 | ) { result in 23 | switch result { 24 | case .success(let data): 25 | observer.onNext(.success(data)) 26 | observer.onCompleted() 27 | case .failure(let error): 28 | observer.onNext(.failure(error)) 29 | } 30 | } 31 | return Disposables.create() 32 | } 33 | } 34 | 35 | 36 | func getMenuDetail(_ date: String, _ mealType: MealType) -> Observable> { 37 | 38 | return Observable.create { observer in 39 | self.wrapper.daechelinRequest( 40 | target: .getMenuDatail(date, mealType), 41 | instance: MenuDetailResponse.self 42 | ) { result in 43 | switch result { 44 | case .success(let data): 45 | observer.onNext(.success(data)) 46 | observer.onCompleted() 47 | case .failure(let error): 48 | observer.onNext(.failure(error)) 49 | } 50 | } 51 | return Disposables.create() 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Network/Provider/ProviderWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProviderWrapper.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/4/24. 6 | // 7 | 8 | import Foundation 9 | import Moya 10 | 11 | class ProviderWrapper: MoyaProvider

{ 12 | 13 | init( 14 | endpointClosure: @escaping MoyaProvider

.EndpointClosure = MoyaProvider.defaultEndpointMapping, 15 | stubClosure: @escaping MoyaProvider

.StubClosure = MoyaProvider.neverStub, callbackQueue: DispatchQueue? = nil, 16 | plugins: [PluginType] = [] 17 | ) { 18 | let session = MoyaProvider

.defaultAlamofireSession() 19 | session.sessionConfiguration.timeoutIntervalForRequest = 3 20 | 21 | super.init( 22 | endpointClosure: endpointClosure, 23 | stubClosure: stubClosure, 24 | session: session, 25 | plugins: plugins 26 | ) 27 | } 28 | 29 | func daechelinRequest( 30 | target: P, 31 | instance: Model.Type, 32 | completion: @escaping(Result) -> () 33 | ) { 34 | self.request(target) { result in 35 | switch result { 36 | 37 | case .success(let response): 38 | if (200 ..< 300).contains(response.statusCode), 39 | let data = try? JSONDecoder().decode(instance, from: response.data) { 40 | completion(.success(data)) 41 | } else { 42 | completion(.failure(.statusCode(response))) 43 | } 44 | case .failure(let moyaError): 45 | completion(.failure(moyaError)) 46 | } 47 | } 48 | } 49 | 50 | func daechelinSimpleRequest( 51 | target: P, 52 | completion: @escaping (Result) -> Void 53 | ) { 54 | self.request(target) { result in 55 | switch result { 56 | 57 | case .success(let response): 58 | if let data = try? response.map(Data.self) { 59 | print(String(data: data, encoding: .utf8)!) 60 | } 61 | case .failure(let moyaError): 62 | print("code: \(moyaError.errorCode)\n", moyaError.localizedDescription) 63 | } 64 | 65 | completion(result) 66 | } 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Network/Provider/RankingProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RankingProvider.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/5/24. 6 | // 7 | 8 | import Foundation 9 | import Moya 10 | import RxSwift 11 | 12 | final class RankingProvider { 13 | static let shared = RankingProvider() 14 | 15 | private let wrapper = ProviderWrapper() 16 | 17 | func getRating(_ mealType: MealType) -> Observable> { 18 | return Observable.create { observer in 19 | self.wrapper.daechelinRequest( 20 | target: .getRanking(mealType), 21 | instance: RankingResponse.self 22 | ) { result in 23 | switch result { 24 | case .success(let data): 25 | observer.onNext(.success(data)) 26 | case .failure(let error): 27 | observer.onNext(.failure(error)) 28 | } 29 | } 30 | return Disposables.create() 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Network/Provider/RatingProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RatingProvider.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/5/24. 6 | // 7 | 8 | import Foundation 9 | import Moya 10 | import RxSwift 11 | 12 | final class RatingProvider { 13 | static let shared = RatingProvider() 14 | 15 | private let wrapper = ProviderWrapper() 16 | 17 | func getRating(_ menuId: Int) -> Observable> { 18 | return Observable.create { observer in 19 | self.wrapper.daechelinRequest( 20 | target: .getRating(menuId), 21 | instance: [RatingResponse].self 22 | ) { result in 23 | switch result { 24 | case .success(let data): 25 | observer.onNext(.success(data)) 26 | case .failure(let error): 27 | observer.onNext(.failure(error)) 28 | } 29 | } 30 | return Disposables.create() 31 | } 32 | } 33 | 34 | 35 | func postRating(_ menuId: Int, _ request: RatingRequest) -> Observable> { 36 | 37 | return Observable.create { observer in 38 | self.wrapper.daechelinSimpleRequest( 39 | target: .postRating(menuId, request) 40 | ) { result in 41 | switch result { 42 | case .success(let data): 43 | observer.onNext(.success(data.data)) 44 | case .failure(let error): 45 | observer.onNext(.failure(error)) 46 | } 47 | } 48 | return Disposables.create() 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Network/Request/RatingRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RatingRequest.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/4/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct RatingRequest: Codable { 11 | let score: Double 12 | let comment: String 13 | } 14 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Network/Response/MenuDetailResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuDetailResponse.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/1/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct MenuDetailResponse: Codable { 11 | let id: Int 12 | let menu: String? 13 | let date: String 14 | let cal: String? 15 | let totalScore: Double 16 | let nutrients: String? 17 | let mealType: MealType 18 | } 19 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Network/Response/MenuResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuResponse.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/1/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct MenuResponse: Codable { 11 | let date: String 12 | let breakfast: String? 13 | let lunch: String? 14 | let dinner: String? 15 | } 16 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Network/Response/RankingResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RankingResponse.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/5/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct RankingResponse: Codable { 11 | let ranking: [Ranking] 12 | } 13 | 14 | struct Ranking: Codable { 15 | let id: Int 16 | let menu: String 17 | let date: String 18 | let cal: String 19 | let totalScore: Double 20 | let ranking: Int 21 | } 22 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Network/Response/RatingResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RatingResponse.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/5/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct RatingResponse: Codable { 11 | let comment: String 12 | } 13 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Network/Service/MenuService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuService.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/4/24. 6 | // 7 | 8 | import Foundation 9 | import Moya 10 | 11 | enum MenuService { 12 | case getMenu(_ date: String) 13 | case getMenuDatail(_ date: String, _ mealType: MealType) 14 | } 15 | 16 | extension MenuService: TargetType { 17 | 18 | var baseURL: URL { 19 | return URL(string: apiUrl + "/menu")! 20 | } 21 | 22 | var path: String { 23 | switch self { 24 | case .getMenu(_): 25 | return "" 26 | case .getMenuDatail(_, _): 27 | return "/detail" 28 | } 29 | } 30 | 31 | var method: Moya.Method { 32 | switch self { 33 | case .getMenu(_): 34 | return .get 35 | case .getMenuDatail(_, _): 36 | return .get 37 | } 38 | } 39 | 40 | var task: Task { 41 | switch self { 42 | case let .getMenu(date): 43 | return .requestParameters( 44 | parameters: ["date": date], 45 | encoding: URLEncoding.default 46 | ) 47 | case let .getMenuDatail(date, mealType): 48 | return .requestParameters( 49 | parameters: ["date": date, "mealType": mealType], 50 | encoding: URLEncoding.default 51 | ) 52 | } 53 | } 54 | 55 | var validationType: Moya.ValidationType { 56 | return .successAndRedirectCodes 57 | } 58 | 59 | var headers: [String: String]? { 60 | var headers = [String: String]() 61 | headers["Content-Type"] = "application/json" 62 | return headers 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Network/Service/RankingService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RankingService.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/5/24. 6 | // 7 | 8 | import Foundation 9 | import Moya 10 | 11 | enum RankingService { 12 | case getRanking(_ mealType: MealType) 13 | } 14 | 15 | extension RankingService: TargetType { 16 | 17 | var baseURL: URL { 18 | return URL(string: apiUrl + "/ranking")! 19 | } 20 | 21 | var path: String { 22 | switch self { 23 | case .getRanking(_): 24 | return "" 25 | } 26 | } 27 | 28 | var method: Moya.Method { 29 | switch self { 30 | case .getRanking(_): 31 | return .get 32 | } 33 | } 34 | 35 | var task: Task { 36 | switch self { 37 | case let .getRanking(mealType): 38 | return .requestParameters( 39 | parameters: ["mealType": mealType], 40 | encoding: URLEncoding.default 41 | ) 42 | } 43 | } 44 | 45 | var validationType: Moya.ValidationType { 46 | return .successAndRedirectCodes 47 | } 48 | 49 | var headers: [String: String]? { 50 | var headers = [String: String]() 51 | headers["Content-Type"] = "application/json" 52 | return headers 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Network/Service/RatingService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RatingService.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/5/24. 6 | // 7 | 8 | import Foundation 9 | import Moya 10 | 11 | enum RatingService { 12 | case getRating(_ menuId: Int) 13 | case postRating(_ menuId: Int, _ request: RatingRequest) 14 | } 15 | 16 | extension RatingService: TargetType { 17 | 18 | var baseURL: URL { 19 | return URL(string: apiUrl + "/rating")! 20 | } 21 | 22 | var path: String { 23 | switch self { 24 | case .getRating(let menuId): 25 | return "/\(menuId)" 26 | case .postRating(let menuId, _): 27 | return "/\(menuId)" 28 | } 29 | } 30 | 31 | var method: Moya.Method { 32 | switch self { 33 | case .getRating(_): 34 | return .get 35 | case .postRating(_, _): 36 | return .post 37 | } 38 | } 39 | 40 | var task: Task { 41 | switch self { 42 | case .getRating(_): 43 | return .requestPlain 44 | case let .postRating(_, request): 45 | let params = request.comment.isEmpty 46 | ? ["score": request.score] 47 | : ["score": request.score, "comment": request.comment] 48 | return .requestParameters( 49 | parameters: params, 50 | encoding: JSONEncoding.default 51 | ) 52 | } 53 | } 54 | 55 | var validationType: Moya.ValidationType { 56 | return .successAndRedirectCodes 57 | } 58 | 59 | var headers: [String: String]? { 60 | var headers = [String: String]() 61 | headers["Content-Type"] = "application/json" 62 | return headers 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Presentation/Base/BaseView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseView.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/1/24. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | import Then 11 | 12 | class BaseView: UIView { 13 | 14 | let bound = UIScreen.main.bounds 15 | 16 | init() { 17 | super.init(frame: .zero) 18 | setUp() 19 | addView() 20 | setLayout() 21 | } 22 | 23 | required init?(coder: NSCoder) { 24 | fatalError("init(coder:) has not been implemented") 25 | } 26 | 27 | deinit { 28 | print("\(type(of: self)): \(#function)") 29 | } 30 | 31 | func setUp() { } 32 | func addView() { } 33 | func setLayout() { } 34 | } 35 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Presentation/Base/BaseViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseViewController.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 4/30/24. 6 | // 7 | 8 | import UIKit 9 | import ReactorKit 10 | import RxCocoa 11 | import SnapKit 12 | import Then 13 | 14 | class BaseVC: UIViewController { 15 | 16 | let bound = UIScreen.main.bounds 17 | var disposeBag: DisposeBag = .init() 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | 22 | view.backgroundColor = Color.background 23 | navigationController?.setNavigationBarHidden(true, animated: true) 24 | setUp() 25 | addView() 26 | setLayout() 27 | configureVC() 28 | configureNavigation() 29 | } 30 | 31 | init(reactor: T?) { 32 | super.init(nibName: nil, bundle: nil) 33 | self.reactor = reactor 34 | } 35 | 36 | required init?(coder: NSCoder) { 37 | fatalError("init(coder:) has not been implemented") 38 | } 39 | 40 | deinit { 41 | print("\(type(of: self)): \(#function)") 42 | } 43 | 44 | func setUp() { } 45 | func addView() { } 46 | func setLayout() { } 47 | func configureVC() { } 48 | func configureNavigation() { } 49 | func bindView(reactor: T) { } 50 | func bindAction(reactor: T) { } 51 | func bindState(reactor: T) { } 52 | } 53 | 54 | extension BaseVC: View { 55 | 56 | func bind(reactor: T) { 57 | bindView(reactor: reactor) 58 | bindAction(reactor: reactor) 59 | bindState(reactor: reactor) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Presentation/Scenes/Home/HomeReactor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeReactor.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 4/30/24. 6 | // 7 | 8 | import UIKit 9 | import ReactorKit 10 | 11 | final class HomeReactor: Reactor { 12 | 13 | // MARK: - Properties 14 | var initialState: State = State() 15 | 16 | // MARK: - Action 17 | enum Action { 18 | case refresh 19 | case fetchMenu 20 | 21 | // button 22 | case calendarButtonDidTap 23 | case tomorrowButtonDidTap 24 | case yesterdayButtonDidTap 25 | } 26 | 27 | // MARK: - Mutation 28 | enum Mutation { 29 | case setDate(Date) 30 | case setMenu(MenuResponse?) 31 | case setRefreshing(Bool) 32 | } 33 | 34 | // MARK: - State 35 | struct State { 36 | var date: Date = Date() 37 | var menu: MenuResponse? 38 | var isRefreshing: Bool = false 39 | } 40 | } 41 | 42 | // MARK: - Mutate 43 | extension HomeReactor { 44 | 45 | private func fetchMenu(date: Date) -> Observable { 46 | return MenuProvider.shared 47 | .getMenu(date.formattingDate(format: "yyyyMMdd")) 48 | .flatMap { result -> Observable in 49 | switch result { 50 | case .success(let data): 51 | return Observable.just(.setMenu(data)) 52 | case .failure(_): 53 | return Observable.empty() 54 | } 55 | } 56 | } 57 | 58 | func mutate(action: Action) -> Observable { 59 | switch action { 60 | 61 | case .fetchMenu: 62 | return fetchMenu(date: currentState.date) 63 | 64 | case .refresh: 65 | return Observable.concat([ 66 | Observable.just(Mutation.setMenu(nil)), 67 | Observable.just(Mutation.setRefreshing(true)), 68 | fetchMenu(date: currentState.date), 69 | Observable.just(Mutation.setRefreshing(false)) 70 | ]) 71 | 72 | case .calendarButtonDidTap: 73 | return Observable.just(Mutation.setDate(Date())) 74 | .concat(fetchMenu(date: Date())) 75 | 76 | case .tomorrowButtonDidTap: 77 | let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: currentState.date) ?? Date() 78 | return Observable.just(Mutation.setDate(tomorrow)) 79 | .concat(fetchMenu(date: tomorrow)) 80 | 81 | case .yesterdayButtonDidTap: 82 | let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: currentState.date) ?? Date() 83 | return Observable.just(Mutation.setDate(yesterday)) 84 | .concat(fetchMenu(date: yesterday)) 85 | } 86 | } 87 | 88 | // MARK: - Reduce 89 | func reduce(state: State, mutation: Mutation) -> State { 90 | var newState = state 91 | switch mutation { 92 | 93 | case .setDate(let date): 94 | newState.date = date 95 | 96 | case .setMenu(let menu): 97 | newState.menu = menu 98 | 99 | case .setRefreshing(let isRefreshing): 100 | newState.isRefreshing = isRefreshing 101 | } 102 | return newState 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Presentation/Scenes/Home/HomeViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeViewController.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 4/30/24. 6 | // 7 | 8 | import UIKit 9 | import RxGesture 10 | 11 | final class HomeViewController: BaseVC { 12 | 13 | // MARK: - Properties 14 | private lazy var container = UIView() 15 | 16 | /// navigation bar 17 | private lazy var navigationBarView = UIView().then { 18 | $0.backgroundColor = Color.white 19 | } 20 | 21 | private lazy var navigationBarSeparateLine = UIView().then { 22 | $0.backgroundColor = Color.lightGray 23 | } 24 | 25 | private lazy var navigationBarItemView = UIView() 26 | 27 | private lazy var logoImage = UIImageView().then { 28 | $0.image = UIImage(icon: .logo) 29 | $0.contentMode = .scaleAspectFit 30 | } 31 | 32 | private lazy var rankingButton = UIButton().then { 33 | $0.setImage(UIImage(icon: .ranking), for: .normal) 34 | $0.imageView!.contentMode = .scaleAspectFit 35 | $0.tintColor = Color.darkGray 36 | } 37 | 38 | private lazy var settingButton = UIButton().then { 39 | $0.setImage(UIImage(icon: .setting), for: .normal) 40 | $0.imageView!.contentMode = .scaleAspectFill 41 | $0.tintColor = Color.darkGray 42 | } 43 | 44 | /// scroll view 45 | private lazy var scrollView = UIScrollView().then { 46 | $0.showsVerticalScrollIndicator = false 47 | $0.alwaysBounceVertical = true 48 | $0.contentInsetAdjustmentBehavior = .always 49 | $0.clipsToBounds = false 50 | } 51 | 52 | private let refreshControl = UIRefreshControl() 53 | 54 | private lazy var fadingBottomView = FadingView(position: .bottom) 55 | 56 | /// calendar button 57 | private lazy var calendarStackView = UIStackView().then { 58 | $0.axis = .horizontal 59 | $0.spacing = 10 60 | $0.distribution = .fill 61 | } 62 | 63 | private lazy var yesterdayButton = UIButton().then { 64 | $0.setImage(UIImage(icon: .leadingArrow), for: .normal) 65 | $0.imageView?.tintColor = Color.darkGray 66 | } 67 | 68 | private lazy var calendarButton = ScaledButton( 69 | scale: 0.98, backgroundColor: Color.white 70 | ).then { 71 | $0.layer.cornerRadius = 16 72 | $0.layer.borderWidth = 1 73 | $0.layer.borderColor = Color.lightGray.cgColor 74 | } 75 | 76 | private lazy var calendarDateLabel = UILabel().then { 77 | $0.text = "2023년 12월 25일 (월)" 78 | $0.textColor = Color.darkGray 79 | $0.font = .systemFont(ofSize: 14, weight: .medium) 80 | } 81 | 82 | private lazy var tomorrowButton = UIButton().then { 83 | $0.setImage(UIImage(icon: .trailingArrow), for: .normal) 84 | $0.imageView?.tintColor = Color.darkGray 85 | } 86 | 87 | /// menu container 88 | private lazy var menuContainerStackView = UIStackView().then { 89 | $0.axis = .vertical 90 | $0.spacing = 20 91 | $0.distribution = .fill 92 | } 93 | 94 | private lazy var breakfastContainer = MenuContainer( 95 | menu: nil, type: .TYPE_BREAKFAST 96 | ) 97 | 98 | private lazy var lunchContainer = MenuContainer( 99 | menu: nil, type: .TYPE_LUNCH 100 | ) 101 | 102 | private lazy var dinnerContainer = MenuContainer( 103 | menu: nil, type: .TYPE_DINNER 104 | ) 105 | 106 | // MARK: - LifeCycle 107 | override func viewWillAppear(_ animated: Bool) { 108 | super.viewWillAppear(true) 109 | 110 | print("\(type(of: self)): \(#function)") 111 | } 112 | 113 | // MARK: - UI 114 | override func configureVC() { 115 | scrollView.refreshControl = refreshControl 116 | } 117 | 118 | override func addView() { 119 | view.addSubview(container) 120 | container.addSubviews( 121 | scrollView, navigationBarView, fadingBottomView 122 | ) 123 | navigationBarView.addSubviews( 124 | navigationBarItemView, navigationBarSeparateLine 125 | ) 126 | navigationBarItemView.addSubviews( 127 | logoImage, rankingButton, settingButton 128 | ) 129 | scrollView.addSubviews( 130 | calendarStackView, menuContainerStackView 131 | ) 132 | calendarStackView.addArrangedSubviews( 133 | yesterdayButton, calendarButton, tomorrowButton 134 | ) 135 | calendarButton.addSubview(calendarDateLabel) 136 | menuContainerStackView.addArrangedSubviews( 137 | breakfastContainer, lunchContainer, dinnerContainer 138 | ) 139 | } 140 | 141 | override func setLayout() { 142 | container.snp.makeConstraints { 143 | $0.edges.equalToSuperview() 144 | } 145 | /// navigation bar 146 | navigationBarView.snp.makeConstraints { 147 | $0.top.horizontalEdges.equalToSuperview() 148 | $0.bottom.equalTo(navigationBarItemView.snp.bottom).offset(16) 149 | } 150 | navigationBarItemView.snp.makeConstraints { 151 | $0.height.equalTo(24) 152 | $0.top.equalTo(view.safeAreaLayoutGuide).inset(16) 153 | $0.horizontalEdges.equalToSuperview().inset(20) 154 | } 155 | navigationBarSeparateLine.snp.makeConstraints { 156 | $0.height.equalTo(1) 157 | $0.bottom.horizontalEdges.equalToSuperview() 158 | } 159 | logoImage.snp.makeConstraints { 160 | $0.height.equalToSuperview() 161 | $0.width.equalTo(108) 162 | $0.top.equalTo(navigationBarItemView.snp.top).offset(2) 163 | $0.leading.equalTo(navigationBarItemView.snp.leading) 164 | } 165 | rankingButton.snp.makeConstraints { 166 | $0.height.equalToSuperview() 167 | $0.trailing.equalTo(settingButton.snp.leading).offset(-10) 168 | } 169 | settingButton.snp.makeConstraints { 170 | $0.height.trailing.equalToSuperview() 171 | } 172 | /// scroll view 173 | scrollView.snp.makeConstraints { 174 | $0.top.equalTo(navigationBarView.snp.bottom).offset(34) 175 | $0.bottom.horizontalEdges.equalTo(view.safeAreaLayoutGuide) 176 | } 177 | fadingBottomView.snp.makeConstraints { 178 | $0.horizontalEdges.equalToSuperview() 179 | $0.bottom.equalTo(container.snp.bottom) 180 | $0.height.equalTo(bound.height / 12) 181 | } 182 | /// calendar button 183 | calendarStackView.snp.makeConstraints { 184 | $0.centerX.equalToSuperview() 185 | } 186 | yesterdayButton.snp.makeConstraints { 187 | $0.leading.equalToSuperview() 188 | } 189 | calendarButton.snp.makeConstraints { 190 | $0.centerX.equalToSuperview() 191 | } 192 | calendarDateLabel.snp.makeConstraints { 193 | $0.verticalEdges.equalToSuperview().inset(6) 194 | $0.horizontalEdges.equalToSuperview().inset(14) 195 | } 196 | tomorrowButton.snp.makeConstraints { 197 | $0.trailing.equalToSuperview() 198 | } 199 | /// menu container 200 | menuContainerStackView.snp.makeConstraints { 201 | $0.top.equalTo(calendarStackView.snp.bottom).offset(20) 202 | $0.width.equalTo(scrollView.snp.width).inset(16) 203 | $0.centerX.equalToSuperview() 204 | } 205 | } 206 | 207 | // MARK: - Reactor 208 | override func bindView(reactor: HomeReactor) { 209 | refreshControl.rx.controlEvent(.valueChanged) 210 | .map { .refresh } 211 | .bind(to: reactor.action) 212 | .disposed(by: disposeBag) 213 | 214 | rankingButton.rx.tap 215 | .subscribe(onNext: { [weak self] in 216 | let vc = RankingViewController(reactor: RankingReactor()) 217 | self?.navigationController?.pushViewController(vc, animated: true) 218 | }) 219 | .disposed(by: disposeBag) 220 | 221 | settingButton.rx.tap 222 | .subscribe(onNext: { [weak self] in 223 | let vc = SettingViewController(reactor: SettingReactor()) 224 | self?.navigationController?.pushViewController(vc, animated: true) 225 | }) 226 | .disposed(by: disposeBag) 227 | 228 | breakfastContainer.rx.tapGesture() 229 | .when(.recognized) 230 | .subscribe(onNext: { [weak self] _ in 231 | guard reactor.currentState.menu?.breakfast != nil else { return } 232 | let vc = MenuInfoViewController( 233 | reactor: MenuInfoReactor( 234 | date: reactor.currentState.date, 235 | type: .TYPE_BREAKFAST 236 | ) 237 | ) 238 | self?.navigationController?.pushViewController(vc, animated: true) 239 | }) 240 | .disposed(by: disposeBag) 241 | 242 | lunchContainer.rx.tapGesture() 243 | .when(.recognized) 244 | .subscribe(onNext: { [weak self] _ in 245 | guard reactor.currentState.menu?.lunch != nil else { return } 246 | let vc = MenuInfoViewController( 247 | reactor: MenuInfoReactor( 248 | date: reactor.currentState.date, 249 | type: .TYPE_LUNCH 250 | ) 251 | ) 252 | self?.navigationController?.pushViewController(vc, animated: true) 253 | }) 254 | .disposed(by: disposeBag) 255 | 256 | dinnerContainer.rx.tapGesture() 257 | .when(.recognized) 258 | .subscribe(onNext: { [weak self] _ in 259 | guard reactor.currentState.menu?.dinner != nil else { return } 260 | let vc = MenuInfoViewController( 261 | reactor: MenuInfoReactor( 262 | date: reactor.currentState.date, 263 | type: .TYPE_DINNER 264 | ) 265 | ) 266 | self?.navigationController?.pushViewController(vc, animated: true) 267 | }) 268 | .disposed(by: disposeBag) 269 | } 270 | 271 | override func bindAction(reactor: HomeReactor) { 272 | reactor.action.onNext(.fetchMenu) 273 | 274 | yesterdayButton.rx.tap 275 | .map { .yesterdayButtonDidTap } 276 | .bind(to: reactor.action) 277 | .disposed(by: disposeBag) 278 | 279 | calendarButton.rx.tap 280 | .map { .calendarButtonDidTap } 281 | .bind(to: reactor.action) 282 | .disposed(by: disposeBag) 283 | 284 | tomorrowButton.rx.tap 285 | .map { .tomorrowButtonDidTap } 286 | .bind(to: reactor.action) 287 | .disposed(by: disposeBag) 288 | } 289 | 290 | override func bindState(reactor: HomeReactor) { 291 | reactor.state.map { $0.date } 292 | .distinctUntilChanged() 293 | .map { "\($0.formattingDate(format: "yyyy년 M월 d일 (E)"))" } 294 | .bind(to: calendarDateLabel.rx.text) 295 | .disposed(by: disposeBag) 296 | 297 | reactor.state.map { $0.isRefreshing } 298 | .distinctUntilChanged() 299 | .bind(to: refreshControl.rx.isRefreshing) 300 | .disposed(by: disposeBag) 301 | 302 | reactor.state.compactMap { $0.menu } 303 | .subscribe(onNext: { [weak self] menuResponse in 304 | let breakfast = menuResponse.breakfast 305 | let lunch = menuResponse.lunch 306 | let dinner = menuResponse.dinner 307 | self?.breakfastContainer.configuration(menu: breakfast) 308 | self?.lunchContainer.configuration(menu: lunch) 309 | self?.dinnerContainer.configuration(menu: dinner) 310 | }) 311 | .disposed(by: disposeBag) 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Presentation/Scenes/Home/MenuContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuContainer.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/2/24. 6 | // 7 | 8 | import UIKit 9 | 10 | final class MenuContainer: BaseView { 11 | 12 | init( 13 | menu: String?, 14 | type: MealType 15 | ) { 16 | super.init() 17 | setUI(for: type) 18 | configuration(menu: menu) 19 | } 20 | 21 | required init?(coder: NSCoder) { 22 | fatalError("init(coder:) has not been implemented") 23 | } 24 | 25 | // MARK: - Properties 26 | private lazy var container = UIView().then { 27 | $0.backgroundColor = Color.white 28 | $0.layer.cornerRadius = 12 29 | $0.layer.shadowRadius = 2 30 | $0.layer.shadowOpacity = 0.9 31 | $0.layer.shadowOffset = CGSize(width: 0, height: 0) 32 | $0.clipsToBounds = false 33 | } 34 | 35 | 36 | private lazy var foodIcon = UIImageView().then { 37 | $0.contentMode = .scaleAspectFit 38 | } 39 | 40 | private lazy var mealView = UIView().then { 41 | $0.layer.cornerRadius = 13 42 | $0.clipsToBounds = true 43 | } 44 | 45 | private lazy var mealLabel = UILabel().then { 46 | $0.text = "meal" 47 | $0.textColor = Color.white 48 | $0.font = .systemFont(ofSize: 16, weight: .semibold) 49 | } 50 | 51 | private lazy var menuLabel = UILabel().then { 52 | $0.textColor = Color.darkGray 53 | $0.font = .systemFont(ofSize: 14, weight: .regular) 54 | $0.numberOfLines = 4 55 | } 56 | 57 | // MARK: - UI 58 | func configuration(menu: String?) { 59 | self.menuLabel.text = menu ?? "급식이 없어요." 60 | } 61 | 62 | override func addView() { 63 | self.addSubview(container) 64 | container.addSubviews(foodIcon, mealView, menuLabel) 65 | mealView.addSubview(mealLabel) 66 | } 67 | 68 | override func setLayout() { 69 | container.snp.makeConstraints { 70 | $0.height.equalTo(120) 71 | $0.top.equalToSuperview() 72 | $0.bottom.equalToSuperview() 73 | $0.horizontalEdges.equalToSuperview() 74 | } 75 | foodIcon.snp.makeConstraints { 76 | $0.leading.equalToSuperview().offset(20) 77 | $0.top.equalToSuperview().offset(10) 78 | } 79 | mealView.snp.makeConstraints { 80 | $0.leading.equalToSuperview().offset(20) 81 | $0.top.equalTo(foodIcon.snp.bottom) 82 | $0.width.equalTo(66) 83 | $0.bottom.equalToSuperview().offset(-16) 84 | } 85 | mealLabel.snp.makeConstraints { 86 | $0.verticalEdges.equalToSuperview().inset(4) 87 | $0.centerX.equalToSuperview() 88 | } 89 | menuLabel.snp.makeConstraints { 90 | $0.leading.equalTo(mealView.snp.trailing).offset(20) 91 | $0.trailing.equalToSuperview().offset(-20) 92 | $0.centerY.equalToSuperview() 93 | } 94 | } 95 | 96 | private func setUI(for type: MealType) { 97 | self.container.layer.shadowColor = Color.getMealColor(for: type).cgColor 98 | self.mealView.backgroundColor = Color.getMealColor(for: type) 99 | 100 | switch type { 101 | case .TYPE_BREAKFAST: 102 | self.foodIcon.image = UIImage(icon: .taco) 103 | self.mealLabel.text = "조식" 104 | case .TYPE_LUNCH: 105 | self.foodIcon.image = UIImage(icon: .burger) 106 | self.mealLabel.text = "중식" 107 | case .TYPE_DINNER: 108 | self.foodIcon.image = UIImage(icon: .ramen) 109 | self.mealLabel.text = "석식" 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Presentation/Scenes/MenuInfo/CommentCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommentCell.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/4/24. 6 | // 7 | 8 | import UIKit 9 | import Then 10 | import SnapKit 11 | 12 | class CommentCell: UITableViewCell { 13 | 14 | // MARK: - Properties 15 | static let reuseIdentifier = "CommentCell" 16 | 17 | var anonymousProfileImageArray: [UIImage] = [.cat, .rabbit, .snake, .elephant, .tiger] 18 | 19 | private lazy var anonymousProfileImageView = UIImageView().then { 20 | $0.image = anonymousProfileImageArray.randomElement() 21 | $0.contentMode = .scaleAspectFit 22 | } 23 | 24 | private lazy var commentStackView = UIStackView().then { 25 | $0.axis = .vertical 26 | $0.spacing = 2 27 | $0.distribution = .fill 28 | } 29 | 30 | private lazy var anonymousUserName = UILabel().then { 31 | $0.text = "익명의 대소고인" 32 | $0.textColor = Color.black 33 | $0.font = .systemFont(ofSize: 14, weight: .medium) 34 | } 35 | 36 | private lazy var comment = UILabel().then { 37 | $0.text = "댓글댓글 댓글댓글 댓글댓글" 38 | $0.textColor = Color.black 39 | $0.font = .systemFont(ofSize: 14, weight: .light) 40 | $0.numberOfLines = 0 41 | } 42 | 43 | // MARK: - Initialization 44 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 45 | super.init(style: style, reuseIdentifier: reuseIdentifier) 46 | 47 | contentView.backgroundColor = Color.background 48 | addView() 49 | setLayout() 50 | } 51 | 52 | required init?(coder: NSCoder) { 53 | fatalError("init(coder:) has not been implemented") 54 | } 55 | 56 | // MARK: - UI 57 | public func configuration(_ comment: RatingResponse) { 58 | self.comment.text = comment.comment 59 | } 60 | 61 | func addView() { 62 | contentView.addSubviews( 63 | anonymousProfileImageView, commentStackView 64 | ) 65 | commentStackView.addArrangedSubviews( 66 | anonymousUserName, comment 67 | ) 68 | } 69 | 70 | func setLayout() { 71 | contentView.snp.makeConstraints { 72 | $0.height.equalTo(70) 73 | $0.width.equalToSuperview() 74 | } 75 | anonymousProfileImageView.snp.makeConstraints { 76 | $0.width.height.equalTo(46) 77 | $0.leading.equalToSuperview().offset(4) 78 | $0.centerY.equalToSuperview() 79 | } 80 | commentStackView.snp.makeConstraints { 81 | $0.leading.equalTo(anonymousProfileImageView.snp.trailing).offset(10) 82 | $0.trailing.equalToSuperview().offset(4) 83 | $0.centerY.equalToSuperview() 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Presentation/Scenes/MenuInfo/MenuInfoReactor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuInfoReactor.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/2/24. 6 | // 7 | 8 | import Foundation 9 | import ReactorKit 10 | 11 | final class MenuInfoReactor: Reactor { 12 | 13 | // MARK: - Properties 14 | var initialState: State 15 | 16 | // MARK: - Action 17 | enum Action { 18 | case refresh 19 | case fetchMenuDetail 20 | case fetchComments 21 | } 22 | 23 | // MARK: - Mutation 24 | enum Mutation { 25 | case setMenuDetail(MenuDetailResponse?) 26 | case setComments([RatingResponse]?) 27 | case setIsFetching 28 | } 29 | 30 | // MARK: - State 31 | struct State { 32 | var date: Date 33 | var type: MealType 34 | var menuDetail: MenuDetailResponse? 35 | var comments: [RatingResponse]? 36 | var isFetching: Bool = false 37 | } 38 | 39 | init(date: Date, type: MealType) { 40 | self.initialState = State(date: date, type: type) 41 | } 42 | } 43 | 44 | // MARK: - Mutate 45 | extension MenuInfoReactor { 46 | 47 | private func fetchMenuDetail() -> Observable { 48 | MenuProvider.shared 49 | .getMenuDetail( 50 | currentState.date.formattingDate(format: "yyyyMMdd"), 51 | currentState.type 52 | ) 53 | .flatMap { result -> Observable in 54 | switch result { 55 | case .success(let data): 56 | return Observable.just(.setMenuDetail(data)) 57 | case .failure(_): 58 | return Observable.just(.setMenuDetail(nil)) 59 | } 60 | } 61 | } 62 | 63 | private func fetchComments() -> Observable { 64 | guard let id = currentState.menuDetail?.id else { return .empty() } 65 | return RatingProvider.shared.getRating(id) 66 | .flatMap { result -> Observable in 67 | switch result { 68 | case .success(let data): 69 | return Observable.just(.setComments( 70 | data.reversed() 71 | .filter { !($0.comment.isEmpty) } 72 | )) 73 | case .failure(_): 74 | return Observable.just(.setComments([])) 75 | } 76 | } 77 | } 78 | 79 | func mutate(action: Action) -> Observable { 80 | switch action { 81 | 82 | case .refresh: 83 | return Observable.concat([ 84 | fetchMenuDetail(), 85 | Observable.just(Mutation.setComments(nil)) 86 | ]) 87 | .flatMap { _ in 88 | Observable.concat([ 89 | Observable.just(Mutation.setComments(nil)), 90 | Observable.just(Mutation.setIsFetching), 91 | self.fetchComments() 92 | ]) 93 | } 94 | 95 | case .fetchMenuDetail: 96 | return fetchMenuDetail() 97 | 98 | case .fetchComments: 99 | return fetchMenuDetail().flatMap { _ in 100 | self.fetchComments() 101 | } 102 | } 103 | } 104 | 105 | // MARK: - Reduce 106 | func reduce(state: State, mutation: Mutation) -> State { 107 | var newState = state 108 | switch mutation { 109 | 110 | case .setMenuDetail(let menuDetail): 111 | newState.menuDetail = menuDetail 112 | newState.isFetching = false 113 | 114 | case .setComments(let comments): 115 | newState.comments = comments 116 | newState.isFetching = false 117 | 118 | case .setIsFetching: 119 | newState.isFetching = true 120 | } 121 | return newState 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Presentation/Scenes/MenuInfo/MenuInfoViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuInfoViewController.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/2/24. 6 | // 7 | 8 | import UIKit 9 | import Cosmos 10 | 11 | final class MenuInfoViewController: BaseVC { 12 | 13 | // MARK: - Properties 14 | private lazy var container = UIView() 15 | 16 | /// navigation bar 17 | private lazy var navigationBarView = UIView().then { 18 | $0.backgroundColor = Color.white 19 | } 20 | 21 | private lazy var navigationBarSeparateLine = UIView().then { 22 | $0.backgroundColor = Color.lightGray 23 | } 24 | 25 | private lazy var navigationBarItemView = UIView() 26 | 27 | private lazy var backButton = UIButton().then { 28 | $0.setImage(UIImage(icon: .leadingArrow), for: .normal) 29 | $0.imageView!.contentMode = .scaleAspectFit 30 | $0.tintColor = Color.black 31 | } 32 | 33 | private lazy var navigationTitle = UILabel().then { 34 | $0.text = "급식 상세 정보" 35 | $0.font = .systemFont(ofSize: 18, weight: .medium) 36 | $0.textColor = Color.black 37 | } 38 | 39 | /// scroll view 40 | private lazy var scrollView = UIScrollView().then { 41 | $0.showsVerticalScrollIndicator = false 42 | $0.alwaysBounceVertical = true 43 | $0.contentInsetAdjustmentBehavior = .always 44 | $0.clipsToBounds = false 45 | $0.delegate = self 46 | } 47 | 48 | private let refreshControl = UIRefreshControl() 49 | 50 | private lazy var scrollStackView = UIStackView().then { 51 | $0.axis = .vertical 52 | $0.spacing = 20 53 | $0.distribution = .fill 54 | } 55 | 56 | private lazy var fadingBottomView = FadingView(position: .bottom) 57 | 58 | /// menu info container 59 | private lazy var menuInfoContainer = UIView().then { 60 | $0.backgroundColor = Color.white 61 | $0.layer.cornerRadius = 12 62 | $0.layer.cornerRadius = 12 63 | $0.layer.shadowRadius = 2 64 | $0.layer.shadowOpacity = 0.9 65 | $0.layer.shadowOffset = CGSize(width: 0, height: 0) 66 | $0.clipsToBounds = false 67 | } 68 | 69 | private lazy var menuDateLabel = UILabel().then { 70 | $0.text = "2월 6일 (월)" 71 | $0.textColor = Color.darkGray 72 | $0.font = .systemFont(ofSize: 18, weight: .medium) 73 | } 74 | 75 | private lazy var mealView = UIView().then { 76 | $0.layer.cornerRadius = 14 77 | $0.clipsToBounds = true 78 | } 79 | 80 | private lazy var mealLabel = UILabel().then { 81 | $0.text = "meal" 82 | $0.textColor = Color.white 83 | $0.font = .systemFont(ofSize: 16, weight: .semibold) 84 | } 85 | 86 | private lazy var starView = CosmosView().then { 87 | $0.settings.fillMode = .half 88 | $0.settings.updateOnTouch = false 89 | $0.settings.starSize = 30 90 | $0.settings.starMargin = 4 91 | $0.settings.filledImage = UIImage(icon: .filledStar) 92 | $0.settings.emptyImage = UIImage(icon: .emptyStar) 93 | } 94 | 95 | private lazy var topSeparateLine = UIView() 96 | 97 | private lazy var menuLabel = UILabel().then { 98 | $0.text = "menu" 99 | $0.textColor = Color.darkGray 100 | $0.font = .systemFont(ofSize: 16, weight: .regular) 101 | $0.numberOfLines = 0 102 | $0.setLineSpacing(lineSpacing: 2, alignment: .center) 103 | } 104 | 105 | private lazy var bottomSeparateLine = UIView() 106 | 107 | private lazy var kcalLabel = UILabel().then { 108 | $0.text = "kcal" 109 | $0.textColor = Color.gray 110 | $0.font = .systemFont(ofSize: 16, weight: .regular) 111 | $0.numberOfLines = 0 112 | } 113 | 114 | private lazy var nutrientsLabel = UILabel().then { 115 | $0.text = "nutrients" 116 | $0.textColor = Color.darkGray 117 | $0.font = .systemFont(ofSize: 16, weight: .regular) 118 | $0.numberOfLines = 0 119 | $0.setLineSpacing(lineSpacing: 2, alignment: .center) 120 | } 121 | 122 | ///fixed menu info container 123 | private lazy var fixedMenuInfoContainer = UIView().then { 124 | $0.backgroundColor = Color.white 125 | $0.layer.shadowColor = menuInfoContainer.layer.shadowColor 126 | $0.layer.cornerRadius = 12 127 | $0.layer.shadowRadius = 2 128 | $0.layer.shadowOpacity = 0.9 129 | $0.layer.shadowOffset = CGSize(width: 0, height: 0) 130 | $0.layer.maskedCorners = CACornerMask( 131 | arrayLiteral: .layerMinXMaxYCorner, .layerMaxXMaxYCorner 132 | ) 133 | $0.clipsToBounds = false 134 | $0.isUserInteractionEnabled = false 135 | } 136 | 137 | private lazy var bottomShadow = UIView().then { bottomShadow in 138 | let gradientLayer = CAGradientLayer().then { 139 | $0.frame = CGRect(x: 0, y: 0, width: view.frame.width - 32, height: 24) 140 | $0.colors = [Color.darkGray.withAlphaComponent(0.3).cgColor, 141 | Color.darkGray.withAlphaComponent(0).cgColor] 142 | $0.startPoint = CGPoint(x: 0.5, y: 0) 143 | $0.endPoint = CGPoint(x: 0.5, y: 1) 144 | $0.cornerRadius = 12 145 | $0.maskedCorners = CACornerMask( 146 | arrayLiteral: .layerMinXMaxYCorner, .layerMaxXMaxYCorner 147 | ) 148 | } 149 | bottomShadow.layer.addSublayer(gradientLayer) 150 | } 151 | 152 | private lazy var fixedMenuLabel = UILabel().then { 153 | $0.textColor = Color.darkGray 154 | $0.font = .systemFont(ofSize: 16, weight: .regular) 155 | $0.numberOfLines = 0 156 | } 157 | 158 | private lazy var fixedBottomSeparateLine = UIView().then { 159 | $0.backgroundColor = bottomSeparateLine.backgroundColor 160 | } 161 | 162 | private lazy var fixedKcalLabel = UILabel().then { 163 | $0.textColor = Color.gray 164 | $0.font = .systemFont(ofSize: 16, weight: .regular) 165 | $0.numberOfLines = 0 166 | } 167 | 168 | private lazy var fixedNutrientsLabel = UILabel().then { 169 | $0.textColor = Color.darkGray 170 | $0.font = .systemFont(ofSize: 16, weight: .regular) 171 | $0.numberOfLines = 0 172 | } 173 | 174 | /// review button 175 | private lazy var reviewButton = ScaledButton(scale: 0.94).then { 176 | $0.backgroundColor = Color.white 177 | $0.layer.cornerRadius = 32 178 | $0.layer.shadowRadius = 2 179 | $0.layer.shadowOpacity = 0.9 180 | $0.layer.shadowOffset = CGSize(width: 0, height: 0) 181 | } 182 | 183 | private lazy var reviewButtonImage = UIImageView().then { 184 | $0.image = UIImage(icon: .review) 185 | $0.contentMode = .scaleAspectFit 186 | $0.tintColor = Color.black 187 | } 188 | 189 | /// comment 190 | private lazy var commentTableView = UITableView().then { 191 | $0.backgroundColor = Color.background 192 | $0.register( 193 | CommentCell.self, 194 | forCellReuseIdentifier: CommentCell.reuseIdentifier 195 | ) 196 | $0.isScrollEnabled = false 197 | $0.allowsSelection = false 198 | $0.separatorStyle = .none 199 | } 200 | 201 | private lazy var emptyCommentsLabel = UILabel().then { 202 | $0.text = "아직 리뷰가 하나도 없어요" 203 | $0.textColor = Color.darkGray 204 | $0.font = .systemFont(ofSize: 16, weight: .medium) 205 | $0.textAlignment = .center 206 | } 207 | 208 | private lazy var emptyCommentsSubLabel = UILabel().then { 209 | $0.text = "직접 리뷰를 작성해 보는 건 어떠신가요??" 210 | $0.textColor = Color.darkGray 211 | $0.font = .systemFont(ofSize: 14, weight: .light) 212 | $0.textAlignment = .center 213 | } 214 | 215 | // MARK: - LifeCycle 216 | override func viewWillAppear(_ animated: Bool) { 217 | super.viewWillAppear(true) 218 | 219 | print("\(type(of: self)): \(#function)") 220 | reactor?.action.onNext(.fetchMenuDetail) 221 | reactor?.action.onNext(.fetchComments) 222 | } 223 | 224 | // MARK: - UI 225 | override func setUp() { 226 | setUIColor() 227 | } 228 | 229 | override func configureVC() { 230 | scrollView.refreshControl = refreshControl 231 | } 232 | 233 | override func addView() { 234 | view.addSubview(container) 235 | /// navigation bar 236 | container.addSubviews( 237 | scrollView, bottomShadow, fixedMenuInfoContainer, 238 | navigationBarView, fadingBottomView, reviewButton 239 | ) 240 | navigationBarView.addSubviews( 241 | navigationBarItemView, navigationBarSeparateLine 242 | ) 243 | navigationBarItemView.addSubviews( 244 | backButton, navigationTitle 245 | ) 246 | /// scroll view 247 | scrollView.addSubview(scrollStackView) 248 | scrollStackView.addArrangedSubviews( 249 | menuInfoContainer, emptyCommentsLabel, 250 | emptyCommentsSubLabel, commentTableView 251 | ) 252 | menuInfoContainer.addSubviews( 253 | menuDateLabel, mealView, starView, 254 | topSeparateLine, menuLabel, bottomSeparateLine, 255 | kcalLabel, nutrientsLabel 256 | ) 257 | fixedMenuInfoContainer.addSubviews( 258 | fixedMenuLabel, fixedBottomSeparateLine, 259 | fixedKcalLabel, fixedNutrientsLabel 260 | ) 261 | reviewButton.addSubview(reviewButtonImage) 262 | mealView.addSubview(mealLabel) 263 | } 264 | 265 | override func setLayout() { 266 | container.snp.makeConstraints { 267 | $0.edges.equalToSuperview() 268 | } 269 | /// navigation bar 270 | navigationBarView.snp.makeConstraints { 271 | $0.top.horizontalEdges.equalToSuperview() 272 | $0.bottom.equalTo(navigationBarItemView.snp.bottom).offset(16) 273 | } 274 | navigationBarItemView.snp.makeConstraints { 275 | $0.height.equalTo(24) 276 | $0.top.equalTo(view.safeAreaLayoutGuide).inset(16) 277 | $0.horizontalEdges.equalToSuperview().inset(16) 278 | } 279 | navigationBarSeparateLine.snp.makeConstraints { 280 | $0.height.equalTo(1) 281 | $0.bottom.horizontalEdges.equalToSuperview() 282 | } 283 | backButton.snp.makeConstraints { 284 | $0.height.leading.equalToSuperview() 285 | } 286 | navigationTitle.snp.makeConstraints { 287 | $0.height.equalToSuperview() 288 | $0.leading.equalTo(backButton.snp.trailing).offset(10) 289 | } 290 | /// scroll view 291 | scrollView.snp.makeConstraints { 292 | $0.top.equalTo(navigationBarView.snp.bottom).offset(20) 293 | $0.horizontalEdges.bottom.equalTo(view.safeAreaLayoutGuide) 294 | } 295 | scrollStackView.snp.makeConstraints { 296 | $0.verticalEdges.equalToSuperview() 297 | $0.horizontalEdges.equalToSuperview().inset(16) 298 | } 299 | fadingBottomView.snp.makeConstraints { 300 | $0.horizontalEdges.equalToSuperview() 301 | $0.bottom.equalTo(container.snp.bottom) 302 | $0.height.equalTo(bound.height / 12) 303 | } 304 | /// menu info container 305 | menuInfoContainer.snp.makeConstraints { 306 | $0.top.equalToSuperview() 307 | $0.bottom.equalTo(nutrientsLabel.snp.bottom).offset(25) 308 | $0.width.equalToSuperview() 309 | $0.centerX.equalToSuperview() 310 | } 311 | menuDateLabel.snp.makeConstraints { 312 | $0.top.equalToSuperview().offset(25) 313 | $0.bottom.equalTo(menuDateLabel.snp.top).offset(18) 314 | $0.centerX.equalToSuperview() 315 | } 316 | mealView.snp.makeConstraints { 317 | $0.width.equalTo(66) 318 | $0.top.equalTo(menuDateLabel.snp.bottom).offset(10) 319 | $0.centerX.equalToSuperview() 320 | } 321 | mealLabel.snp.makeConstraints { 322 | $0.centerX.centerY.equalToSuperview() 323 | $0.verticalEdges.equalToSuperview().inset(4) 324 | } 325 | starView.snp.makeConstraints { 326 | $0.top.equalTo(mealView.snp.bottom).offset(10) 327 | $0.centerX.equalToSuperview() 328 | } 329 | topSeparateLine.snp.makeConstraints { 330 | $0.width.equalTo(menuInfoContainer.snp.width).dividedBy(2) 331 | $0.top.equalTo(starView.snp.bottom).offset(15) 332 | $0.bottom.equalTo(topSeparateLine.snp.top).offset(1) 333 | $0.centerX.equalToSuperview() 334 | } 335 | menuLabel.snp.makeConstraints { 336 | $0.top.equalTo(topSeparateLine.snp.bottom).offset(20) 337 | $0.centerX.equalToSuperview() 338 | } 339 | bottomSeparateLine.snp.makeConstraints { 340 | $0.width.equalTo(menuInfoContainer.snp.width).dividedBy(2) 341 | $0.top.equalTo(menuLabel.snp.bottom).offset(20) 342 | $0.bottom.equalTo(bottomSeparateLine.snp.top).offset(1) 343 | $0.centerX.equalToSuperview() 344 | } 345 | kcalLabel.snp.makeConstraints { 346 | $0.top.equalTo(bottomSeparateLine.snp.bottom).offset(15) 347 | $0.centerX.equalToSuperview() 348 | } 349 | nutrientsLabel.snp.makeConstraints { 350 | $0.top.equalTo(kcalLabel.snp.bottom).offset(4) 351 | $0.centerX.equalToSuperview() 352 | } 353 | /// fixed menu info container 354 | fixedMenuInfoContainer.snp.makeConstraints { 355 | $0.top.equalTo(navigationBarView.snp.bottom) 356 | $0.bottom.equalTo(fixedNutrientsLabel.snp.bottom).offset(25) 357 | $0.width.equalTo(scrollView.snp.width).inset(16) 358 | $0.centerX.equalToSuperview() 359 | } 360 | fixedMenuLabel.snp.makeConstraints { 361 | $0.top.equalTo(fixedMenuInfoContainer.snp.top).offset(20) 362 | $0.centerX.equalToSuperview() 363 | } 364 | fixedBottomSeparateLine.snp.makeConstraints { 365 | $0.width.equalTo(fixedMenuInfoContainer.snp.width).dividedBy(2) 366 | $0.top.equalTo(fixedMenuLabel.snp.bottom).offset(20) 367 | $0.bottom.equalTo(fixedBottomSeparateLine.snp.top).offset(1) 368 | $0.centerX.equalToSuperview() 369 | } 370 | fixedKcalLabel.snp.makeConstraints { 371 | $0.top.equalTo(fixedBottomSeparateLine.snp.bottom).offset(15) 372 | $0.centerX.equalToSuperview() 373 | } 374 | fixedNutrientsLabel.snp.makeConstraints { 375 | $0.top.equalTo(fixedKcalLabel.snp.bottom).offset(4) 376 | $0.centerX.equalToSuperview() 377 | } 378 | bottomShadow.snp.makeConstraints { 379 | $0.top.equalTo(fixedMenuInfoContainer.snp.bottom).offset(-12) 380 | $0.leading.equalTo(fixedMenuInfoContainer.snp.leading) 381 | $0.centerX.equalToSuperview() 382 | } 383 | /// review button 384 | reviewButton.snp.makeConstraints { 385 | $0.width.height.equalTo(64) 386 | $0.trailing.equalTo(view.safeAreaLayoutGuide).inset(16) 387 | $0.bottom.equalTo(view.safeAreaLayoutGuide).inset(20) 388 | } 389 | reviewButtonImage.snp.makeConstraints { 390 | $0.width.height.equalTo(30) 391 | $0.centerY.centerX.equalToSuperview() 392 | } 393 | /// comment 394 | commentTableView.snp.makeConstraints { 395 | $0.width.equalTo(scrollView.snp.width).inset(16) 396 | $0.height.equalTo(100) 397 | $0.centerX.equalToSuperview() 398 | } 399 | emptyCommentsLabel.snp.makeConstraints { 400 | $0.centerX.equalToSuperview() 401 | } 402 | emptyCommentsSubLabel.snp.makeConstraints { 403 | $0.top.equalTo(emptyCommentsLabel.snp.bottom).offset(4) 404 | $0.centerX.equalToSuperview() 405 | } 406 | } 407 | 408 | private func setUIColor() { 409 | guard let type = reactor?.currentState.type else { return } 410 | let color = Color.getMealColor(for: reactor!.currentState.type) 411 | 412 | [menuInfoContainer, reviewButton].forEach { 413 | $0.layer.shadowColor = color.cgColor 414 | } 415 | [bottomSeparateLine, topSeparateLine].forEach { 416 | $0.backgroundColor = color.withAlphaComponent(0.7) 417 | } 418 | mealView.backgroundColor = color 419 | mealLabel.text = { 420 | switch type { 421 | case .TYPE_BREAKFAST: return "조식" 422 | case .TYPE_LUNCH: return "중식" 423 | case .TYPE_DINNER: return "석식" 424 | } 425 | }() 426 | } 427 | 428 | // MARK: - Reactor 429 | override func bindView(reactor: MenuInfoReactor) { 430 | refreshControl.rx.controlEvent(.valueChanged) 431 | .map { .refresh } 432 | .bind(to: reactor.action) 433 | .disposed(by: disposeBag) 434 | 435 | backButton.rx.tap 436 | .subscribe(onNext: { [weak self] in 437 | self?.navigationController?.popViewController(animated: true) 438 | }) 439 | .disposed(by: disposeBag) 440 | 441 | reviewButton.rx.tap 442 | .subscribe(onNext: { [weak self] in 443 | let menuId = reactor.currentState.menuDetail?.id ?? 0 444 | let vc = ReviewViewController(reactor: ReviewReactor(menuId: menuId)) 445 | self?.navigationController?.pushViewController(vc, animated: true) 446 | }) 447 | .disposed(by: disposeBag) 448 | } 449 | 450 | override func bindAction(reactor: MenuInfoReactor) { 451 | reactor.action.onNext(.fetchMenuDetail) 452 | reactor.action.onNext(.fetchComments) 453 | } 454 | 455 | override func bindState(reactor: MenuInfoReactor) { 456 | reactor.state.map { $0.date } 457 | .distinctUntilChanged() 458 | .map { "\($0.formattingDate(format: "M월 d일 (E)"))" } 459 | .bind(to: menuDateLabel.rx.text) 460 | .disposed(by: disposeBag) 461 | 462 | reactor.state.map { $0.isFetching } 463 | .distinctUntilChanged() 464 | .bind(to: refreshControl.rx.isRefreshing) 465 | .disposed(by: disposeBag) 466 | 467 | reactor.state.compactMap { $0.menuDetail } 468 | .map { $0.totalScore } 469 | .bind(to: starView.rx.rating) 470 | .disposed(by: disposeBag) 471 | 472 | reactor.state.compactMap { $0.menuDetail } 473 | .map { $0.menu } 474 | .subscribe(onNext: { [weak self] menu in 475 | let menu = menu?.replacingOccurrences(of: " ", with: "\n") 476 | self?.menuLabel.text = menu 477 | self?.fixedMenuLabel.text = menu 478 | self?.fixedMenuLabel.setLineSpacing(lineSpacing: 2, alignment: .center) 479 | }) 480 | .disposed(by: disposeBag) 481 | 482 | reactor.state.compactMap { $0.menuDetail } 483 | .map { $0.cal } 484 | .subscribe(onNext: { [weak self] cal in 485 | self?.kcalLabel.text = cal 486 | self?.fixedKcalLabel.text = cal 487 | }) 488 | .disposed(by: disposeBag) 489 | 490 | reactor.state.compactMap { $0.menuDetail } 491 | .map { $0.nutrients } 492 | .subscribe(onNext: { [weak self] nutrients in 493 | let nutrientsArray = nutrients?.components(separatedBy: ", ")[0...2] 494 | let replacingNutrients = nutrientsArray?.map { 495 | $0.replacingOccurrences(of: "(g)", with: "") + "g" 496 | }.joined(separator: "\n") 497 | self?.nutrientsLabel.text = replacingNutrients 498 | self?.fixedNutrientsLabel.text = replacingNutrients 499 | self?.fixedNutrientsLabel.setLineSpacing(lineSpacing: 2, alignment: .center) 500 | }) 501 | .disposed(by: disposeBag) 502 | 503 | reactor.state.compactMap { $0.comments } 504 | .bind(to: commentTableView.rx.items( 505 | cellIdentifier: CommentCell.reuseIdentifier, 506 | cellType: CommentCell.self) 507 | ) { _, comment, cell in 508 | cell.configuration(comment) 509 | } 510 | .disposed(by: disposeBag) 511 | 512 | reactor.state.compactMap { $0.comments } 513 | .filter { !$0.isEmpty } 514 | .subscribe(onNext: { [weak self] comments in 515 | self?.emptyCommentsLabel.removeFromSuperview() 516 | self?.emptyCommentsSubLabel.removeFromSuperview() 517 | 518 | let commentsCount = comments.count 519 | self?.commentTableView.snp.updateConstraints { 520 | $0.height.equalTo((commentsCount * 70) + 100) 521 | } 522 | self?.commentTableView.layoutIfNeeded() 523 | }) 524 | .disposed(by: disposeBag) 525 | } 526 | } 527 | 528 | // MARK: - UIScrollViewDelegate 529 | extension MenuInfoViewController: UIScrollViewDelegate { 530 | 531 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 532 | 533 | let currentPosition = scrollView.contentOffset.y + scrollView.safeAreaInsets.top 534 | 535 | if currentPosition >= 155 { 536 | fixedMenuInfoContainer.isHidden = false 537 | } else { 538 | fixedMenuInfoContainer.isHidden = true 539 | bottomShadow.isHidden = true 540 | } 541 | 542 | if currentPosition >= 180 { 543 | UIView.animate(withDuration: 0.3) { 544 | self.bottomShadow.alpha = 1 545 | } 546 | bottomShadow.isHidden = false 547 | } else { 548 | UIView.animate(withDuration: 0.3) { 549 | self.bottomShadow.alpha = 0 550 | } 551 | } 552 | } 553 | } 554 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Presentation/Scenes/Ranking/RankingCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RankingCell.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/5/24. 6 | // 7 | 8 | import UIKit 9 | import RxSwift 10 | import RxCocoa 11 | import Then 12 | import SnapKit 13 | import Cosmos 14 | 15 | class RankingCell: UITableViewCell { 16 | 17 | // MARK: - Properties 18 | static let reuseIdentifier = "RankingCell" 19 | 20 | private lazy var container = UIButton().then { 21 | $0.backgroundColor = Color.white 22 | $0.layer.cornerRadius = 12 23 | $0.layer.shadowRadius = 2 24 | $0.layer.shadowOpacity = 0.9 25 | $0.layer.shadowOffset = CGSize(width: 0, height: 0) 26 | $0.clipsToBounds = false 27 | } 28 | 29 | private lazy var spacer = UIView().then { 30 | $0.backgroundColor = Color.background 31 | } 32 | 33 | private lazy var crownImage = UIImageView().then { 34 | $0.image = UIImage(icon: .crown) 35 | $0.contentMode = .scaleAspectFit 36 | } 37 | 38 | private lazy var rankingLabel = UILabel().then { 39 | $0.text = "1위" 40 | $0.textColor = Color.breakfast 41 | $0.font = .systemFont(ofSize: 22, weight: .regular) 42 | } 43 | 44 | private lazy var starView = CosmosView().then { 45 | $0.settings.fillMode = .half 46 | $0.settings.updateOnTouch = false 47 | $0.settings.starSize = 24 48 | $0.settings.starMargin = 1 49 | $0.settings.filledImage = UIImage(icon: .filledStar) 50 | $0.settings.emptyImage = UIImage(icon: .emptyStar) 51 | } 52 | 53 | private lazy var menuLabel = UILabel().then { 54 | $0.textColor = Color.darkGray 55 | $0.font = .systemFont(ofSize: 16, weight: .regular) 56 | $0.numberOfLines = 0 57 | } 58 | 59 | private lazy var dateLabel = UILabel().then { 60 | $0.textColor = Color.lightGray 61 | $0.font = .systemFont(ofSize: 14, weight: .regular) 62 | } 63 | 64 | // MARK: - Initialization 65 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 66 | super.init(style: style, reuseIdentifier: reuseIdentifier) 67 | 68 | addView() 69 | setLayout() 70 | } 71 | 72 | required init?(coder: NSCoder) { 73 | fatalError("init(coder:) has not been implemented") 74 | } 75 | 76 | // MARK: - UI 77 | public func configuration(_ data: Ranking, _ mealType: MealType) { 78 | contentView.backgroundColor = Color.background 79 | let color = Color.getMealColor(for: mealType) 80 | 81 | container.layer.shadowColor = color.cgColor 82 | crownImage.tintColor = color 83 | rankingLabel.text = "\(data.ranking)위" 84 | rankingLabel.textColor = color 85 | starView.rating = data.totalScore 86 | menuLabel.text = data.menu 87 | dateLabel.text = data.date.stringToDate(format: "yyyyMMdd").formattingDate(format: "yyyy년 M월 d일 (E)") 88 | } 89 | 90 | func addView() { 91 | contentView.addSubview(spacer) 92 | spacer.addSubview(container) 93 | container.addSubviews( 94 | crownImage, rankingLabel, starView, 95 | menuLabel, dateLabel 96 | ) 97 | } 98 | 99 | func setLayout() { 100 | spacer.snp.makeConstraints { 101 | $0.edges.equalToSuperview() 102 | $0.height.equalTo(144) 103 | } 104 | container.snp.makeConstraints { 105 | $0.top.equalTo(spacer.snp.top).inset(2) 106 | $0.horizontalEdges.equalTo(spacer).inset(2) 107 | $0.bottom.equalTo(dateLabel.snp.bottom).offset(16) 108 | } 109 | crownImage.snp.makeConstraints { 110 | $0.width.height.equalTo(20) 111 | $0.top.equalToSuperview().offset(18) 112 | $0.leading.equalToSuperview().offset(24) 113 | } 114 | rankingLabel.snp.makeConstraints { 115 | $0.top.equalToSuperview().offset(16) 116 | $0.leading.equalTo(crownImage.snp.trailing).offset(8) 117 | } 118 | starView.snp.makeConstraints { 119 | $0.top.equalToSuperview().offset(16) 120 | $0.leading.equalTo(rankingLabel.snp.trailing).offset(12) 121 | } 122 | menuLabel.snp.makeConstraints { 123 | $0.top.equalTo(rankingLabel.snp.bottom).offset(8) 124 | $0.leading.trailing.equalToSuperview().inset(24) 125 | } 126 | dateLabel.snp.makeConstraints { 127 | $0.top.equalTo(menuLabel.snp.bottom).offset(2) 128 | $0.leading.equalToSuperview().offset(24) 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Presentation/Scenes/Ranking/RankingReactor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RankingReactor.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/2/24. 6 | // 7 | 8 | import Foundation 9 | import ReactorKit 10 | 11 | final class RankingReactor: Reactor { 12 | 13 | // MARK: - Properties 14 | var initialState: State = State() 15 | 16 | // MARK: - Action 17 | enum Action { 18 | case refresh 19 | case fetchRanking 20 | case setMealType(MealType) 21 | } 22 | 23 | // MARK: - Mutation 24 | enum Mutation { 25 | case setMealType(MealType) 26 | case setBreakfastRanking(RankingResponse?) 27 | case setLunchRanking(RankingResponse?) 28 | case setDinnerRanking(RankingResponse?) 29 | } 30 | 31 | // MARK: - State 32 | struct State { 33 | var mealType: MealType = .TYPE_LUNCH 34 | var breakfastRanking: RankingResponse? 35 | var lunchRanking: RankingResponse? 36 | var dinnerRanking: RankingResponse? 37 | } 38 | } 39 | 40 | // MARK: - Mutate 41 | extension RankingReactor { 42 | 43 | private func fetchBreakfastRanking() -> Observable { 44 | return RankingProvider.shared 45 | .getRating(.TYPE_BREAKFAST) 46 | .map { result -> Mutation in 47 | switch result { 48 | case .success(let data): 49 | return .setBreakfastRanking(data) 50 | case .failure: 51 | return .setBreakfastRanking(nil) 52 | } 53 | } 54 | } 55 | 56 | private func fetchLunchRanking() -> Observable { 57 | return RankingProvider.shared 58 | .getRating(.TYPE_LUNCH) 59 | .map { result -> Mutation in 60 | switch result { 61 | case .success(let data): 62 | return .setLunchRanking(data) 63 | case .failure: 64 | return .setLunchRanking(nil) 65 | } 66 | } 67 | } 68 | 69 | private func fetchDinnerRanking() -> Observable { 70 | return RankingProvider.shared 71 | .getRating(.TYPE_DINNER) 72 | .map { result -> Mutation in 73 | switch result { 74 | case .success(let data): 75 | return .setDinnerRanking(data) 76 | case .failure: 77 | return .setDinnerRanking(nil) 78 | } 79 | } 80 | } 81 | 82 | func mutate(action: Action) -> Observable { 83 | switch action { 84 | case .refresh: 85 | return .empty() 86 | 87 | case .fetchRanking: 88 | let breakfastObservable = fetchBreakfastRanking() 89 | let lunchObservable = fetchLunchRanking() 90 | let dinnerObservable = fetchDinnerRanking() 91 | 92 | return Observable.merge(breakfastObservable, lunchObservable, dinnerObservable) 93 | 94 | case .setMealType(let mealType): 95 | return .just(.setMealType(mealType)) 96 | } 97 | } 98 | 99 | // MARK: - Reduce 100 | func reduce(state: State, mutation: Mutation) -> State { 101 | var newState = state 102 | switch mutation { 103 | 104 | case .setMealType(let mealType): 105 | newState.mealType = mealType 106 | 107 | case .setBreakfastRanking(let ranking): 108 | newState.breakfastRanking = ranking 109 | 110 | case .setLunchRanking(let ranking): 111 | newState.lunchRanking = ranking 112 | 113 | case .setDinnerRanking(let ranking): 114 | newState.dinnerRanking = ranking 115 | } 116 | return newState 117 | } 118 | } 119 | 120 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Presentation/Scenes/Ranking/RankingViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RankingViewController.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 4/30/24. 6 | // 7 | 8 | import UIKit 9 | 10 | final class RankingViewController: BaseVC { 11 | 12 | // MARK: - Properties 13 | private lazy var container = UIView() 14 | 15 | /// navigation bar 16 | private lazy var navigationBarView = UIView().then { 17 | $0.backgroundColor = Color.white 18 | } 19 | 20 | private lazy var navigationBarSeparateLine = UIView().then { 21 | $0.backgroundColor = Color.lightGray 22 | } 23 | 24 | private lazy var navigationBarItemView = UIView() 25 | 26 | private lazy var backButton = UIButton().then { 27 | $0.setImage(UIImage(icon: .leadingArrow), for: .normal) 28 | $0.imageView!.contentMode = .scaleAspectFit 29 | $0.tintColor = Color.black 30 | } 31 | 32 | private lazy var navigationTitle = UILabel().then { 33 | $0.text = "대슐랭 랭킹" 34 | $0.font = .systemFont(ofSize: 18, weight: .medium) 35 | $0.textColor = Color.black 36 | } 37 | 38 | /// scroll view 39 | private lazy var scrollView = UIScrollView().then { 40 | $0.showsVerticalScrollIndicator = false 41 | $0.alwaysBounceVertical = true 42 | $0.contentInsetAdjustmentBehavior = .always 43 | $0.clipsToBounds = false 44 | } 45 | 46 | private let refreshControl = UIRefreshControl() 47 | 48 | private lazy var scrollStackView = UIStackView().then { 49 | $0.axis = .vertical 50 | $0.spacing = 20 51 | $0.distribution = .fill 52 | } 53 | 54 | private lazy var fadingBottomView = FadingView(position: .bottom) 55 | 56 | /// ranking 57 | private lazy var breakfastButton = ScaledButton(scale: 0.95).then { 58 | $0.setTitle("조식 랭킹", for: .normal) 59 | $0.setTitleColor(Color.darkGray, for: .normal) 60 | $0.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium) 61 | $0.backgroundColor = Color.breakfast 62 | $0.layer.cornerRadius = 8 63 | $0.layer.shadowRadius = 2 64 | $0.layer.shadowOpacity = 0.5 65 | $0.layer.shadowOffset = CGSize(width: 0, height: 0) 66 | $0.layer.shadowColor = Color.getMealColor(for: .TYPE_BREAKFAST).cgColor 67 | } 68 | private lazy var lunchButton = ScaledButton(scale: 0.95).then { 69 | $0.setTitle("중식 랭킹", for: .normal) 70 | $0.setTitleColor(Color.darkGray, for: .normal) 71 | $0.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium) 72 | $0.backgroundColor = Color.lunch 73 | $0.layer.cornerRadius = 8 74 | $0.layer.shadowRadius = 2 75 | $0.layer.shadowOpacity = 0.5 76 | $0.layer.shadowOffset = CGSize(width: 0, height: 0) 77 | $0.layer.shadowColor = Color.getMealColor(for: .TYPE_LUNCH).cgColor 78 | } 79 | 80 | private lazy var dinnerButton = ScaledButton(scale: 0.95).then { 81 | $0.setTitle("석식 랭킹", for: .normal) 82 | $0.setTitleColor(Color.darkGray, for: .normal) 83 | $0.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium) 84 | $0.backgroundColor = Color.dinner 85 | $0.layer.cornerRadius = 8 86 | $0.layer.shadowRadius = 2 87 | $0.layer.shadowOpacity = 0.5 88 | $0.layer.shadowOffset = CGSize(width: 0, height: 0) 89 | $0.layer.shadowColor = Color.getMealColor(for: .TYPE_DINNER).cgColor 90 | } 91 | 92 | private lazy var rankingTableView = UITableView().then { 93 | $0.backgroundColor = Color.background 94 | $0.register( 95 | RankingCell.self, 96 | forCellReuseIdentifier: RankingCell.reuseIdentifier 97 | ) 98 | $0.isScrollEnabled = false 99 | $0.allowsSelection = false 100 | $0.separatorStyle = .none 101 | } 102 | 103 | // MARK: - LifeCycle 104 | override func viewWillAppear(_ animated: Bool) { 105 | super.viewWillAppear(true) 106 | } 107 | 108 | // MARK: - UI 109 | override func addView() { 110 | view.addSubview(container) 111 | /// navigation bar 112 | container.addSubviews( 113 | scrollView, breakfastButton, lunchButton, dinnerButton, 114 | navigationBarView, fadingBottomView 115 | ) 116 | navigationBarView.addSubviews( 117 | navigationBarItemView, navigationBarSeparateLine 118 | ) 119 | navigationBarItemView.addSubviews( 120 | backButton, navigationTitle 121 | ) 122 | /// scroll view 123 | scrollView.addSubview(scrollStackView) 124 | scrollStackView.addArrangedSubviews( 125 | rankingTableView 126 | ) 127 | } 128 | 129 | override func setLayout() { 130 | container.snp.makeConstraints { 131 | $0.edges.equalToSuperview() 132 | } 133 | /// navigation bar 134 | navigationBarView.snp.makeConstraints { 135 | $0.top.horizontalEdges.equalToSuperview() 136 | $0.bottom.equalTo(navigationBarItemView.snp.bottom).offset(16) 137 | } 138 | navigationBarItemView.snp.makeConstraints { 139 | $0.height.equalTo(24) 140 | $0.top.equalTo(view.safeAreaLayoutGuide).inset(16) 141 | $0.horizontalEdges.equalToSuperview().inset(16) 142 | } 143 | navigationBarSeparateLine.snp.makeConstraints { 144 | $0.height.equalTo(1) 145 | $0.bottom.horizontalEdges.equalToSuperview() 146 | } 147 | backButton.snp.makeConstraints { 148 | $0.height.leading.equalToSuperview() 149 | } 150 | navigationTitle.snp.makeConstraints { 151 | $0.height.equalToSuperview() 152 | $0.leading.equalTo(backButton.snp.trailing).offset(10) 153 | } 154 | /// scroll view 155 | scrollView.snp.makeConstraints { 156 | $0.top.equalTo(navigationBarView.snp.bottom).offset(20) 157 | $0.horizontalEdges.bottom.equalTo(view.safeAreaLayoutGuide) 158 | } 159 | scrollStackView.snp.makeConstraints { 160 | $0.verticalEdges.equalToSuperview() 161 | $0.horizontalEdges.equalToSuperview().inset(16) 162 | } 163 | fadingBottomView.snp.makeConstraints { 164 | $0.horizontalEdges.equalToSuperview() 165 | $0.bottom.equalTo(container.snp.bottom) 166 | $0.height.equalTo(bound.height / 12) 167 | } 168 | /// ranking 169 | breakfastButton.snp.makeConstraints { 170 | $0.top.equalTo(navigationBarView.snp.bottom).offset(20) 171 | $0.leading.equalTo(scrollStackView.snp.leading) 172 | $0.width.equalTo(bound.width / 3 - 20) 173 | $0.height.equalTo(26) 174 | } 175 | lunchButton.snp.makeConstraints { 176 | $0.top.equalTo(navigationBarView.snp.bottom).offset(20) 177 | $0.width.equalTo(bound.width / 3 - 20) 178 | $0.height.equalTo(26) 179 | $0.centerX.equalToSuperview() 180 | } 181 | dinnerButton.snp.makeConstraints { 182 | $0.top.equalTo(navigationBarView.snp.bottom).offset(20) 183 | $0.trailing.equalTo(scrollStackView.snp.trailing) 184 | $0.width.equalTo(bound.width / 3 - 20) 185 | $0.height.equalTo(26) 186 | } 187 | rankingTableView.snp.makeConstraints { 188 | $0.top.equalTo(scrollView.snp.top).offset(55) 189 | $0.width.equalTo(scrollView.snp.width).inset(16) 190 | $0.height.equalTo(1440) 191 | $0.centerX.equalToSuperview() 192 | } 193 | } 194 | 195 | private func setUI(for type: MealType) { 196 | let buttonColorMapping: [(button: UIButton, isSelected: Bool)] = [ 197 | (breakfastButton, type == .TYPE_BREAKFAST), 198 | (lunchButton, type == .TYPE_LUNCH), 199 | (dinnerButton, type == .TYPE_DINNER) 200 | ] 201 | 202 | buttonColorMapping.forEach { mapping in 203 | let button = mapping.button 204 | let color = mapping.isSelected ? Color.getMealColor(for: type) : Color.white 205 | let titleColor = mapping.isSelected ? Color.white : Color.darkGray 206 | 207 | button.backgroundColor = color 208 | button.setTitleColor(titleColor, for: .normal) 209 | } 210 | } 211 | 212 | // MARK: - Reactor 213 | override func bindView(reactor: RankingReactor) { 214 | backButton.rx.tap 215 | .subscribe(onNext: { [weak self] in 216 | self?.navigationController?.popViewController(animated: true) 217 | }) 218 | .disposed(by: disposeBag) 219 | 220 | breakfastButton.rx.tap 221 | .map { Reactor.Action.setMealType(.TYPE_BREAKFAST) } 222 | .bind(to: reactor.action) 223 | .disposed(by: disposeBag) 224 | 225 | lunchButton.rx.tap 226 | .map { Reactor.Action.setMealType(.TYPE_LUNCH) } 227 | .bind(to: reactor.action) 228 | .disposed(by: disposeBag) 229 | 230 | dinnerButton.rx.tap 231 | .map { Reactor.Action.setMealType(.TYPE_DINNER) } 232 | .bind(to: reactor.action) 233 | .disposed(by: disposeBag) 234 | } 235 | 236 | override func bindAction(reactor: RankingReactor) { 237 | reactor.action.onNext(.fetchRanking) 238 | } 239 | 240 | override func bindState(reactor: RankingReactor) { 241 | reactor.state.map { $0.mealType } 242 | .distinctUntilChanged() 243 | .subscribe(onNext: { [weak self] type in 244 | self?.rankingTableView.reloadData() 245 | self?.setUI(for: type) 246 | }) 247 | .disposed(by: disposeBag) 248 | 249 | reactor.state.map { $0.mealType } 250 | .compactMap { mealType -> [Ranking]? in 251 | switch mealType { 252 | case .TYPE_BREAKFAST: 253 | return reactor.currentState.breakfastRanking?.ranking 254 | case .TYPE_LUNCH: 255 | return reactor.currentState.lunchRanking?.ranking 256 | case .TYPE_DINNER: 257 | return reactor.currentState.dinnerRanking?.ranking 258 | } 259 | } 260 | .bind(to: rankingTableView.rx.items( 261 | cellIdentifier: RankingCell.reuseIdentifier, 262 | cellType: RankingCell.self) 263 | ) { _, ranking, cell in 264 | cell.configuration(ranking, reactor.currentState.mealType) 265 | } 266 | .disposed(by: disposeBag) 267 | } 268 | 269 | } 270 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Presentation/Scenes/Review/ReviewReactor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReviewReactor.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/2/24. 6 | // 7 | 8 | import Foundation 9 | import ReactorKit 10 | 11 | final class ReviewReactor: Reactor { 12 | 13 | // MARK: - Properties 14 | var initialState: State 15 | 16 | // MARK: - Action 17 | enum Action { 18 | case completeReview 19 | case setReviewText(String) 20 | case setReviewScore(Double) 21 | } 22 | 23 | // MARK: - Mutation 24 | enum Mutation { 25 | case setReviewText(String) 26 | case setReviewScore(Double) 27 | } 28 | 29 | // MARK: - State 30 | struct State { 31 | var menuId: Int 32 | var reviewText: String = "" 33 | var score: Double = 0.0 34 | } 35 | 36 | init(menuId: Int) { 37 | self.initialState = State(menuId: menuId) 38 | } 39 | } 40 | 41 | // MARK: - Mutate 42 | extension ReviewReactor { 43 | 44 | private func postReview() -> Observable { 45 | return RatingProvider.shared 46 | .postRating( 47 | currentState.menuId, 48 | RatingRequest( 49 | score: currentState.score, 50 | comment: currentState.reviewText 51 | ) 52 | ) 53 | .flatMap { result -> Observable in 54 | switch result { 55 | case .success(_): 56 | return Observable.empty() 57 | case .failure(_): 58 | return Observable.empty() 59 | } 60 | } 61 | } 62 | 63 | func mutate(action: Action) -> Observable { 64 | switch action { 65 | 66 | case let .setReviewText(text): 67 | return .just(.setReviewText(text)) 68 | 69 | case let .setReviewScore(rating): 70 | return .just(.setReviewScore(rating)) 71 | 72 | case .completeReview: 73 | return postReview() 74 | } 75 | } 76 | 77 | // MARK: - Reduce 78 | func reduce(state: State, mutation: Mutation) -> State { 79 | var newState = state 80 | switch mutation { 81 | 82 | case .setReviewText(let reviewText): 83 | newState.reviewText = reviewText 84 | 85 | case .setReviewScore(let score): 86 | newState.score = score 87 | } 88 | return newState 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Presentation/Scenes/Review/ReviewViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReviewViewController.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 4/30/24. 6 | // 7 | 8 | import UIKit 9 | import Cosmos 10 | 11 | final class ReviewViewController: BaseVC { 12 | 13 | // MARK: - Properties 14 | private lazy var container = UIView() 15 | 16 | /// navigation bar 17 | private lazy var navigationBarView = UIView().then { 18 | $0.backgroundColor = Color.white 19 | } 20 | 21 | private lazy var navigationBarSeparateLine = UIView().then { 22 | $0.backgroundColor = Color.lightGray 23 | } 24 | 25 | private lazy var navigationBarItemView = UIView() 26 | 27 | private lazy var backButton = UIButton().then { 28 | $0.setImage(UIImage(icon: .leadingArrow), for: .normal) 29 | $0.imageView!.contentMode = .scaleAspectFit 30 | $0.tintColor = Color.black 31 | } 32 | 33 | private lazy var navigationTitle = UILabel().then { 34 | $0.text = "급식 리뷰" 35 | $0.font = .systemFont(ofSize: 18, weight: .medium) 36 | $0.textColor = Color.black 37 | } 38 | 39 | /// review text view 40 | private lazy var reviewContainer = UIView().then { 41 | $0.backgroundColor = Color.white 42 | $0.layer.cornerRadius = 12 43 | $0.layer.shadowColor = Color.black.cgColor 44 | $0.layer.shadowOpacity = 0.02 45 | $0.layer.shadowOffset = CGSize(width: 0, height: 4) 46 | $0.layer.shadowRadius = 10 47 | } 48 | 49 | private lazy var reviewTextView = UITextView().then { 50 | $0.font = .systemFont(ofSize: 20, weight: .regular) 51 | $0.textColor = Color.darkGray 52 | $0.delegate = self 53 | $0.isScrollEnabled = false 54 | textViewDidChange($0) 55 | } 56 | 57 | private lazy var reviewPlaceHolder = UILabel().then { 58 | $0.text = "리뷰를 작성해주세요." 59 | $0.font = .systemFont(ofSize: 20, weight: .regular) 60 | $0.textColor = Color.lightGray 61 | } 62 | 63 | private lazy var reviewTextCountingLabel = UILabel().then { 64 | $0.text = "0 / 50" 65 | $0.font = .systemFont(ofSize: 16, weight: .semibold) 66 | $0.textColor = Color.darkGray 67 | } 68 | 69 | private lazy var reviewCompleteButton = ScaledButton(scale: 0.95).then { 70 | $0.setTitle("완료", for: .normal) 71 | $0.titleLabel?.font = .systemFont(ofSize: 16, weight: .semibold) 72 | $0.setTitleColor(Color.darkGray, for: .normal) 73 | } 74 | 75 | private lazy var starView = CosmosView().then { 76 | $0.rating = 0.0 77 | $0.settings.fillMode = .half 78 | $0.settings.starSize = 30 79 | $0.settings.starMargin = 4 80 | $0.settings.minTouchRating = 0.0 81 | $0.settings.filledImage = UIImage(icon: .filledStar) 82 | $0.settings.emptyImage = UIImage(icon: .emptyStar) 83 | } 84 | 85 | // MARK: - LifeCycle 86 | override func viewWillAppear(_ animated: Bool) { 87 | super.viewWillAppear(true) 88 | 89 | print("\(type(of: self)): \(#function)") 90 | } 91 | 92 | // MARK: - UI 93 | override func addView() { 94 | view.addSubview(container) 95 | /// navigation bar 96 | container.addSubviews( 97 | starView, reviewContainer, navigationBarView 98 | ) 99 | navigationBarView.addSubviews( 100 | navigationBarItemView, navigationBarSeparateLine 101 | ) 102 | navigationBarItemView.addSubviews( 103 | backButton, navigationTitle 104 | ) 105 | reviewContainer.addSubviews( 106 | reviewTextView, reviewTextCountingLabel, reviewCompleteButton 107 | ) 108 | reviewTextView.addSubview(reviewPlaceHolder) 109 | } 110 | 111 | override func setLayout() { 112 | container.snp.makeConstraints { 113 | $0.edges.equalToSuperview() 114 | } 115 | /// navigation bar 116 | navigationBarView.snp.makeConstraints { 117 | $0.top.horizontalEdges.equalToSuperview() 118 | $0.bottom.equalTo(navigationBarItemView.snp.bottom).offset(16) 119 | } 120 | navigationBarItemView.snp.makeConstraints { 121 | $0.height.equalTo(24) 122 | $0.top.equalTo(view.safeAreaLayoutGuide).inset(16) 123 | $0.horizontalEdges.equalToSuperview().inset(16) 124 | } 125 | navigationBarSeparateLine.snp.makeConstraints { 126 | $0.height.equalTo(1) 127 | $0.bottom.horizontalEdges.equalToSuperview() 128 | } 129 | backButton.snp.makeConstraints { 130 | $0.height.leading.equalToSuperview() 131 | } 132 | navigationTitle.snp.makeConstraints { 133 | $0.height.equalToSuperview() 134 | $0.leading.equalTo(backButton.snp.trailing).offset(10) 135 | } 136 | /// reivew text view 137 | reviewContainer.snp.makeConstraints { 138 | $0.top.equalTo(navigationBarView.snp.bottom).offset(20) 139 | $0.bottom.equalTo(reviewTextView.snp.bottom).offset(54) 140 | $0.width.equalToSuperview().inset(16) 141 | $0.centerX.equalToSuperview() 142 | } 143 | reviewTextView.snp.makeConstraints { 144 | $0.top.equalToSuperview().offset(14) 145 | $0.height.equalTo(40) 146 | $0.width.equalToSuperview().inset(16) 147 | $0.centerX.equalToSuperview() 148 | } 149 | reviewPlaceHolder.snp.makeConstraints { 150 | $0.leading.equalTo(reviewTextView).offset(4) 151 | $0.centerY.equalToSuperview() 152 | } 153 | reviewTextCountingLabel.snp.makeConstraints { 154 | $0.height.equalTo(18) 155 | $0.bottom.equalToSuperview().inset(20) 156 | $0.leading.equalToSuperview().inset(22) 157 | } 158 | reviewCompleteButton.snp.makeConstraints { 159 | $0.height.equalTo(18) 160 | $0.bottom.equalToSuperview().inset(20) 161 | $0.trailing.equalToSuperview().inset(22) 162 | } 163 | starView.snp.makeConstraints { 164 | $0.top.equalTo(reviewContainer.snp.bottom).offset(20) 165 | $0.leading.equalToSuperview().offset(26) 166 | } 167 | } 168 | 169 | // MARK: - Reactor 170 | override func bindView(reactor: ReviewReactor) { 171 | backButton.rx.tap 172 | .subscribe(onNext: { [weak self] in 173 | self?.navigationController?.popViewController(animated: true) 174 | }) 175 | .disposed(by: disposeBag) 176 | 177 | reviewCompleteButton.rx.tap 178 | .subscribe(onNext: { [weak self] in 179 | let action = ReviewReactor.Action.completeReview 180 | reactor.action.onNext(action) 181 | self?.navigationController?.popViewController(animated: true) 182 | }) 183 | .disposed(by: disposeBag) 184 | 185 | reviewTextView.rx.text.orEmpty 186 | .map(ReviewReactor.Action.setReviewText) 187 | .bind(to: reactor.action) 188 | .disposed(by: disposeBag) 189 | 190 | starView.rx.didFinishTouchingCosmos 191 | .onNext { score in 192 | let action = ReviewReactor.Action.setReviewScore(score) 193 | reactor.action.onNext(action) 194 | } 195 | } 196 | 197 | override func bindAction(reactor: ReviewReactor) { 198 | } 199 | 200 | override func bindState(reactor: ReviewReactor) { 201 | reactor.state.map { $0.reviewText } 202 | .bind(to: reviewTextView.rx.text) 203 | .disposed(by: disposeBag) 204 | 205 | reactor.state.map { $0.score } 206 | .bind(to: starView.rx.rating) 207 | .disposed(by: disposeBag) 208 | } 209 | } 210 | 211 | extension ReviewViewController { 212 | 213 | /// keypad down 214 | override func touchesBegan(_ touches: Set, with event: UIEvent?) { 215 | super.touchesBegan(touches, with: event) 216 | 217 | self.view.endEditing(true) 218 | } 219 | } 220 | 221 | extension ReviewViewController: UITextViewDelegate { 222 | 223 | func textViewDidChange(_ textView: UITextView) { 224 | /// reactive text view 225 | let size = CGSize(width: self.reviewContainer.frame.width - 32, height: .infinity) 226 | textView.constraints.forEach { 227 | if $0.firstAttribute == .height { 228 | $0.constant = textView.sizeThatFits(size).height 229 | } 230 | } 231 | /// limit the number of review text count 232 | let textCount = textView.text.count < 50 ? textView.text.count : 50 233 | let maxCount = 50 234 | textView.text = String(textView.text.prefix(maxCount)) 235 | 236 | reviewTextCountingLabel.text = "\(textCount) / \(maxCount)" 237 | reviewTextCountingLabel.textColor = textCount >= maxCount ? Color.error : Color.darkGray 238 | } 239 | 240 | func textViewDidBeginEditing(_ textView: UITextView) { 241 | reviewPlaceHolder.isHidden = true 242 | } 243 | 244 | func textViewDidEndEditing(_ textView: UITextView) { 245 | reviewPlaceHolder.isHidden = !textView.text.isEmpty 246 | } 247 | 248 | func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { 249 | let currentText = textView.text ?? "" 250 | guard let stringRange = Range(range, in: currentText) else { return false } 251 | let updatedText = currentText.replacingCharacters(in: stringRange, with: text) 252 | return updatedText.count <= 100 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Presentation/Scenes/Setting/SettingReactor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingReactor.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/2/24. 6 | // 7 | 8 | import Foundation 9 | import ReactorKit 10 | 11 | final class SettingReactor: Reactor { 12 | 13 | // MARK: - Properties 14 | var initialState: State = State() 15 | 16 | // MARK: - Action 17 | enum Action { 18 | 19 | } 20 | 21 | // MARK: - Mutation 22 | enum Mutation { 23 | 24 | } 25 | 26 | // MARK: - State 27 | struct State { 28 | 29 | } 30 | } 31 | 32 | // MARK: - Mutate 33 | extension SettingReactor { 34 | 35 | func mutate(action: Action) -> Observable { 36 | 37 | } 38 | 39 | // MARK: - Reduce 40 | func reduce(state: State, mutation: Mutation) -> State { 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Presentation/Scenes/Setting/SettingViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingViewController.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 4/30/24. 6 | // 7 | 8 | import UIKit 9 | import RxCocoa 10 | import SnapKit 11 | import Then 12 | import StoreKit 13 | 14 | final class SettingViewController: BaseVC { 15 | 16 | // MARK: - Properties 17 | private lazy var container = UIView() 18 | 19 | /// navigation bar 20 | private lazy var navigationBarView = UIView().then { 21 | $0.backgroundColor = Color.white 22 | } 23 | 24 | private lazy var navigationBarSeparateLine = UIView().then { 25 | $0.backgroundColor = Color.lightGray 26 | } 27 | 28 | private lazy var navigationBarItemView = UIView() 29 | 30 | private lazy var backButton = UIButton().then { 31 | $0.setImage(UIImage(icon: .leadingArrow), for: .normal) 32 | $0.imageView!.contentMode = .scaleAspectFit 33 | $0.tintColor = Color.black 34 | } 35 | 36 | private lazy var navigationTitle = UILabel().then { 37 | $0.text = "설정" 38 | $0.font = .systemFont(ofSize: 18, weight: .medium) 39 | $0.textColor = Color.black 40 | } 41 | 42 | /// setting 43 | private lazy var settingButtonStackView = UIStackView().then { 44 | $0.axis = .vertical 45 | $0.spacing = 20 46 | $0.distribution = .fill 47 | } 48 | 49 | private lazy var privacyPolicyButton = ScaledButton( 50 | scale: 0.98, backgroundColor: Color.white 51 | ).then { 52 | $0.layer.cornerRadius = 8 53 | } 54 | 55 | private lazy var privacyPolicyContainerLeadingItem = UILabel().then { 56 | $0.text = "개인정보 처리방침" 57 | $0.textColor = Color.black 58 | $0.font = .systemFont(ofSize: 16, weight: .regular) 59 | } 60 | 61 | private lazy var privacyPolicyContainerTrailingItem = UIImageView().then { 62 | $0.image = UIImage(icon: .trailingArrow) 63 | $0.contentMode = .scaleAspectFit 64 | $0.tintColor = Color.black 65 | } 66 | 67 | private lazy var deleteReviewButton = ScaledButton( 68 | scale: 0.98, backgroundColor: Color.white 69 | ).then { 70 | $0.layer.cornerRadius = 12 71 | } 72 | 73 | private lazy var deleteReviewLeadingItem = UILabel().then { 74 | $0.text = "작성한 리뷰 삭제 요청" 75 | $0.textColor = Color.black 76 | $0.font = .systemFont(ofSize: 16, weight: .regular) 77 | } 78 | 79 | private lazy var deleteReviewTrailingItem = UIImageView().then { 80 | $0.image = UIImage(icon: .trailingArrow) 81 | $0.contentMode = .scaleAspectFit 82 | $0.tintColor = Color.black 83 | } 84 | 85 | private lazy var appVersionButton = ScaledButton( 86 | scale: 0.98, backgroundColor: Color.white 87 | ).then { 88 | $0.layer.cornerRadius = 12 89 | } 90 | 91 | private lazy var appVersionLeadingItem = UILabel().then { 92 | $0.text = "앱 버전" 93 | $0.textColor = Color.black 94 | $0.font = .systemFont(ofSize: 16, weight: .regular) 95 | } 96 | 97 | private lazy var appVersionTrailingItem = UILabel().then { 98 | $0.text = appVersion 99 | $0.textColor = Color.error 100 | $0.font = .systemFont(ofSize: 15, weight: .regular) 101 | } 102 | 103 | // MARK: - LifeCycle 104 | override func viewWillAppear(_ animated: Bool) { 105 | super.viewWillAppear(true) 106 | 107 | } 108 | 109 | // MARK: - UI 110 | override func addView() { 111 | view.addSubview(container) 112 | /// navigation bar 113 | container.addSubviews( 114 | settingButtonStackView, navigationBarView 115 | ) 116 | navigationBarView.addSubviews( 117 | navigationBarItemView, navigationBarSeparateLine 118 | ) 119 | navigationBarItemView.addSubviews( 120 | backButton, navigationTitle 121 | ) 122 | /// setting 123 | settingButtonStackView.addArrangedSubviews( 124 | privacyPolicyButton, deleteReviewButton, appVersionButton 125 | ) 126 | privacyPolicyButton.addSubviews( 127 | privacyPolicyContainerLeadingItem, privacyPolicyContainerTrailingItem 128 | ) 129 | deleteReviewButton.addSubviews( 130 | deleteReviewLeadingItem, deleteReviewTrailingItem 131 | ) 132 | appVersionButton.addSubviews( 133 | appVersionLeadingItem, appVersionTrailingItem 134 | ) 135 | } 136 | 137 | override func setLayout() { 138 | container.snp.makeConstraints { 139 | $0.edges.equalToSuperview() 140 | } 141 | /// navigation bar 142 | navigationBarView.snp.makeConstraints { 143 | $0.top.horizontalEdges.equalToSuperview() 144 | $0.bottom.equalTo(navigationBarItemView.snp.bottom).offset(16) 145 | } 146 | navigationBarItemView.snp.makeConstraints { 147 | $0.height.equalTo(24) 148 | $0.top.equalTo(view.safeAreaLayoutGuide).inset(16) 149 | $0.horizontalEdges.equalToSuperview().inset(16) 150 | } 151 | navigationBarSeparateLine.snp.makeConstraints { 152 | $0.height.equalTo(1) 153 | $0.bottom.horizontalEdges.equalToSuperview() 154 | } 155 | backButton.snp.makeConstraints { 156 | $0.height.leading.equalToSuperview() 157 | } 158 | navigationTitle.snp.makeConstraints { 159 | $0.height.equalToSuperview() 160 | $0.leading.equalTo(backButton.snp.trailing).offset(10) 161 | } 162 | /// setting 163 | settingButtonStackView.snp.makeConstraints { 164 | $0.top.equalTo(navigationBarView.snp.bottom).offset(20) 165 | $0.horizontalEdges.equalToSuperview().inset(16) 166 | } 167 | privacyPolicyButton.snp.makeConstraints { 168 | $0.top.equalTo(settingButtonStackView.snp.top) 169 | $0.height.equalTo(50) 170 | $0.width.equalToSuperview() 171 | } 172 | privacyPolicyContainerLeadingItem.snp.makeConstraints { 173 | $0.leading.equalToSuperview().offset(16) 174 | $0.centerY.equalToSuperview() 175 | } 176 | privacyPolicyContainerTrailingItem.snp.makeConstraints { 177 | $0.width.height.equalTo(20) 178 | $0.trailing.equalToSuperview().inset(16) 179 | $0.centerY.equalToSuperview() 180 | } 181 | deleteReviewButton.snp.makeConstraints { 182 | $0.height.equalTo(50) 183 | $0.width.equalToSuperview() 184 | } 185 | deleteReviewLeadingItem.snp.makeConstraints { 186 | $0.leading.equalToSuperview().offset(16) 187 | $0.centerY.equalToSuperview() 188 | } 189 | deleteReviewTrailingItem.snp.makeConstraints { 190 | $0.trailing.equalToSuperview().inset(16) 191 | $0.centerY.equalToSuperview() 192 | } 193 | appVersionButton.snp.makeConstraints { 194 | $0.height.equalTo(50) 195 | $0.width.equalToSuperview() 196 | } 197 | appVersionLeadingItem.snp.makeConstraints { 198 | $0.leading.equalToSuperview().offset(16) 199 | $0.centerY.equalToSuperview() 200 | } 201 | appVersionTrailingItem.snp.makeConstraints { 202 | $0.trailing.equalToSuperview().inset(16) 203 | $0.centerY.equalToSuperview() 204 | } 205 | } 206 | 207 | // MARK: - Reactor 208 | override func bindView(reactor: SettingReactor) { 209 | backButton.rx.tap 210 | .subscribe(onNext: { [weak self] in 211 | self?.navigationController?.popViewController(animated: true) 212 | }) 213 | .disposed(by: disposeBag) 214 | 215 | privacyPolicyButton.rx.tap 216 | .subscribe(onNext: { _ in 217 | if let url = URL(string: "https://min-gyu.notion.site/43f3fa6077c346c692359f790d79cd7a?pvs=74") { 218 | UIApplication.shared.open(url, options: [:], completionHandler: nil) 219 | } 220 | }) 221 | .disposed(by: disposeBag) 222 | 223 | deleteReviewButton.rx.tap 224 | .subscribe(onNext: { _ in 225 | let subjectEncoded = "[대슐랭 가이드] 리뷰 삭제 요청".addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)! 226 | let bodyEncoded = "예시 20240101 / 점심 / 내용 -> 삭제 부탁드립니다".addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)! 227 | let mailtoString = "mailto:dev.gyuuu@gmail.com?subject=\(subjectEncoded)&body=\(bodyEncoded)" 228 | if let mailtoURL = URL(string: mailtoString), UIApplication.shared.canOpenURL(mailtoURL) { 229 | UIApplication.shared.open(mailtoURL, options: [:], completionHandler: nil) 230 | } 231 | }) 232 | .disposed(by: disposeBag) 233 | 234 | appVersionButton.rx.tap 235 | .subscribe(onNext: { _ in 236 | SKStoreReviewController.requestReview() 237 | }) 238 | .disposed(by: disposeBag) 239 | } 240 | 241 | override func bindAction(reactor: SettingReactor) { 242 | } 243 | 244 | override func bindState(reactor: SettingReactor) { 245 | 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Shared/Assets/Color/Color.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Color.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/1/24. 6 | // 7 | 8 | import UIKit 9 | 10 | public class Color { 11 | 12 | // MARK: - Component Color 13 | public static let background: UIColor = .init(hex: "#F9F9F9") 14 | public static let breakfast: UIColor = .init(hex: "#FFAF51") 15 | public static let lunch: UIColor = .init(hex: "#ABC97B") 16 | public static let dinner: UIColor = .init(hex: "#A18CF6") 17 | 18 | // MARK: - UI Color 19 | public static let darkGray: UIColor = .init(hex: "#4E4D4D") 20 | public static let lightGray: UIColor = .init(hex: "#D7D7D7") 21 | public static let gray: UIColor = .init(hex: "#A0A0A0") 22 | public static let black: UIColor = .init(hex: "#292D32") 23 | public static let white: UIColor = .init(hex: "#FFFFFF") 24 | public static let error: UIColor = .init(hex: "#C52222") 25 | 26 | static func getMealColor(for type: MealType) -> UIColor { 27 | switch type { 28 | case .TYPE_BREAKFAST: 29 | return Color.breakfast 30 | case .TYPE_LUNCH: 31 | return Color.lunch 32 | case .TYPE_DINNER: 33 | return Color.dinner 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Shared/Assets/Icon/Icon.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Icon.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/2/24. 6 | // 7 | 8 | import UIKit 9 | 10 | public enum Icon: String { 11 | 12 | // MARK: - logo 13 | case logo = "logo" 14 | 15 | // MARK: - Icon 16 | case ranking = "ranking" 17 | case setting = "setting" 18 | case review = "review" 19 | case crown = "crown" 20 | 21 | /// start 22 | case emptyStar = "star_empty" 23 | case filledStar = "star_filled" 24 | 25 | /// arrow 26 | case bottomArrow = "arrow_down" 27 | case leadingArrow = "arrow_left" 28 | case trailingArrow = "arrow_right" 29 | 30 | /// anonymous profile 31 | case cat = "cat" 32 | case rabbit = "rabbit" 33 | case snake = "snake" 34 | case elephant = "elephant" 35 | case tiger = "tiger" 36 | 37 | // MARK: - Food 38 | case taco = "taco" 39 | case burger = "burger" 40 | case ramen = "ramen" 41 | } 42 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Shared/Assets/Icon/UIImage+Ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+Ext.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/2/24. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIImage { 11 | 12 | convenience init?(icon: Icon) { 13 | self 14 | .init(named: icon.rawValue) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Shared/Component/FadingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FadingView.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/9/24. 6 | // 7 | 8 | import UIKit 9 | 10 | class FadingView: BaseView { 11 | 12 | private var position: FadingPosition? 13 | 14 | enum FadingPosition { 15 | case top 16 | case bottom 17 | } 18 | 19 | init( 20 | position: FadingPosition 21 | ) { 22 | super.init() 23 | 24 | self.position = position 25 | setupFadeLayer() 26 | } 27 | 28 | required init?(coder: NSCoder) { 29 | fatalError("init(coder:) has not been implemented") 30 | } 31 | 32 | private func setupFadeLayer() { 33 | let fadeLayer = CAGradientLayer() 34 | fadeLayer.frame = CGRect(x: 0, y: 0, width: bound.width, height: bound.height / 12) 35 | fadeLayer.colors = [Color.background.withAlphaComponent(0).cgColor, 36 | Color.background.withAlphaComponent(0.75).cgColor, 37 | Color.background.withAlphaComponent(1).cgColor] 38 | fadeLayer.startPoint = CGPoint(x: 0.5, y: position == .top ? 1 : 0) 39 | fadeLayer.endPoint = CGPoint(x: 0.5, y: position == .top ? 0 : 1) 40 | layer.addSublayer(fadeLayer) 41 | isUserInteractionEnabled = false 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Shared/Component/ScaledButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScaledButton.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/4/24. 6 | // 7 | 8 | import UIKit 9 | 10 | class ScaledButton: UIButton { 11 | 12 | private var defaultBackgroundColor: UIColor? 13 | private var scale: CGFloat = 0 14 | 15 | override var isHighlighted: Bool { 16 | didSet { 17 | UIView.animate(withDuration: 0.15) { 18 | self.transform = self.isHighlighted 19 | ? CGAffineTransform(scaleX: self.scale, y: self.scale) 20 | : .identity 21 | if self.defaultBackgroundColor != nil { 22 | self.backgroundColor = self.isHighlighted 23 | ? self.defaultBackgroundColor?.darken(by: 0.1) 24 | : self.defaultBackgroundColor 25 | } 26 | } 27 | } 28 | } 29 | 30 | init( 31 | scale: CGFloat, 32 | backgroundColor: UIColor? = nil 33 | ) { 34 | super.init(frame: .zero) 35 | 36 | self.defaultBackgroundColor = backgroundColor 37 | self.scale = scale 38 | self.backgroundColor = backgroundColor 39 | } 40 | 41 | required init?(coder: NSCoder) { 42 | fatalError("init(coder:) has not been implemented") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Shared/Enum/MealType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MealType.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/1/24. 6 | // 7 | 8 | import Foundation 9 | 10 | enum MealType: String, Codable { 11 | case TYPE_BREAKFAST 12 | case TYPE_LUNCH 13 | case TYPE_DINNER 14 | } 15 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Shared/Extension/Foundation/Date+Ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Date+Ext.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/2/24. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Date { 11 | 12 | func formattingDate(format: String) -> String { 13 | let dateFormatter = DateFormatter() 14 | dateFormatter.dateFormat = format 15 | dateFormatter.locale = Locale(identifier: "ko_KR") 16 | return dateFormatter.string(from: self) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Shared/Extension/Foundation/String+Ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Ext.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/8/24. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | 12 | func stringToDate(format: String) -> Date { 13 | let dateFormatter = DateFormatter() 14 | dateFormatter.dateFormat = format 15 | dateFormatter.locale = Locale(identifier: "ko_KR") 16 | return dateFormatter.date(from: self) ?? Date() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Shared/Extension/UIKit/UIColor+Ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Ext.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/1/24. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIColor { 11 | 12 | convenience init(hex: String) { 13 | let hex = hex.trimmingCharacters( 14 | in: CharacterSet.alphanumerics.inverted 15 | ) 16 | var int: UInt64 = 0 17 | Scanner(string: hex).scanHexInt64(&int) 18 | self 19 | .init( 20 | red: CGFloat(int >> 16) / 255, 21 | green: CGFloat(int >> 8 & 0xFF) / 255, 22 | blue: CGFloat(int & 0xFF) / 255, 23 | alpha: 1 24 | ) 25 | } 26 | 27 | func darken(by percentage: CGFloat) -> UIColor { 28 | var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0, alpha: CGFloat = 0 29 | 30 | getRed(&red, green: &green, blue: &blue, alpha: &alpha) 31 | 32 | red = max(0.0, min(1.0, red - percentage)) 33 | green = max(0.0, min(1.0, green - percentage)) 34 | blue = max(0.0, min(1.0, blue - percentage)) 35 | 36 | return UIColor(red: red, green: green, blue: blue, alpha: alpha) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Shared/Extension/UIKit/UILabel+Ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UILabel+Ext.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/3/24. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UILabel { 11 | 12 | func setLineSpacing(lineSpacing: CGFloat = 0.0, alignment: NSTextAlignment = .center) { 13 | guard let labelText = self.text else { return } 14 | 15 | let paragraphStyle = NSMutableParagraphStyle() 16 | paragraphStyle.lineSpacing = lineSpacing 17 | paragraphStyle.lineHeightMultiple = 0 18 | paragraphStyle.alignment = alignment 19 | 20 | let attributedString = NSMutableAttributedString(string: labelText) 21 | attributedString.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: attributedString.length)) 22 | 23 | self.attributedText = attributedString 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Shared/Extension/UIKit/UINavigationController+Ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UINavigationController.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/2/24. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UINavigationController: ObservableObject, UIGestureRecognizerDelegate { 11 | 12 | override open func viewDidLoad() { 13 | super.viewDidLoad() 14 | 15 | interactivePopGestureRecognizer?.delegate = self 16 | } 17 | 18 | public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { 19 | return viewControllers.count > 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Shared/Extension/UIKit/UIStackView+Ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIStackView+Ext.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/2/24. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIStackView { 11 | 12 | func addArrangedSubviews(_ views: UIView...) { 13 | views.forEach(addArrangedSubview(_:)) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /DaechelinGuide/DaechelinGuide/Sources/Shared/Extension/UIKit/UIView+Ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Ext.swift 3 | // DaechelinGuide 4 | // 5 | // Created by 이민규 on 5/2/24. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIView { 11 | 12 | func addSubviews(_ subView: UIView...) { 13 | subView.forEach(addSubview(_:)) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 대슐랭 가이드 - 대소고 급식 앱 2 | 3 | - 대구소프트웨어고등학교의 급식을 리뷰하고 랭킹을 매기는 서비스 입니다.
4 | - 다운로드 : [App Store 링크](https://apps.apple.com/us/app/%EB%8C%80%EC%8A%90%EB%9E%AD-%EA%B0%80%EC%9D%B4%EB%93%9C-%EB%8C%80%EC%86%8C%EA%B3%A0-%EA%B8%89%EC%8B%9D-%EC%95%B1/id1671086233) 5 | 6 | ## 미리보기 7 |

8 | 9 | 10 | 11 | 12 |

13 | --------------------------------------------------------------------------------