diff --git a/MMM.xcodeproj/project.pbxproj b/MMM.xcodeproj/project.pbxproj index 697878f5..4535a6b7 100644 --- a/MMM.xcodeproj/project.pbxproj +++ b/MMM.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 450E3B292BEC583C00F6AA75 /* Modifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 450E3B282BEC583C00F6AA75 /* Modifiers.swift */; }; 45AFC2A92AC296E1004DF76E /* CustomPushTextSettingReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45AFC2A82AC296E1004DF76E /* CustomPushTextSettingReactor.swift */; }; 45AFC2AB2AC2A564004DF76E /* CustomPushTextSettingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45AFC2AA2AC2A564004DF76E /* CustomPushTextSettingViewController.swift */; }; 45B878C52B341A3400EF27FB /* BottomPageControlView2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45B878C42B341A3400EF27FB /* BottomPageControlView2.swift */; }; @@ -66,7 +67,8 @@ 45E909202BBFB27D00E3DF20 /* CommonText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45E9091F2BBFB27D00E3DF20 /* CommonText.swift */; }; 45E909222BBFB2AC00E3DF20 /* AlertText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45E909212BBFB2AC00E3DF20 /* AlertText.swift */; }; 45E909242BBFC62400E3DF20 /* CustomCalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45E909232BBFC62400E3DF20 /* CustomCalendarView.swift */; }; - 45FD3C852B887E1500C14A49 /* BudgetSettingSubTitleModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FD3C842B887E1500C14A49 /* BudgetSettingSubTitleModifier.swift */; }; + 45EA263F2BCF483D001BAC57 /* TooltipView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45EA263E2BCF483D001BAC57 /* TooltipView.swift */; }; + 45FBBB392BEDFB1500F9FAC6 /* UpsertEconomicPlanResDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBBB382BEDFB1500F9FAC6 /* UpsertEconomicPlanResDto.swift */; }; 6A0379752A2443B9005EE07F /* Ex+UITextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A0379742A2443B9005EE07F /* Ex+UITextView.swift */; }; 6A03BB4C2ADD555D00F86756 /* AddCategoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A03BB4B2ADD555D00F86756 /* AddCategoryViewController.swift */; }; 6A03FEE32ADBDB7300DA3D15 /* AddCategoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A03FEE22ADBDB7300DA3D15 /* AddCategoryView.swift */; }; @@ -171,7 +173,7 @@ C411940B29DAEDF9003DBC15 /* CustomAlertViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C411940A29DAEDF9003DBC15 /* CustomAlertViewController.swift */; }; C4170B4E2A89FCA800CE0A65 /* StatisticsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4170B4D2A89FCA800CE0A65 /* StatisticsViewController.swift */; }; C4170B502A89FD1100CE0A65 /* StatisticsReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4170B4F2A89FD1100CE0A65 /* StatisticsReactor.swift */; }; - C4170B532A937AAC00CE0A65 /* StatisticsTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4170B522A937AAC00CE0A65 /* StatisticsTitleView.swift */; }; + C4170B532A937AAC00CE0A65 /* StatisticsBudgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4170B522A937AAC00CE0A65 /* StatisticsBudgetView.swift */; }; C4170B552A93982F00CE0A65 /* StatisticsAverageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4170B542A93982F00CE0A65 /* StatisticsAverageView.swift */; }; C4170B572A93A58A00CE0A65 /* StatisticsCategoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4170B562A93A58A00CE0A65 /* StatisticsCategoryView.swift */; }; C4170B592A94853400CE0A65 /* StatisticsActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4170B582A94853400CE0A65 /* StatisticsActivityView.swift */; }; @@ -195,6 +197,8 @@ C4351B4C2A1601D3005F95BE /* HomeFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4351B4B2A1601D3005F95BE /* HomeFilterView.swift */; }; C4351B4E2A166DB0005F95BE /* SemanticContentAttributeButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4351B4D2A166DB0005F95BE /* SemanticContentAttributeButton.swift */; }; C4351B502A1691FB005F95BE /* HighlightViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4351B4F2A1691FB005F95BE /* HighlightViewController.swift */; }; + C43779552BF2367800FD9AE2 /* StatisticsBudgetBottomSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43779542BF2367800FD9AE2 /* StatisticsBudgetBottomSheetViewController.swift */; }; + C43779572BF2371E00FD9AE2 /* StatisticsBudgetBottomSheetReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43779562BF2371E00FD9AE2 /* StatisticsBudgetBottomSheetReactor.swift */; }; C43FF1562B1C5A1E009394D6 /* StatisticsSectionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43FF1552B1C5A1E009394D6 /* StatisticsSectionModel.swift */; }; C446BF1B2A430B1400DFA788 /* SnackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C446BF1A2A430B1400DFA788 /* SnackView.swift */; }; C44807DE29E4FE8000D4B090 /* Ex+Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44807DD29E4FE8000D4B090 /* Ex+Publisher.swift */; }; @@ -221,6 +225,11 @@ C46409D42AB8CAE600A899AD /* WithdrawReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46409D32AB8CAE600A899AD /* WithdrawReactor.swift */; }; C46409D62AB8DB9000A899AD /* BaseTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46409D52AB8DB8F00A899AD /* BaseTableViewCell.swift */; }; C46409DA2AB9625E00A899AD /* BottomSheetViewController2.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46409D92AB9625E00A899AD /* BottomSheetViewController2.swift */; }; + C465FF392BC4407400D2E43D /* OnBoardingPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C465FF382BC4407400D2E43D /* OnBoardingPageViewController.swift */; }; + C465FF3B2BC524B700D2E43D /* HomeReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C465FF3A2BC524B700D2E43D /* HomeReactor.swift */; }; + C465FF3D2BC57AA200D2E43D /* HomeOnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C465FF3C2BC57AA200D2E43D /* HomeOnboardingViewController.swift */; }; + C465FF352BB4364100D2E43D /* Budget.swift in Sources */ = {isa = PBXBuildFile; fileRef = C465FF342BB4364100D2E43D /* Budget.swift */; }; + C465FF372BBC106700D2E43D /* StatisticsSum.swift in Sources */ = {isa = PBXBuildFile; fileRef = C465FF362BBC106700D2E43D /* StatisticsSum.swift */; }; C466F4202B1758DF00254FED /* Ex+SkeletonLoadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C466F41F2B1758DF00254FED /* Ex+SkeletonLoadable.swift */; }; C466F4272B1873E600254FED /* CategoryDetailSectionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C466F4262B1873E600254FED /* CategoryDetailSectionModel.swift */; }; C466F42D2B18795000254FED /* CategorySkeletonDetailCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C466F42C2B18794F00254FED /* CategorySkeletonDetailCell.swift */; }; @@ -236,6 +245,8 @@ C46D5E672AD1826800EBE5D7 /* CategoryAddUpperBottomSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46D5E662AD1826800EBE5D7 /* CategoryAddUpperBottomSheetViewController.swift */; }; C46D5E6E2AD9798800EBE5D7 /* CategoryEditDragCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46D5E6D2AD9798800EBE5D7 /* CategoryEditDragCollectionViewCell.swift */; }; C494A2512A9B8A0A00980A71 /* TabBarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C494A2502A9B8A0A00980A71 /* TabBarViewModel.swift */; }; + C49D0DBB2B7B686F0025FBAD /* StatisticsYetBudgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49D0DBA2B7B686F0025FBAD /* StatisticsYetBudgetView.swift */; }; + C4B334D32BFF0B6300C77FA0 /* StatisticsLast.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B334D22BFF0B6300C77FA0 /* StatisticsLast.swift */; }; C4BBD2642AB9DE8C0055FA65 /* StatisticsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BBD2632AB9DE8C0055FA65 /* StatisticsProvider.swift */; }; C4BBD2662AB9E16D0055FA65 /* DateBottomSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BBD2652AB9E16D0055FA65 /* DateBottomSheetViewController.swift */; }; C4BBD26A2AB9E1DA0055FA65 /* DateBottomSheetReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BBD2692AB9E1DA0055FA65 /* DateBottomSheetReactor.swift */; }; @@ -355,6 +366,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 450E3B282BEC583C00F6AA75 /* Modifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modifiers.swift; sourceTree = ""; }; 45AFC2A82AC296E1004DF76E /* CustomPushTextSettingReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPushTextSettingReactor.swift; sourceTree = ""; }; 45AFC2AA2AC2A564004DF76E /* CustomPushTextSettingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPushTextSettingViewController.swift; sourceTree = ""; }; 45B878C42B341A3400EF27FB /* BottomPageControlView2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomPageControlView2.swift; sourceTree = ""; }; @@ -395,7 +407,8 @@ 45E9091F2BBFB27D00E3DF20 /* CommonText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonText.swift; sourceTree = ""; }; 45E909212BBFB2AC00E3DF20 /* AlertText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertText.swift; sourceTree = ""; }; 45E909232BBFC62400E3DF20 /* CustomCalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomCalendarView.swift; sourceTree = ""; }; - 45FD3C842B887E1500C14A49 /* BudgetSettingSubTitleModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BudgetSettingSubTitleModifier.swift; sourceTree = ""; }; + 45EA263E2BCF483D001BAC57 /* TooltipView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TooltipView.swift; sourceTree = ""; }; + 45FBBB382BEDFB1500F9FAC6 /* UpsertEconomicPlanResDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpsertEconomicPlanResDto.swift; sourceTree = ""; }; 6A0379742A2443B9005EE07F /* Ex+UITextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Ex+UITextView.swift"; sourceTree = ""; }; 6A03BB4B2ADD555D00F86756 /* AddCategoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddCategoryViewController.swift; sourceTree = ""; }; 6A03FEE22ADBDB7300DA3D15 /* AddCategoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddCategoryView.swift; sourceTree = ""; }; @@ -506,7 +519,7 @@ C411940A29DAEDF9003DBC15 /* CustomAlertViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAlertViewController.swift; sourceTree = ""; }; C4170B4D2A89FCA800CE0A65 /* StatisticsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsViewController.swift; sourceTree = ""; }; C4170B4F2A89FD1100CE0A65 /* StatisticsReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsReactor.swift; sourceTree = ""; }; - C4170B522A937AAC00CE0A65 /* StatisticsTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsTitleView.swift; sourceTree = ""; }; + C4170B522A937AAC00CE0A65 /* StatisticsBudgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsBudgetView.swift; sourceTree = ""; }; C4170B542A93982F00CE0A65 /* StatisticsAverageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsAverageView.swift; sourceTree = ""; }; C4170B562A93A58A00CE0A65 /* StatisticsCategoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsCategoryView.swift; sourceTree = ""; }; C4170B582A94853400CE0A65 /* StatisticsActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsActivityView.swift; sourceTree = ""; }; @@ -529,6 +542,8 @@ C4351B4B2A1601D3005F95BE /* HomeFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeFilterView.swift; sourceTree = ""; }; C4351B4D2A166DB0005F95BE /* SemanticContentAttributeButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticContentAttributeButton.swift; sourceTree = ""; }; C4351B4F2A1691FB005F95BE /* HighlightViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightViewController.swift; sourceTree = ""; }; + C43779542BF2367800FD9AE2 /* StatisticsBudgetBottomSheetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsBudgetBottomSheetViewController.swift; sourceTree = ""; }; + C43779562BF2371E00FD9AE2 /* StatisticsBudgetBottomSheetReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsBudgetBottomSheetReactor.swift; sourceTree = ""; }; C43FF1552B1C5A1E009394D6 /* StatisticsSectionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsSectionModel.swift; sourceTree = ""; }; C446BF1A2A430B1400DFA788 /* SnackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnackView.swift; sourceTree = ""; }; C44807DD29E4FE8000D4B090 /* Ex+Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Ex+Publisher.swift"; sourceTree = ""; }; @@ -555,6 +570,11 @@ C46409D32AB8CAE600A899AD /* WithdrawReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WithdrawReactor.swift; sourceTree = ""; }; C46409D52AB8DB8F00A899AD /* BaseTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTableViewCell.swift; sourceTree = ""; }; C46409D92AB9625E00A899AD /* BottomSheetViewController2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheetViewController2.swift; sourceTree = ""; }; + C465FF382BC4407400D2E43D /* OnBoardingPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnBoardingPageViewController.swift; sourceTree = ""; }; + C465FF3A2BC524B700D2E43D /* HomeReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeReactor.swift; sourceTree = ""; }; + C465FF3C2BC57AA200D2E43D /* HomeOnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeOnboardingViewController.swift; sourceTree = ""; }; + C465FF342BB4364100D2E43D /* Budget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Budget.swift; sourceTree = ""; }; + C465FF362BBC106700D2E43D /* StatisticsSum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsSum.swift; sourceTree = ""; }; C466F41F2B1758DF00254FED /* Ex+SkeletonLoadable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Ex+SkeletonLoadable.swift"; sourceTree = ""; }; C466F4262B1873E600254FED /* CategoryDetailSectionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryDetailSectionModel.swift; sourceTree = ""; }; C466F42C2B18794F00254FED /* CategorySkeletonDetailCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategorySkeletonDetailCell.swift; sourceTree = ""; }; @@ -569,6 +589,8 @@ C46D5E662AD1826800EBE5D7 /* CategoryAddUpperBottomSheetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryAddUpperBottomSheetViewController.swift; sourceTree = ""; }; C46D5E6D2AD9798800EBE5D7 /* CategoryEditDragCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryEditDragCollectionViewCell.swift; sourceTree = ""; }; C494A2502A9B8A0A00980A71 /* TabBarViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabBarViewModel.swift; sourceTree = ""; }; + C49D0DBA2B7B686F0025FBAD /* StatisticsYetBudgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsYetBudgetView.swift; sourceTree = ""; }; + C4B334D22BFF0B6300C77FA0 /* StatisticsLast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsLast.swift; sourceTree = ""; }; C4BBD2632AB9DE8C0055FA65 /* StatisticsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsProvider.swift; sourceTree = ""; }; C4BBD2652AB9E16D0055FA65 /* DateBottomSheetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateBottomSheetViewController.swift; sourceTree = ""; }; C4BBD2692AB9E1DA0055FA65 /* DateBottomSheetReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateBottomSheetReactor.swift; sourceTree = ""; }; @@ -813,6 +835,7 @@ children = ( 45D831CA2BBD3DF700FE3D72 /* BudgetSlider.swift */, 45D831CB2BBD3DF700FE3D72 /* PriceTextFieldView.swift */, + 45EA263E2BCF483D001BAC57 /* TooltipView.swift */, ); path = Views; sourceTree = ""; @@ -832,6 +855,7 @@ isa = PBXGroup; children = ( 45D831E32BBD429C00FE3D72 /* Ex+View.swift */, + 450E3B282BEC583C00F6AA75 /* Modifiers.swift */, ); path = SwiftUI; sourceTree = ""; @@ -881,14 +905,6 @@ path = Text; sourceTree = ""; }; - 45FD3C832B887DEB00C14A49 /* Modifiers */ = { - isa = PBXGroup; - children = ( - 45FD3C842B887E1500C14A49 /* BudgetSettingSubTitleModifier.swift */, - ); - path = Modifiers; - sourceTree = ""; - }; 6A08B47B29FF5D3D0080719E /* Utilities */ = { isa = PBXGroup; children = ( @@ -916,6 +932,7 @@ 6A8605B52A24C83D00DF68BD /* UpdateResDto.swift */, C4C792A72A35916D007FD6AA /* WithdrawResDto.swift */, C4F1A9752AFD2E4F00843708 /* WidgetResDto.swift */, + 45FBBB382BEDFB1500F9FAC6 /* UpsertEconomicPlanResDto.swift */, ); path = Responses; sourceTree = ""; @@ -1025,6 +1042,7 @@ C4E96B7D2AF919C300C59983 /* ChallengeReactor.swift */, 45D401232B33D11500CE0351 /* DetailReactor.swift */, 45CA1E9C2B4CE7EF002019E6 /* EditActivityReactor.swift */, + C465FF3A2BC524B700D2E43D /* HomeReactor.swift */, C4170B4F2A89FD1100CE0A65 /* StatisticsReactor.swift */, C46409D12AB8A6A400A899AD /* ProfileReactor.swift */, C46409D32AB8CAE600A899AD /* WithdrawReactor.swift */, @@ -1035,7 +1053,6 @@ 6A7DE6B829F771F8009354EF /* Configuration */ = { isa = PBXGroup; children = ( - 45FD3C832B887DEB00C14A49 /* Modifiers */, 6A8B776229FF92CC0095ECFC /* Constants.swift */, ); path = Configuration; @@ -1226,6 +1243,7 @@ C41193ED29D3A79D003DBC15 /* Models */ = { isa = PBXGroup; children = ( + C465FF342BB4364100D2E43D /* Budget.swift */, C4BBD2942AC075E00055FA65 /* Category.swift */, C46409B32AB5941300A899AD /* CategoryEdit.swift */, C4BBD2782ABDC8870055FA65 /* CategoryBar.swift */, @@ -1234,6 +1252,8 @@ C4BBD2922AC04C550055FA65 /* CategoryEditSectionModel.swift */, C46409AF2AB590EE00A899AD /* CategoryMainSectionModel.swift */, C43FF1552B1C5A1E009394D6 /* StatisticsSectionModel.swift */, + C4B334D22BFF0B6300C77FA0 /* StatisticsLast.swift */, + C465FF362BBC106700D2E43D /* StatisticsSum.swift */, C433A07029EE5516008B26DF /* EconomicActivity.swift */, 6A14E70A2A9DCA7F0071805D /* ModelType.swift */, C4D7FC862A08942E00565291 /* Monthly.swift */, @@ -1337,7 +1357,8 @@ C4170B562A93A58A00CE0A65 /* StatisticsCategoryView.swift */, C4170B5C2A94BA9000CE0A65 /* StatisticsSatisfactionView.swift */, C460DBA92A96056100530351 /* StatisticsSatisfactionTableViewCell.swift */, - C4170B522A937AAC00CE0A65 /* StatisticsTitleView.swift */, + C4170B522A937AAC00CE0A65 /* StatisticsBudgetView.swift */, + C49D0DBA2B7B686F0025FBAD /* StatisticsYetBudgetView.swift */, ); path = Views; sourceTree = ""; @@ -1376,6 +1397,8 @@ C4351B4F2A1691FB005F95BE /* HighlightViewController.swift */, C4199DD829FE6BE1006E5DA8 /* DatePickerViewController.swift */, C4C8B68C2A3ACDC700FAF931 /* LoadingViewController.swift */, + C465FF382BC4407400D2E43D /* OnBoardingPageViewController.swift */, + C465FF3C2BC57AA200D2E43D /* HomeOnboardingViewController.swift */, ); path = Home; sourceTree = ""; @@ -1407,6 +1430,7 @@ C46D5E662AD1826800EBE5D7 /* CategoryAddUpperBottomSheetViewController.swift */, C46D5E622AD174AD00EBE5D7 /* CategoryEditUpperBottomSheetViewController.swift */, C4BBD2652AB9E16D0055FA65 /* DateBottomSheetViewController.swift */, + C43779542BF2367800FD9AE2 /* StatisticsBudgetBottomSheetViewController.swift */, C4C84FEE2B0744F500F7757B /* StatisticsDateBottomSheetViewController.swift */, C4BBD26B2ABAD4A60055FA65 /* SatisfactionBottomSheetViewController.swift */, ); @@ -1456,6 +1480,7 @@ C46D5E642AD1752700EBE5D7 /* CategoryEditUpperBottomSheetReactor.swift */, C4EB8CB52AC44448005FBAF1 /* CategoryEditBottomSheetReactor.swift */, C4BBD2692AB9E1DA0055FA65 /* DateBottomSheetReactor.swift */, + C43779562BF2371E00FD9AE2 /* StatisticsBudgetBottomSheetReactor.swift */, C4C84FEC2B07440300F7757B /* StatisticsDateBottomSheetReactor.swift */, C4BBD26D2ABAD6140055FA65 /* SatisfactionBottomSheetReactor.swift */, 45D878A32B5A5B550098F30B /* StarPickerSheetReactor.swift */, @@ -1788,6 +1813,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + C4B334D32BFF0B6300C77FA0 /* StatisticsLast.swift in Sources */, C411940429D53DDF003DBC15 /* ManagementViewController.swift in Sources */, C41193F529D3D7FB003DBC15 /* Ex+NSObject.swift in Sources */, 45D878A42B5A5B550098F30B /* StarPickerSheetReactor.swift in Sources */, @@ -1797,6 +1823,7 @@ C4170B572A93A58A00CE0A65 /* StatisticsCategoryView.swift in Sources */, C460DBAA2A96056100530351 /* StatisticsSatisfactionTableViewCell.swift in Sources */, C4C792AD2A388486007FD6AA /* Ex+UIScreen.swift in Sources */, + C43779572BF2371E00FD9AE2 /* StatisticsBudgetBottomSheetReactor.swift in Sources */, C411940B29DAEDF9003DBC15 /* CustomAlertViewController.swift in Sources */, 45D831D02BBD3DF700FE3D72 /* BudgetDetail03View.swift in Sources */, C4170B4E2A89FCA800CE0A65 /* StatisticsViewController.swift in Sources */, @@ -1807,6 +1834,7 @@ C41193FD29D42BD9003DBC15 /* ProfileFooterView.swift in Sources */, C44807E129E648D300D4B090 /* HomeViewController.swift in Sources */, C4BBD2662AB9E16D0055FA65 /* DateBottomSheetViewController.swift in Sources */, + C465FF392BC4407400D2E43D /* OnBoardingPageViewController.swift in Sources */, C46409BE2AB5B0A000A899AD /* BaseCollectionReusableView.swift in Sources */, 45D3C2332B4CD74E004ECC0A /* EditActivityViewController2.swift in Sources */, 6A2B5D5D2A97415000CCBFBE /* CustomPushTextSettingView.swift in Sources */, @@ -1822,6 +1850,7 @@ 45E909242BBFC62400E3DF20 /* CustomCalendarView.swift in Sources */, C4BBD28F2AC049130055FA65 /* CategoryEditViewController.swift in Sources */, C4DBAA2E29DC16A400D1BB97 /* WithdrawViewController.swift in Sources */, + 45EA263F2BCF483D001BAC57 /* TooltipView.swift in Sources */, 6ADEAFDF2A53B81300299A0E /* Ex+Window.swift in Sources */, 45D831D22BBD3DF700FE3D72 /* BudgetDetail02View.swift in Sources */, 6A223FFC2A1B3D590058C877 /* EditActivityViewController.swift in Sources */, @@ -1833,10 +1862,12 @@ 6A15D5372A5138D800B60CC8 /* Tracking.swift in Sources */, C46409D42AB8CAE600A899AD /* WithdrawReactor.swift in Sources */, C4170B652A95D03500CE0A65 /* CategoryMainViewController.swift in Sources */, + 45FBBB392BEDFB1500F9FAC6 /* UpsertEconomicPlanResDto.swift in Sources */, C4C84FEF2B0744F500F7757B /* StatisticsDateBottomSheetViewController.swift in Sources */, 45D401242B33D11500CE0351 /* DetailReactor.swift in Sources */, C44B61BF2AA9A85B00950226 /* CategorySegmentedControl.swift in Sources */, C4F1A9742AFD2C5800843708 /* WidgetReqDto.swift in Sources */, + C43779552BF2367800FD9AE2 /* StatisticsBudgetBottomSheetViewController.swift in Sources */, 6A0D5B992A4C0E3300958E72 /* ToastView.swift in Sources */, C46409B02AB590EE00A899AD /* CategoryMainSectionModel.swift in Sources */, C4351B4A2A15C935005F95BE /* HomeFilterViewController.swift in Sources */, @@ -1845,7 +1876,7 @@ 6A1322E92A088DD700CEEC81 /* BaseDetailViewController.swift in Sources */, C4170B5D2A94BA9000CE0A65 /* StatisticsSatisfactionView.swift in Sources */, 45D80FCF2B4B7DDA00697EDC /* AppstoreCheck.swift in Sources */, - C4170B532A937AAC00CE0A65 /* StatisticsTitleView.swift in Sources */, + C4170B532A937AAC00CE0A65 /* StatisticsBudgetView.swift in Sources */, C41193E329D287E6003DBC15 /* Ex+KeychainWrapper.swift in Sources */, C494A2512A9B8A0A00980A71 /* TabBarViewModel.swift in Sources */, C4BBD2892AC03C680055FA65 /* CategoryEditCollectionViewCellReactor.swift in Sources */, @@ -1857,7 +1888,6 @@ 45D831D62BBD3DF700FE3D72 /* BudgetSettingView.swift in Sources */, 6A08B47829FF5C7E0080719E /* APIParameters.swift in Sources */, 45D07CCF2AC4071A001C08AB /* CustomPushTimeSettingViewController.swift in Sources */, - 45FD3C852B887E1500C14A49 /* BudgetSettingSubTitleModifier.swift in Sources */, C46409DA2AB9625E00A899AD /* BottomSheetViewController2.swift in Sources */, C433BA052AE95E2D000793CB /* CategoryEmptyCollectionViewCell.swift in Sources */, C4BBD2912AC0498E0055FA65 /* CategoryEditReactor.swift in Sources */, @@ -1923,10 +1953,12 @@ C41193EA29D2D7FE003DBC15 /* MMMResource.swift in Sources */, 45D831CF2BBD3DF700FE3D72 /* BudgetDetail01View.swift in Sources */, 6ADC27E02A2AEEF400C56777 /* Ex+UIImage.swift in Sources */, + C465FF352BB4364100D2E43D /* Budget.swift in Sources */, 6A381E0A29E8F40700E0974F /* AddViewController.swift in Sources */, C4EB8CB62AC44448005FBAF1 /* CategoryEditBottomSheetReactor.swift in Sources */, 6A0F234029DBA8AA0063673B /* DataExportViewController.swift in Sources */, C4F1A9762AFD2E4F00843708 /* WidgetResDto.swift in Sources */, + C49D0DBB2B7B686F0025FBAD /* StatisticsYetBudgetView.swift in Sources */, 45D831DC2BBD403100FE3D72 /* AddScheduleRepetitionView.swift in Sources */, 45D831E02BBD405500FE3D72 /* AddScheduleTapView.swift in Sources */, C4BBD2642AB9DE8C0055FA65 /* StatisticsProvider.swift in Sources */, @@ -1961,6 +1993,7 @@ C4170B672A95EBFA00CE0A65 /* CategoryMainReactor.swift in Sources */, 45D2FA482B5E094F00DADA5B /* CategorySheetViewController.swift in Sources */, C433A07329EEE751008B26DF /* Ex+Date.swift in Sources */, + C465FF3B2BC524B700D2E43D /* HomeReactor.swift in Sources */, C4EA5B4029FA1A40008D1564 /* HomeHeaderView.swift in Sources */, C4E96B7E2AF919C300C59983 /* ChallengeReactor.swift in Sources */, 45E909202BBFB27D00E3DF20 /* CommonText.swift in Sources */, @@ -1989,6 +2022,7 @@ C41193E629D287E6003DBC15 /* Ex+Color.swift in Sources */, 45D878A22B5A56380098F30B /* StarPickerSheetViewController.swift in Sources */, C46409BA2AB5996D00A899AD /* BaseView.swift in Sources */, + C465FF372BBC106700D2E43D /* StatisticsSum.swift in Sources */, 6A1322ED2A088F7F00CEEC81 /* DetailViewController.swift in Sources */, C4DBAA2C29DBCB8B00D1BB97 /* Ex+Button.swift in Sources */, 45D831DE2BBD403100FE3D72 /* RadioButton.swift in Sources */, @@ -2004,6 +2038,7 @@ 45B878C92B342B0600EF27FB /* DetailProvider.swift in Sources */, 6A1322F12A089B2900CEEC81 /* BasePaddingLabel.swift in Sources */, 45D2FA4A2B5E096C00DADA5B /* CategorySheetViewReactor.swift in Sources */, + C465FF3D2BC57AA200D2E43D /* HomeOnboardingViewController.swift in Sources */, C4D7FC872A08942E00565291 /* Monthly.swift in Sources */, C46D5E552AD078DA00EBE5D7 /* CategoryEditTableViewCell.swift in Sources */, C4BBD2872AC03C300055FA65 /* CategoryEditCollectionViewCell.swift in Sources */, @@ -2025,6 +2060,7 @@ C46409B82AB598D100A899AD /* BaseCollectionViewCell.swift in Sources */, C41193F729D3D954003DBC15 /* Ex+UITableView.swift in Sources */, C46D5E572AD0799900EBE5D7 /* CategoryEditTableViewCellReactor.swift in Sources */, + 450E3B292BEC583C00F6AA75 /* Modifiers.swift in Sources */, C4D7FC892A08A61C00565291 /* CalendarCell.swift in Sources */, C433A06D29EE53BB008B26DF /* HomeTableViewCell.swift in Sources */, C46409C02AB5BE7900A899AD /* CategoryEditReqDto.swift in Sources */, diff --git a/MMM.xcodeproj/project.xcworkspace/xcuserdata/parkjungwoo.xcuserdatad/UserInterfaceState.xcuserstate b/MMM.xcodeproj/project.xcworkspace/xcuserdata/parkjungwoo.xcuserdatad/UserInterfaceState.xcuserstate index 9f638970..a173a175 100644 Binary files a/MMM.xcodeproj/project.xcworkspace/xcuserdata/parkjungwoo.xcuserdatad/UserInterfaceState.xcuserstate and b/MMM.xcodeproj/project.xcworkspace/xcuserdata/parkjungwoo.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/MMM/Resources/Assets/APIParameters.swift b/MMM/Resources/Assets/APIParameters.swift index 0bd8487c..52941436 100644 --- a/MMM/Resources/Assets/APIParameters.swift +++ b/MMM/Resources/Assets/APIParameters.swift @@ -133,6 +133,12 @@ struct APIParameters{ /// 카테고리 조회 struct CategoryListReqDto: Encodable { } + + struct UpsertEconomicPlanReqDto: Encodable { + var budgetAmt: Int + var economicPlanYM: String + var estimatedEarningAmt: Int + } } struct APIHeader { diff --git a/MMM/Resources/Assets/Color.xcassets/Secondary/yellow100.colorset/Contents.json b/MMM/Resources/Assets/Color.xcassets/Secondary/yellow100.colorset/Contents.json index bd8abfc7..e4bf4e73 100644 --- a/MMM/Resources/Assets/Color.xcassets/Secondary/yellow100.colorset/Contents.json +++ b/MMM/Resources/Assets/Color.xcassets/Secondary/yellow100.colorset/Contents.json @@ -5,8 +5,8 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0xC0", - "green" : "0xF1", + "blue" : "0x98", + "green" : "0xE2", "red" : "0xFF" } }, diff --git a/MMM/Resources/Assets/Icon.xcassets/FAB/iconStarGray24.imageset/Contents.json b/MMM/Resources/Assets/Icon.xcassets/FAB/iconStarGray24.imageset/Contents.json new file mode 100644 index 00000000..8c21be73 --- /dev/null +++ b/MMM/Resources/Assets/Icon.xcassets/FAB/iconStarGray24.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "iconStarGray24.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "iconStarGray24@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "iconStarGray24@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MMM/Resources/Assets/Icon.xcassets/FAB/iconStarGray24.imageset/iconStarGray24.png b/MMM/Resources/Assets/Icon.xcassets/FAB/iconStarGray24.imageset/iconStarGray24.png new file mode 100644 index 00000000..003171c0 Binary files /dev/null and b/MMM/Resources/Assets/Icon.xcassets/FAB/iconStarGray24.imageset/iconStarGray24.png differ diff --git a/MMM/Resources/Assets/Icon.xcassets/FAB/iconStarGray24.imageset/iconStarGray24@2x.png b/MMM/Resources/Assets/Icon.xcassets/FAB/iconStarGray24.imageset/iconStarGray24@2x.png new file mode 100644 index 00000000..7b0ed3af Binary files /dev/null and b/MMM/Resources/Assets/Icon.xcassets/FAB/iconStarGray24.imageset/iconStarGray24@2x.png differ diff --git a/MMM/Resources/Assets/Icon.xcassets/FAB/iconStarGray24.imageset/iconStarGray24@3x.png b/MMM/Resources/Assets/Icon.xcassets/FAB/iconStarGray24.imageset/iconStarGray24@3x.png new file mode 100644 index 00000000..aaaffd0a Binary files /dev/null and b/MMM/Resources/Assets/Icon.xcassets/FAB/iconStarGray24.imageset/iconStarGray24@3x.png differ diff --git a/MMM/Resources/Assets/Icon.xcassets/iconCharacterHappy.imageset/iconCharacterHappy.png b/MMM/Resources/Assets/Icon.xcassets/iconCharacterHappy.imageset/iconCharacterHappy.png index 0ff5610a..02ee5c00 100644 Binary files a/MMM/Resources/Assets/Icon.xcassets/iconCharacterHappy.imageset/iconCharacterHappy.png and b/MMM/Resources/Assets/Icon.xcassets/iconCharacterHappy.imageset/iconCharacterHappy.png differ diff --git a/MMM/Resources/Assets/Icon.xcassets/iconCharacterHappy.imageset/iconCharacterHappy@2x.png b/MMM/Resources/Assets/Icon.xcassets/iconCharacterHappy.imageset/iconCharacterHappy@2x.png index 36c2d609..08f4bc91 100644 Binary files a/MMM/Resources/Assets/Icon.xcassets/iconCharacterHappy.imageset/iconCharacterHappy@2x.png and b/MMM/Resources/Assets/Icon.xcassets/iconCharacterHappy.imageset/iconCharacterHappy@2x.png differ diff --git a/MMM/Resources/Assets/Icon.xcassets/iconCharacterHappy.imageset/iconCharacterHappy@3x.png b/MMM/Resources/Assets/Icon.xcassets/iconCharacterHappy.imageset/iconCharacterHappy@3x.png index d7495bc2..dc97bd30 100644 Binary files a/MMM/Resources/Assets/Icon.xcassets/iconCharacterHappy.imageset/iconCharacterHappy@3x.png and b/MMM/Resources/Assets/Icon.xcassets/iconCharacterHappy.imageset/iconCharacterHappy@3x.png differ diff --git a/MMM/Resources/Assets/Icon.xcassets/iconCheckboxDisable.imageset/Contents.json b/MMM/Resources/Assets/Icon.xcassets/iconCheckboxDisable.imageset/Contents.json new file mode 100644 index 00000000..fff0ea84 --- /dev/null +++ b/MMM/Resources/Assets/Icon.xcassets/iconCheckboxDisable.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "iconCheckboxDisable.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/MMM/Resources/Assets/Icon.xcassets/iconCheckboxDisable.imageset/iconCheckboxDisable.svg b/MMM/Resources/Assets/Icon.xcassets/iconCheckboxDisable.imageset/iconCheckboxDisable.svg new file mode 100644 index 00000000..95832c02 --- /dev/null +++ b/MMM/Resources/Assets/Icon.xcassets/iconCheckboxDisable.imageset/iconCheckboxDisable.svg @@ -0,0 +1,4 @@ + + + + diff --git a/MMM/Resources/Assets/Icon.xcassets/iconCheckboxEnableOrange.imageset/Contents.json b/MMM/Resources/Assets/Icon.xcassets/iconCheckboxEnableOrange.imageset/Contents.json new file mode 100644 index 00000000..5f7f6e35 --- /dev/null +++ b/MMM/Resources/Assets/Icon.xcassets/iconCheckboxEnableOrange.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "iconCheckboxEnableOrange.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/MMM/Resources/Assets/Icon.xcassets/iconCheckboxEnableOrange.imageset/iconCheckboxEnableOrange.svg b/MMM/Resources/Assets/Icon.xcassets/iconCheckboxEnableOrange.imageset/iconCheckboxEnableOrange.svg new file mode 100644 index 00000000..d60021cd --- /dev/null +++ b/MMM/Resources/Assets/Icon.xcassets/iconCheckboxEnableOrange.imageset/iconCheckboxEnableOrange.svg @@ -0,0 +1,4 @@ + + + + diff --git a/MMM/Resources/Assets/Icon.xcassets/iconPopup01.imageset/Contents.json b/MMM/Resources/Assets/Icon.xcassets/iconPopup01.imageset/Contents.json new file mode 100644 index 00000000..629a9774 --- /dev/null +++ b/MMM/Resources/Assets/Icon.xcassets/iconPopup01.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "iconPopup01.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "iconPopup01@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "iconPopup01@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MMM/Resources/Assets/Icon.xcassets/iconPopup01.imageset/iconPopup01.png b/MMM/Resources/Assets/Icon.xcassets/iconPopup01.imageset/iconPopup01.png new file mode 100644 index 00000000..65ef20f0 Binary files /dev/null and b/MMM/Resources/Assets/Icon.xcassets/iconPopup01.imageset/iconPopup01.png differ diff --git a/MMM/Resources/Assets/Icon.xcassets/iconPopup01.imageset/iconPopup01@2x.png b/MMM/Resources/Assets/Icon.xcassets/iconPopup01.imageset/iconPopup01@2x.png new file mode 100644 index 00000000..a35fff99 Binary files /dev/null and b/MMM/Resources/Assets/Icon.xcassets/iconPopup01.imageset/iconPopup01@2x.png differ diff --git a/MMM/Resources/Assets/Icon.xcassets/iconPopup01.imageset/iconPopup01@3x.png b/MMM/Resources/Assets/Icon.xcassets/iconPopup01.imageset/iconPopup01@3x.png new file mode 100644 index 00000000..0ffb11d5 Binary files /dev/null and b/MMM/Resources/Assets/Icon.xcassets/iconPopup01.imageset/iconPopup01@3x.png differ diff --git a/MMM/Resources/Assets/Icon.xcassets/iconPopup02.imageset/Contents.json b/MMM/Resources/Assets/Icon.xcassets/iconPopup02.imageset/Contents.json new file mode 100644 index 00000000..4be93f31 --- /dev/null +++ b/MMM/Resources/Assets/Icon.xcassets/iconPopup02.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "iconPopup02.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "iconPopup02@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "iconPopup02@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MMM/Resources/Assets/Icon.xcassets/iconPopup02.imageset/iconPopup02.png b/MMM/Resources/Assets/Icon.xcassets/iconPopup02.imageset/iconPopup02.png new file mode 100644 index 00000000..8c34706e Binary files /dev/null and b/MMM/Resources/Assets/Icon.xcassets/iconPopup02.imageset/iconPopup02.png differ diff --git a/MMM/Resources/Assets/Icon.xcassets/iconPopup02.imageset/iconPopup02@2x.png b/MMM/Resources/Assets/Icon.xcassets/iconPopup02.imageset/iconPopup02@2x.png new file mode 100644 index 00000000..71a19440 Binary files /dev/null and b/MMM/Resources/Assets/Icon.xcassets/iconPopup02.imageset/iconPopup02@2x.png differ diff --git a/MMM/Resources/Assets/Icon.xcassets/iconPopup02.imageset/iconPopup02@3x.png b/MMM/Resources/Assets/Icon.xcassets/iconPopup02.imageset/iconPopup02@3x.png new file mode 100644 index 00000000..279b7a66 Binary files /dev/null and b/MMM/Resources/Assets/Icon.xcassets/iconPopup02.imageset/iconPopup02@3x.png differ diff --git a/MMM/Resources/Assets/Icon.xcassets/iconPopup03.imageset/Contents.json b/MMM/Resources/Assets/Icon.xcassets/iconPopup03.imageset/Contents.json new file mode 100644 index 00000000..3a5b9bbc --- /dev/null +++ b/MMM/Resources/Assets/Icon.xcassets/iconPopup03.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "iconPopup03.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "iconPopup03@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "iconPopup03@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MMM/Resources/Assets/Icon.xcassets/iconPopup03.imageset/iconPopup03.png b/MMM/Resources/Assets/Icon.xcassets/iconPopup03.imageset/iconPopup03.png new file mode 100644 index 00000000..5c2d8d27 Binary files /dev/null and b/MMM/Resources/Assets/Icon.xcassets/iconPopup03.imageset/iconPopup03.png differ diff --git a/MMM/Resources/Assets/Icon.xcassets/iconPopup03.imageset/iconPopup03@2x.png b/MMM/Resources/Assets/Icon.xcassets/iconPopup03.imageset/iconPopup03@2x.png new file mode 100644 index 00000000..5481a5e5 Binary files /dev/null and b/MMM/Resources/Assets/Icon.xcassets/iconPopup03.imageset/iconPopup03@2x.png differ diff --git a/MMM/Resources/Assets/Icon.xcassets/iconPopup03.imageset/iconPopup03@3x.png b/MMM/Resources/Assets/Icon.xcassets/iconPopup03.imageset/iconPopup03@3x.png new file mode 100644 index 00000000..98d12d65 Binary files /dev/null and b/MMM/Resources/Assets/Icon.xcassets/iconPopup03.imageset/iconPopup03@3x.png differ diff --git a/MMM/Resources/Assets/Icon.xcassets/iconPopup04.imageset/Contents.json b/MMM/Resources/Assets/Icon.xcassets/iconPopup04.imageset/Contents.json new file mode 100644 index 00000000..fbf3c2c1 --- /dev/null +++ b/MMM/Resources/Assets/Icon.xcassets/iconPopup04.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "iconPopup04.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "iconPopup04@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "iconPopup04@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MMM/Resources/Assets/Icon.xcassets/iconPopup04.imageset/iconPopup04.png b/MMM/Resources/Assets/Icon.xcassets/iconPopup04.imageset/iconPopup04.png new file mode 100644 index 00000000..5e73a53d Binary files /dev/null and b/MMM/Resources/Assets/Icon.xcassets/iconPopup04.imageset/iconPopup04.png differ diff --git a/MMM/Resources/Assets/Icon.xcassets/iconPopup04.imageset/iconPopup04@2x.png b/MMM/Resources/Assets/Icon.xcassets/iconPopup04.imageset/iconPopup04@2x.png new file mode 100644 index 00000000..5e60b72f Binary files /dev/null and b/MMM/Resources/Assets/Icon.xcassets/iconPopup04.imageset/iconPopup04@2x.png differ diff --git a/MMM/Resources/Assets/Icon.xcassets/iconPopup04.imageset/iconPopup04@3x.png b/MMM/Resources/Assets/Icon.xcassets/iconPopup04.imageset/iconPopup04@3x.png new file mode 100644 index 00000000..1a4d83ae Binary files /dev/null and b/MMM/Resources/Assets/Icon.xcassets/iconPopup04.imageset/iconPopup04@3x.png differ diff --git a/MMM/Resources/Assets/Icon.xcassets/imageBackgroundBoost366.imageset/Contents.json b/MMM/Resources/Assets/Icon.xcassets/imageBackgroundBoost366.imageset/Contents.json index cc53323c..b9a54dac 100644 --- a/MMM/Resources/Assets/Icon.xcassets/imageBackgroundBoost366.imageset/Contents.json +++ b/MMM/Resources/Assets/Icon.xcassets/imageBackgroundBoost366.imageset/Contents.json @@ -1,8 +1,19 @@ { "images" : [ { - "filename" : "imageBackgroundBoost366.svg", - "idiom" : "universal" + "filename" : "imageBackgroundBoost366.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "imageBackgroundBoost366@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "imageBackgroundBoost366@3x.png", + "idiom" : "universal", + "scale" : "3x" } ], "info" : { diff --git a/MMM/Resources/Assets/Icon.xcassets/imageBackgroundBoost366.imageset/imageBackgroundBoost366.png b/MMM/Resources/Assets/Icon.xcassets/imageBackgroundBoost366.imageset/imageBackgroundBoost366.png new file mode 100644 index 00000000..0d1d26c5 Binary files /dev/null and b/MMM/Resources/Assets/Icon.xcassets/imageBackgroundBoost366.imageset/imageBackgroundBoost366.png differ diff --git a/MMM/Resources/Assets/Icon.xcassets/imageBackgroundBoost366.imageset/imageBackgroundBoost366.svg b/MMM/Resources/Assets/Icon.xcassets/imageBackgroundBoost366.imageset/imageBackgroundBoost366.svg deleted file mode 100644 index 9b27e500..00000000 --- a/MMM/Resources/Assets/Icon.xcassets/imageBackgroundBoost366.imageset/imageBackgroundBoost366.svg +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/MMM/Resources/Assets/Icon.xcassets/imageBackgroundBoost366.imageset/imageBackgroundBoost366@2x.png b/MMM/Resources/Assets/Icon.xcassets/imageBackgroundBoost366.imageset/imageBackgroundBoost366@2x.png new file mode 100644 index 00000000..ef5ff6ab Binary files /dev/null and b/MMM/Resources/Assets/Icon.xcassets/imageBackgroundBoost366.imageset/imageBackgroundBoost366@2x.png differ diff --git a/MMM/Resources/Assets/Icon.xcassets/imageBackgroundBoost366.imageset/imageBackgroundBoost366@3x.png b/MMM/Resources/Assets/Icon.xcassets/imageBackgroundBoost366.imageset/imageBackgroundBoost366@3x.png new file mode 100644 index 00000000..90780f98 Binary files /dev/null and b/MMM/Resources/Assets/Icon.xcassets/imageBackgroundBoost366.imageset/imageBackgroundBoost366@3x.png differ diff --git a/MMM/Resources/Assets/Icon.xcassets/imageCalenderUsecase144.imageset/Contents.json b/MMM/Resources/Assets/Icon.xcassets/imageCalenderUsecase144.imageset/Contents.json index ced79d21..dca5876f 100644 --- a/MMM/Resources/Assets/Icon.xcassets/imageCalenderUsecase144.imageset/Contents.json +++ b/MMM/Resources/Assets/Icon.xcassets/imageCalenderUsecase144.imageset/Contents.json @@ -8,5 +8,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" } } diff --git a/MMM/Resources/Assets/Icon.xcassets/imageCalenderUsecase144.imageset/imageCalenderUsecase144.svg b/MMM/Resources/Assets/Icon.xcassets/imageCalenderUsecase144.imageset/imageCalenderUsecase144.svg index 6305f40d..acf1e10c 100644 --- a/MMM/Resources/Assets/Icon.xcassets/imageCalenderUsecase144.imageset/imageCalenderUsecase144.svg +++ b/MMM/Resources/Assets/Icon.xcassets/imageCalenderUsecase144.imageset/imageCalenderUsecase144.svg @@ -1,40 +1,26 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MMM/Resources/Configuration/Constants.swift b/MMM/Resources/Configuration/Constants.swift index 707461e7..547ea45d 100644 --- a/MMM/Resources/Configuration/Constants.swift +++ b/MMM/Resources/Configuration/Constants.swift @@ -25,6 +25,7 @@ final class Constants { case statisticsDate // 현재 통계 날짜 case isInit // 첫 진입인지 case isHomeLoading // 탭 이동을 통한 Home 접근인지 + case onBoardingFlag // 홈에서 온보딩 팝업 여부 } /** diff --git a/MMM/Resources/Configuration/Modifiers/BudgetSettingSubTitleModifier.swift b/MMM/Resources/Configuration/Modifiers/BudgetSettingSubTitleModifier.swift deleted file mode 100644 index 0b1b9d8b..00000000 --- a/MMM/Resources/Configuration/Modifiers/BudgetSettingSubTitleModifier.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// BudgetSettingSubTitleModifier.swift -// MMM -// -// Created by yuraMacBookPro on 2/23/24. -// - -import SwiftUI - -struct BudgetSettingSubTitleModifier: ViewModifier { - func body(content: Content) -> some View { - content - .font(Font(R.Font.title3)) - .frame(width: .infinity) - .padding(EdgeInsets(top: 10, leading: 16, bottom: 10, trailing: 16)) - .background(Color(R.Color.gray800)) - .cornerRadius(4.0) - } -} diff --git a/MMM/Resources/Extensions/SwiftUI/Ex+View.swift b/MMM/Resources/Extensions/SwiftUI/Ex+View.swift index b094ae31..a865cdbc 100644 --- a/MMM/Resources/Extensions/SwiftUI/Ex+View.swift +++ b/MMM/Resources/Extensions/SwiftUI/Ex+View.swift @@ -19,6 +19,10 @@ extension View { HalfSheetHelper(sheetView: sheetView(), showSheet: showSheet, onEnd: onEnd) } } + + func navigationTransition(start insertion: Edge, to removal: Edge) -> some View { + self.transition(.asymmetric(insertion: .move(edge: insertion), removal: .move(edge: removal))) + } } // UIKit Integration @@ -83,3 +87,25 @@ final class CustomHostingController: UIHostingController } } } + +struct ShakeEffect: GeometryEffect { + var amount: CGFloat = 10 + var shakesPerUnit: CGFloat = 3 + var animatableData: CGFloat + + func effectValue(size: CGSize) -> ProjectionTransform { + ProjectionTransform(CGAffineTransform(translationX: amount * sin(animatableData * .pi * shakesPerUnit), y: 0)) + } +} + +extension View { + func shake(animatableData: CGFloat) -> some View { + modifier(ShakeEffect(animatableData: animatableData)) + } + + /// Automatically triggers a shake animation based on a boolean flag. + func autoShake(shakeCount: Binding, triggerFlag: Bool) -> some View { + self.modifier(AutoShakeModifier(shakeCount: shakeCount, triggerFlag: triggerFlag)) + } +} + diff --git a/MMM/Resources/Extensions/SwiftUI/Modifiers.swift b/MMM/Resources/Extensions/SwiftUI/Modifiers.swift new file mode 100644 index 00000000..193ae08a --- /dev/null +++ b/MMM/Resources/Extensions/SwiftUI/Modifiers.swift @@ -0,0 +1,36 @@ +// +// Modifiers.swift +// MMM +// +// Created by yuraMacBookPro on 5/9/24. +// + +import SwiftUI + +struct BudgetSettingSubTitleModifier: ViewModifier { + func body(content: Content) -> some View { + content + .font(Font(R.Font.title3)) + .padding(EdgeInsets(top: 10, leading: 16, bottom: 10, trailing: 16)) + .background(Color(R.Color.gray800)) + .cornerRadius(4.0) + } +} + +struct AutoShakeModifier: ViewModifier { + @Binding var shakeCount: CGFloat + let triggerFlag: Bool + + func body(content: Content) -> some View { + content + .shake(animatableData: shakeCount) + .onChange(of: triggerFlag) { isActive in + if isActive { + UIDevice.vibrate() + withAnimation(.linear(duration: 0.2)) { + shakeCount += 1 + } + } + } + } +} diff --git a/MMM/Resources/Extensions/UIKit/Ex+Color.swift b/MMM/Resources/Extensions/UIKit/Ex+Color.swift index 2f8b98b2..b5760901 100644 --- a/MMM/Resources/Extensions/UIKit/Ex+Color.swift +++ b/MMM/Resources/Extensions/UIKit/Ex+Color.swift @@ -7,6 +7,7 @@ // import UIKit +import SwiftUI public extension MMMResource { enum Color { @@ -63,3 +64,9 @@ public extension MMMResource { public static let red500 = UIColor(named: "red500", in: .main, compatibleWith: nil)! } } + +public extension UIColor { + var suColor: Color { + return Color(self) + } +} diff --git a/MMM/Resources/Extensions/UIKit/Ex+Date.swift b/MMM/Resources/Extensions/UIKit/Ex+Date.swift index 4bba3f10..32320ef7 100644 --- a/MMM/Resources/Extensions/UIKit/Ex+Date.swift +++ b/MMM/Resources/Extensions/UIKit/Ex+Date.swift @@ -51,7 +51,7 @@ extension Date { return dateformat.string(from: self) } - /// format: yyMM + /// format: yyyyMM func getFormattedYM() -> String { let dateformat = DateFormatter() dateformat.locale = Locale(identifier: "ko_KR") @@ -59,6 +59,14 @@ extension Date { dateformat.dateFormat = "yyyyMM" return dateformat.string(from: self) } + + /// 이전달 + func previousMonth() -> Date { + guard let previousMonth = Calendar.current.date(byAdding: .month, value: -1, to: self) else { + return self + } + return previousMonth + } func getFormattedTime() -> String { let dateformat = DateFormatter() diff --git a/MMM/Resources/Extensions/UIKit/Ex+Font.swift b/MMM/Resources/Extensions/UIKit/Ex+Font.swift index e38d877e..f9939e4b 100644 --- a/MMM/Resources/Extensions/UIKit/Ex+Font.swift +++ b/MMM/Resources/Extensions/UIKit/Ex+Font.swift @@ -7,6 +7,7 @@ // import UIKit +import SwiftUI public extension MMMResource { enum Font { @@ -90,6 +91,9 @@ public extension MMMResource { // MARK: - medium14 /// Weight : medium, Size : 14 public static let medium14 = prtendard(family: .medium, size: 14) + // MARK: - medium16 + /// Weight : medium, Size : 16 + public static let medium16 = prtendard(family: .medium, size: 16) } } @@ -113,3 +117,8 @@ public extension MMMResource { } } +public extension UIFont { + var suFont: Font { + return Font(self) + } +} diff --git a/MMM/Resources/Extensions/UIKit/Ex+Icon.swift b/MMM/Resources/Extensions/UIKit/Ex+Icon.swift index a21a5631..c10eefa5 100644 --- a/MMM/Resources/Extensions/UIKit/Ex+Icon.swift +++ b/MMM/Resources/Extensions/UIKit/Ex+Icon.swift @@ -16,13 +16,15 @@ public extension MMMResource { public static let arrowExpandLess16 = UIImage(named: "iconArrowExpandLess16", in: .main, compatibleWith: nil) public static let arrowExpandMore16 = UIImage(named: "iconArrowExpandMore16", in: .main, compatibleWith: nil) public static let arrowNext16 = UIImage(named: "iconArrowNext16", in: .main, compatibleWith: nil) - public static let arrowNextWhite16 = UIImage(named: "iconArrowNextWhite16", in: .main, compatibleWith: nil) + public static let arrowNextWhite16 = UIImage(named: "iconArrowNextWhite16", in: .main, compatibleWith: nil) public static let arrowNext24 = UIImage(named: "iconArrowNext24", in: .main, compatibleWith: nil) - public static let arrowNextBlack24 = UIImage(named: "iconArrowNextBlack24", in: .main, compatibleWith: nil) - public static let arrowBackBlack24 = UIImage(named: "iconArrowBackBlack24", in: .main, compatibleWith: nil) + public static let arrowNextBlack24 = UIImage(named: "iconArrowNextBlack24", in: .main, compatibleWith: nil) + public static let arrowBackBlack24 = UIImage(named: "iconArrowBackBlack24", in: .main, compatibleWith: nil) public static let arrowUp32 = UIImage(named: "iconArrowUp32", in: .main, compatibleWith: nil) + public static let iconBudgetSettingCalendar = UIImage(named: "iconBudgetSettingCalendar", in: .main, compatibleWith: nil)! public static let camera48 = UIImage(named: "iconCamera48", in: .main, compatibleWith: nil) public static let cancel = UIImage(named: "iconCancel", in: .main, compatibleWith: nil) + public static let characterBudget = UIImage(named: "iconCharacterBudget", in: .main, compatibleWith: nil) public static let characterHappy = UIImage(named: "iconCharacterHappy", in: .main, compatibleWith: nil) public static let checkActive = UIImage(named: "iconCheckActive", in: .main, compatibleWith: nil) public static let checkGray24 = UIImage(named: "iconCheckGray24", in: .main, compatibleWith: nil) @@ -41,6 +43,10 @@ public extension MMMResource { public static let minus16 = UIImage(named: "iconMinus16", in: .main, compatibleWith: nil) public static let photoBlank64 = UIImage(named: "iconPhotoBlank64", in: .main, compatibleWith: nil) public static let plus16 = UIImage(named: "iconPlus16", in: .main, compatibleWith: nil) + public static let iconPopup01 = UIImage(named: "iconPopup01", in: .main, compatibleWith: nil) + public static let iconPopup02 = UIImage(named: "iconPopup02", in: .main, compatibleWith: nil) + public static let iconPopup03 = UIImage(named: "iconPopup03", in: .main, compatibleWith: nil) + public static let iconPopup04 = UIImage(named: "iconPopup04", in: .main, compatibleWith: nil) public static let rain = UIImage(named: "iconRain", in: .main, compatibleWith: nil) public static let report16 = UIImage(named: "iconReport16", in: .main, compatibleWith: nil) public static let setting = UIImage(named: "iconSetting", in: .main, compatibleWith: nil) @@ -49,51 +55,49 @@ public extension MMMResource { public static let star24 = UIImage(named: "iconStar24", in: .main, compatibleWith: nil) public static let star36 = UIImage(named: "iconStar36", in: .main, compatibleWith: nil) public static let star48 = UIImage(named: "iconStar48", in: .main, compatibleWith: nil) - public static let iconEditBlack24 = UIImage(named: "iconEditBlack24", in: .main, compatibleWith: nil) - public static let iconEditGray24 = UIImage(named: "iconEditGray24", in: .main, compatibleWith: nil) - public static let iconCategory24 = UIImage(named: "iconCategory24", in: .main, compatibleWith: nil) - public static let iconArrowNextGray16 = UIImage(named: "iconArrowNextGray16", in: .main, compatibleWith: nil)! - public static let iconCheckGray16 = UIImage(named: "iconCheckGray16", in: .main, compatibleWith: nil) - public static let iconCheckWhite16 = UIImage(named: "iconCheckWhite16", in: .main, compatibleWith: nil) - public static let iconCharacterEmptyCategory = UIImage(named: "iconCharacterEmptyCategory", in: .main, compatibleWith: nil) - public static let iconRadiobuttonActive = UIImage(named: "iconRadiobuttonActive", in: .main, compatibleWith: nil) - public static let iconRadiobuttonEnable = UIImage(named: "iconRadiobuttonEnable", in: .main, compatibleWith: nil) - public static let iconRepeat24 = UIImage(named: "iconRepeat24", in: .main, compatibleWith: nil) - public static let iconBudgetSettingCalendar = UIImage(named: "iconBudgetSettingCalendar", in: .main, compatibleWith: nil)! - public static let iconSliderThumb = UIImage(named: "iconSliderThumb", in: .main, compatibleWith: nil)! - public static let imageBackgroundBoost366 = UIImage(named: "imageBackgroundBoost366", in: .main, compatibleWith: nil)! - public static let imageCalenderUsecase144 = UIImage(named: "imageCalenderUsecase144", in: .main, compatibleWith: nil)! - public static let iconCheckboxEnable = UIImage(named: "iconCheckboxEnable", in: .main, compatibleWith: nil)! - + public static let iconEditBlack24 = UIImage(named: "iconEditBlack24", in: .main, compatibleWith: nil) + public static let iconEditGray24 = UIImage(named: "iconEditGray24", in: .main, compatibleWith: nil) + public static let iconCategory24 = UIImage(named: "iconCategory24", in: .main, compatibleWith: nil) + public static let iconArrowNextGray16 = UIImage(named: "iconArrowNextGray16", in: .main, compatibleWith: nil)! + public static let iconCheckGray16 = UIImage(named: "iconCheckGray16", in: .main, compatibleWith: nil) + public static let iconCheckWhite16 = UIImage(named: "iconCheckWhite16", in: .main, compatibleWith: nil) + public static let iconCharacterEmptyCategory = UIImage(named: "iconCharacterEmptyCategory", in: .main, compatibleWith: nil) + public static let iconRadiobuttonActive = UIImage(named: "iconRadiobuttonActive", in: .main, compatibleWith: nil) + public static let iconRadiobuttonEnable = UIImage(named: "iconRadiobuttonEnable", in: .main, compatibleWith: nil) + public static let iconRepeat24 = UIImage(named: "iconRepeat24", in: .main, compatibleWith: nil) + public static let iconCheckboxEnableOrange = UIImage(named: "iconCheckboxEnableOrange", in: .main, compatibleWith: nil)! + public static let iconCheckboxDisable = UIImage(named: "iconCheckboxDisable", in: .main, compatibleWith: nil)! + // MARK: - FAB public static let onboarding1 = UIImage(named: "iconOnboarding1", in: .main, compatibleWith: nil) public static let onboarding2 = UIImage(named: "iconOnboarding2", in: .main, compatibleWith: nil) public static let mypageBg = UIImage(named: "iconMypageBg", in: .main, compatibleWith: nil) - public static let iconStarBlack8 = UIImage(named: "iconStarBlack8", in: .main, compatibleWith: nil) - public static let iconStarDisabled8 = UIImage(named: "iconStarDisabled8", in: .main, compatibleWith: nil) - public static let iconStarGray8 = UIImage(named: "iconStarGray8", in: .main, compatibleWith: nil) + public static let iconStarBlack8 = UIImage(named: "iconStarBlack8", in: .main, compatibleWith: nil) + public static let iconStarDisabled8 = UIImage(named: "iconStarDisabled8", in: .main, compatibleWith: nil) + public static let iconStarGray8 = UIImage(named: "iconStarGray8", in: .main, compatibleWith: nil) public static let iconStarBlack16 = UIImage(named: "iconStarBlack16", in: .main, compatibleWith: nil) public static let iconStarDisabled16 = UIImage(named: "iconStarDisabled16", in: .main, compatibleWith: nil) - public static let iconStarGray16 = UIImage(named: "iconStarGray16", in: .main, compatibleWith: nil) - public static let iconStarBlack48 = UIImage(named: "iconStarBlack48", in: .main, compatibleWith: nil) - public static let iconStarGray48 = UIImage(named: "iconStarGray48", in: .main, compatibleWith: nil) + public static let iconStarGray16 = UIImage(named: "iconStarGray16", in: .main, compatibleWith: nil) + public static let iconStarGray24 = UIImage(named: "iconStarGray24", in: .main, compatibleWith: nil) + public static let iconStarBlack48 = UIImage(named: "iconStarBlack48", in: .main, compatibleWith: nil) + public static let iconStarGray48 = UIImage(named: "iconStarGray48", in: .main, compatibleWith: nil) public static let iconStarOrange36 = UIImage(named: "iconStarOrange36", in: .main, compatibleWith: nil) public static let iconStarOrange48 = UIImage(named: "iconStarOrange48", in: .main, compatibleWith: nil) public static let iconStarYellow24 = UIImage(named: "iconStarYellow24", in: .main, compatibleWith: nil) - - + public static let imageBackgroundBoost366 = UIImage(named: "imageBackgroundBoost366", in: .main, compatibleWith: nil)! + public static let imageCalenderUsecase144 = UIImage(named: "imageCalenderUsecase144", in: .main, compatibleWith: nil)! - // MARK: - TabBar Items - public static let iconGroupActive = UIImage(named: "iconGroupActive", in: .main, compatibleWith: nil) - public static let iconGroupInActive = UIImage(named: "iconGroupInActive", in: .main, compatibleWith: nil) - public static let iconMoneyActive = UIImage(named: "iconMoneyActive", in: .main, compatibleWith: nil) - public static let iconMoneyInActive = UIImage(named: "iconMoneyInActive", in: .main, compatibleWith: nil) - public static let iconMypageActive = UIImage(named: "iconMypageActive", in: .main, compatibleWith: nil) - public static let iconMypageInActive = UIImage(named: "iconMypageInActive", in: .main, compatibleWith: nil) - public static let iconSeedActive = UIImage(named: "iconSeedActive", in: .main, compatibleWith: nil) - public static let iconSeedInActive = UIImage(named: "iconSeedInActive", in: .main, compatibleWith: nil) - public static let iconPlus = UIImage(named: "iconPlus", in: .main, compatibleWith: nil) - public static let iconChallengeActive = UIImage(named: "iconChallengeActive", in: .main, compatibleWith: nil) - public static let iconChallengeInactive = UIImage(named: "iconChallengeInactive", in: .main, compatibleWith: nil) + // MARK: - TabBar Items + public static let iconGroupActive = UIImage(named: "iconGroupActive", in: .main, compatibleWith: nil) + public static let iconGroupInActive = UIImage(named: "iconGroupInActive", in: .main, compatibleWith: nil) + public static let iconMoneyActive = UIImage(named: "iconMoneyActive", in: .main, compatibleWith: nil) + public static let iconMoneyInActive = UIImage(named: "iconMoneyInActive", in: .main, compatibleWith: nil) + public static let iconMypageActive = UIImage(named: "iconMypageActive", in: .main, compatibleWith: nil) + public static let iconMypageInActive = UIImage(named: "iconMypageInActive", in: .main, compatibleWith: nil) + public static let iconSeedActive = UIImage(named: "iconSeedActive", in: .main, compatibleWith: nil) + public static let iconSeedInActive = UIImage(named: "iconSeedInActive", in: .main, compatibleWith: nil) + public static let iconPlus = UIImage(named: "iconPlus", in: .main, compatibleWith: nil) + public static let iconChallengeActive = UIImage(named: "iconChallengeActive", in: .main, compatibleWith: nil) + public static let iconChallengeInactive = UIImage(named: "iconChallengeInactive", in: .main, compatibleWith: nil) } } diff --git a/MMM/Resources/Extensions/UIKit/Ex+UITextField.swift b/MMM/Resources/Extensions/UIKit/Ex+UITextField.swift index 10879f4b..9a16cd8d 100644 --- a/MMM/Resources/Extensions/UIKit/Ex+UITextField.swift +++ b/MMM/Resources/Extensions/UIKit/Ex+UITextField.swift @@ -31,6 +31,7 @@ extension UITextField { case " 원": self.tag = 1 case "만원": self.tag = 2 case "원": self.tag = 3 + case " 만원": self.tag = 4 default: self.tag = 0 } } @@ -40,18 +41,30 @@ extension UITextField { } @objc func clear(sender: AnyObject) { - if tag == 3 { - self.text = "원" - self.textColor = R.Color.white - - // cursor 위치 변경 - if let newPosition = self.position(from: self.beginningOfDocument, offset: 0) { - let newSelectedRange = self.textRange(from: newPosition, to: newPosition) - self.selectedTextRange = newSelectedRange - } - } else { - self.text = "" - } + switch tag { + case 3: + self.text = "원" + self.textColor = R.Color.white + + // cursor 위치 변경 + if let newPosition = self.position(from: self.beginningOfDocument, offset: 0) { + let newSelectedRange = self.textRange(from: newPosition, to: newPosition) + self.selectedTextRange = newSelectedRange + } + + case 2: + self.text = "원" + self.textColor = R.Color.white + + // cursor 위치 변경 + if let newPosition = self.position(from: self.beginningOfDocument, offset: 0) { + let newSelectedRange = self.textRange(from: newPosition, to: newPosition) + self.selectedTextRange = newSelectedRange + } + default: + self.text = "" + } + sendActions(for: .editingChanged) } } @@ -126,8 +139,8 @@ extension UITextField: UITextFieldDelegate { numberFormatter.numberStyle = .decimal // 콤마 생성 guard let price = Int(newStringOnlyNumber), let result = numberFormatter.string(from: NSNumber(value: price)) else { - self.text = tag == 3 ? "원" : "" - self.textColor = tag == 3 ? R.Color.white : R.Color.gray900 // 빈배열로 만든후 + self.text = tag == 3 || tag == 2 ? "원" : "" + self.textColor = tag == 3 || tag == 2 ? R.Color.white : R.Color.gray900 // 빈배열로 만든후 sendActions(for: .editingChanged) return false } @@ -139,18 +152,25 @@ extension UITextField: UITextFieldDelegate { case 1: // Detail 수정 unit = " 원" limit = 100_000_000 + self.textColor = price > limit ? R.Color.red500 : R.Color.gray900 case 2: // Home 설정 unit = " 만원" limit = 10_000 + self.textColor = price > limit ? R.Color.red500 : R.Color.white case 3: // Add 추가 unit = " 원" limit = 100_000_000 + self.textColor = price > limit ? R.Color.red500 : R.Color.white + case 4: + unit = " 만원" + limit = 10_000 + self.textColor = price > limit ? R.Color.red500 : R.Color.gray900 default: break } // 단위에 따른 color 변경 - self.textColor = price > limit ? R.Color.red500 : self.tag == 3 ? R.Color.white : R.Color.gray900 +// self.textColor = price > limit ? R.Color.red500 : self.tag == 3 || self.tag == 2 ? R.Color.white : R.Color.gray900 // 범위가 넘어갈 경우 if price > limit { diff --git a/MMM/Sources/Models/Budget.swift b/MMM/Sources/Models/Budget.swift new file mode 100644 index 00000000..ebbe547f --- /dev/null +++ b/MMM/Sources/Models/Budget.swift @@ -0,0 +1,17 @@ +// +// Budget.swift +// MMM +// +// Created by geonhyeong on 3/27/24. +// + +import Foundation + +struct Budget: Codable, Equatable { + let dateYM: String? + let budget, estimatedEarning: Int? + + static func getDummy() -> Self { + return Budget(dateYM: "202404", budget: 40000, estimatedEarning: 40000) + } +} diff --git a/MMM/Sources/Models/CategoryBar.swift b/MMM/Sources/Models/CategoryBar.swift index d147adf0..d4c849eb 100644 --- a/MMM/Sources/Models/CategoryBar.swift +++ b/MMM/Sources/Models/CategoryBar.swift @@ -34,4 +34,26 @@ struct CategoryBar: Codable, Equatable { CategoryBar(title: "높은 안모오오옥", price: "가격", ratio: 2.0) ] } + + static func getPayList() -> [Self] { + return [ + CategoryBar(title: "", price: "", ratio: 38.2), + CategoryBar(title: "", price: "", ratio: 31), + CategoryBar(title: "", price: "", ratio: 15), + CategoryBar(title: "", price: "", ratio: 6), + CategoryBar(title: "", price: "", ratio: 5), + CategoryBar(title: "", price: "", ratio: 2) + ] + } + + static func getEarnList() -> [Self] { + return [ + CategoryBar(title: "", price: "", ratio: 65.2), + CategoryBar(title: "", price: "", ratio: 15), + CategoryBar(title: "", price: "", ratio: 8), + CategoryBar(title: "", price: "", ratio: 3), + CategoryBar(title: "", price: "", ratio: 4), + CategoryBar(title: "", price: "", ratio: 2) + ] + } } diff --git a/MMM/Sources/Models/Responses/StatisticsResDto.swift b/MMM/Sources/Models/Responses/StatisticsResDto.swift index 6238fefa..7c53083b 100644 --- a/MMM/Sources/Models/Responses/StatisticsResDto.swift +++ b/MMM/Sources/Models/Responses/StatisticsResDto.swift @@ -28,3 +28,24 @@ struct StatisticsCategoryResDto: Decodable { var message: String? var status: String } + +// v1/economic-plan/{dateYM} +struct StatisticsBudgetResDto: Decodable { + var data: Budget + var message: String? + var status: String +} + +// v1/economic-plan/{dateYM} +struct StatisticsSumResDto: Decodable { + var data: StatisticsSum + var message: String? + var status: String +} + +// v1/economic-plan/latest-updated +struct StatisticsLastResDto: Decodable { + var data: StatisticsLast + var message: String? + var status: String +} diff --git a/MMM/Sources/Models/Responses/UpsertEconomicPlanResDto.swift b/MMM/Sources/Models/Responses/UpsertEconomicPlanResDto.swift new file mode 100644 index 00000000..0c287ee1 --- /dev/null +++ b/MMM/Sources/Models/Responses/UpsertEconomicPlanResDto.swift @@ -0,0 +1,18 @@ +// +// UpsertEconomicPlanResDto.swift +// MMM +// +// Created by yuraMacBookPro on 5/10/24. +// + +import Foundation + +struct UpsertEconomicPlanResDto: Codable { + var data: Data + var message: String? + var status: String + + struct Data: Codable { + var economicPlanNo: String + } +} diff --git a/MMM/Sources/Models/StatisticsLast.swift b/MMM/Sources/Models/StatisticsLast.swift new file mode 100644 index 00000000..5274e265 --- /dev/null +++ b/MMM/Sources/Models/StatisticsLast.swift @@ -0,0 +1,14 @@ +// +// StatisticsLast.swift +// MMM +// +// Created by geonhyeong on 5/23/24. +// + +import Foundation + +struct StatisticsLast: Codable, Equatable { + let budget: Int? + let dateYM: String? + let estimatedEarning: Int? +} diff --git a/MMM/Sources/Models/StatisticsSum.swift b/MMM/Sources/Models/StatisticsSum.swift new file mode 100644 index 00000000..df196901 --- /dev/null +++ b/MMM/Sources/Models/StatisticsSum.swift @@ -0,0 +1,13 @@ +// +// StatisticsSum.swift +// MMM +// +// Created by geonhyeong on 4/2/24. +// + +import Foundation + +struct StatisticsSum: Codable, Equatable { + let dateYM: String? + let economicActivitySumAmt: Int? +} diff --git a/MMM/Sources/Reactor/BottomSheet/StatisticsBudgetBottomSheetReactor.swift b/MMM/Sources/Reactor/BottomSheet/StatisticsBudgetBottomSheetReactor.swift new file mode 100644 index 00000000..a2361ba2 --- /dev/null +++ b/MMM/Sources/Reactor/BottomSheet/StatisticsBudgetBottomSheetReactor.swift @@ -0,0 +1,83 @@ +// +// StatisticsBudgetBottomSheetReactor.swift +// MMM +// +// Created by geonhyeong on 5/13/24. +// + +import RxSwift +import ReactorKit +import Foundation + +final class StatisticsBudgetBottomSheetReactor: Reactor { + // 사용자의 액션 + enum Action { + case change + case setApply + case dismiss + } + + // 처리 단위 + enum Mutation { + case dismiss + case setError + } + + // 현재 상태를 기록 + struct State { + var applyInfo: APIParameters.UpsertEconomicPlanReqDto + var dismiss: Bool = false + } + + // MARK: Properties + let initialState: State + let provider: ServiceProviderProtocol + + // 초기 State 설정 + init(provider: ServiceProviderProtocol, applyInfo: APIParameters.UpsertEconomicPlanReqDto) { + self.initialState = State(applyInfo: applyInfo) + self.provider = provider + } +} +//MARK: - Mutate, Reduce +extension StatisticsBudgetBottomSheetReactor { + /// Action이 들어온 경우, 어떤 처리를 할건지 분기 + func mutate(action: Action) -> Observable { + switch action { + case .change: + return provider.statisticsProvider.changeBudge().map { _ in .dismiss } + case .setApply: + return .concat([ + postEconomicPlan() + ]) + case .dismiss: + return .just(.dismiss) + } + } + + /// 이전 상태와 처리 단위(Mutation)를 받아서 다음 상태(State)를 반환하는 함수 + func reduce(state: State, mutation: Mutation) -> State { + var newState = state + + switch mutation { + case .dismiss: + newState.dismiss = true + case .setError: + newState.dismiss = true + } + + return newState + } +} +//MARK: - Action +extension StatisticsBudgetBottomSheetReactor { + func postEconomicPlan() -> Observable { + + return MMMAPIService().upsertEconomicPlan(request: currentState.applyInfo) + .map { (response, error) -> Mutation in + self.provider.statisticsProvider.loadData() + return .dismiss + } + .catchAndReturn(.setError) + } +} diff --git a/MMM/Sources/Reactor/HomeReactor.swift b/MMM/Sources/Reactor/HomeReactor.swift new file mode 100644 index 00000000..c643cc9a --- /dev/null +++ b/MMM/Sources/Reactor/HomeReactor.swift @@ -0,0 +1,51 @@ +// +// HomeReactor.swift +// MMM +// +// Created by geonhyeong on 4/9/24. +// + +import ReactorKit + +final class HomeReactor: Reactor { + // 사용자의 액션 + enum Action { + } + + // 처리 단위 + enum Mutation { + } + + // 현재 상태를 기록 + struct State { + } + + // MARK: Properties + let initialState: State + + // 초기 State 설정 + init() { + initialState = State() + } +} +//MARK: - Mutate, Reduce +extension HomeReactor { + /// Action이 들어온 경우, 어떤 처리를 할건지 분기 + func mutate(action: Action) -> Observable { + switch action { + } + } + + /// 이전 상태와 처리 단위(Mutation)를 받아서 다음 상태(State)를 반환하는 함수 + func reduce(state: State, mutation: Mutation) -> State { + var newState = state + + switch mutation { + } + + return newState + } +} +//MARK: - Action +extension HomeReactor { +} diff --git a/MMM/Sources/Reactor/StatisticsReactor.swift b/MMM/Sources/Reactor/StatisticsReactor.swift index 3a219ca1..6c647cf0 100644 --- a/MMM/Sources/Reactor/StatisticsReactor.swift +++ b/MMM/Sources/Reactor/StatisticsReactor.swift @@ -12,10 +12,12 @@ final class StatisticsReactor: Reactor { // 사용자의 액션 enum Action { case loadData - case pagination(contentHeight: CGFloat, contentOffsetY: CGFloat, scrollViewHeight: CGFloat) // pagination - case didTapMoreButton // 카테고리 더보기 - case didTapSatisfactionButton // 만족도 선택 + case pagination(contentHeight: CGFloat, contentOffsetY: CGFloat, scrollViewHeight: CGFloat) // pagination + case didTapMoreButton // 카테고리 더보기 + case didTapSatisfactionButton // 만족도 선택 case selectCell(IndexPath, StatisticsItem) + case didTapNewTitleView // 예산 설정하기 탭(임시로 averageView에 넣음) + case isSummary // 요약보기/닫기 } // 처리 단위 @@ -26,9 +28,16 @@ final class StatisticsReactor: Reactor { case setDate(Date) case setSatisfaction(Satisfaction) case setAverage(Double) + case setSummary + case setLastPlan(StatisticsLast) + case resetSummary + case setBudget(Budget, Bool) + case setPaySum(StatisticsSum) case presentSatisfaction(Bool) case pushMoreCategory(Bool) case pushDetail(IndexPath, EconomicActivity, Bool) + case pushBudgetSetting(Bool) + case setDialog(Bool) case setLoading(Bool) case setError } @@ -36,21 +45,29 @@ final class StatisticsReactor: Reactor { // 현재 상태를 기록 struct State { var date = Date() // 월 + var lastPlan: StatisticsLast = .init(budget: nil, dateYM: nil, estimatedEarning: nil) + var budget: Budget = .init(dateYM: Date().getFormattedYM(), budget: nil, estimatedEarning: nil) + var preBudget: Budget = .init(dateYM: Date().previousMonth().getFormattedYM(), budget: nil, estimatedEarning: nil) // 이전 달 + var paySum: StatisticsSum = .init(dateYM: Date().getFormattedYM(), economicActivitySumAmt: nil) + var percent: Int = 0 var average: Double = 0.0 // 평균값 var satisfaction: Satisfaction = .low // 만족도 var activityList: [StatisticsSectionModel] = [] var payBarList: [CategoryBar] = [] // 지출 카테고리 - var earnBarList: [CategoryBar] = [] // 수입 카테고리 + var earnBarList: [CategoryBar] = [] // 수입 카테고리 var activitySatisfactionList: [EconomicActivity] = [] var activityDisappointingList: [EconomicActivity] = [] var isLoading = true // 로딩 var isPushMoreCategory = false var isPresentSatisfaction = false var isPushDetail = false + var isSummary = true // true: 요약보기, false: 닫기 + var isDialog = false // 다이어로그를 노출시켜야하는지 여부 var detailData: (IndexPath: IndexPath, info: EconomicActivity)? var curSatisfaction: Satisfaction = .low var totalItem: Int = 0 // item의 총 갯수 var isInit = true // 최초진입 + @Pulse var isPushBudgetSetting = false } // MARK: Properties @@ -58,8 +75,8 @@ final class StatisticsReactor: Reactor { let provider: ServiceProviderProtocol var currentPage: Int = 0 var totalPage: Int = 0 - var totalItem: Int = 0 - + var totalItem: Int = 0 + init(provider: ServiceProviderProtocol) { self.initialState = State() self.provider = provider @@ -73,11 +90,14 @@ extension StatisticsReactor { case .loadData: return .concat([ .just(.setLoading(true)), - self.getStatisticsAverage(currentState.date), // 평균값 - self.getCategory(currentState.date, "01"), // 지출 카테고리 - self.getCategory(currentState.date, "02"), // 수입 카테고리 - self.getStatisticsList(currentState.date, "01", true), // 아쉬운 List - self.getStatisticsList(currentState.date, "03", true), // 만족스러운 List + self.getBudget(currentState.date), // 예산 + self.getBudget(currentState.date.previousMonth(), true),// 이전 예산 + self.getSum(currentState.date, type: "01"), // 현재 지출 + self.getStatisticsAverage(currentState.date), // 평균값 + self.getCategory(currentState.date, "01"), // 지출 카테고리 + self.getCategory(currentState.date, "02"), // 수입 카테고리 + self.getStatisticsList(currentState.date, "01", true), // 아쉬운 List + self.getStatisticsList(currentState.date, "03", true), // 만족스러운 List self.getStatisticsList(currentState.date, self.currentState.satisfaction.id, true), // viewWillAppear일때, 현재 만족도를 불러와야한다. .just(.setLoading(false)) ]) @@ -105,28 +125,52 @@ extension StatisticsReactor { .just(.pushDetail(indexPath, item, true)), .just(.pushDetail(indexPath, item, false)) ]) + case .didTapNewTitleView: + return .concat([ + .just(.pushBudgetSetting(true)), + .just(.pushBudgetSetting(false)) + ]) + case .isSummary: + return .just(.setSummary) } } /// 각각의 stream을 변형 func transform(mutation: Observable) -> Observable { - let event = provider.statisticsProvider.event.flatMap { event -> Observable in + let event = provider.statisticsProvider.event.flatMap { [self] event -> Observable in switch event { + case .loadData: + return .concat([ + self.getBudget(currentState.date), // 예산 + self.getBudget(currentState.date.previousMonth(), true),// 이전 예산 + ]) case let .updateDate(date): return .concat([ + .just(.setDialog(false)), .just(.setDate(date)), + .just(.resetSummary), + self.getBudget(date), + self.getBudget(date.previousMonth(), true), + self.getLastPlan(), + self.getSum(date, type: "01"), self.getStatisticsAverage(date), self.getCategory(date, "02"), self.getCategory(date, "01"), self.getStatisticsList(date, "01", true), self.getStatisticsList(date, "03", true), - self.getStatisticsList(date, self.currentState.satisfaction.id) // viewWillAppear일때, 현재 만족도를 불러와야한다. + self.getStatisticsList(date, self.currentState.satisfaction.id), // viewWillAppear일때, 현재 만족도를 불러와야한다. + .just(.setDialog(Date() < date)) ]) case let .updateSatisfaction(satisfaction): return .concat([ .just(.setSatisfaction(satisfaction)), self.getStatisticsList(self.currentState.date, satisfaction.id), ]) + case .changeBudge: + return .concat([ + .just(.pushBudgetSetting(true)), + .just(.pushBudgetSetting(false)) + ]) } } @@ -173,20 +217,55 @@ extension StatisticsReactor { // 카테고리 추가할때 사용하기 위해 저장 Constants.setKeychain(date.getFormattedYMD(), forKey: Constants.KeychainKey.statisticsDate) + case let .setBudget(budget, isPreMonth): + // 임시 +// if newState.date.getFormattedYM() == "202402" || newState.date.getFormattedYM() == "202404" { +// newState.budget = .init(dateYM: "", budget: 1000000, estimatedEarning: 200000000) +// } else { + if isPreMonth { + newState.preBudget = budget + } else { + newState.budget = budget + } +// } + case let .setLastPlan(lastPlan): + newState.lastPlan = lastPlan + case let .setPaySum(sum): +// let sum: StatisticsSum = .init(dateYM: "", economicActivitySumAmt: 3000) // 임시 + + newState.paySum = sum + + if let budget = newState.budget.budget, let economicActivitySumAmt = sum.economicActivitySumAmt { + + // 논리적인 오류 방지 + if economicActivitySumAmt == 0 { + newState.percent = 0 + } else { + // Budget은 setPaySum보다 빠르게 API를 불러옴 + // 지출 / 예산 + newState.percent = economicActivitySumAmt / budget + } + } case let .setAverage(average): newState.average = average case let .setSatisfaction(satisfaction): - switch satisfaction { - case .hight: - Tracking.StatiBudget.rating45LogEvent() - case .middle: - Tracking.StatiBudget.rating3LogEvent() - case .low: - Tracking.StatiBudget.rating12LogEvent() - } + switch satisfaction { + case .hight: + Tracking.StatiBudget.rating45LogEvent() + case .middle: + Tracking.StatiBudget.rating3LogEvent() + case .low: + Tracking.StatiBudget.rating12LogEvent() + } newState.satisfaction = satisfaction + case let .setDialog(isDialog): + newState.isDialog = isDialog case let .setLoading(isLoading): newState.isLoading = isLoading + case .setSummary: + newState.isSummary = !newState.isSummary + case .resetSummary: + newState.isSummary = true case let .presentSatisfaction(isPresent): newState.isPresentSatisfaction = isPresent case let .pushMoreCategory(isPush): @@ -194,6 +273,8 @@ extension StatisticsReactor { case let .pushDetail(indexPath, data, isPush): newState.isPushDetail = isPush newState.detailData = (indexPath, data) + case let .pushBudgetSetting(isPush): + newState.isPushBudgetSetting = isPush case .setError: newState.isLoading = false } @@ -203,6 +284,24 @@ extension StatisticsReactor { } //MARK: - Action extension StatisticsReactor { + // 예산 불러오기 + func getBudget(_ date: Date, _ isPreMonth: Bool = false) -> Observable { + return MMMAPIService().getBudget(dateYM: date.getFormattedYM()) + .map { (response, error) -> Mutation in + return .setBudget(response.data, isPreMonth) + } + .catchAndReturn(.setError) + } + + // 경제활동 총합 불러오기 + func getSum(_ date: Date, type: String) -> Observable { + return MMMAPIService().getStatisticsSum(dateYM: date.getFormattedYM(), economicActivityDvcd: type) + .map { (response, error) -> Mutation in + return .setPaySum(response.data) + } + .catchAndReturn(.setError) + } + // 경제활동 만족도 평균값 불러오기 func getStatisticsAverage(_ date: Date) -> Observable { return MMMAPIService().getStatisticsAverage(date.getFormattedYM()) @@ -217,7 +316,7 @@ extension StatisticsReactor { return MMMAPIService().getStatisticsList(dateYM: date.getFormattedYM(), valueScoreDvcd: type, limit: 15, offset: 0) .map { (response, error) -> Mutation in self.totalPage = response.totalPageCnt - self.totalItem = response.totalItemCnt + self.totalItem = response.totalItemCnt return .setList(response.selectListMonthlyByValueScoreOutputDto, type, reset) } .catchAndReturn(.setError) @@ -247,6 +346,15 @@ extension StatisticsReactor { .catchAndReturn(.setError) } + // 가장 최근 수정한 경제계획 조회 + private func getLastPlan() -> Observable { + return MMMAPIService().getStatisticsLast() + .map { (response, error) -> Mutation in + return .setLastPlan(response.data) + } + .catchAndReturn(.setError) + } + private func convertSectionModel(_ list: [EconomicActivity]) -> [StatisticsSectionModel] { guard !list.isEmpty else { return [] diff --git a/MMM/Sources/Services/APIRouter.swift b/MMM/Sources/Services/APIRouter.swift index 7ea95fe5..496ea1e9 100644 --- a/MMM/Sources/Services/APIRouter.swift +++ b/MMM/Sources/Services/APIRouter.swift @@ -172,4 +172,17 @@ final class APIRouter { self.path += "/\(dateYMD)/weekly" } } + + struct upsertEconomicPlanReqDto: Request { + typealias ReturnType = UpsertEconomicPlanResDto + var path: String = "/v1/economic-plan" + var method: HTTPMethod = .post + var headers: [String : String]? + var body: [String : Any]? + + init(headers: APIHeader.Default, body: APIParameters.UpsertEconomicPlanReqDto) { + self.headers = headers.asDictionary as? [String: String] + self.body = body.asDictionary + } + } } diff --git a/MMM/Sources/Services/Network/APIType.swift b/MMM/Sources/Services/Network/APIType.swift index 1330ce9f..c4d3fa2d 100644 --- a/MMM/Sources/Services/Network/APIType.swift +++ b/MMM/Sources/Services/Network/APIType.swift @@ -10,8 +10,8 @@ import Moya import RxSwift enum MMMAPI { - // MARK: - V1 API - + // MARK: - V1 API + // MARK: - Push case push(PushReqDto) case pushAgreeListSelect @@ -21,17 +21,22 @@ enum MMMAPI { case getStaticsticsAverage(dateYM: String) // 월간 만족도 평균값 case getStatisticsList(dateYM: String, valueScoreDvcd: String, limit: Int = 15, offset: Int = 0) // 만족도별 목록 case getStatisticsCategory(dateYM: String, economicActivityDvcd: String) - + case getSelectedActivity(activityId: String) + case getBudget(dateYM: String) // 월간 예산 + case getStatisticsSum(dateYM: String, economicActivityDvcd: String) // 해당 연월 기준 월간 경제활동 총합 조회 + case getStatisticsLast // 가장 최근 수정한 경제계획 조회 API + case upsertEconomicPlan(info: APIParameters.UpsertEconomicPlanReqDto) + // MARK: - Category Main -// case getAddCategoryList(CategoryListReqDto) //경제활동구분 코드 기준 카테고리별 월간 경제활동 목록 전체 조회 + // case getAddCategoryList(CategoryListReqDto) //경제활동구분 코드 기준 카테고리별 월간 경제활동 목록 전체 조회 case getCategoryList(CategoryListReqDto) // 경제활동구분 코드 기준 카테고리별 월간 경제활동 목록 전체 조회 case getCategoryDetailList(CategoryDetailListReqDto) // 카테고리 코드별 월간 경제활동 목록 조회 - + // MARK: - Category Edit case getCategoryEdit(CategoryEditReqDto) case getCategoryEditHeader(CategoryEditReqDto) case putCategoryEdit(PutCategoryEditReqDto) - + // MARK: - Profile case exportToExcel case getSummary @@ -39,11 +44,11 @@ enum MMMAPI { // MARK: - Widget case getWeely(WidgetReqDto) - - // MARK: - V2 API - - // MARK: - Staticstics - case getDetailActivity(activityId: String) + + // MARK: - V2 API + + // MARK: - Staticstics + case getDetailActivity(activityId: String) } extension MMMAPI: BaseNetworkService { @@ -56,7 +61,7 @@ extension MMMAPI: BaseNetworkService { /// router에 사용될 세부 경로 var path: String { switch self { - // MARK: - V1 API + // MARK: - V1 API case .push: return "/push" case .pushAgreeListSelect: @@ -69,6 +74,16 @@ extension MMMAPI: BaseNetworkService { return "/economic_activity/\(dateYM)/\(valueScoreDvcd)/list" case let .getStatisticsCategory(dateYM, economicActivityDvcd): return "/economic_activity/\(dateYM)/\(economicActivityDvcd)/upper-category/list" + case .getSelectedActivity: + return "/economic_activity/detail/select" + case let .getBudget(dateYM): + return "/v1/economic-plan/\(dateYM)" + case let .getStatisticsSum(dateYM, economicActivityDvcd): + return "/economic_activity/\(dateYM)/\(economicActivityDvcd)/sum" + case .getStatisticsLast: + return "/v1/economic-plan/latest-updated" + case .upsertEconomicPlan: + return "/v1/economic-plan" case let .getCategoryList(request): return "/economic_activity/\(request.dateYM)/\(request.economicActivityDvcd)/category/list" case let .getCategoryDetailList(request): @@ -87,20 +102,20 @@ extension MMMAPI: BaseNetworkService { return "/login/delete" case let .getWeely(request): return "economic_activity​/\(request.dateYMD)/weekly" - - // MARK: - V2 API - case .getDetailActivity: - return "/v2/economic_activity/detail" + + // MARK: - V2 API + case .getDetailActivity: + return "/v2/economic_activity/detail" } } /// 메서드 방식 선택 var method: Moya.Method { switch self { - // MARK: - V1 API - case .push, .pushAgreeListSelect, .pushAgreeUpdate: + // MARK: - V1 API + case .push, .pushAgreeListSelect, .pushAgreeUpdate, .upsertEconomicPlan: return .post - case .getStaticsticsAverage, .getStatisticsList, .getStatisticsCategory: + case .getStaticsticsAverage, .getStatisticsList, .getStatisticsCategory, .getBudget, .getStatisticsSum, .getSelectedActivity, .getStatisticsLast: return .get case .getCategoryList, .getCategoryDetailList, .getCategoryEdit, .getCategoryEditHeader: return .get @@ -110,10 +125,10 @@ extension MMMAPI: BaseNetworkService { return .post case .getWeely: return .get - - // MARK: - V2 API - case .getDetailActivity: - return .get + + // MARK: - V2 API + case .getDetailActivity: + return .get } } @@ -123,14 +138,16 @@ extension MMMAPI: BaseNetworkService { /// parameter || body가 없을 경우 .requestPlain 설정 var task: Moya.Task { switch self { - // MARK: - V1 API + // MARK: - V1 API case let .push(request): return .requestParameters(parameters: request.asDictionary, encoding: JSONEncoding.default) case .pushAgreeListSelect: return .requestPlain case let .pushAgreeUpdate(request): return .requestParameters(parameters: request.asDictionary, encoding: JSONEncoding.default) - case .getStaticsticsAverage, .getStatisticsCategory: + case let .upsertEconomicPlan(request): + return .requestParameters(parameters: request.asDictionary, encoding: JSONEncoding.default) + case .getStaticsticsAverage, .getStatisticsCategory, .getBudget, .getStatisticsSum, .getSelectedActivity, .getStatisticsLast: return .requestPlain case let .getStatisticsList(_, _, limit, offset): return .requestParameters(parameters: ["limit":limit, "offset":offset], encoding: URLEncoding.default) @@ -144,11 +161,11 @@ extension MMMAPI: BaseNetworkService { return .requestPlain case .getWeely: return .requestPlain // get이지만 따로 필요한 값이 없다. - - // MARK: - V2 API - case let .getDetailActivity(activityId): - return .requestParameters(parameters: ["economicActivityNo" : activityId], encoding: URLEncoding.default) - } + + // MARK: - V2 API + case let .getDetailActivity(activityId): + return .requestParameters(parameters: ["economicActivityNo" : activityId], encoding: URLEncoding.default) + } } /// Header 전달 diff --git a/MMM/Sources/Services/Network/MMMAPIService.swift b/MMM/Sources/Services/Network/MMMAPIService.swift index c87f79fc..75ec4334 100644 --- a/MMM/Sources/Services/Network/MMMAPIService.swift +++ b/MMM/Sources/Services/Network/MMMAPIService.swift @@ -58,7 +58,11 @@ protocol MMMAPIServiceble: BaseAPIService { func getStatisticsAverage(_ dateYM: String) -> Observable<(StatisticsAvgResDto, Error?)> func getStatisticsList(dateYM: String, valueScoreDvcd: String, limit: Int, offset: Int) -> Observable<(StatisticsListResDto, Error?)> func getStatisticsCategory(dateYM: String, economicActivityDvcd: String) -> Observable<(StatisticsCategoryResDto, Error?)> - + func getBudget(dateYM: String) -> Observable<(StatisticsBudgetResDto, Error?)> + func getStatisticsSum(dateYM: String, economicActivityDvcd: String) -> Observable<(StatisticsSumResDto, Error?)> + func getStatisticsLast() -> Observable<(StatisticsLastResDto, Error?)> + func upsertEconomicPlan(request: APIParameters.UpsertEconomicPlanReqDto) -> Observable<(UpsertEconomicPlanResDto, Error?)> + // MARK: - Statistics Detail func getDetailActivity(_ aactivityIdc: String) -> Observable<(SelectDetailResDto, Error?)> @@ -122,6 +126,24 @@ struct MMMAPIService: MMMAPIServiceble { return provider().request(MMMAPI.getDetailActivity(activityId: activityId), type: SelectDetailResDto.self).asObservable() } + // 해당 연월 기준 경제계획 조회 API + func getBudget(dateYM: String) -> RxSwift.Observable<(StatisticsBudgetResDto, (any Error)?)> { + return provider().request(MMMAPI.getBudget(dateYM: dateYM), type: StatisticsBudgetResDto.self).asObservable() + } + + func getStatisticsSum(dateYM: String, economicActivityDvcd: String) -> RxSwift.Observable<(StatisticsSumResDto, (any Error)?)> { + return provider().request(MMMAPI.getStatisticsSum(dateYM: dateYM, economicActivityDvcd: economicActivityDvcd), type: StatisticsSumResDto.self).asObservable() + } + + // 가장 최근 수정한 경제계획 조회 API + func getStatisticsLast() -> RxSwift.Observable<(StatisticsLastResDto, Error?)> { + return provider().request(MMMAPI.getStatisticsLast, type: StatisticsLastResDto.self).asObservable() + } + + func upsertEconomicPlan(request: APIParameters.UpsertEconomicPlanReqDto) -> RxSwift.Observable<(UpsertEconomicPlanResDto, Error?)> { + return provider().request(MMMAPI.upsertEconomicPlan(info: request), type: UpsertEconomicPlanResDto.self).asObservable() + } + // MARK: - Category Main 요청 API // 경제활동구분 코드 기준 카테고리별 월간 경제활동 목록 전체 조회 func getCategoryList(_ request: CategoryListReqDto) -> RxSwift.Observable<(CategoryListResDto, Error?)> { @@ -168,6 +190,7 @@ struct MMMAPIService: MMMAPIServiceble { } } + // MARK: MoyaProvider 네트워크 공통 로직 extension MoyaProvider { func request(_ target: TargetType, type: T.Type) -> RxSwift.Single<(T, Error?)> { diff --git a/MMM/Sources/Services/StatisticsProvider.swift b/MMM/Sources/Services/StatisticsProvider.swift index 50d57192..c9e45bb1 100644 --- a/MMM/Sources/Services/StatisticsProvider.swift +++ b/MMM/Sources/Services/StatisticsProvider.swift @@ -9,26 +9,40 @@ import RxSwift import Foundation enum StatisticsEvent { + case loadData case updateDate(Date) + case changeBudge case updateSatisfaction(Satisfaction) } protocol StatisticsServiceProtocol { var event: PublishSubject { get } + func loadData() func updateDate(to date: Date) -> Observable + func changeBudge() -> Observable func updateSatisfaction(to satisfaction: Satisfaction) -> Observable } final class StatisticsProvider: StatisticsServiceProtocol { let event = PublishSubject() + func loadData() { + event.onNext(.loadData) + } + func updateDate(to date: Date) -> Observable { event.onNext(.updateDate(date)) return .just(date) } + func changeBudge() -> Observable { + event.onNext(.changeBudge) + + return .just(true) + } + func updateSatisfaction(to satisfaction: Satisfaction) -> Observable { event.onNext(.updateSatisfaction(satisfaction)) diff --git a/MMM/Sources/ViewController/BottomSheet/StatisticsBudgetBottomSheetViewController.swift b/MMM/Sources/ViewController/BottomSheet/StatisticsBudgetBottomSheetViewController.swift new file mode 100644 index 00000000..420f6048 --- /dev/null +++ b/MMM/Sources/ViewController/BottomSheet/StatisticsBudgetBottomSheetViewController.swift @@ -0,0 +1,212 @@ +// +// StatisticsBudgetBottomSheetViewController.swift +// MMM +// +// Created by geonhyeong on 5/13/24. +// + +import UIKit +import SnapKit +import Then +import ReactorKit + +// 상속하지 않으려면 final 꼭 붙이기 +final class StatisticsBudgetBottomSheetViewController: BottomSheetViewController2, View { + typealias Reactor = StatisticsBudgetBottomSheetReactor + + // MARK: - Properties + private var curBudget: Int = 0 + private var totalSaving: Int = 0 + private var type: UIDatePicker.Mode = .date + private var isDark: Bool = false // 다크 모드 지정 + private var height: CGFloat + + // MARK: - UI Components + private lazy var containerView = UIView() + private lazy var titleLabel = UILabel() + private lazy var contentLabel = UILabel() + private lazy var buttonStackView = UIStackView() + private lazy var chageButton = UIButton() + private lazy var sameButton = UIButton() + + init(curBudget: Int, totalSaving: Int, type: UIDatePicker.Mode = .date, date: Date = Date(), height: CGFloat, isDark: Bool = false) { + self.curBudget = curBudget + self.totalSaving = totalSaving + self.height = height + self.type = type + self.isDark = isDark + super.init(mode: .fixed, isDark: isDark) + } + + override func viewDidLoad() { + super.viewDidLoad() + } + + override func touchesBegan(_: Set, with _: UIEvent?) { + view.endEditing(true) + } + + func bind(reactor: StatisticsBudgetBottomSheetReactor) { + bindState(reactor) + bindAction(reactor) + } +} +//MARK: - Bind +extension StatisticsBudgetBottomSheetViewController { + // MARK: 데이터 변경 요청 및 버튼 클릭시 요청 로직(View -> Reactor) + private func bindAction(_ reactor: StatisticsBudgetBottomSheetReactor) { + // 예산 변경하기 + chageButton.rx.tap + .withUnretained(self) + .map { .change } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + // 똑같이 적용하기 + sameButton.rx.tap + .withUnretained(self) + .map { .setApply } + .bind(to: reactor.action) + .disposed(by: disposeBag) + } + + // MARK: 데이터 바인딩 처리 (Reactor -> View) + private func bindState(_ reactor: StatisticsBudgetBottomSheetReactor) { + reactor.state + .map { $0.dismiss } + .distinctUntilChanged() + .filter { $0 == true } + .subscribe(onNext: { [weak self] _ in + self?.dismiss(animated: true) + }) + .disposed(by: disposeBag) + } +} +//MARK: - Action +extension StatisticsBudgetBottomSheetViewController { + private func setMutiText() -> NSMutableAttributedString { + let attributedText1 = NSMutableAttributedString(string: "가장 최근에 설정한 지출 예산인\n") + let attributedText2 = NSMutableAttributedString(string: "{\(curBudget.withCommas())}원") + let attributedText3 = NSMutableAttributedString(string: "을 적용할까요?") + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineSpacing = 5 + + let textAttributes: [NSAttributedString.Key : Any] = [ + NSAttributedString.Key.font: R.Font.h5, + NSAttributedString.Key.foregroundColor: R.Color.black, + NSAttributedString.Key.paragraphStyle: paragraphStyle + ] + + attributedText1.addAttributes(textAttributes, range: NSMakeRange(0, attributedText1.length)) + + let textAttributes2: [NSAttributedString.Key : Any] = [ + NSAttributedString.Key.font: R.Font.h5, + NSAttributedString.Key.foregroundColor: R.Color.orange500 + ] + attributedText2.addAttributes(textAttributes2, range: NSMakeRange(0, attributedText2.length)) + attributedText3.addAttributes(textAttributes, range: NSMakeRange(0, attributedText3.length)) + attributedText1.append(attributedText2) + attributedText1.append(attributedText3) + + return attributedText1 + } +} +//MARK: - Attribute & Hierarchy & Layouts +extension StatisticsBudgetBottomSheetViewController { + // 초기 셋업할 코드들 + override func setAttribute() { + super.setAttribute() + + containerView = containerView.then { + $0.backgroundColor = isDark ? R.Color.gray900 : .white + } + + titleLabel = titleLabel.then { + $0.attributedText = setMutiText() + $0.font = R.Font.h5 + $0.textAlignment = .center + $0.numberOfLines = 2 + } + + contentLabel = contentLabel.then { + let attrString = NSMutableAttributedString(string: "한 달에 이만큼 지출하면\n총 {\(totalSaving.withCommas())}원을 저축할 수 있어요") + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineSpacing = 5 + attrString.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: NSMakeRange(0, attrString.length)) + $0.attributedText = attrString + $0.textColor = R.Color.gray800 + $0.font = R.Font.body1 + $0.textAlignment = .center + $0.numberOfLines = 2 + } + + buttonStackView = buttonStackView.then { + $0.spacing = 8 + $0.distribution = .fillEqually + } + + chageButton = chageButton.then { + $0.setTitle("예산 변경하기", for: .normal) + $0.backgroundColor = R.Color.white + $0.setTitleColor(R.Color.orange700, for: .normal) + $0.setTitleColor(R.Color.orange700.withAlphaComponent(0.7), for: .highlighted) + $0.contentHorizontalAlignment = .center + $0.titleLabel?.font = R.Font.title1 + $0.layer.borderWidth = 1 + $0.layer.borderColor = R.Color.gray200.cgColor + $0.layer.cornerRadius = 4 + $0.contentEdgeInsets = .init(top: 10, left: 10, bottom: 10, right: 10) // touch 영역 늘리기 + } + + sameButton = sameButton.then { + $0.setTitle("똑같이 적용하기", for: .normal) + $0.setTitleColor(R.Color.gray100, for: .normal) + $0.setTitleColor(R.Color.gray100.withAlphaComponent(0.7), for: .highlighted) + $0.backgroundColor = R.Color.orange500 + $0.contentHorizontalAlignment = .center + $0.titleLabel?.font = R.Font.title1 + $0.layer.cornerRadius = 4 + $0.contentEdgeInsets = .init(top: 10, left: 10, bottom: 10, right: 10) // touch 영역 늘리기 + } + } + + override func setHierarchy() { + super.setHierarchy() + + buttonStackView.addArrangedSubviews(chageButton, sameButton) + containerView.addSubviews(titleLabel, contentLabel, buttonStackView) + addContentView(view: containerView) + } + + override func setLayout() { + super.setLayout() + + containerView.snp.makeConstraints { + $0.height.equalTo(height - 32.0) // 32(Super Class의 Drag 영역)를 반드시 뺴줘야한다. + } + + titleLabel.snp.makeConstraints { + $0.top.equalToSuperview().inset(5) + $0.centerX.equalToSuperview() + } + + contentLabel.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(18) + $0.centerX.equalToSuperview() + } + + buttonStackView.snp.makeConstraints { + $0.top.equalTo(contentLabel.snp.bottom).offset(25) + $0.horizontalEdges.equalToSuperview().inset(23) + $0.centerX.equalToSuperview() + } + + chageButton.snp.makeConstraints { + $0.height.equalTo(56) + } + + sameButton.snp.makeConstraints { + $0.height.equalTo(56) + } + } +} diff --git a/MMM/Sources/ViewController/Home/HomeOnboardingViewController.swift b/MMM/Sources/ViewController/Home/HomeOnboardingViewController.swift new file mode 100644 index 00000000..3fb99c29 --- /dev/null +++ b/MMM/Sources/ViewController/Home/HomeOnboardingViewController.swift @@ -0,0 +1,101 @@ +// +// HomeOnboardingViewController.swift +// MMM +// +// Created by geonhyeong on 4/9/24. +// + +import UIKit +import Then +import SnapKit +import ReactorKit + +// 상속하지 않으려면 final 꼭 붙이기 +final class HomeOnboardingViewController: BaseViewController, View { + typealias Reactor = HomeReactor + + // MARK: - Properties + // MARK: - UI Components + private lazy var imageView: UIImageView = .init() + private lazy var titleLabel: UILabel = .init() + private lazy var contentLabel: UILabel = .init() + + init(image: UIImage?, title: String, content: String) { + super.init(nibName: nil, bundle: nil) + self.imageView.image = image + self.titleLabel.text = title + self.contentLabel.text = content + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func bind(reactor: HomeReactor) { + bindState(reactor) + bindAction(reactor) + } +} +//MARK: - Bind +extension HomeOnboardingViewController { + // MARK: 데이터 변경 요청 및 버튼 클릭시 요청 로직(View -> Reactor) + private func bindAction(_ reactor: HomeReactor) { + } + + // MARK: 데이터 바인딩 처리 (Reactor -> View) + private func bindState(_ reactor: HomeReactor) { + } +} +//MARK: - Action +extension HomeOnboardingViewController { +} +//MARK: - Attribute & Hierarchy & Layouts +extension HomeOnboardingViewController { + override func setAttribute() { + super.setAttribute() + + imageView = imageView.then { + $0.image = imageView.image + $0.sizeToFit() + } + + titleLabel = titleLabel.then { + $0.text = titleLabel.text + $0.textColor = R.Color.black + $0.font = R.Font.title1 + } + + contentLabel = contentLabel.then { + $0.text = contentLabel.text + $0.textColor = R.Color.gray700 + $0.font = R.Font.body3 + $0.textAlignment = .center + $0.numberOfLines = 2 + } + } + + override func setHierarchy() { + super.setHierarchy() + + view.addSubviews(imageView, titleLabel, contentLabel) + } + + override func setLayout() { + super.setLayout() + + imageView.snp.makeConstraints { + $0.top.horizontalEdges.equalToSuperview() + $0.height.equalTo(172) + } + + titleLabel.snp.makeConstraints { + $0.centerX.equalToSuperview() + $0.top.equalTo(imageView.snp.bottom).offset(38) + } + + contentLabel.snp.makeConstraints { + $0.centerX.equalToSuperview() + $0.top.equalTo(titleLabel.snp.bottom).offset(12) + } + } +} diff --git a/MMM/Sources/ViewController/Home/HomeViewController.swift b/MMM/Sources/ViewController/Home/HomeViewController.swift index 8336097f..04ba5347 100644 --- a/MMM/Sources/ViewController/Home/HomeViewController.swift +++ b/MMM/Sources/ViewController/Home/HomeViewController.swift @@ -20,6 +20,7 @@ final class HomeViewController: UIViewController { private lazy var cancellable: Set = .init() private let viewModel = HomeViewModel() var disposeBag = DisposeBag() + private var onBoardingViewFlag = false // MARK: - UI Components private lazy var monthButtonItem = UIBarButtonItem() @@ -36,7 +37,8 @@ final class HomeViewController: UIViewController { private lazy var dayLabel = UILabel() private lazy var scopeGesture = UIPanGestureRecognizer() private lazy var loadView = LoadingViewController() - + private lazy var popupView = OnBoardingPageViewController() // 2.0.3 popup + // Empty & Error UI private lazy var emptyView = HomeEmptyView() private lazy var errorBgView = UIView() @@ -67,8 +69,6 @@ final class HomeViewController: UIViewController { // print("getCustomPuhsNudge : \(Common.getCustomPuhsNudge())") // print("getNudgeIfPushRestricted : \(Common.getNudgeIfPushRestricted())") // - - } override func viewWillAppear(_ animated: Bool) { @@ -86,6 +86,11 @@ final class HomeViewController: UIViewController { } fetchData() + + // 첫 한번만 Onboarding popup 작동 + if let value = Constants.getKeychainValueByBool(forKey: Constants.KeychainKey.onBoardingFlag), value { + setPopup() + } } override func viewDidAppear(_ animated: Bool) { @@ -211,6 +216,12 @@ private extension HomeViewController { setLayout() } + func setPopup() { + self.popupView.modalPresentationStyle = .overFullScreen + self.present(popupView, animated: true) + Constants.setKeychain(false, forKey: Constants.KeychainKey.onBoardingFlag) + } + private func bind() { // Foreground 상태 감지(알람 설정은 밖에서 하기 때문에) // 귀찮아서 그냥 rxswift 써버림 -> 나중에 바꿔야함 23/12/11 - pjw @@ -343,6 +354,7 @@ private extension HomeViewController { // [view] view.backgroundColor = R.Color.gray900 view.addGestureRecognizer(self.scopeGesture) + popupView.reactor = HomeReactor() let view = UIView(frame: .init(origin: .zero, size: .init(width: 150, height: 30))) monthButton = monthButton.then { diff --git a/MMM/Sources/ViewController/Home/OnBoardingPageViewController.swift b/MMM/Sources/ViewController/Home/OnBoardingPageViewController.swift new file mode 100644 index 00000000..68263d01 --- /dev/null +++ b/MMM/Sources/ViewController/Home/OnBoardingPageViewController.swift @@ -0,0 +1,184 @@ +// +// OnBoardingPageViewController.swift +// MMM +// +// Created by geonhyeong on 4/9/24. +// + +import UIKit +import Then +import SnapKit +import ReactorKit + +final class OnBoardingPageViewController: BaseViewController, View { + typealias Reactor = HomeReactor + // MARK: - Properties + private lazy var pages: [UIViewController] = [] + private lazy var pageControll: UIPageControl = UIPageControl() + private lazy var currentIndex = 0 { // currentIndex가 변할때마다 pageControll.currentPage 값을 변경 + didSet { + pageControll.currentPage = currentIndex + } + } + + // MARK: - UI Components + private lazy var bgView: UIView = .init() + private lazy var contentView: UIView = .init() + private lazy var pageVC: UIPageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil) + private lazy var nextBtn: UIButton = .init() + private lazy var finishBtn: UIButton = .init() + + func bind(reactor: HomeReactor) { + bindState(reactor) + bindAction(reactor) + } +} +//MARK: - Bind +extension OnBoardingPageViewController { + // MARK: 데이터 변경 요청 및 버튼 클릭시 요청 로직(View -> Reactor) + private func bindAction(_ reactor: HomeReactor) { + } + + // MARK: 데이터 바인딩 처리 (Reactor -> View) + private func bindState(_ reactor: HomeReactor) { + nextBtn.rx.tap + .subscribe(onNext: nextStep) + .disposed(by: disposeBag) + + finishBtn.rx.tap + .subscribe(onNext: finishStep) + .disposed(by: disposeBag) + } +} +//MARK: - Action +extension OnBoardingPageViewController { + // 다음 Step으로 넘어가기 + func nextStep() { + self.currentIndex += 1 + self.pageVC.setViewControllers([pages[currentIndex]], direction: .forward, animated: true) + + if currentIndex == pages.count - 1 { + self.showFinishButton() + } else { + self.hideFinishButton() + } + } + + // Popup 닫기 + func finishStep() { + dismiss(animated: true) + } + + func showFinishButton() { + nextBtn.isHidden = true + finishBtn.isHidden = false + } + + func hideFinishButton() { + nextBtn.isHidden = false + finishBtn.isHidden = true + } +} +//MARK: - Attribute & Hierarchy & Layouts +extension OnBoardingPageViewController { + override func setup() { + setMakeView() + super.setup() + } + + func setMakeView() { + let view1 = HomeOnboardingViewController(image: R.Icon.iconPopup01, title: "가계부 작성은 왜 해야 할까요?", content: "mmm과 함께 가계부를 적을 때\n어떤 좋은 점들이 있는지 알려드릴게요") + let view2 = HomeOnboardingViewController(image: R.Icon.iconPopup02, title: "먼저, 나의 경제 패턴을 알 수 있어요", content: "작성한 경제활동들을 카테고리별로\n정리하여 어떻게 쓰고 있는지 파악해요") + let view3 = HomeOnboardingViewController(image: R.Icon.iconPopup03, title: "같은 돈이라도 더 잘 소비할 수 있어요", content: "월별 통계를 통해 나의 경제활동에 대한\n평가를 스스로 해보며 판단하는 힘이 생겨요") + let view4 = HomeOnboardingViewController(image: R.Icon.iconPopup04, title: "끝으로, 나의 경제 목표 달성에 가까이!", content: "mmm이 체계적인 관리를 통해\n원하는 꿈을 이룰 수 있도록 도와줄거에요.") + + pages.append(view1) + pages.append(view2) + pages.append(view3) + pages.append(view4) + } + + override func setAttribute() { + super.setAttribute() + + bgView = bgView.then { + $0.backgroundColor = R.Color.black + $0.alpha = 0.7 + } + + contentView = contentView.then { + $0.backgroundColor = R.Color.white + $0.layer.cornerRadius = 16 + } + + pageVC = pageVC.then { + $0.setViewControllers([pages[currentIndex]], direction: .forward, animated: false) + $0.view.isUserInteractionEnabled = false // Scroll 방지 + } + + pageControll = pageControll.then { + $0.currentPageIndicatorTintColor = R.Color.orange500 + $0.pageIndicatorTintColor = R.Color.gray200 + $0.numberOfPages = pages.count + $0.currentPage = currentIndex + $0.isUserInteractionEnabled = false // Touch 방지 + } + + nextBtn = nextBtn.then { + $0.setTitle("다음", for: .normal) + $0.backgroundColor = R.Color.gray900 + $0.setTitleColor(R.Color.white, for: .normal) + $0.titleLabel?.font = R.Font.body2 + $0.layer.cornerRadius = 4 + } + + finishBtn = finishBtn.then { + $0.setTitle("닫기", for: .normal) + $0.backgroundColor = R.Color.gray900 + $0.setTitleColor(R.Color.white, for: .normal) + $0.titleLabel?.font = R.Font.body2 + $0.layer.cornerRadius = 4 + $0.isHidden = true // 첫 Step에 숨기기 + } + } + + override func setHierarchy() { + super.setHierarchy() + + view.addSubviews(bgView, contentView) + contentView.addSubviews(pageVC.view, pageControll, nextBtn, finishBtn) + } + + override func setLayout() { + super.setLayout() + + bgView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + contentView.snp.makeConstraints { + $0.width.equalTo(312) + $0.height.equalTo(392) + $0.center.equalToSuperview() + } + + pageControll.snp.makeConstraints { + $0.top.equalToSuperview().inset(203) + $0.horizontalEdges.equalToSuperview() + } + + pageVC.view.snp.makeConstraints { + $0.edges.equalToSuperview().inset(24) + } + + nextBtn.snp.makeConstraints { + $0.bottom.horizontalEdges.equalTo(pageVC.view) + $0.height.equalTo(40) + } + + finishBtn.snp.makeConstraints { + $0.bottom.horizontalEdges.equalTo(pageVC.view) + $0.height.equalTo(40) + } + } +} diff --git a/MMM/Sources/ViewController/Statistics/Budget/BudgetDetail/BudgetDetail01View.swift b/MMM/Sources/ViewController/Statistics/Budget/BudgetDetail/BudgetDetail01View.swift index d1135b47..37fce207 100644 --- a/MMM/Sources/ViewController/Statistics/Budget/BudgetDetail/BudgetDetail01View.swift +++ b/MMM/Sources/ViewController/Statistics/Budget/BudgetDetail/BudgetDetail01View.swift @@ -13,23 +13,44 @@ struct BudgetDetail01View: View { var body: some View { VStack { HStack { - Text(""" - ***이번 달 수입***과 더불어 - 고정 지출과 예상 지출들을 고려하여 - ***실천가능한 저축 목표***를 세워보세요! - """) - .font(Font(R.Font.body0)) - .foregroundStyle(Color(R.Color.white)) + Text("내가 고정적으로 쓰는 돈을 고려하여\n**이번 달의 수입**과 **저축할 금액**을 적고\n매일 ") + .font(Font(R.Font.h6)) // 일관된 폰트 적용 + .foregroundColor(R.Color.white.suColor) + + Text("**얼마 지출할 수 있는지**") + .font(Font(R.Font.h6)) // 같은 폰트로 유지 + .foregroundColor(R.Color.yellow300.suColor) + // 변경하고자 하는 색상 적용 + Text(" 알아보세요!") + .font(Font(R.Font.h6)) + .foregroundColor(R.Color.white.suColor) Spacer() } - + Image(uiImage: R.Icon.iconBudgetSettingCalendar) .padding(.top, 89) + + Spacer() + +// TooltipView(text: "최근 설정한 지출 예산은 {0}만원이에요", color: R.Color.black.suColor) +// .padding(.top, 21) + +// Button { +// +// } label: { +// Text("이전 예산 그대로 설정") +// .frame(maxWidth: /*@START_MENU_TOKEN@*/.infinity/*@END_MENU_TOKEN@*/, minHeight: 56) +// +// .font(Font(R.Font.title1)) +// .foregroundStyle(R.Color.gray100.suColor) +// .background(R.Color.orange500.suColor) +// } +// .padding(.top, 12) + // .padding([.leading, .trailing, .bottom], 24) + } } } #Preview { - BudgetDetail01View(budgetSettingViewModel: BudgetSettingViewModel()) + BudgetDetail01View(budgetSettingViewModel: BudgetSettingViewModel(budget: Budget.getDummy(), dateYM: "202404")) } diff --git a/MMM/Sources/ViewController/Statistics/Budget/BudgetDetail/BudgetDetail02View.swift b/MMM/Sources/ViewController/Statistics/Budget/BudgetDetail/BudgetDetail02View.swift index d686f61f..88f727ba 100644 --- a/MMM/Sources/ViewController/Statistics/Budget/BudgetDetail/BudgetDetail02View.swift +++ b/MMM/Sources/ViewController/Statistics/Budget/BudgetDetail/BudgetDetail02View.swift @@ -6,10 +6,12 @@ // import SwiftUI +import Combine struct BudgetDetail02View: View { - @ObservedObject var budgetSettingViewModel: BudgetSettingViewModel - @State var price = "" + @ObservedObject var viewModel: BudgetSettingViewModel + @FocusState private var isFocus: Bool + @State private var cancellables = Set() private let subTitle = "이번 달 예상 수입은 얼마인가요?" @@ -20,23 +22,42 @@ struct BudgetDetail02View: View { .foregroundStyle(Color(R.Color.gray200)) .padding(.bottom, 16) - PriceTextFieldViewRepresentable(viewModel: budgetSettingViewModel) + PriceTextFieldViewRepresentable(viewModel: viewModel) + .focused($isFocus) .frame(height: 40) + .onAppear { + viewModel.$isFocusTextField + .sinkOnMainThread { isFocus in + self.isFocus = false + } + .store(in: &cancellables) + } HStack(spacing: 4) { Spacer() - Text("지난 달 수입") - Text("000,000원") + + if viewModel.isBudgetAmtValid { + Group { + if let budget = viewModel.budget.budget { + Text("지난 달 작성한 수입") + Text("\(budget.withCommas())원") + } + } + .foregroundStyle(R.Color.gray300.suColor) + + } else { + Text("최대 작성 단위을 넘어선 금액이에요. (최대 1억)") + .foregroundStyle(R.Color.red500.suColor) + } } .font(Font(R.Font.body3)) - .foregroundStyle(Color(R.Color.gray300)) - + .autoShake(shakeCount: $viewModel.shakes, triggerFlag: !viewModel.isBudgetAmtValid) Spacer() } } } #Preview { - BudgetDetail02View(budgetSettingViewModel: BudgetSettingViewModel()) + BudgetDetail02View(viewModel: BudgetSettingViewModel(budget: Budget.getDummy(), dateYM: "202404")) } diff --git a/MMM/Sources/ViewController/Statistics/Budget/BudgetDetail/BudgetDetail03View.swift b/MMM/Sources/ViewController/Statistics/Budget/BudgetDetail/BudgetDetail03View.swift index e27bd50e..853503bb 100644 --- a/MMM/Sources/ViewController/Statistics/Budget/BudgetDetail/BudgetDetail03View.swift +++ b/MMM/Sources/ViewController/Statistics/Budget/BudgetDetail/BudgetDetail03View.swift @@ -9,71 +9,144 @@ import SwiftUI import Combine struct BudgetDetail03View: View { - @ObservedObject var budgetSettingViewModel: BudgetSettingViewModel - @State private var price = 100.0 + @ObservedObject var viewModel: BudgetSettingViewModel + @FocusState var isFocus: Bool + @State private var cancellables = Set() - private var priceText: String { + private var silderWidth: CGFloat { + UIScreen.width - 88 // 전체 길이 - Padding * 20 + } + + private var toolTipText: String { get { - return "\(Int(price))" +// return viewModel.estimatedEarningAmt / viewModel.budgetAmt + return "\(Int(viewModel.estimatedpercentage))" } } - let content = ["전문가들이 추천하는 저축률이에요.\n시드머니가 필요한 사회초년생에게 적합해요.", + let contentArr = ["전문가들이 추천하는 저축률이에요.\n시드머니가 필요한 사회초년생에게 적합해요.", " 권장 저축률보다 목표가 낮아요.\n이번 달 큰 소비 계획이 있으신가 봐요.", "가급적 이 정도는 저축하는 것을 추천해요.\n목표가 높으면 더 좋고요!", "이 기세라면 돈을 빠르게 모을 수 있어요.\n꾸준히 종잣돈을 모아봐요.", "경제적 자립에 한 발짝 다가갈 수 있어요.\n절약을 응원할게요!"] + private var contentText: String { + switch viewModel.estimatedpercentage { + case 0..<25: + return contentArr[0] + case 25..<50: + return contentArr[1] + case 50..<75: + return contentArr[2] + default: + return contentArr[3] + } + } + + @State private var showingSheet: Bool = false + + private var isTextFieldSheetConfrimButtonOn: Bool { + viewModel.estimatedEarningAmtForTextField != 0 + } + + private var estimatedEarningLabel: String { +// let amount = Double(viewModel.budgetAmt) * Double(viewModel.estimatedpercentage) / 100 +// viewModel.estimatedEarningAmt = amount + let amount = viewModel.estimatedEarning + return "\(Int(amount))" + } + var body: some View { VStack { HStack { Text("수입의 얼마를 모으실 건가요?") .font(Font(R.Font.regular20)) .foregroundStyle(Color(R.Color.gray200)) - .padding(.bottom, 136) Spacer() } - - Text("\(priceText)만원") - .font(Font(R.Font.h2)) - .foregroundStyle(Color(R.Color.white)) - .padding(.bottom, 22) - Text(content[0]) - .multilineTextAlignment(.center) - .font(Font(R.Font.medium14)) - .foregroundStyle(Color(R.Color.orange100)) - .padding(.bottom, 32) + VStack { + Text("\(estimatedEarningLabel)만원") + .font(Font(R.Font.h2)) + .foregroundStyle(Color(R.Color.white)) + .padding(.bottom, 22) + + Text(contentText) + .multilineTextAlignment(.center) + .font(Font(R.Font.medium14)) + .foregroundStyle(Color(R.Color.orange100)) + .padding(.bottom, 32) + + Button { + showingSheet = true + } label: { + Text("직접 입력하기") + .underline() + .font(Font(R.Font.body3)) + .foregroundStyle(Color(R.Color.gray500)) + } + Spacer() - Button { - debugPrint("직접 입력하기 tapped") - } label: { - Text("직접 입력하기") - .underline() - .font(Font(R.Font.body3)) - .foregroundStyle(Color(R.Color.gray500)) + // 슬라이더 값에 따라 툴팁 위치 동적 조정 + TooltipView(text: "\(toolTipText)%", color: R.Color.orange500.suColor) + .offset(x: -(silderWidth / 2) + (silderWidth / 100) * viewModel.estimatedpercentage, y: -30) // 가정: 슬라이더 너비가 300pt + + BudgetSlider(value: $viewModel.estimatedpercentage, range: 0...100) + .padding([.leading, .trailing], 20) } - - Spacer() - - // 툴팁으로 사용될 텍스트 뷰 - Text("\(priceText)%") - .padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) - .background(Color(R.Color.orange500)) - .font(Font(R.Font.body1)) - .foregroundStyle(Color(R.Color.white)) - .cornerRadius(8) -// .overlay(RoundedRectangle(cornerRadius: 5).stroke(Color.blue, lineWidth: 2)) - .offset(x: 0, y: -30) // 툴팁 위치 조정 - // 슬라이더 값에 따라 툴팁 위치 동적 조정 - .offset(x: CGFloat((price - 100)), y: 0) // 가정: 슬라이더 너비가 300pt - - BudgetSlider(value: $price, range: 0...200) - + .padding(.top, 136) } + .sheet(isPresented: $showingSheet, content: { + VStack(spacing: 24) { + HStack { + Text("이번 달 저축 금액") + .font(R.Font.h5.suFont) + .foregroundStyle(R.Color.black.suColor) + + Spacer() + + Button { +// viewModel.estimatedpercentage = 100.0 / Double(viewModel.budgetAmt / viewModel.estimatedEarningAmtForTextField) + + viewModel.estimatedpercentage = Double(viewModel.estimatedEarningAmtForTextField) / Double(viewModel.budgetAmt) * 100.0 + + showingSheet.toggle() + } label: { + Text("확인") + .font(R.Font.title3.suFont) + .foregroundStyle(isTextFieldSheetConfrimButtonOn ? R.Color.black.suColor : R.Color.gray500.suColor) + } + .disabled(!isTextFieldSheetConfrimButtonOn) + } + .padding(.top, 32) + + VStack(alignment: .leading, spacing: 12) { + PriceTextFieldViewRepresentable(viewModel: viewModel) + .padding(.top, 16) + .focused($isFocus) + .frame(height: 40) + .onAppear { + viewModel.$isFocusTextField + .sinkOnMainThread { isFocus in + self.isFocus = false + } + .store(in: &cancellables) + } + if !viewModel.isEstimatedEarningAmtValid { + Text("예상 수입을 넘어선 금액이에요 (최대 {예상수입}만원)") + .font(R.Font.body3.suFont) + .foregroundStyle(R.Color.red500.suColor) + .autoShake(shakeCount: $viewModel.shakes, triggerFlag: viewModel.isEstimatedEarningAmtValid) + } + } + Spacer() + } + .padding([.leading, .trailing], 24) + .presentationDetents([.height(174)]) + }) } } #Preview { - BudgetDetail03View(budgetSettingViewModel: BudgetSettingViewModel()) + BudgetDetail03View(viewModel: BudgetSettingViewModel(budget: Budget.getDummy(), dateYM: "202404")) } diff --git a/MMM/Sources/ViewController/Statistics/Budget/BudgetDetail/BudgetDetail04View.swift b/MMM/Sources/ViewController/Statistics/Budget/BudgetDetail/BudgetDetail04View.swift index 7ec18c58..3ecdee45 100644 --- a/MMM/Sources/ViewController/Statistics/Budget/BudgetDetail/BudgetDetail04View.swift +++ b/MMM/Sources/ViewController/Statistics/Budget/BudgetDetail/BudgetDetail04View.swift @@ -8,9 +8,9 @@ import SwiftUI struct BudgetDetail04View: View { - @ObservedObject var budgetSettingViewModel: BudgetSettingViewModel + @ObservedObject var viewModel: BudgetSettingViewModel - let title = "일별 환산된 적정 지출 금액을\n달력에서 확인하시겠어요?" + let title = "모으는 돈을 제외하고\n이번 달 지출할 수 있는 예산 금액" let subTitle = "월 지출 예산을 일별로 환산하면 다음과 같아요." var body: some View { @@ -21,12 +21,12 @@ struct BudgetDetail04View: View { .foregroundStyle(Color(R.Color.gray200)) VStack(alignment: .leading, spacing: 3) { - Text("{100}만원") + Text("\(viewModel.remainBudget)만원") .font(Font(R.Font.h2)) .foregroundStyle(Color(R.Color.white)) Rectangle() .fill(Color(R.Color.orange500)) - .frame(width: .infinity, height: 2) + .frame(height: 2) } .padding(EdgeInsets(top: 10, leading: 16, bottom: 10, trailing: 16)) .background(Color(R.Color.gray800)) @@ -42,7 +42,7 @@ struct BudgetDetail04View: View { Text("일일 적정 지출 금액 ") .foregroundColor(Color(R.Color.white)) + - Text("{3}만원 ") + Text("\(viewModel.dailyRemainBudget)만원 ") .foregroundColor(Color(R.Color.orange500)) + Text("이하") @@ -55,5 +55,5 @@ struct BudgetDetail04View: View { } #Preview { - BudgetDetail04View(budgetSettingViewModel: BudgetSettingViewModel()) + BudgetDetail04View(viewModel: BudgetSettingViewModel(budget: Budget.getDummy(), dateYM: "202404")) } diff --git a/MMM/Sources/ViewController/Statistics/Budget/BudgetDetail/BudgetDetail05View.swift b/MMM/Sources/ViewController/Statistics/Budget/BudgetDetail/BudgetDetail05View.swift index 91075709..e34b2756 100644 --- a/MMM/Sources/ViewController/Statistics/Budget/BudgetDetail/BudgetDetail05View.swift +++ b/MMM/Sources/ViewController/Statistics/Budget/BudgetDetail/BudgetDetail05View.swift @@ -9,56 +9,81 @@ import SwiftUI import Combine struct BudgetDetail05View: View { - @ObservedObject var budgetSettingViewModel: BudgetSettingViewModel + @ObservedObject var viewModel: BudgetSettingViewModel - let title = "일별 환산된 적정 지출 금액을\n달력에서 확인하시겠어요?" + private var title: String { + if viewModel.currentStep == .complete { + return "지출 예산이 세워졌어요!\n시작이 반이랍니다.\n이번 달도 해낼 수 있어요!" + } else { + return "일별 환산된 적정 지출 금액을\n달력에서 확인하시겠어요?" + } + } + + var body: some View { VStack { HStack { Text(title) .font(Font(R.Font.regular20)) .foregroundStyle(Color(R.Color.gray200)) - - Spacer() - } - .padding(.bottom, 14) - - HStack { - Text("일일 적정 지출 금액 ") - .foregroundColor(Color(R.Color.white)) - + - Text("{3}만원 ") - .foregroundColor(Color(R.Color.orange500)) Spacer() } - .modifier(BudgetSettingSubTitleModifier()) - .padding(.bottom, 14) - - Image(uiImage: R.Icon.imageCalenderUsecase144) - - .frame(width: .infinity) - VStack(alignment: .leading) { - HStack { - CalendarLabel(price: 3, color: R.Color.orange400) - CalendarLabel(price: 3, color: R.Color.orange200) - } - .padding(.bottom, 51) - - Button { - debugPrint("checkbox tapped") - } label: { - /*@START_MENU_TOKEN@*/Text("Button")/*@END_MENU_TOKEN@*/ + if viewModel.currentStep == .complete { + Spacer() + Image(uiImage: R.Icon.imageBackgroundBoost366) + } else { + VStack { + + + HStack { + Text("일일 적정 지출 금액 ") + .foregroundColor(Color(R.Color.white)) + + + Text("\(viewModel.dailyRemainBudget)만원 ") + .foregroundColor(Color(R.Color.orange500)) + + Spacer() + } + .modifier(BudgetSettingSubTitleModifier()) + .padding([.top, .bottom], 14) + + Image(uiImage: R.Icon.imageCalenderUsecase144) + .resizable() + .scaledToFit() + .padding(.bottom, 23) + + VStack(alignment: .leading) { + HStack { + CalendarLabel(price: viewModel.dailyRemainBudget, color: R.Color.orange400) + CalendarLabel(price: viewModel.dailyRemainBudget, color: R.Color.orange200) + Spacer() + } + .padding(.bottom, 51) + + Button { + viewModel.isCalenderCheckboxEnable.toggle() + } label: { + HStack(spacing: 8) { + Image(uiImage: viewModel.isCalenderCheckboxEnable ? R.Icon.iconCheckboxEnableOrange : R.Icon.iconCheckboxDisable) + Text("달력에 표시하기") + .foregroundStyle(R.Color.gray100.suColor) + .font(R.Font.medium16.suFont) + } + + } + } } } } + } } #Preview { - BudgetDetail05View(budgetSettingViewModel: BudgetSettingViewModel()) + BudgetDetail05View(viewModel: BudgetSettingViewModel(budget: Budget.getDummy(), dateYM: "202404")) } @@ -69,6 +94,8 @@ struct CalendarLabel: View { VStack { HStack(spacing: 4) { Text("{\(price)}만원 이상 지출") + .font(R.Font.medium14.suFont) + .foregroundStyle(R.Color.gray300.suColor) Circle() .fill(Color(color)) diff --git a/MMM/Sources/ViewController/Statistics/Budget/BudgetDetail/Views/BudgetSlider.swift b/MMM/Sources/ViewController/Statistics/Budget/BudgetDetail/Views/BudgetSlider.swift index 20b520e2..808e79fa 100644 --- a/MMM/Sources/ViewController/Statistics/Budget/BudgetDetail/Views/BudgetSlider.swift +++ b/MMM/Sources/ViewController/Statistics/Budget/BudgetDetail/Views/BudgetSlider.swift @@ -6,12 +6,15 @@ // import SwiftUI +import CoreHaptics struct BudgetSlider: View { @Binding var value: Double // 슬라이더의 현재 값 var range: ClosedRange // 슬라이더의 범위 var step: Double = 5.0 // 슬라이더의 단위 변경 let divisions: Int = 4 // 5개의 구간을 만들기 위한 나눔 수 (0을 포함하여 총 5개) + private let hapticFeedback = UIImpactFeedbackGenerator(style: .medium) + var body: some View { GeometryReader { geometry in @@ -62,7 +65,13 @@ struct BudgetSlider: View { let dragValue = max(0, min(Double(gesture.location.x / width), 1)) let newValue = dragValue * (range.upperBound - range.lowerBound) + range.lowerBound let roundedValue = round(newValue / step) * step - self.value = roundedValue + + if roundedValue != value { + value = roundedValue + // 햅틱 기능 추가 + self.hapticFeedback.impactOccurred() + } + } // 현재 값에 따른 핸들의 위치를 계산하는 메소드 diff --git a/MMM/Sources/ViewController/Statistics/Budget/BudgetDetail/Views/PriceTextFieldView.swift b/MMM/Sources/ViewController/Statistics/Budget/BudgetDetail/Views/PriceTextFieldView.swift index f43e84a7..d87bc6b7 100644 --- a/MMM/Sources/ViewController/Statistics/Budget/BudgetDetail/Views/PriceTextFieldView.swift +++ b/MMM/Sources/ViewController/Statistics/Budget/BudgetDetail/Views/PriceTextFieldView.swift @@ -9,12 +9,14 @@ import UIKit import Then import SnapKit import SwiftUI +import Combine final class PriceTextFieldView: BaseView { // MARK: - UI Components private lazy var priceTextField = UITextField() private lazy var warningLabel = UILabel() private let viewModel: BudgetSettingViewModel + private lazy var cancellable: Set = .init() init(viewModel: BudgetSettingViewModel) { self.viewModel = viewModel @@ -31,14 +33,25 @@ final class PriceTextFieldView: BaseView { extension PriceTextFieldView { override func setAttribute() { priceTextField = priceTextField.then { -// $0.text = "원" -// $0.placeholder = "만원 단위로 입력" - $0.attributedPlaceholder = NSAttributedString(string: "만원 단위로 입력", attributes: [NSAttributedString.Key.foregroundColor : R.Color.gray500]) +// if let price = Int(viewModel.budgetAmt) { +// $0.text = price.withCommas() + "만원" +// } + + let isStep2 = viewModel.currentStep == .income + + if viewModel.budgetAmt != 0 { + $0.text = isStep2 ? viewModel.budgetAmt.withCommas() : "" + } + + let placeholder = "만원 단위로 입력" + + + $0.attributedPlaceholder = NSAttributedString(string: placeholder, attributes: [NSAttributedString.Key.foregroundColor: R.Color.gray500]) $0.font = R.Font.h2 - $0.textColor = R.Color.white + $0.textColor = isStep2 ? R.Color.white : R.Color.gray900 $0.keyboardType = .numberPad // 숫자 키보드 $0.tintColor = R.Color.gray400 // cursor color - $0.setNumberMode(unit: "원") // 단위 설정 + $0.setNumberMode(unit: isStep2 ? "만원" : " 만원") // 단위 설정 $0.setClearButton(with: R.Icon.cancel, mode: .always) // clear 버튼 } @@ -66,6 +79,23 @@ extension PriceTextFieldView { $0.leading.trailing.equalToSuperview() } } + + override func setBind() { + // 현재 스탭에 따라서 binding을 다르게 해줌 + if viewModel.currentStep == .income { + priceTextField.textPublisher + .map{String(Array($0).filter{$0.isNumber})} // 숫자만 추출 + .map { Int($0) ?? 0 } + .assignOnMainThread(to: \.budgetAmt, on: viewModel) + .store(in: &cancellable) + } else if viewModel.currentStep == .expense { + priceTextField.textPublisher + .map{String(Array($0).filter{$0.isNumber})} // 숫자만 추출 + .map { Int($0) ?? 0 } + .assignOnMainThread(to: \.estimatedEarningAmtForTextField, on: viewModel) + .store(in: &cancellable) + } + } } struct PriceTextFieldViewRepresentable: UIViewRepresentable { diff --git a/MMM/Sources/ViewController/Statistics/Budget/BudgetDetail/Views/TooltipView.swift b/MMM/Sources/ViewController/Statistics/Budget/BudgetDetail/Views/TooltipView.swift new file mode 100644 index 00000000..a3a4e6e1 --- /dev/null +++ b/MMM/Sources/ViewController/Statistics/Budget/BudgetDetail/Views/TooltipView.swift @@ -0,0 +1,36 @@ +// +// TooltipView.swift +// MMM +// +// Created by yuraMacBookPro on 4/17/24. +// + +import SwiftUI + +struct TooltipView: View { + var text: String + var color: Color + + var body: some View { + // 툴팁으로 사용될 텍스트 뷰 + ZStack(alignment: .bottom) { + Text(text) + .padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) + .background(color) + .font(Font(R.Font.body1)) + .foregroundStyle(Color(R.Color.white)) + .cornerRadius(8) + + Rectangle() + .fill(color) + .frame(width: 12, height: 12) + .rotationEffect(.degrees(45)) + .offset(y: 6) + } + + } +} + +#Preview { + TooltipView(text: "100", color: R.Color.orange500.suColor) +} diff --git a/MMM/Sources/ViewController/Statistics/Budget/BudgetSettingView.swift b/MMM/Sources/ViewController/Statistics/Budget/BudgetSettingView.swift index fc52c4f3..e458e826 100644 --- a/MMM/Sources/ViewController/Statistics/Budget/BudgetSettingView.swift +++ b/MMM/Sources/ViewController/Statistics/Budget/BudgetSettingView.swift @@ -8,65 +8,156 @@ import SwiftUI struct BudgetSettingView: View { - @State var currentStep: String = "3" - @StateObject var budgetSettingViewModel = BudgetSettingViewModel() + @Environment(\.presentationMode) var presentationMode + @StateObject var viewModel: BudgetSettingViewModel + @FocusState private var isFocus: Bool + @Environment(\.dismiss) private var dismiss + + private var nextButtonTitle: String { + viewModel.currentStep == .complete ? "완료" : "다음" + } + + private var startTransition: Edge { + return viewModel.transition ? .leading : .trailing + } + + private var toTransition: Edge { + return viewModel.transition ? .trailing : .leading + } + + var body: some View { NavigationView { VStack { - SegmentedView(selected: $currentStep) + SegmentedView(selected:$viewModel.currentStep) .padding(.top, 16) .padding([.leading, .trailing], 24) VStack { - switch currentStep { - case "1": - BudgetDetail01View(budgetSettingViewModel: budgetSettingViewModel) - case "2": - BudgetDetail02View(budgetSettingViewModel: budgetSettingViewModel) - case "3": - BudgetDetail03View(budgetSettingViewModel: budgetSettingViewModel) - case "4": - BudgetDetail04View(budgetSettingViewModel: budgetSettingViewModel) - case "5": - BudgetDetail05View(budgetSettingViewModel: budgetSettingViewModel) - default: - EmptyView() + switch viewModel.currentStep { + case .main: + BudgetDetail01View(budgetSettingViewModel: viewModel) + .navigationTransition(start: startTransition, to: toTransition) + case .income: + BudgetDetail02View(viewModel: viewModel) + .onTapGesture { + // 강제로 탭 제스처를 만들어서 전체 뷰에 대한 터치 이벤트를 막음 + } + .navigationTransition(start: startTransition, to: toTransition) + case .expense: + BudgetDetail03View(viewModel: viewModel) + .onTapGesture { + // 강제로 탭 제스처를 만들어서 전체 뷰에 대한 터치 이벤트를 막음 + } + .navigationTransition(start: startTransition, to: toTransition) + case .budget: + BudgetDetail04View(viewModel: viewModel) + .navigationTransition(start: startTransition, to: toTransition) + case .calendar, .complete: + BudgetDetail05View(viewModel: viewModel) + .navigationTransition(start: startTransition, to: toTransition) } - - } .padding(.top, 48) .padding([.leading, .trailing], 24) + .animation(.easeInOut, value: viewModel.currentStep) Spacer() - - Button { - debugPrint("다음버튼 tapped") - } label: { - Text("다음") - .frame(maxWidth: /*@START_MENU_TOKEN@*/.infinity/*@END_MENU_TOKEN@*/, minHeight: 56) + HStack(spacing: 8) { + if !viewModel.isFirstStep { + // 이전 버튼 + Button { + viewModel.transition = true + + switch viewModel.currentStep { + case .main: + break + case .income: + viewModel.isFirstStep = true + viewModel.currentStep = .main + case .expense: + viewModel.currentStep = .income + case .budget: + viewModel.currentStep = .expense + case .calendar: + viewModel.currentStep = .budget + case .complete: + viewModel.currentStep = .calendar + } + } label: { + Text("이전") + .frame(maxWidth: .infinity, minHeight: 56) + .font(R.Font.title1.suFont) + .foregroundStyle(R.Color.gray100.suColor) + .background(R.Color.gray800.suColor) + } + } + + // 다음 버튼 + Button { + viewModel.transition = false + + switch viewModel.currentStep { + case .main: + viewModel.isFirstStep = false + viewModel.currentStep = .income + case .income: + viewModel.currentStep = .expense + case .expense: + viewModel.currentStep = .budget + case .budget: + viewModel.currentStep = .calendar + case .calendar: + viewModel.currentStep = .complete + case .complete: + viewModel.upsertEconomicPlan() + } + } label: { + Text(nextButtonTitle) + .frame(maxWidth: .infinity, minHeight: 56) + .font(R.Font.title1.suFont) + .foregroundStyle(viewModel.isNextButtonDisable ? R.Color.gray400.suColor : R.Color.gray100.suColor) + .background(viewModel.isNextButtonDisable ? R.Color.gray600.suColor: (viewModel.isFirstStep ? R.Color.gray800.suColor : R.Color.orange500.suColor)) - .font(Font(R.Font.title1)) - .foregroundStyle(Color(R.Color.gray100)) - .background(Color(R.Color.gray800)) + } + // step2에서 값을 입력하지 않았을 경우 disable + .disabled(viewModel.isNextButtonDisable) } .padding([.leading, .trailing, .bottom], 24) } + .navigationBarBackButtonHidden() .background(Color(R.Color.gray900)) + .onTapGesture { + viewModel.dismissKeyboard() + } + .navigationTitle("지출 예산 설정하기") + .navigationBarTitleDisplayMode(.inline) + .foregroundStyle(R.Color.white.suColor) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { + dismiss() + } label: { + Text("닫기") + .foregroundStyle(R.Color.white.suColor) + .font(R.Font.body1.suFont) + } + } + } } } } #Preview { - BudgetSettingView() + BudgetSettingView(viewModel: BudgetSettingViewModel(budget: Budget.getDummy(), dateYM: "202404")) } struct SegmentedView: View { - - let segments: [String] = ["1", "2", "3", "4", "5"] - @Binding var selected: String + + let segments: [BudgetSettingViewModel.CurrentStep] = [.main, .income, .expense, .budget, .calendar] + @Binding var selected: BudgetSettingViewModel.CurrentStep @Namespace var name - + var body: some View { HStack(spacing: 0) { ForEach(segments, id: \.self) { segment in @@ -78,9 +169,10 @@ struct SegmentedView: View { Capsule() .fill(Color(R.Color.gray600)) .frame(height: 2) - if selected == segment { + // 5번 페이지에서는 두개의 화면을 쓰기 때문에 .complete == .calendar를 동일 취급해줘야함 + if selected == segment || (selected == .complete && segment == .calendar) { Capsule() - .fill(selected == segment ? Color(R.Color.white) : Color(R.Color.gray600)) + .fill(Color(R.Color.white)) .frame(height: 2) .matchedGeometryEffect(id: "Tab", in: name) } @@ -88,6 +180,7 @@ struct SegmentedView: View { .padding([.leading, .trailing], 1) } } + .disabled(true) } } } @@ -100,8 +193,9 @@ final class BudgetSettingHostingController: UIHostingController UIViewController { - let vc = BudgetSettingHostingController(rootView: BudgetSettingView()) + func budgetSettingViewUI(_ budget: Budget, _ dateYM: String) -> UIViewController { + let viewModel = BudgetSettingViewModel(budget: budget, dateYM: dateYM) + let vc = BudgetSettingHostingController(rootView: BudgetSettingView(viewModel: viewModel)) return vc } } diff --git a/MMM/Sources/ViewController/Statistics/Budget/BudgetSettingViewModel.swift b/MMM/Sources/ViewController/Statistics/Budget/BudgetSettingViewModel.swift index 55f8af61..7870c236 100644 --- a/MMM/Sources/ViewController/Statistics/Budget/BudgetSettingViewModel.swift +++ b/MMM/Sources/ViewController/Statistics/Budget/BudgetSettingViewModel.swift @@ -9,18 +9,164 @@ import SwiftUI import Combine final class BudgetSettingViewModel: ObservableObject { - @Published var budget: Int = 0 - @Published var priceInput: String = "" - - // MARK: - Public properties - // 들어온 퍼블리셔의 값 일치 여부를 반환하는 퍼블리셔 - lazy var isPriceVaild: AnyPublisher = $priceInput - .map {0 <= Int($0) ?? 0 && Int($0) ?? 0 <= 10_000 } // 1억(1,000만원)보다 작을 경우 - .eraseToAnyPublisher() - - lazy var isValidByWon: AnyPublisher = $priceInput - .map { 0 <= Int($0) ?? 0 && Int($0) ?? 0 <= 100_000_000 } // 1억(1,000만원)보다 작을 경우 - .eraseToAnyPublisher() -} + // 이전 달 예산 금액 + @Published var budget: Budget + var dateYM: String + + // budget02에서 사용하는 price property + @Published var budgetAmt: Int = 0 + // budget03에서 사용하는 price property + @Published var estimatedEarningAmtForTextField: Int = 0 + @Published var estimatedpercentage: Double = 0.0 + + @Published var isFocusTextField: Bool = false + // step1일 경우 버튼의 사이즈가 다르기 때문에 사용 + @Published var isFirstStep: Bool = true + + @Published var isCalenderCheckboxEnable: Bool = false + @Published var transition: Bool = true // true이면 next + // 현재 segment의 step + @Published var currentStep: CurrentStep = .main + // 들어온 퍼블리셔의 값 일치 여부를 반환하는 퍼블리셔 -> budget02에서 textfield가 1억 넘었을 경우를 나타냄 + @Published var isBudgetAmtValid: Bool = true + // budget03에서 slider 퍼센트에 따라서 estimatedEarningAmt를 계산하는 연산 프로퍼티 + + // 한달에 저축할 총 예산 + var estimatedEarning: Double { + Double(budgetAmt) * Double(estimatedpercentage) / 100 + } + // 하루에 지출할 수 있는 남은 예산 + var remainBudget: Int { + budgetAmt - Int(estimatedEarning) + } + + var dailyRemainBudget: Int { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyyMM" + + let dateString = budget.dateYM ?? "" + + // 문자열로부터 Date 객체 생성 + guard let date = dateFormatter.date(from: dateString) else { + print("날짜 형식이 잘못되었습니다.") + return 0 + } + + // Calendar 객체를 사용하여 월의 마지막 일자 구하기 + let calendar = Calendar.current + let range = calendar.range(of: .day, in: .month, for: date) + + let dailyAmt = remainBudget / (range?.count ?? 30) + + return dailyAmt + } + + // 수입보다 저축 금액이 큰지 판단하여 warning label을 띄워주는 변수 -> budget03 + @Published var isEstimatedEarningAmtValid: Bool = true + // shake 에니메이션 상수 + @Published var shakes: CGFloat = 0 + + @Published var isLoading = true + + private var cancellables = Set() + @Published var response: UpsertEconomicPlanResDto? + + enum CurrentStep { + case main // 01 : 예산 세팅 + case income // 02 : 예상 수입 설정 + case expense // 03 : 지출 예산 설정 + case budget // 04 : 사용가능 예산 + case calendar // 05 : 날짜 + case complete // 05 : 완료 + } + + // step2에서 예상수익을 입력하지 않았을 떄를 판단하는 변수 + @Published var isNextButtonDisable: Bool = false + + + init(budget: Budget, dateYM: String) { + self.budget = budget + self.dateYM = dateYM + bind() + } + + private func bind() { + // 세 변수를 합친 값을 계산하고 새로운 속성으로 사용 + Publishers.CombineLatest($currentStep, $isBudgetAmtValid) + .map { currentStep , isBudgetAmtValid in + // 예상 수입이 비어있지 않고, 현재 단계가 income이 아니면 버튼을 활성화 OR 예산을 정확히 입력하지 않았을 경우 + return currentStep == .income && !isBudgetAmtValid + } + .sink(receiveValue: { [weak self] isActive in + guard let self = self else { return } + self.isNextButtonDisable = isActive + }) + .store(in: &cancellables) + + $budgetAmt + .map { amt in + return 0 < amt && amt <= 10_000 + } + .assign(to: &$isBudgetAmtValid) + + $estimatedEarningAmtForTextField + .map { amt in + return 0 < amt && amt <= 10_000 + } + .assign(to: &$isEstimatedEarningAmtValid) + } + + + func dismissKeyboard() { + isFocusTextField.toggle() + } + + func upsertEconomicPlan() { + guard let token = Constants.getKeychainValue(forKey: Constants.KeychainKey.token) else { return } + self.isLoading = true + + APIClient.dispatch( + APIRouter.upsertEconomicPlanReqDto( + headers: APIHeader.Default(token: token), + body:APIParameters.UpsertEconomicPlanReqDto( + budgetAmt: budgetAmt * 10000, + economicPlanYM: dateYM, + estimatedEarningAmt: Int(estimatedEarning * 10000)))) + .sink { data in + switch data { + case .failure(_): + // 실패했을 경우 alert 같은 걸 띄워줘야함 + debugPrint("upsert fail") + break + case .finished: + break + } + self.isLoading = false + } receiveValue: { response in + print(response) + self.response = response + + // UIWindowScene을 통해 현재 활성화된 씬을 찾고, 그 씬의 windows 배열에서 visible인 윈도우를 찾습니다. + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootViewController = windowScene.windows.first(where: { $0.isKeyWindow })?.rootViewController { + // UINavigationController를 찾아 pop합니다. + let navigationController = self.findNavigationController(viewController: rootViewController) + navigationController?.popViewController(animated: true) + } + } + .store(in: &cancellables) + } + + // UIViewController에서 UINavigationController을 찾는 재귀 함수 + func findNavigationController(viewController: UIViewController) -> UINavigationController? { + if let navigationController = viewController as? UINavigationController { + return navigationController + } else if let presentedViewController = viewController.presentedViewController { + return findNavigationController(viewController: presentedViewController) + } else { + return nil + } + } +} diff --git a/MMM/Sources/ViewController/Statistics/StatisticsViewController.swift b/MMM/Sources/ViewController/Statistics/StatisticsViewController.swift index 7a7a81e2..ce897e11 100644 --- a/MMM/Sources/ViewController/Statistics/StatisticsViewController.swift +++ b/MMM/Sources/ViewController/Statistics/StatisticsViewController.swift @@ -12,6 +12,7 @@ import RxCocoa import ReactorKit import RxDataSources import UIKit +import RxGesture // 상속하지 않으려면 final 꼭 붙이기 final class StatisticsViewController: BaseViewController, View { @@ -24,9 +25,11 @@ final class StatisticsViewController: BaseViewController, View { } // MARK: - Properties + private var isFirst: Bool = false private var month: Date = Date() private var satisfaction: Satisfaction = .low private var timer: DispatchSourceTimer? // rank(순위)를 변경하는 시간 + private var isBudget: Bool = false private lazy var dataSource: DataSource = RxTableViewSectionedReloadDataSource(configureCell: { dataSource, tv, indexPath, item -> UITableViewCell in guard let reactor = self.reactor else { return .init() } @@ -59,9 +62,10 @@ final class StatisticsViewController: BaseViewController, View { private lazy var monthButtonItem = UIBarButtonItem() private lazy var monthButton = SemanticContentAttributeButton() private lazy var headerView = UIView() - private lazy var titleView = StatisticsTitleView() - private lazy var averageView = StatisticsAverageView() + private lazy var budgetView = StatisticsBudgetView() + private lazy var yetBudgetView = StatisticsYetBudgetView() private lazy var categoryView = StatisticsCategoryView() + private lazy var averageView = StatisticsAverageView() private lazy var activityView = StatisticsActivityView(timer: timer) private lazy var satisfactionView = StatisticsSatisfactionView() // 만족도 선택 private lazy var tableView = UITableView() @@ -78,11 +82,10 @@ final class StatisticsViewController: BaseViewController, View { } override func viewWillAppear(_ animated: Bool) { - Tracking.StatiBudget.pageViewLogEvent() - - guard let reactor = reactor else { return } - super.viewWillAppear(animated) + Tracking.StatiBudget.pageViewLogEvent() + self.navigationController?.setNavigationBarHidden(false, animated: animated) + guard let reactor = reactor else { return } // Root View인 NavigationView에 item 수정하기 if let navigationController = self.navigationController { if let rootVC = navigationController.viewControllers.first { @@ -152,6 +155,12 @@ extension StatisticsViewController { } .bind(to: reactor.action) .disposed(by: disposeBag) + + yetBudgetView.rx.tapGesture() + .when(.recognized) + .map { _ in .didTapNewTitleView } + .bind(to: reactor.action) + .disposed(by: disposeBag) } // MARK: 데이터 바인딩 처리 (Reactor -> View) @@ -163,10 +172,39 @@ extension StatisticsViewController { .bind(onNext: setMonth) // '월' 변경 .disposed(by: disposeBag) + reactor.state + .map { $0.isDialog } + .distinctUntilChanged() // 중복값 무시 + .filter { $0 } + .bind(onNext: presentDiaglog) // 다이어로그 노출 + .disposed(by: disposeBag) + + reactor.state + .map { $0.budget } + .distinctUntilChanged() // 중복값 무시 + .bind(onNext: setBudget) // 예산 변경 + .disposed(by: disposeBag) + + reactor.state + .map { $0.paySum } + .distinctUntilChanged() // 중복값 무시 + .bind(onNext: setCurrentPay) // 현재 지출 + .disposed(by: disposeBag) + reactor.state .map { $0.average } .distinctUntilChanged() // 중복값 무시 - .bind(onNext: averageView.setData) // 평균 변경 + .withUnretained(self) + .subscribe(onNext: { this, average in // 평균 변경 + if this.headerView.subviews.contains(this.satisfactionView) { + this.satisfactionView.snp.updateConstraints { + $0.top.equalTo(this.averageView.snp.bottom).offset(26) + } + } + + this.averageView.setData(average: average) + this.tableView.reloadData() + }) .disposed(by: disposeBag) reactor.state @@ -191,6 +229,27 @@ extension StatisticsViewController { }) .disposed(by: disposeBag) + // 요약하기 + reactor.state + .map { $0.isSummary } + .distinctUntilChanged() // 중복값 무시 + .withUnretained(self) + .bind { (this, isSummary) in + if this.isFirst { // 처음 화면에 접근했을 경우 + this.activityView.isHidden = isSummary + + this.headerView.frame.size.height = isSummary ? this.headerView.frame.height - 90 : this.headerView.frame.height + 90 + + this.satisfactionView.snp.updateConstraints { + $0.top.equalTo(this.averageView.snp.bottom).offset(isSummary ? 26 : 116) + } + + this.tableView.reloadData() + } else { + this.isFirst = true + } + }.disposed(by: disposeBag) + // 로딩 발생 // 다음 배포때, 스켈레톤 처리 // reactor.state @@ -232,6 +291,13 @@ extension StatisticsViewController { .filter { $0 } // true일때만 화면 전환 .bind(onNext: pushDetail) .disposed(by: disposeBag) + + reactor.state + .map { $0.isPushBudgetSetting } + .distinctUntilChanged() + .filter { $0 } + .bind(onNext: pushBudgetSettingViewController) + .disposed(by: disposeBag) } } //MARK: - Action @@ -245,6 +311,18 @@ extension StatisticsViewController { self.present(vc, animated: true, completion: nil) } + // Dialog 설정 + private func presentDiaglog(_ isDialog: Bool) { + guard let reactor = self.reactor, reactor.currentState.budget.budget == nil else { + return + } + let lastPlan = reactor.currentState.lastPlan + + let vc = StatisticsBudgetBottomSheetViewController(curBudget: lastPlan.budget ?? 0, totalSaving: lastPlan.estimatedEarning ?? 0, height: 292) + vc.reactor = StatisticsBudgetBottomSheetReactor(provider: reactor.provider, applyInfo: .init(budgetAmt: lastPlan.budget ?? 0, economicPlanYM: reactor.currentState.date.getFormattedYM(), estimatedEarningAmt: lastPlan.estimatedEarning ?? 0)) + self.present(vc, animated: true, completion: nil) + } + // 카테고리 더보기 private func pushCategoryViewController(_ isPush: Bool) { Tracking.StatiBudget.btnCategoryLogEvent() @@ -290,6 +368,49 @@ extension StatisticsViewController { self.present(vc, animated: true, completion: nil) } + /// 예산 설정 유무 따른 UI 변경 + private func setBudget(_ budgetInfo: Budget) { + guard let budget = budgetInfo.budget, let earn = budgetInfo.estimatedEarning else { + // 예산 설정이 안되었을 경우 + self.isBudget = false + self.budgetView.isHidden = true + self.yetBudgetView.isHidden = false + + self.headerView.frame.size.height = 418 + + if self.headerView.subviews.contains(categoryView) { + categoryView.snp.remakeConstraints { + $0.top.equalTo(yetBudgetView.snp.bottom).offset(12) + $0.leading.trailing.equalToSuperview().inset(UI.sideMargin) + $0.height.equalTo(146) + } + } + return + } + + self.isBudget = true + self.headerView.frame.size.height = 461 + self.budgetView.isHidden = false + self.yetBudgetView.isHidden = true + + self.budgetView.setBudget(estimatedEarning: budget) // 예상 수입 + + if self.headerView.subviews.contains(categoryView) { + categoryView.snp.remakeConstraints { + $0.top.equalTo(budgetView.snp.bottom).offset(12) + $0.leading.trailing.equalToSuperview().inset(UI.sideMargin) + $0.height.equalTo(146) + } + } + self.tableView.reloadData() + } + + // 해당 연월 기준 월간 경제활동 총합 조회 + private func setCurrentPay(_ sum: StatisticsSum) { + guard let economicActivitySumAmt = sum.economicActivitySumAmt else { return } + self.budgetView.setCurrentPay(currentPayLabel: economicActivitySumAmt) + } + /// '월' 및 범위 변경 private func setMonth(_ date: Date) { // 올해인지 판별 @@ -299,16 +420,6 @@ extension StatisticsViewController { monthButton.setTitle(date.getFormattedDate(format: "M월"), for: .normal) } - // 범위 변경 - let month = date.getFormattedDate(format: "MM") - var end = date.lastDay() ?? "01" - - // 이번달 인지 판별 - if date.getFormattedYM() == Date().getFormattedYM() { - end = Date().getFormattedDate(format: "dd") - } - - self.titleView.setData(startDate: "\(month).01", endDate: "\(month).\(end)") self.month = date } @@ -317,6 +428,28 @@ extension StatisticsViewController { satisfactionView.setData(title: satisfaction.title, score: satisfaction.score) self.satisfaction = satisfaction } + + /// 에산설정 뷰 + private func pushBudgetSettingViewController(_ isPush: Bool) { + guard let reactor = reactor else { return } +// reactor.currentState.preBudget // 임시: 사용하렴 정우야 + + + var dateComponents = DateComponents() + dateComponents.month = 0 + let newDate = Calendar.current.date(byAdding: dateComponents, to: reactor.currentState.date) + + let dateYM = newDate?.getFormattedYM() ?? "" + print(dateYM) + + let budget = reactor.currentState.preBudget + let interface = BudgetSettingViewInterface() + let vc = interface.budgetSettingViewUI(budget, dateYM) + + self.navigationController?.setNavigationBarHidden(true, animated: false) + + navigationController?.pushViewController(vc, animated: true) + } } //MARK: - Attribute & Hierarchy & Layouts extension StatisticsViewController: SkeletonLoadable { @@ -337,8 +470,10 @@ extension StatisticsViewController: SkeletonLoadable { view.backgroundColor = R.Color.gray900 headerView.backgroundColor = R.Color.gray900 + budgetView.reactor = self.reactor categoryView.reactor = self.reactor // reactor 주입 activityView.reactor = self.reactor // reactor 주입 + averageView.reactor = self.reactor // reactor 주입 satisfactionView.reactor = self.reactor // reactor 주입 let firstGroup = makeAnimationGroup(startColor: R.Color.gray800, endColor: R.Color.gray600) @@ -386,10 +521,6 @@ extension StatisticsViewController: SkeletonLoadable { $0.rowHeight = UITableView.automaticDimension } - headerView = headerView.then { - $0.frame = .init(x: 0, y: 0, width: view.bounds.width, height: 551) - } - emptyView = emptyView.then { $0.frame = .init(x: 0, y: 0, width: view.bounds.width, height: 248) } @@ -399,38 +530,44 @@ extension StatisticsViewController: SkeletonLoadable { super.setHierarchy() view.addSubviews(tableView) - headerView.addSubviews(titleView, averageView, categoryView, activityView, satisfactionView) + headerView.addSubviews(budgetView, yetBudgetView, categoryView, averageView, activityView, satisfactionView) } override func setLayout() { super.setLayout() - titleView.snp.makeConstraints { - $0.top.equalToSuperview().inset(32) - $0.leading.equalToSuperview().inset(24) - $0.trailing.equalToSuperview().inset(UI.sideMargin) + budgetView.snp.makeConstraints { + $0.top.equalToSuperview().inset(12) + $0.leading.trailing.equalToSuperview().inset(24) + $0.height.equalTo(135) } - averageView.snp.makeConstraints { - $0.top.equalTo(titleView.snp.bottom) + yetBudgetView.snp.makeConstraints { + $0.top.equalToSuperview().inset(24) $0.leading.trailing.equalToSuperview().inset(UI.sideMargin) - $0.height.equalTo(64) + $0.height.equalTo(80) } categoryView.snp.makeConstraints { - $0.top.equalTo(averageView.snp.bottom).offset(16) + $0.top.equalTo(yetBudgetView.isHidden ? budgetView.snp.bottom : yetBudgetView.snp.bottom).offset(12) $0.leading.trailing.equalToSuperview().inset(UI.sideMargin) $0.height.equalTo(146) } + + averageView.snp.makeConstraints { + $0.top.equalTo(categoryView.snp.bottom).offset(12) + $0.leading.trailing.equalToSuperview().inset(UI.sideMargin) + $0.height.equalTo(50) + } activityView.snp.makeConstraints { - $0.top.equalTo(categoryView.snp.bottom).offset(16) + $0.top.equalTo(averageView.snp.bottom).offset(-14) $0.leading.trailing.equalToSuperview().inset(UI.sideMargin) $0.height.equalTo(100) } satisfactionView.snp.makeConstraints { - $0.top.equalTo(activityView.snp.bottom).offset(24) + $0.top.equalTo(averageView.snp.bottom).offset(26) $0.leading.trailing.equalToSuperview() $0.bottom.equalToSuperview() } @@ -439,5 +576,7 @@ extension StatisticsViewController: SkeletonLoadable { tableView.snp.makeConstraints { $0.edges.equalToSuperview() } + + activityView.isHidden = true // Timer가 돌아가지 않는 Bug 해결 } } diff --git a/MMM/Sources/ViewController/Statistics/Views/StatisticsActivityView.swift b/MMM/Sources/ViewController/Statistics/Views/StatisticsActivityView.swift index 9080af83..6db66905 100644 --- a/MMM/Sources/ViewController/Statistics/Views/StatisticsActivityView.swift +++ b/MMM/Sources/ViewController/Statistics/Views/StatisticsActivityView.swift @@ -252,7 +252,7 @@ extension StatisticsActivityView: SkeletonLoadable { setTimer() } - private func setTimer() { + func setTimer() { timer?.setEventHandler(handler: moveToIndex) } diff --git a/MMM/Sources/ViewController/Statistics/Views/StatisticsAverageView.swift b/MMM/Sources/ViewController/Statistics/Views/StatisticsAverageView.swift index 480dfbf6..daac7d39 100644 --- a/MMM/Sources/ViewController/Statistics/Views/StatisticsAverageView.swift +++ b/MMM/Sources/ViewController/Statistics/Views/StatisticsAverageView.swift @@ -5,30 +5,76 @@ // Created by geonhyeong on 2023/08/21. // +import UIKit import Then import SnapKit -import UIKit +import RxCocoa +import ReactorKit // 상속하지 않으려면 final 꼭 붙이기 -final class StatisticsAverageView: BaseView { +final class StatisticsAverageView: BaseView, View { typealias Reactor = StatisticsReactor // MARK: - Constants private enum UI { static let sideMargin: CGFloat = 20 - static let starLeading: CGFloat = 8 + static let starLeading: CGFloat = 4 } // MARK: - UI Components private lazy var titleLabel = UILabel() // 이번 달 경제활동 만족도 private lazy var satisfactionLabel = UILabel() private lazy var starImageView = UIImageView() // ⭐️ + private lazy var sumurryButton = UIButton() // 요약보기 + + func bind(reactor: StatisticsReactor) { + bindState(reactor) + bindAction(reactor) + } } +//MARK: - Bind +extension StatisticsAverageView { + // MARK: 데이터 변경 요청 및 버튼 클릭시 요청 로직(View -> Reactor) + private func bindAction(_ reactor: StatisticsReactor) { + // 요약보기/닫기 + sumurryButton.rx.tap + .map { .isSummary } + .bind(to: reactor.action) + .disposed(by: disposeBag) + } + + // MARK: 데이터 바인딩 처리 (Reactor -> View) + private func bindState(_ reactor: StatisticsReactor) { + // 요약하기 + reactor.state + .map { $0.isSummary } + .distinctUntilChanged() // 중복값 무시 + .withUnretained(self) + .bind { (this, isSummary) in + this.sumurryButton.setTitle(isSummary ? "요약보기" : "닫기", for: .normal) + }.disposed(by: disposeBag) + } +} + //MARK: - Action extension StatisticsAverageView { // 외부에서 설정 func setData(average: Double) { - satisfactionLabel.text = String(average) + if average == 0.0 { + starImageView.image = R.Icon.iconStarGray24 + titleLabel.text = "경제활동에 만족도를 설정해주세요" + titleLabel.textColor = R.Color.gray700 + satisfactionLabel.text = "0 점" + satisfactionLabel.textColor = R.Color.gray500 + sumurryButton.isHidden = true + } else { + starImageView.image = R.Icon.iconStarYellow24 + titleLabel.text = "경제활동 만족도" + titleLabel.textColor = R.Color.gray200 + satisfactionLabel.text = "\(average) 점" + satisfactionLabel.textColor = R.Color.white + sumurryButton.isHidden = false + } } func isLoading(_ isLoading: Bool) { @@ -46,45 +92,57 @@ extension StatisticsAverageView { backgroundColor = R.Color.black layer.cornerRadius = 10 - titleLabel = titleLabel.then { - $0.text = "이번 달 경제활동 만족도" - $0.font = R.Font.prtendard(family: .medium, size: 20) - $0.textColor = R.Color.white + starImageView = starImageView.then { + $0.image = R.Icon.iconStarYellow24 + $0.contentMode = .scaleAspectFit } satisfactionLabel = satisfactionLabel.then { - $0.text = "0.0" - $0.font = R.Font.prtendard(family: .bold, size: 36) - $0.textColor = R.Color.orange500 + $0.text = "0.0점" + $0.font = R.Font.prtendard(family: .bold, size: 18) + $0.textColor = R.Color.white } - starImageView = starImageView.then { - $0.image = R.Icon.iconStarYellow24 - $0.contentMode = .scaleAspectFit + titleLabel = titleLabel.then { + $0.text = "경제활동 만족도" + $0.font = R.Font.body3 + $0.textColor = R.Color.gray200 + } + + sumurryButton = sumurryButton.then { + $0.setTitle("요약보기", for: .normal) + $0.setTitleColor(R.Color.gray500, for: .normal) + $0.titleLabel?.font = R.Font.body4 } } override func setHierarchy() { super.setHierarchy() - addSubviews(titleLabel, satisfactionLabel, starImageView) + addSubviews(starImageView, satisfactionLabel, titleLabel, sumurryButton) } override func setLayout() { super.setLayout() - titleLabel.snp.makeConstraints { + starImageView.snp.makeConstraints { $0.centerY.equalToSuperview() $0.leading.equalToSuperview().inset(UI.sideMargin) } satisfactionLabel.snp.makeConstraints { $0.centerY.equalToSuperview() + $0.leading.equalTo(starImageView.snp.trailing).offset(8) } - starImageView.snp.makeConstraints { + titleLabel.snp.makeConstraints { + $0.centerY.equalToSuperview() + $0.leading.equalTo(satisfactionLabel.snp.trailing).offset(8) + } + + sumurryButton.snp.makeConstraints { $0.centerY.equalToSuperview() - $0.leading.equalTo(satisfactionLabel.snp.trailing).offset(UI.starLeading) + $0.height.equalToSuperview() $0.trailing.equalToSuperview().inset(UI.sideMargin) } } diff --git a/MMM/Sources/ViewController/Statistics/Views/StatisticsBudgetView.swift b/MMM/Sources/ViewController/Statistics/Views/StatisticsBudgetView.swift new file mode 100644 index 00000000..2f4de453 --- /dev/null +++ b/MMM/Sources/ViewController/Statistics/Views/StatisticsBudgetView.swift @@ -0,0 +1,491 @@ +// +// StatisticsBudgetView.swift +// MMM +// +// Created by geonhyeong on 2023/08/21. +// + +import Then +import SnapKit +import UIKit +import ReactorKit + +// 상속하지 않으려면 final 꼭 붙이기 +final class StatisticsBudgetView: BaseView, View { + // MARK: - Constants + private enum UI { + static let titleLabelTop: CGFloat = 6 + static let skTitleBottom: CGFloat = 16 + } + + enum State { + case less // 더 적음 + case more // 더 많음 + case over // 매우 많음 + + var title: String { + switch self { + case .less: return "예산보다 적게 지출하고 있어요 " + case .more: return "적정소비보다 더 지출하고 있어요 " + case .over: return "예산보다 많이 지출하고 있어요" + } + } + + var subTitle: String { + switch self { + case .less: return "이렇게만 지출하면 당신도 저축왕!" + case .more: return "예산을 위해 오늘의 지출을 줄여봐요" + case .over: return "다음 달은 조금 더 힘내볼까요? " + } + } + + var textColor: UIColor { + switch self { + case .less: return R.Color.black + case .more: return R.Color.white + case .over: return R.Color.white + } + } + + var barColor: UIColor { + switch self { + case .less: return R.Color.yellow600 + case .more: return R.Color.yellow800 + case .over: return R.Color.black + } + } + + var backColor: UIColor { + switch self { + case .less: return R.Color.yellow100 + case .more: return R.Color.black + case .over: return R.Color.black + } + } + } + // MARK: - Properties + private lazy var state: State = .less + private let totalWidth: CGFloat = UIScreen.width - 24 * 2 + private let monthList: [CGFloat] = [0, 30, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + private var standardLeading: CGFloat { + return setDate() + } + + // MARK: - UI Components + private lazy var titleLabel = UILabel() + private lazy var subTitleLabel = UILabel() + private lazy var imageView = UIImageView() // Boost 아이콘 + private lazy var totalBarView = UIView() + private lazy var currentBarView = UIView() + private lazy var percentLabel = UILabel() + private lazy var currentPayLabel = UILabel() // 현재 지출 + private lazy var separatorView = UIView() + private lazy var settingBudgetLabel = UILabel() // 예산 + private lazy var settingButton = UIButton() // 설정 + private lazy var standardView = UIView() + private lazy var standardLabel = UILabel() + private lazy var dotLine = UIView() // 점선 + + // 스켈레톤 UI + private lazy var skTitleView = UIView() + private lazy var rangeLayer = CAGradientLayer() + private lazy var titleLayer = CAGradientLayer() + + override func layoutSubviews() { + super.layoutSubviews() + + rangeLayer.frame = subTitleLabel.bounds + rangeLayer.cornerRadius = 4 + + titleLayer.frame = skTitleView.bounds + titleLayer.cornerRadius = 4 + } + + func bind(reactor: StatisticsReactor) { + bindState(reactor) + bindAction(reactor) + } +} +//MARK: - Bind +extension StatisticsBudgetView { + // MARK: 데이터 변경 요청 및 버튼 클릭시 요청 로직(View -> Reactor) + private func bindAction(_ reactor: StatisticsReactor) { + + settingButton.rx.tap + .map { .didTapNewTitleView } + .bind(to: reactor.action) + .disposed(by: disposeBag) + } + + // MARK: 데이터 바인딩 처리 (Reactor -> View) + private func bindState(_ reactor: StatisticsReactor) { + + reactor.state + .map { $0.percent } + .distinctUntilChanged() // 중복값 무시 + .bind(onNext: setPerent) // 그래프 값 변경 + .disposed(by: disposeBag) + + reactor.state + .map { $0.date } + .distinctUntilChanged() // 중복값 무시 + .bind(onNext: chageDate) + .disposed(by: disposeBag) + } +} +//MARK: - Action +extension StatisticsBudgetView { + // 외부에서 설정 + func setBudget(estimatedEarning: Int) { + self.settingBudgetLabel.text = "예산 \(estimatedEarning.withCommas()) 원" + } + + // 외부에서 설정 + func setCurrentPay(currentPayLabel: Int) { + self.currentPayLabel.text = "현재 \(currentPayLabel.withCommas()) 원" + } + + func chageDate(date: Date) { + let isToday = date.getFormattedYM() == Date().getFormattedYM() + let leading = setDate(date: isToday ? Date() : date, isLast: !isToday) + let month = Int(date.getFormattedDate(format: "MM")) ?? 1 + let totalDay: CGFloat = monthList[month] - 1 + let today: Int = Int(Date().getFormattedDate(format: "dd")) ?? 1 + let day: CGFloat = !isToday ? totalDay : CGFloat(today) + + standardView.snp.updateConstraints { + $0.leading.equalTo(totalBarView).offset(leading) + } + + dotLine.snp.remakeConstraints { + if day < 4 { + $0.leading.equalToSuperview().offset(5) + } else if day > totalDay - 2 { + $0.trailing.equalToSuperview().offset(-5) + } else { + $0.centerX.equalTo(standardLabel) + } + $0.top.equalToSuperview().inset(10) + $0.bottom.equalToSuperview() + } + } + + func setDate(date: Date = Date(), isLast: Bool = false) -> CGFloat { + let month = Int(date.getFormattedDate(format: "MM")) ?? 1 + let totalDay: CGFloat = monthList[month] - 1 + let today: Int = Int(date.getFormattedDate(format: "dd")) ?? 1 + let day: CGFloat = isLast ? totalDay : CGFloat(today) - 1 + let padding: CGFloat = 5 * 2 + let leading = (totalWidth - padding) / totalDay * day + let diff: CGFloat = day + 1 < 4 ? 0 : totalDay - 2 < day + 1 ? 52 : 26 + + return leading - diff + } + + func setPerent(percent: Int) { +// let percent = 101 // 임시 + self.percentLabel.text = "\(percent)%" + + let percent = percent > 100 ? 100 : percent + let width = totalWidth * Double(percent) / 100.0 + + self.currentBarView.snp.updateConstraints { + $0.width.equalTo(width) + } + + // 11% 미만일때, Text 위치 변경 + if percent < 11 { + percentLabel.snp.remakeConstraints { + $0.centerY.equalTo(currentBarView) + $0.leading.equalTo(currentBarView.snp.trailing).offset(6) + } + } else { + percentLabel.snp.remakeConstraints { + $0.centerY.equalTo(currentBarView) + $0.trailing.equalTo(currentBarView).offset(-6) + } + } + + if percent > 100 { + state = .over + } else if width < standardView.frame.minX + dotLine.frame.maxX { + state = .less + } else { + state = .more + } + + titleLabel.text = state.title + subTitleLabel.text = state.subTitle + totalBarView.backgroundColor = state.backColor + currentBarView.backgroundColor = state.barColor + percentLabel.textColor = state.textColor + } + + func isLoading(_ isLoading: Bool) { + titleLabel.isHidden = isLoading + imageView.isHidden = isLoading + + skTitleView.isHidden = !isLoading + rangeLayer.isHidden = !isLoading + titleLayer.isHidden = !isLoading + } + + //MARK: 임시 주석 (예산 업데이트로 인해 빠짐) - Text 부분적으로 Bold 처리 +// private func setSubTextBold() -> NSMutableAttributedString { +// let attributedText1 = NSMutableAttributedString(string: "부스트와 함께\n") +// let attributedText2 = NSMutableAttributedString(string: "만족하는 경제습관 ") +// let attributedText3 = NSMutableAttributedString(string: "만들기!") +// +// // 일반 Text 속성 +// let paragraphStyle = NSMutableParagraphStyle() +// paragraphStyle.lineSpacing = 4 +// let textAttributes1: [NSAttributedString.Key : Any] = [ +// .font: R.Font.body1, +// .foregroundColor: R.Color.white, +// .paragraphStyle: paragraphStyle +// ] +// +// // Bold Text 속성 +// let textAttributes2: [NSAttributedString.Key : Any] = [ +// .font: R.Font.title3, +// .foregroundColor: R.Color.white +// ] +// +// attributedText1.addAttributes(textAttributes1, range: NSMakeRange(0, attributedText1.length)) +// attributedText2.addAttributes(textAttributes2, range: NSMakeRange(0, attributedText2.length)) +// attributedText3.addAttributes(textAttributes1, range: NSMakeRange(0, attributedText3.length)) +// +// attributedText1.append(attributedText2) +// attributedText1.append(attributedText3) +// return attributedText1 +// } +} +//MARK: - Attribute & Hierarchy & Layouts +extension StatisticsBudgetView: SkeletonLoadable { + // 초기 셋업할 코드들 + override func setup() { + super.setup() + + setStandard() + setDotLine() + } + + func setDotLine() { + let layer = CAShapeLayer() + layer.strokeColor = R.Color.black.cgColor + layer.lineDashPattern = [2, 2] + + let path = UIBezierPath() + let point1 = CGPoint(x: dotLine.frame.midX, y: dotLine.frame.minY) + let point2 = CGPoint(x: dotLine.frame.midX, y: dotLine.frame.maxY) + + path.move(to: point1) + path.addLine(to: point2) + + layer.path = path.cgPath + dotLine.layer.addSublayer(layer) + } + + func setStandard() { + let date = Date() + let month = Int(date.getFormattedDate(format: "MM")) ?? 1 + let today: Int = Int(date.getFormattedDate(format: "dd")) ?? 1 + let totalDay: Int = Int(monthList[month]) + + standardView.snp.makeConstraints { + $0.leading.equalTo(totalBarView).offset(standardLeading) + $0.bottom.equalTo(totalBarView) + $0.width.equalTo(63) + $0.height.equalTo(49) + } + + standardLabel.snp.makeConstraints { + $0.top.leading.trailing.equalToSuperview() + $0.width.equalToSuperview() + $0.height.equalTo(22) + } + + dotLine.snp.makeConstraints { + if today < 4 { + $0.leading.equalToSuperview().offset(5) + } else if today > totalDay - 2 { + $0.trailing.equalToSuperview().offset(-5) + } else { + $0.centerX.equalTo(standardLabel) + } + $0.top.equalToSuperview().inset(10) + $0.bottom.equalToSuperview() + } + } + + // 초기 셋업할 코드들 + override func setAttribute() { + super.setAttribute() + + let firstGroup = makeAnimationGroup(startColor: R.Color.gray800, endColor: R.Color.gray600) + firstGroup.beginTime = 0.0 + rangeLayer = rangeLayer.then { + $0.isHidden = true // 임시: 다음 배포 + $0.startPoint = CGPoint(x: 0, y: 0.5) + $0.endPoint = CGPoint(x: 1, y: 0.5) + $0.add(firstGroup, forKey: "backgroundColor") + } + + titleLayer = titleLayer.then { + $0.isHidden = true // 임시: 다음 배포 + $0.startPoint = CGPoint(x: 0, y: 0.5) + $0.endPoint = CGPoint(x: 1, y: 0.5) + $0.add(firstGroup, forKey: "backgroundColor") + } + + skTitleView = skTitleView.then { + $0.isHidden = true // 임시: 다음 배포 + $0.frame = .init(origin: .zero, size: .init(width: 164, height: 24)) + $0.layer.addSublayer(titleLayer) + } + + titleLabel = titleLabel.then { + $0.text = state.title + $0.font = R.Font.prtendard(family: .bold, size: 16) + $0.textColor = R.Color.gray200 + } + + subTitleLabel = subTitleLabel.then { + $0.text = state.subTitle + $0.font = R.Font.body5 + $0.textColor = R.Color.gray300 + $0.layer.addSublayer(rangeLayer) + } + + imageView = imageView.then { + $0.image = R.Icon.characterHappy + $0.contentMode = .scaleAspectFit + } + + totalBarView = totalBarView.then { + $0.layer.cornerRadius = 4 + $0.backgroundColor = R.Color.yellow100 + } + + currentBarView = currentBarView.then { + $0.layer.cornerRadius = 4 + $0.backgroundColor = state.barColor + } + + standardLabel = standardLabel.then { + $0.text = "적정지출" + $0.textColor = R.Color.white + $0.font = R.Font.body5 + $0.clipsToBounds = true // cornerRadius 적용 + $0.layer.cornerRadius = 10 + $0.textAlignment = .center + $0.backgroundColor = R.Color.black + } + + dotLine = dotLine.then { + $0.frame = .init(x: 0, y: 0, width: 1, height: 39) + } + + percentLabel = percentLabel.then { + $0.text = "0%" + $0.textColor = state.textColor + $0.font = R.Font.body4 + } + + currentPayLabel = currentPayLabel.then { + $0.text = "현재 지출 0원" + $0.textColor = R.Color.yellow300 + $0.font = R.Font.body3 + } + + separatorView = separatorView.then { + $0.backgroundColor = R.Color.gray700 + } + + settingBudgetLabel = settingBudgetLabel.then { + $0.text = "예산 0원" + $0.textColor = R.Color.yellow050 + $0.font = R.Font.body3 + } + + settingButton = settingButton.then { + $0.setTitle("설정", for: .normal) + $0.setTitleColor(R.Color.gray500, for: .normal) + $0.titleLabel?.font = R.Font.body3 + $0.configuration?.contentInsets = .init(top: 10, leading: 10, bottom: 10, trailing: 10) // touch 영역 늘리기 + } + } + + override func setHierarchy() { + super.setHierarchy() + + addSubviews(titleLabel, subTitleLabel, imageView, skTitleView, totalBarView, currentBarView, percentLabel, standardView, currentPayLabel, separatorView, settingBudgetLabel, settingButton) + standardView.addSubviews(standardLabel, dotLine) + } + + override func setLayout() { + super.setLayout() + + titleLabel.snp.makeConstraints { + $0.top.equalToSuperview().inset(10) + $0.leading.equalToSuperview() + } + + subTitleLabel.snp.makeConstraints { + $0.top.equalTo(titleLabel.snp.bottom).offset(4) + $0.leading.equalToSuperview() + } + + imageView.snp.makeConstraints { + $0.leading.lessThanOrEqualTo(titleLabel.snp.trailing) + $0.top.trailing.equalToSuperview() + } + + skTitleView.snp.makeConstraints { + $0.bottom.equalToSuperview().inset(UI.skTitleBottom) + $0.leading.equalToSuperview() + $0.width.equalTo(164) + $0.height.equalTo(24) + } + + totalBarView.snp.makeConstraints { + $0.top.equalTo(subTitleLabel.snp.bottom).offset(30) + $0.horizontalEdges.equalToSuperview() + $0.height.equalTo(22) + } + + currentBarView.snp.makeConstraints { + $0.top.leading.equalTo(totalBarView) + $0.width.equalTo(totalWidth * 0.5) + $0.height.equalTo(22) + } + + percentLabel.snp.makeConstraints { + $0.centerY.equalTo(currentBarView) + $0.trailing.equalTo(currentBarView).offset(-6) + } + + currentPayLabel.snp.makeConstraints { + $0.top.equalTo(percentLabel.snp.bottom).offset(13) + $0.leading.equalToSuperview() + } + + separatorView.snp.makeConstraints { + $0.centerY.equalTo(currentPayLabel) + $0.leading.equalTo(currentPayLabel.snp.trailing).offset(10) + $0.width.equalTo(1) + $0.height.equalTo(9) + } + + settingBudgetLabel.snp.makeConstraints { + $0.top.equalTo(currentPayLabel) + $0.leading.equalTo(separatorView.snp.trailing).offset(10) + } + + settingButton.snp.makeConstraints { + $0.centerY.equalTo(currentPayLabel) + $0.trailing.equalToSuperview() + $0.leading.greaterThanOrEqualTo(settingBudgetLabel.snp.trailing).offset(10) + } + } +} diff --git a/MMM/Sources/ViewController/Statistics/Views/StatisticsCategoryView.swift b/MMM/Sources/ViewController/Statistics/Views/StatisticsCategoryView.swift index 52ec80c4..cdd8d842 100644 --- a/MMM/Sources/ViewController/Statistics/Views/StatisticsCategoryView.swift +++ b/MMM/Sources/ViewController/Statistics/Views/StatisticsCategoryView.swift @@ -33,6 +33,8 @@ final class StatisticsCategoryView: BaseView, View { // MARK: - Properties private lazy var alphaList: [CGFloat] = [1, 0.8, 0.5, 0.3, 0.2] + private lazy var alphaPayEmptyList: [CGFloat] = [1, 0.8, 0.5, 0.3, 0.2] + private lazy var alphaEarnEmptyList: [CGFloat] = [1, 0.8, 0.5, 0.3, 0.2] private lazy var barWidth: CGFloat = UIScreen.width - UI.rankViewSide - UI.sideMargin - 20 * 2 // 전체 Bar 길이 // MARK: - UI Components @@ -109,23 +111,32 @@ extension StatisticsCategoryView { } //MARK: - Action extension StatisticsCategoryView { + // 외부에서 설정 + func setData(isEmpty: Bool) { + payLabel.textColor = isEmpty ? R.Color.gray500 : R.Color.white + earnLabel.textColor = isEmpty ? R.Color.gray500 : R.Color.white + } + func convertData(_ data: [CategoryBar], _ type: String) { let isEmpty = data.isEmpty let str = data.enumerated().map { "\($0.offset + 1)위 \($0.element.title)"}.joined(separator: " " ) + payLabel.textColor = isEmpty ? R.Color.gray500 : R.Color.white + earnLabel.textColor = isEmpty ? R.Color.gray500 : R.Color.white + if type == "01" { payEmptyLabel.isHidden = !isEmpty payRankLabel.isHidden = isEmpty - payBarStackView.isHidden = isEmpty +// payBarStackView.isHidden = isEmpty payRankLabel.text = str } else { earnEmptyLabel.isHidden = !isEmpty earnRankLabel.isHidden = isEmpty - earnBarStackView.isHidden = isEmpty +// earnBarStackView.isHidden = isEmpty earnRankLabel.text = str } - convertBar(data, type) + convertBar(data, type, isEmpty) } func isLoading(_ isLoading: Bool) { @@ -138,14 +149,13 @@ extension StatisticsCategoryView { earnBarView.isHidden = !isLoading } - func convertBar(_ data: [CategoryBar], _ type: String) { - guard !data.isEmpty else { return } - + func convertBar(_ data: [CategoryBar], _ type: String, _ isEmpty: Bool) { + let data = isEmpty ? type == "01" ? CategoryBar.getPayList() : CategoryBar.getEarnList() : data let unit = barWidth / 100.0 let cnt = data.count let barView: UIStackView = type == "01" ? payBarStackView : earnBarStackView barView.subviews.forEach { $0.removeFromSuperview() } // 기존에 있던 subView 제거 - let color: UIColor = type == "01" ? R.Color.orange500 : R.Color.blue500 + let color: UIColor = isEmpty ? R.Color.gray700 : type == "01" ? R.Color.orange500 : R.Color.blue500 let minimumWidth = 3.0 var sumSmail = 0 var flag = true @@ -268,11 +278,10 @@ extension StatisticsCategoryView: SkeletonLoadable { payBarStackView = payBarStackView.then { $0.axis = .horizontal $0.spacing = 2 - $0.isHidden = true } payEmptyLabel = payEmptyLabel.then { - $0.text = "아직 카테고리에 작성된 경제활동이 없어요" + $0.text = "경제활동에 카테고리를 설정해주세요" $0.font = R.Font.body3 $0.textColor = R.Color.gray700 $0.textAlignment = .center @@ -298,12 +307,11 @@ extension StatisticsCategoryView: SkeletonLoadable { earnBarStackView = earnBarStackView.then { $0.axis = .horizontal $0.spacing = 2 - $0.isHidden = true $0.distribution = .equalSpacing } earnEmptyLabel = earnEmptyLabel.then { - $0.text = "아직 카테고리에 작성된 경제활동이 없어요" + $0.text = "경제활동에 카테고리를 설정해주세요" $0.font = R.Font.body3 $0.textColor = R.Color.gray700 $0.textAlignment = .center @@ -349,9 +357,8 @@ extension StatisticsCategoryView: SkeletonLoadable { } payEmptyLabel.snp.makeConstraints { - $0.leading.equalTo(payLabel.snp.trailing).offset(12) - $0.trailing.equalToSuperview().inset(20) - $0.centerY.equalTo(payLabel) + $0.top.equalToSuperview().offset(UI.payRankLabelTop) + $0.leading.equalTo(earnLabel.snp.trailing).offset(12) } earnLabel.snp.makeConstraints { @@ -373,9 +380,8 @@ extension StatisticsCategoryView: SkeletonLoadable { } earnEmptyLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(UI.earnRankLabelTop) $0.leading.equalTo(earnLabel.snp.trailing).offset(12) - $0.trailing.equalToSuperview().inset(20) - $0.centerY.equalTo(earnLabel) } // 스켈레톤 Layer diff --git a/MMM/Sources/ViewController/Statistics/Views/StatisticsTitleView.swift b/MMM/Sources/ViewController/Statistics/Views/StatisticsTitleView.swift deleted file mode 100644 index b95ac6c1..00000000 --- a/MMM/Sources/ViewController/Statistics/Views/StatisticsTitleView.swift +++ /dev/null @@ -1,163 +0,0 @@ -// -// StatisticsTitleView.swift -// MMM -// -// Created by geonhyeong on 2023/08/21. -// - -import Then -import SnapKit -import UIKit - -// 상속하지 않으려면 final 꼭 붙이기 -final class StatisticsTitleView: BaseView { - // MARK: - Constants - private enum UI { - static let titleLabelTop: CGFloat = 6 - static let skTitleBottom: CGFloat = 16 - } - - // MARK: - UI Components - private lazy var rangeLabel = UILabel() // 통계 범위 - private lazy var titleLabel = UILabel() - private lazy var imageView = UIImageView() // Boost 아이콘 - // 스켈레톤 UI - private lazy var skTitleView = UIView() - private lazy var rangeLayer = CAGradientLayer() - private lazy var titleLayer = CAGradientLayer() - - override func layoutSubviews() { - super.layoutSubviews() - - rangeLayer.frame = rangeLabel.bounds - rangeLayer.cornerRadius = 4 - - titleLayer.frame = skTitleView.bounds - titleLayer.cornerRadius = 4 - } -} -//MARK: - Action -extension StatisticsTitleView { - // 외부에서 설정 - func setData(startDate: String, endDate: String) { - rangeLabel.text = startDate + " ~ " + endDate - } - - func isLoading(_ isLoading: Bool) { - titleLabel.isHidden = isLoading - imageView.isHidden = isLoading - - skTitleView.isHidden = !isLoading - rangeLayer.isHidden = !isLoading - titleLayer.isHidden = !isLoading - } - - // Text 부분적으로 Bold 처리 - private func setSubTextBold() -> NSMutableAttributedString { - let attributedText1 = NSMutableAttributedString(string: "부스트와 함께\n") - let attributedText2 = NSMutableAttributedString(string: "만족하는 경제습관 ") - let attributedText3 = NSMutableAttributedString(string: "만들기!") - - // 일반 Text 속성 - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineSpacing = 4 - let textAttributes1: [NSAttributedString.Key : Any] = [ - .font: R.Font.body1, - .foregroundColor: R.Color.white, - .paragraphStyle: paragraphStyle - ] - - // Bold Text 속성 - let textAttributes2: [NSAttributedString.Key : Any] = [ - .font: R.Font.title3, - .foregroundColor: R.Color.white - ] - - attributedText1.addAttributes(textAttributes1, range: NSMakeRange(0, attributedText1.length)) - attributedText2.addAttributes(textAttributes2, range: NSMakeRange(0, attributedText2.length)) - attributedText3.addAttributes(textAttributes1, range: NSMakeRange(0, attributedText3.length)) - - attributedText1.append(attributedText2) - attributedText1.append(attributedText3) - return attributedText1 - } -} -//MARK: - Attribute & Hierarchy & Layouts -extension StatisticsTitleView: SkeletonLoadable { - // 초기 셋업할 코드들 - override func setAttribute() { - super.setAttribute() - - let firstGroup = makeAnimationGroup(startColor: R.Color.gray800, endColor: R.Color.gray600) - firstGroup.beginTime = 0.0 - rangeLayer = rangeLayer.then { - $0.isHidden = true // 임시: 다음 배포 - $0.startPoint = CGPoint(x: 0, y: 0.5) - $0.endPoint = CGPoint(x: 1, y: 0.5) - $0.add(firstGroup, forKey: "backgroundColor") - } - - titleLayer = titleLayer.then { - $0.isHidden = true // 임시: 다음 배포 - $0.startPoint = CGPoint(x: 0, y: 0.5) - $0.endPoint = CGPoint(x: 1, y: 0.5) - $0.add(firstGroup, forKey: "backgroundColor") - } - - skTitleView = skTitleView.then { - $0.isHidden = true // 임시: 다음 배포 - $0.frame = .init(origin: .zero, size: .init(width: 164, height: 24)) - $0.layer.addSublayer(titleLayer) - } - - rangeLabel = rangeLabel.then { - let month = Date().getFormattedDate(format: "MM") // 이번달 - let today = Date().getFormattedDate(format: "dd") // 오늘날짜 - $0.text = "\(month).01 ~ \(month).\(today)" - $0.font = R.Font.prtendard(family: .medium, size: 12) - $0.textColor = R.Color.gray500 - $0.layer.addSublayer(rangeLayer) - } - - titleLabel = titleLabel.then { - $0.attributedText = setSubTextBold() - $0.numberOfLines = 2 - } - - imageView = imageView.then { - $0.image = R.Icon.characterHappy - $0.contentMode = .scaleAspectFit - } - } - - override func setHierarchy() { - super.setHierarchy() - - addSubviews(rangeLabel, titleLabel, imageView, skTitleView) - } - - override func setLayout() { - super.setLayout() - - rangeLabel.snp.makeConstraints { - $0.top.leading.equalToSuperview() - } - - titleLabel.snp.makeConstraints { - $0.top.equalTo(rangeLabel.snp.bottom).offset(UI.titleLabelTop) - $0.leading.equalToSuperview() - } - - imageView.snp.makeConstraints { - $0.leading.lessThanOrEqualTo(titleLabel.snp.trailing) - $0.top.trailing.bottom.equalToSuperview() - } - - skTitleView.snp.makeConstraints { - $0.bottom.equalToSuperview().inset(UI.skTitleBottom) - $0.leading.equalToSuperview() - $0.width.equalTo(164) - $0.height.equalTo(24) - } - } -} diff --git a/MMM/Sources/ViewController/Statistics/Views/StatisticsYetBudgetView.swift b/MMM/Sources/ViewController/Statistics/Views/StatisticsYetBudgetView.swift new file mode 100644 index 00000000..623fe9f0 --- /dev/null +++ b/MMM/Sources/ViewController/Statistics/Views/StatisticsYetBudgetView.swift @@ -0,0 +1,99 @@ +// +// StatisticsYetBudgetView.swift +// MMM +// +// Created by geonhyeong on 2/13/24. +// + +import Then +import SnapKit +import UIKit + +// 상속하지 않으려면 final 꼭 붙이기 +final class StatisticsYetBudgetView: BaseView { + // MARK: - Constants + private enum UI { + static let titleLabelTop: CGFloat = 6 + } + + // MARK: - UI Components + private lazy var titleLabel = UILabel() + private lazy var imageView = UIImageView() // Boost 아이콘 + + override func layoutSubviews() { + super.layoutSubviews() + } +} +//MARK: - Action +extension StatisticsYetBudgetView { + // Text 부분적으로 강조 처리 + private func setSubTextColor() -> NSMutableAttributedString { + let attributedText1 = NSMutableAttributedString(string: "누구나 실천 가능한\n") + let attributedText2 = NSMutableAttributedString(string: "이번 달 지출 에산 ") + let attributedText3 = NSMutableAttributedString(string: "설정하기") + + // 일반 Text 속성 + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineSpacing = 4 + let textAttributes1: [NSAttributedString.Key : Any] = [ + .font: R.Font.title3, + .foregroundColor: R.Color.white, + .paragraphStyle: paragraphStyle + ] + + // 강조 Text 속성 + let textAttributes2: [NSAttributedString.Key : Any] = [ + .font: R.Font.title3, + .foregroundColor: R.Color.orange400 + ] + + attributedText1.addAttributes(textAttributes1, range: NSMakeRange(0, attributedText1.length)) + attributedText2.addAttributes(textAttributes2, range: NSMakeRange(0, attributedText2.length)) + attributedText3.addAttributes(textAttributes1, range: NSMakeRange(0, attributedText3.length)) + + attributedText1.append(attributedText2) + attributedText1.append(attributedText3) + return attributedText1 + } +} +//MARK: - Attribute & Hierarchy & Layouts +extension StatisticsYetBudgetView: SkeletonLoadable { + // 초기 셋업할 코드들 + override func setAttribute() { + super.setAttribute() + + backgroundColor = R.Color.black + layer.cornerRadius = 10 + + titleLabel = titleLabel.then { + $0.attributedText = setSubTextColor() + $0.numberOfLines = 2 + } + + imageView = imageView.then { + $0.image = R.Icon.characterBudget + $0.contentMode = .scaleAspectFit + } + } + + override func setHierarchy() { + super.setHierarchy() + + addSubviews(titleLabel, imageView) + } + + override func setLayout() { + super.setLayout() + + titleLabel.snp.makeConstraints { + $0.centerY.equalToSuperview() + $0.leading.equalToSuperview().inset(20) + } + + imageView.snp.makeConstraints { + $0.leading.lessThanOrEqualTo(titleLabel.snp.trailing) + $0.top.bottom.equalToSuperview() + $0.trailing.equalToSuperview() + } + } +} diff --git a/MMM/Sources/ViewModels/HomeViewModel.swift b/MMM/Sources/ViewModels/HomeViewModel.swift index fc3cf0af..f57a9e29 100644 --- a/MMM/Sources/ViewModels/HomeViewModel.swift +++ b/MMM/Sources/ViewModels/HomeViewModel.swift @@ -127,7 +127,8 @@ extension HomeViewModel { UserDefaults.shared.set(response.pay, forKey: "pay") WidgetCenter.shared.reloadAllTimelines() } - }).store(in: &cancellable) + }) + .store(in: &cancellable) } func getWeeklyList(_ date1YM: String, _ date2YM: String) {