diff --git a/MLS/.swiftlint.yml b/MLS/.swiftlint.yml index 16cb8d52..904d2c12 100644 --- a/MLS/.swiftlint.yml +++ b/MLS/.swiftlint.yml @@ -1,3 +1,7 @@ +excluded: + - "**/.build" + - "**/SourcePackages" + # 기본(default) 룰이 아닌 룰들을 활성화 opt_in_rules: - sorted_imports diff --git a/MLS/MLS.xcodeproj/project.pbxproj b/MLS/MLS.xcodeproj/project.pbxproj index 46e458d9..6c989cc7 100644 --- a/MLS/MLS.xcodeproj/project.pbxproj +++ b/MLS/MLS.xcodeproj/project.pbxproj @@ -15,11 +15,14 @@ 084A25562DB93BC800C395C0 /* Data.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 084A25542DB93BC800C395C0 /* Data.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 084A25D72DB93E2C00C395C0 /* Core.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 084A25D62DB93E2C00C395C0 /* Core.framework */; }; 084A25D82DB93E2C00C395C0 /* Core.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 084A25D62DB93E2C00C395C0 /* Core.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 085598AC2FB4A6B7003F315F /* MLSRecommendationFeature in Frameworks */ = {isa = PBXBuildFile; productRef = 085598AB2FB4A6B7003F315F /* MLSRecommendationFeature */; }; + 085598AE2FB4A6BD003F315F /* MLSRecommendationFeatureInterface in Frameworks */ = {isa = PBXBuildFile; productRef = 085598AD2FB4A6BD003F315F /* MLSRecommendationFeatureInterface */; }; 0858ABFC2DCFDBF20060EBCA /* DesignSystem.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0858ABFB2DCFDBF20060EBCA /* DesignSystem.framework */; }; 0858ABFD2DCFDBF20060EBCA /* DesignSystem.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0858ABFB2DCFDBF20060EBCA /* DesignSystem.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 085A7F752DAF99570046663F /* .swiftlint.yml in Resources */ = {isa = PBXBuildFile; fileRef = 085A7F742DAF99570046663F /* .swiftlint.yml */; }; 085BDF5E2DF6B6B3009CFB90 /* DataMock.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 085BDF5D2DF6B6B3009CFB90 /* DataMock.framework */; }; 085BDF5F2DF6B6B3009CFB90 /* DataMock.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 085BDF5D2DF6B6B3009CFB90 /* DataMock.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 088636972FB4664E00006D4A /* MLSRecommendationFeatureTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 088636962FB4664E00006D4A /* MLSRecommendationFeatureTesting */; }; 08DA51B42E1B9827009097A6 /* FirebaseFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = 08DA51B32E1B9827009097A6 /* FirebaseFirestore */; }; 08DA51B62E1B9827009097A6 /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = 08DA51B52E1B9827009097A6 /* FirebaseMessaging */; }; 08DA58A72E1E5BE3009097A6 /* DictionaryFeature.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 08DA58A62E1E5BE3009097A6 /* DictionaryFeature.framework */; }; @@ -146,8 +149,8 @@ 08DA58A62E1E5BE3009097A6 /* DictionaryFeature.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DictionaryFeature.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 08DA58A92E1E5BEB009097A6 /* DictionaryFeatureInterface.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DictionaryFeatureInterface.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 08F7A9232F86745C00EF5C06 /* MLSAuthFeatureExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MLSAuthFeatureExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 77217FBE2F9A04CF000915EF /* MLSMyPageFeatureExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MLSMyPageFeatureExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 08F7DC612F9DEA8000EF5C06 /* MLSRecommendationFeatureExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MLSRecommendationFeatureExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 77217FBE2F9A04CF000915EF /* MLSMyPageFeatureExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MLSMyPageFeatureExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 772199F12E0E7EC800A7B58C /* AuthFeatureInterface.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AuthFeatureInterface.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7721A5032E0EE7AE00A7B58C /* BaseFeature.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = BaseFeature.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 77660AD12DD0D361007A4EF3 /* KakaoConfig.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = KakaoConfig.xcconfig; sourceTree = ""; }; @@ -179,20 +182,19 @@ ); target = 08F7A9222F86745C00EF5C06 /* MLSAuthFeatureExample */; }; - 77217FCF2F9A04D0000915EF /* Exceptions for "MLSMyPageFeatureExample" folder in "MLSMyPageFeatureExample" target */ = { - isa = PBXFileSystemSynchronizedBuildFileExceptionSet; - membershipExceptions = ( - Info.plist, - ); - target = 77217FBD2F9A04CF000915EF /* MLSMyPageFeatureExample */; - }; - - 08F7DC722F9DEA8100EF5C06 /* Exceptions for "MLSRecommendationFeatureExample" folder in "MLSRecommendationFeatureExample" target */ = { - isa = PBXFileSystemSynchronizedBuildFileExceptionSet; - membershipExceptions = ( - Info.plist, - ); - target = 08F7DC602F9DEA8000EF5C06 /* MLSRecommendationFeatureExample */; + 08F7DC722F9DEA8100EF5C06 /* Exceptions for "MLSRecommendationFeatureExample" folder in "MLSRecommendationFeatureExample" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 08F7DC602F9DEA8000EF5C06 /* MLSRecommendationFeatureExample */; + }; + 77217FCF2F9A04D0000915EF /* Exceptions for "MLSMyPageFeatureExample" folder in "MLSMyPageFeatureExample" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 77217FBD2F9A04CF000915EF /* MLSMyPageFeatureExample */; }; 77FA688B2F72C7380064B6EB /* Exceptions for "MLSDesignSystemExample" folder in "MLSDesignSystemExample" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; @@ -220,14 +222,6 @@ path = MLSAuthFeatureExample; sourceTree = ""; }; - 77217FBF2F9A04CF000915EF /* MLSMyPageFeatureExample */ = { - isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - 77217FCF2F9A04D0000915EF /* Exceptions for "MLSMyPageFeatureExample" folder in "MLSMyPageFeatureExample" target */, - ); - path = MLSMyPageFeatureExample; - sourceTree = ""; - }; 08F7DC622F9DEA8000EF5C06 /* MLSRecommendationFeatureExample */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( @@ -236,6 +230,14 @@ path = MLSRecommendationFeatureExample; sourceTree = ""; }; + 77217FBF2F9A04CF000915EF /* MLSMyPageFeatureExample */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 77217FCF2F9A04D0000915EF /* Exceptions for "MLSMyPageFeatureExample" folder in "MLSMyPageFeatureExample" target */, + ); + path = MLSMyPageFeatureExample; + sourceTree = ""; + }; 77BEB0412DBA84B0002FFCFC /* MLSTests */ = { isa = PBXFileSystemSynchronizedRootGroup; path = MLSTests; @@ -270,6 +272,7 @@ 08DA51B42E1B9827009097A6 /* FirebaseFirestore in Frameworks */, 7777F7042E9EAB8400F53D68 /* BookmarkFeatureInterface.framework in Frameworks */, 084A25552DB93BC800C395C0 /* Data.framework in Frameworks */, + 085598AC2FB4A6B7003F315F /* MLSRecommendationFeature in Frameworks */, 084A25D72DB93E2C00C395C0 /* Core.framework in Frameworks */, 08ED49282DCFDED4002C21A2 /* RxCocoa in Frameworks */, 7777F7082E9EAC0D00F53D68 /* MyPageFeature.framework in Frameworks */, @@ -278,6 +281,7 @@ 77660AD52DD0D3DD007A4EF3 /* KakaoSDKAuth in Frameworks */, 772199F22E0E7EC800A7B58C /* AuthFeatureInterface.framework in Frameworks */, 779A49102E1AD26D00ABDE4F /* BookmarkFeatureInterface.framework in Frameworks */, + 085598AE2FB4A6BD003F315F /* MLSRecommendationFeatureInterface in Frameworks */, 084A25522DB93BC500C395C0 /* Domain.framework in Frameworks */, 77660AD72DD0D3DD007A4EF3 /* KakaoSDKUser in Frameworks */, 085BDF5E2DF6B6B3009CFB90 /* DataMock.framework in Frameworks */, @@ -297,6 +301,17 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 08F7DC5E2F9DEA8000EF5C06 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 08F7DC802F9DEA8100EF5C06 /* MLSRecommendationFeature in Frameworks */, + 08F7DC822F9DEA8100EF5C06 /* MLSRecommendationFeatureInterface in Frameworks */, + 088636972FB4664E00006D4A /* MLSRecommendationFeatureTesting in Frameworks */, + 08F7DC842F9DEA8100EF5C06 /* MLSCore in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 77217FBB2F9A04CF000915EF /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -305,16 +320,6 @@ 77DE6DEF2F9F9EA1007FD8AC /* MLSAuthFeatureTesting in Frameworks */, 773681502F9A2CE3002DC773 /* MLSMyPageFeatureTesting in Frameworks */, 7736814E2F9A2CE3002DC773 /* MLSMyPageFeatureInterface in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 08F7DC5E2F9DEA8000EF5C06 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 08F7DC802F9DEA8100EF5C06 /* MLSRecommendationFeature in Frameworks */, - 08F7DC822F9DEA8100EF5C06 /* MLSRecommendationFeatureInterface in Frameworks */, - 08F7DC842F9DEA8100EF5C06 /* MLSCore in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -474,6 +479,8 @@ 08DA51B52E1B9827009097A6 /* FirebaseMessaging */, 770ADB1E2E433EDA00270506 /* RxKeyboard */, 77B1F9942EE06A4E00AE4B4D /* RxGesture */, + 085598AB2FB4A6B7003F315F /* MLSRecommendationFeature */, + 085598AD2FB4A6BD003F315F /* MLSRecommendationFeatureInterface */, ); productName = MLS; productReference = 087D3EE82DA7972C002F924D /* MLS.app */; @@ -504,57 +511,58 @@ productReference = 08F7A9232F86745C00EF5C06 /* MLSAuthFeatureExample.app */; productType = "com.apple.product-type.application"; }; - 77217FBD2F9A04CF000915EF /* MLSMyPageFeatureExample */ = { - isa = PBXNativeTarget; - buildConfigurationList = 77217FD02F9A04D0000915EF /* Build configuration list for PBXNativeTarget "MLSMyPageFeatureExample" */; - buildPhases = ( - 77217FBA2F9A04CF000915EF /* Sources */, - 77217FBB2F9A04CF000915EF /* Frameworks */, - 77217FBC2F9A04CF000915EF /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - fileSystemSynchronizedGroups = ( - 77217FBF2F9A04CF000915EF /* MLSMyPageFeatureExample */, - ); - name = MLSMyPageFeatureExample; - packageProductDependencies = ( - 77217FD32F9A05D7000915EF /* MLSMyPageFeature */, - 7736814D2F9A2CE3002DC773 /* MLSMyPageFeatureInterface */, - 7736814F2F9A2CE3002DC773 /* MLSMyPageFeatureTesting */, - 77DE6DEE2F9F9EA1007FD8AC /* MLSAuthFeatureTesting */, - ); - productName = MLSMyPageFeatureExample; - productReference = 77217FBE2F9A04CF000915EF /* MLSMyPageFeatureExample.app */; - productType = "com.apple.product-type.application"; - }; - 08F7DC602F9DEA8000EF5C06 /* MLSRecommendationFeatureExample */ = { - isa = PBXNativeTarget; - buildConfigurationList = 08F7DC732F9DEA8100EF5C06 /* Build configuration list for PBXNativeTarget "MLSRecommendationFeatureExample" */; - buildPhases = ( - 08F7DC5D2F9DEA8000EF5C06 /* Sources */, - 08F7DC5E2F9DEA8000EF5C06 /* Frameworks */, - 08F7DC5F2F9DEA8000EF5C06 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - fileSystemSynchronizedGroups = ( - 08F7DC622F9DEA8000EF5C06 /* MLSRecommendationFeatureExample */, - ); - name = MLSRecommendationFeatureExample; - packageProductDependencies = ( - 08F7DC812F9DEA8100EF5C06 /* MLSRecommendationFeature */, - 08F7DC832F9DEA8100EF5C06 /* MLSRecommendationFeatureInterface */, - 08F7DC852F9DEA8100EF5C06 /* MLSCore */, - ); - productName = MLSRecommendationFeatureExample; - productReference = 08F7DC612F9DEA8000EF5C06 /* MLSRecommendationFeatureExample.app */; - productType = "com.apple.product-type.application"; - }; + 08F7DC602F9DEA8000EF5C06 /* MLSRecommendationFeatureExample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 08F7DC732F9DEA8100EF5C06 /* Build configuration list for PBXNativeTarget "MLSRecommendationFeatureExample" */; + buildPhases = ( + 08F7DC5D2F9DEA8000EF5C06 /* Sources */, + 08F7DC5E2F9DEA8000EF5C06 /* Frameworks */, + 08F7DC5F2F9DEA8000EF5C06 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 08F7DC622F9DEA8000EF5C06 /* MLSRecommendationFeatureExample */, + ); + name = MLSRecommendationFeatureExample; + packageProductDependencies = ( + 08F7DC812F9DEA8100EF5C06 /* MLSRecommendationFeature */, + 08F7DC832F9DEA8100EF5C06 /* MLSRecommendationFeatureInterface */, + 08F7DC852F9DEA8100EF5C06 /* MLSCore */, + 088636962FB4664E00006D4A /* MLSRecommendationFeatureTesting */, + ); + productName = MLSRecommendationFeatureExample; + productReference = 08F7DC612F9DEA8000EF5C06 /* MLSRecommendationFeatureExample.app */; + productType = "com.apple.product-type.application"; + }; + 77217FBD2F9A04CF000915EF /* MLSMyPageFeatureExample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 77217FD02F9A04D0000915EF /* Build configuration list for PBXNativeTarget "MLSMyPageFeatureExample" */; + buildPhases = ( + 77217FBA2F9A04CF000915EF /* Sources */, + 77217FBB2F9A04CF000915EF /* Frameworks */, + 77217FBC2F9A04CF000915EF /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 77217FBF2F9A04CF000915EF /* MLSMyPageFeatureExample */, + ); + name = MLSMyPageFeatureExample; + packageProductDependencies = ( + 77217FD32F9A05D7000915EF /* MLSMyPageFeature */, + 7736814D2F9A2CE3002DC773 /* MLSMyPageFeatureInterface */, + 7736814F2F9A2CE3002DC773 /* MLSMyPageFeatureTesting */, + 77DE6DEE2F9F9EA1007FD8AC /* MLSAuthFeatureTesting */, + ); + productName = MLSMyPageFeatureExample; + productReference = 77217FBE2F9A04CF000915EF /* MLSMyPageFeatureExample.app */; + productType = "com.apple.product-type.application"; + }; 77BEB03F2DBA84B0002FFCFC /* MLSTests */ = { isa = PBXNativeTarget; buildConfigurationList = 77BEB0482DBA84B0002FFCFC /* Build configuration list for PBXNativeTarget "MLSTests" */; @@ -621,12 +629,12 @@ 08F7A9222F86745C00EF5C06 = { CreatedOnToolsVersion = 26.1.1; }; - 77217FBD2F9A04CF000915EF = { - CreatedOnToolsVersion = 26.1.1; - }; 08F7DC602F9DEA8000EF5C06 = { CreatedOnToolsVersion = 26.1.1; }; + 77217FBD2F9A04CF000915EF = { + CreatedOnToolsVersion = 26.1.1; + }; 77BEB03F2DBA84B0002FFCFC = { CreatedOnToolsVersion = 16.2; TestTargetID = 087D3EE72DA7972C002F924D; @@ -713,13 +721,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 77217FBC2F9A04CF000915EF /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; 08F7DC5F2F9DEA8000EF5C06 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -727,6 +728,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 77217FBC2F9A04CF000915EF /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 77BEB03E2DBA84B0002FFCFC /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -779,13 +787,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 77217FBA2F9A04CF000915EF /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; 08F7DC5D2F9DEA8000EF5C06 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -793,6 +794,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 77217FBA2F9A04CF000915EF /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 77BEB03C2DBA84B0002FFCFC /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1095,151 +1103,151 @@ }; name = Release; }; - 77217FD12F9A04D0000915EF /* Debug */ = { + 08F7DC742F9DEA8100EF5C06 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = W5QTRMS954; ENABLE_USER_SCRIPT_SANDBOXING = YES; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = MLSMyPageFeatureExample/Info.plist; + INFOPLIST_FILE = MLSRecommendationFeatureExample/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 15.6; + IPHONEOS_DEPLOYMENT_TARGET = 26.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.donggle.MLSDesignSystemExample.MLSMyPageFeatureExample; + PRODUCT_BUNDLE_IDENTIFIER = com.donggle.MLS.RecommendationFeatureExample; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = MLSRecommendationFeatureProvisioningProfileDev; STRING_CATALOG_GENERATE_SYMBOLS = YES; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; - SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; - 77217FD22F9A04D0000915EF /* Release */ = { + 08F7DC752F9DEA8100EF5C06 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = W5QTRMS954; ENABLE_USER_SCRIPT_SANDBOXING = YES; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = MLSMyPageFeatureExample/Info.plist; + INFOPLIST_FILE = MLSRecommendationFeatureExample/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 15.6; + IPHONEOS_DEPLOYMENT_TARGET = 26.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.donggle.MLSDesignSystemExample.MLSMyPageFeatureExample; + PRODUCT_BUNDLE_IDENTIFIER = com.donggle.MLS.RecommendationFeatureExample; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = MLSRecommendationFeatureProvisioningProfile; STRING_CATALOG_GENERATE_SYMBOLS = YES; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; - SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; - 08F7DC742F9DEA8100EF5C06 /* Debug */ = { + 77217FD12F9A04D0000915EF /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = W5QTRMS954; ENABLE_USER_SCRIPT_SANDBOXING = YES; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = MLSRecommendationFeatureExample/Info.plist; + INFOPLIST_FILE = MLSMyPageFeatureExample/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 26.1; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.donggle.MLS.RecommendationFeatureExample; + PRODUCT_BUNDLE_IDENTIFIER = com.donggle.MLSDesignSystemExample.MLSMyPageFeatureExample; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = MLSRecommendationFeatureProvisioningProfileDev; STRING_CATALOG_GENERATE_SYMBOLS = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; }; name = Debug; }; - 08F7DC752F9DEA8100EF5C06 /* Release */ = { + 77217FD22F9A04D0000915EF /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = W5QTRMS954; ENABLE_USER_SCRIPT_SANDBOXING = YES; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = MLSRecommendationFeatureExample/Info.plist; + INFOPLIST_FILE = MLSMyPageFeatureExample/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 26.1; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.donggle.MLS.RecommendationFeatureExample; + PRODUCT_BUNDLE_IDENTIFIER = com.donggle.MLSDesignSystemExample.MLSMyPageFeatureExample; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = MLSRecommendationFeatureProvisioningProfile; STRING_CATALOG_GENERATE_SYMBOLS = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; }; name = Release; }; @@ -1387,15 +1395,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 77217FD02F9A04D0000915EF /* Build configuration list for PBXNativeTarget "MLSMyPageFeatureExample" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 77217FD12F9A04D0000915EF /* Debug */, - 77217FD22F9A04D0000915EF /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; 08F7DC732F9DEA8100EF5C06 /* Build configuration list for PBXNativeTarget "MLSRecommendationFeatureExample" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -1405,6 +1404,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 77217FD02F9A04D0000915EF /* Build configuration list for PBXNativeTarget "MLSMyPageFeatureExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 77217FD12F9A04D0000915EF /* Debug */, + 77217FD22F9A04D0000915EF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 77BEB0482DBA84B0002FFCFC /* Build configuration list for PBXNativeTarget "MLSTests" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -1485,6 +1493,18 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 085598AB2FB4A6B7003F315F /* MLSRecommendationFeature */ = { + isa = XCSwiftPackageProductDependency; + productName = MLSRecommendationFeature; + }; + 085598AD2FB4A6BD003F315F /* MLSRecommendationFeatureInterface */ = { + isa = XCSwiftPackageProductDependency; + productName = MLSRecommendationFeatureInterface; + }; + 088636962FB4664E00006D4A /* MLSRecommendationFeatureTesting */ = { + isa = XCSwiftPackageProductDependency; + productName = MLSRecommendationFeatureTesting; + }; 08DA51B32E1B9827009097A6 /* FirebaseFirestore */ = { isa = XCSwiftPackageProductDependency; package = 08DA51B22E1B9827009097A6 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; diff --git a/MLS/MLS/Application/AppCoordinator.swift b/MLS/MLS/Application/AppCoordinator.swift index 947e4dea..03f1764b 100644 --- a/MLS/MLS/Application/AppCoordinator.swift +++ b/MLS/MLS/Application/AppCoordinator.swift @@ -6,6 +6,7 @@ import BaseFeature import BookmarkFeatureInterface import DesignSystem import DictionaryFeatureInterface +import MLSRecommendationFeatureInterface import MyPageFeatureInterface import RxSwift @@ -13,6 +14,7 @@ import RxSwift public final class AppCoordinator: AppCoordinatorProtocol { // MARK: - Properties public var window: UIWindow? + private let recommendationMainFactory: RecommendationMainFactory private let dictionaryMainViewFactory: DictionaryMainViewFactory private let bookmarkMainFactory: BookmarkMainFactory private let myPageMainFactory: MyPageMainFactory @@ -23,12 +25,14 @@ public final class AppCoordinator: AppCoordinatorProtocol { // MARK: - Init public init( window: UIWindow?, + recommendationMainFactory: RecommendationMainFactory, dictionaryMainViewFactory: DictionaryMainViewFactory, bookmarkMainFactory: BookmarkMainFactory, myPageMainFactory: MyPageMainFactory, loginFactory: LoginFactory ) { self.window = window + self.recommendationMainFactory = recommendationMainFactory self.dictionaryMainViewFactory = dictionaryMainViewFactory self.bookmarkMainFactory = bookmarkMainFactory self.myPageMainFactory = myPageMainFactory @@ -37,11 +41,21 @@ public final class AppCoordinator: AppCoordinatorProtocol { // MARK: - Public Methods public func showMainTab() { - let tabBar = BottomTabBarController(viewControllers: [ - dictionaryMainViewFactory.make(), - bookmarkMainFactory.make(), - myPageMainFactory.make() - ]) + let tabItems: [TabItem] = [ + TabItem(title: "추천", icon: UIImage(systemName: "star.fill") ?? UIImage()), + TabItem(title: "도감", icon: DesignSystemAsset.image(named: "dictionary") ?? UIImage()), + TabItem(title: "북마크", icon: DesignSystemAsset.image(named: "bookmarkList") ?? UIImage()), + TabItem(title: "MY", icon: DesignSystemAsset.image(named: "mypage") ?? UIImage()) + ] + let tabBar = BottomTabBarController( + viewControllers: [ + recommendationMainFactory.make(), + dictionaryMainViewFactory.make(), + bookmarkMainFactory.make(), + myPageMainFactory.make() + ], + tabItems: tabItems + ) let navigationController = UINavigationController(rootViewController: tabBar) navigationController.isNavigationBarHidden = true diff --git a/MLS/MLS/Application/AppDelegate.swift b/MLS/MLS/Application/AppDelegate.swift index 909c3fae..ed7678b5 100644 --- a/MLS/MLS/Application/AppDelegate.swift +++ b/MLS/MLS/Application/AppDelegate.swift @@ -16,6 +16,8 @@ import Domain import DomainInterface import Firebase import KakaoSDKCommon +import MLSRecommendationFeature +import MLSRecommendationFeatureInterface import MyPageFeature import MyPageFeatureInterface import os @@ -127,6 +129,9 @@ private extension AppDelegate { DIContainer.register(type: AppCoordinatorProtocol.self) { AppCoordinator( window: nil, + recommendationMainFactory: DIContainer.resolve( + type: RecommendationMainFactory.self + ), dictionaryMainViewFactory: DIContainer.resolve( type: DictionaryMainViewFactory.self ), @@ -214,6 +219,9 @@ private extension AppDelegate { tokenInterceptor: DIContainer.resolve(type: Interceptor.self, name: "tokenInterceptor") ) } + DIContainer.register(type: RecommendationRepository.self) { + RecommendationRepositoryImpl() + } } func registerUseCase() { @@ -1248,5 +1256,10 @@ private extension AppDelegate { DIContainer.register(type: PolicyFactory.self) { PolicyFactoryImpl() } + DIContainer.register(type: RecommendationMainFactory.self) { + RecommendationMainFactoryImpl( + repository: DIContainer.resolve(type: RecommendationRepository.self) + ) + } } } diff --git a/MLS/MLSCore/Sources/MLSCore/Network/NetworkProviderImpl.swift b/MLS/MLSCore/Sources/MLSCore/Network/NetworkProviderImpl.swift index 38b7be28..fadff14a 100644 --- a/MLS/MLSCore/Sources/MLSCore/Network/NetworkProviderImpl.swift +++ b/MLS/MLSCore/Sources/MLSCore/Network/NetworkProviderImpl.swift @@ -2,7 +2,7 @@ import Foundation import RxSwift -public final class NetworkProviderImpl: NetworkProvider { +public final class NetworkProviderImpl: NetworkProvider, Loggable { private let session: URLSession @@ -17,32 +17,32 @@ public final class NetworkProviderImpl: NetworkProvider { public func requestData(endPoint: T, interceptor: Interceptor?) -> Observable { return Observable.create { [weak self] observer in - print("🚀 requestData: 요청 시작 - \(endPoint)") + self?.logDebug("Core requestData: 요청 시작 - \(endPoint)") self?.sendRequest(endPoint: endPoint, interceptor: interceptor, completion: { result in switch result { case .success(let data): - print("✅ requestData: 응답 수신") + self?.logDebug("Core requestData: 응답 수신") if let data = data { - print("📦 requestData: 응답 데이터 있음 - \(String(data: data, encoding: .utf8) ?? "디코딩 실패")") + self?.logDebug("Core requestData: 응답 데이터 있음 - \(String(data: data, encoding: .utf8) ?? "디코딩 실패")") do { let decoded = try JSONDecoder().decode(T.Response.self, from: data) - print("🎯 requestData: 디코딩 성공 - \(decoded)") + self?.logDebug("Core requestData: 디코딩 성공 - \(decoded)") observer.onNext(decoded) observer.onCompleted() } catch { - print("❌ requestData: 디코딩 실패 - \(error)") + self?.logError("Core requestData: 디코딩 실패 - \(error)") observer.onError(NetworkError.decodeError(error)) } } else { - print("⚠️ requestData: 응답 데이터 없음") + self?.logWarning("Core requestData: 응답 데이터 없음") observer.onError(NetworkError.noData) } case .failure(let error): - print("🔥 requestData: 네트워크 실패 - \(error)") + self?.logError("🔥 requestData: 네트워크 실패 - \(error)") observer.onError(error) } }) @@ -53,7 +53,7 @@ public final class NetworkProviderImpl: NetworkProvider { errors .enumerated() .flatMap { attempt, error -> Observable in - print("🔁 requestData: 재시도 \(attempt + 1)회 - 에러: \(error)") + self.logWarning("🔁 requestData: 재시도 \(attempt + 1)회 - 에러: \(error)") if attempt < self.retryAttempt, let networkError = error as? NetworkError, networkError == .retry { return Observable.just(()) } else { @@ -94,10 +94,6 @@ public final class NetworkProviderImpl: NetworkProvider { } private extension NetworkProviderImpl { - /// 엔드 포인트를 이용하여 요청을 보내기 위한 함수 - /// - Parameters: - /// - endPoint: 요청을 위한 엔드포인트 객체 - /// - completion: 응답 결과 func sendRequest(endPoint: T, interceptor: Interceptor?, completion: @escaping (Result) -> Void) { do { var request = try endPoint.getUrlRequest() @@ -113,7 +109,7 @@ private extension NetworkProviderImpl { completion(.success(data)) case .failure(let error): completion(.failure(error)) - print("API 통신에러 \(error)") + logError("API 통신에러 \(error)") } } task.resume() @@ -122,12 +118,6 @@ private extension NetworkProviderImpl { } } - /// 통신간의 유효성 검사를 위한 함수 - /// - Parameters: - /// - data: 통신 결과로 돌려받은 데이터 - /// - response: 상태코드를 포함한 통신 응답 - /// - error: 통신간에 발생한 에러 - /// - Returns: 유효성검사 결과에 따른 데이터와 에러 func checkValidation( data: Data?, response: URLResponse?, @@ -135,7 +125,6 @@ private extension NetworkProviderImpl { interceptor: Interceptor? ) -> Result { - // 1️⃣ 네트워크 레벨 에러 먼저 체크 if let error { if let urlError = error as? URLError, urlError.code == .unsupportedURL { return .failure(.urlRequest(error)) @@ -143,14 +132,11 @@ private extension NetworkProviderImpl { return .failure(.network(error)) } - // 2️⃣ HTTP 응답 객체 확인 guard let httpResponse = response as? HTTPURLResponse else { return .failure(.httpError) } - // 3️⃣ 상태 코드 기반 검사 guard (200 ... 299).contains(httpResponse.statusCode) else { - // ❗️여기서만 인터셉터 개입 if let interceptor = interceptor, interceptor.retry(data: data, response: response, error: error) { return .failure(.retry) @@ -160,7 +146,6 @@ private extension NetworkProviderImpl { return .failure(.statusError(httpResponse.statusCode, errorMessage)) } - // ✅ 성공 응답 return .success(data) } } diff --git a/MLS/MLSCore/Sources/MLSCore/Network/TokenInterceptor.swift b/MLS/MLSCore/Sources/MLSCore/Network/TokenInterceptor.swift new file mode 100644 index 00000000..9df5df11 --- /dev/null +++ b/MLS/MLSCore/Sources/MLSCore/Network/TokenInterceptor.swift @@ -0,0 +1,32 @@ +import Foundation +import Security + +public final class TokenInterceptor: Interceptor { + + public init() {} + + public func adapt(_ request: URLRequest) -> URLRequest { + guard let token = fetchAccessToken() else { return request } + var adapted = request + adapted.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + return adapted + } + + public func retry(data: Data?, response: URLResponse?, error: Error?) -> Bool { + return false + } + + private func fetchAccessToken() -> String? { + let query: NSDictionary = [ + kSecClass: kSecClassGenericPassword, + kSecAttrService: "keyChain", + kSecAttrAccount: "accessToken", + kSecReturnData: true, + kSecMatchLimit: kSecMatchLimitOne + ] + var ref: AnyObject? + guard SecItemCopyMatching(query, &ref) == errSecSuccess, + let data = ref as? Data else { return nil } + return String(data: data, encoding: .utf8) + } +} diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/CardList.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/CardList.swift index 16101abf..c5d967e7 100644 --- a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/CardList.swift +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/CardList.swift @@ -47,6 +47,8 @@ public final class CardList: UIView { static let iconSize: CGFloat = 24 static let mapImageSize: CGFloat = 40 static let tagHeight: CGFloat = 24 + static let tagHorizontalInset: CGFloat = 10 + static let tagVerticalInset: CGFloat = 4 } // MARK: - Properties @@ -78,7 +80,7 @@ public final class CardList: UIView { public let imageView = ItemImageView(image: nil, cornerRadius: Constant.imageRadius, inset: Constant.imageInset, backgroundColor: .listMap) private lazy var textLabelStackView: UIStackView = { - let view = UIStackView(arrangedSubviews: [rankTag, mainTextLabel, subTextLabel]) + let view = UIStackView(arrangedSubviews: [rankContainer, mainTextLabel, subTextLabel]) view.axis = .vertical view.spacing = Constant.stackViewSpacing view.alignment = .leading @@ -129,7 +131,20 @@ public final class CardList: UIView { private let badge = Badge(style: .currentQuest) - private let rankTag = TagChip(style: .text, text: "순위") + private let rankContainer = { + let view = UIView() + view.backgroundColor = .primary50 + view.layer.cornerRadius = 12 + view.clipsToBounds = true + return view + }() + + private let rankTag = { + let label = UILabel() + label.font = .korFont(style: .semiBold, size: 14) + label.textColor = .primary700 + return label + }() public init() { super.init(frame: .zero) @@ -153,6 +168,7 @@ private extension CardList { addSubview(iconButton) addSubview(dropInfoStack) addSubview(badge) + rankContainer.addSubview(rankTag) } func setupConstraints() { @@ -177,10 +193,15 @@ private extension CardList { make.trailing.equalToSuperview().inset(Constant.cardTrailingInset) } - rankTag.snp.makeConstraints { make in + rankContainer.snp.makeConstraints { make in make.height.equalTo(Constant.tagHeight) } + rankTag.snp.makeConstraints { make in + make.horizontalEdges.equalToSuperview().inset(Constant.tagHorizontalInset) + make.verticalEdges.equalToSuperview().inset(Constant.tagVerticalInset) + } + iconButton.snp.makeConstraints { make in make.centerY.equalToSuperview() make.trailing.equalToSuperview().inset(Constant.cardTrailingInset) @@ -250,24 +271,24 @@ public extension CardList { iconButton.isHidden = true dropInfoStack.isHidden = true badge.isHidden = true - rankTag.isHidden = true + rankContainer.isHidden = true case .detailStackText: iconButton.isHidden = true dropInfoStack.isHidden = false badge.isHidden = true - rankTag.isHidden = true + rankContainer.isHidden = true case .detailStackBadge(let type): iconButton.isHidden = true dropInfoStack.isHidden = false badge.isHidden = false - rankTag.isHidden = true + rankContainer.isHidden = true badge.update(style: type) case .recommended(let rank): iconButton.isHidden = false dropInfoStack.isHidden = true subTextLabel.isHidden = true badge.isHidden = true - rankTag.isHidden = false + rankContainer.isHidden = false rankTag.text = "\(rank)위" default: iconButton.isHidden = false diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tabbar/BottomTabBarController.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tabbar/BottomTabBarController.swift index 0a3ebd03..c2cea63a 100644 --- a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tabbar/BottomTabBarController.swift +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tabbar/BottomTabBarController.swift @@ -14,13 +14,14 @@ public final class BottomTabBarController: UITabBarController { private let customTabBar: BottomTabBar // MARK: - Init - public init(viewControllers: [UIViewController], initialIndex: Int = 0) { - tabItems = [ + public init(viewControllers: [UIViewController], tabItems: [TabItem]? = nil, initialIndex: Int = 0) { + let resolvedItems = tabItems ?? [ TabItem(title: "도감", icon: DesignSystemAsset.image(named: "dictionary")), TabItem(title: "북마크", icon: DesignSystemAsset.image(named: "bookmarkList")), TabItem(title: "MY", icon: DesignSystemAsset.image(named: "mypage")) ] - customTabBar = BottomTabBar(tabItems: tabItems, selectedIndex: initialIndex) + self.tabItems = resolvedItems + customTabBar = BottomTabBar(tabItems: resolvedItems, selectedIndex: initialIndex) super.init(nibName: nil, bundle: nil) configureUI(controllers: viewControllers) selectedIndex = initialIndex diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipView.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipView.swift index 4eddb2ed..89c244eb 100644 --- a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipView.swift +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/Tooltip/TooltipView.swift @@ -29,6 +29,7 @@ final class TooltipView: UIView { init(text: String, tooltipPosition: TooltipPosition) { self.tooltipPosition = tooltipPosition super.init(frame: .zero) + translatesAutoresizingMaskIntoConstraints = false addViews() setupConstraints() configureUI(text: text) diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/Factory/TooltipFactory.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/Factory/TooltipFactory.swift index 55c86e10..5f6e56d7 100644 --- a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/Factory/TooltipFactory.swift +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Layouts/Factory/TooltipFactory.swift @@ -51,10 +51,6 @@ public extension TooltipFactory { let frame = anchorView.convert(anchorView.bounds, to: window) - tooltip.frame.origin = CGPoint(x: 0, y: 0) - tooltip.setNeedsLayout() - tooltip.layoutIfNeeded() - let tooltipSize = tooltip.systemLayoutSizeFitting( UIView.layoutFittingCompressedSize ) @@ -84,12 +80,10 @@ public extension TooltipFactory { y = frame.maxY + 8 } - tooltip.frame = CGRect( - x: x, - y: y, - width: tooltipSize.width, - height: tooltipSize.height - ) + tooltip.snp.makeConstraints { make in + make.leading.equalToSuperview().offset(x) + make.top.equalToSuperview().offset(y) + } tooltip.alpha = 0 UIView.animate(withDuration: 0.25) { diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/DTOs/JobDTO.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/DTOs/JobDTO.swift new file mode 100644 index 00000000..375b28f8 --- /dev/null +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/DTOs/JobDTO.swift @@ -0,0 +1,13 @@ +struct JobResponseDTO: Decodable { + let success: Bool + let code: String? + let message: String? + let data: JobDTO? +} + +struct JobDTO: Decodable { + let jobId: Int + let jobName: String + let jobLevel: Int + let parentJobId: Int? +} diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/DTOs/RecommendationMapDTO.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/DTOs/RecommendationMapDTO.swift new file mode 100644 index 00000000..543e92ed --- /dev/null +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/DTOs/RecommendationMapDTO.swift @@ -0,0 +1,27 @@ +import MLSRecommendationFeatureInterface + +struct RecommendationMapDTO: Decodable { + let mapId: Int + let score: Double + let iconUrl: String + let nameKr: String + let bookmarkId: Int? +} + +extension RecommendationMapDTO { + func toDomain() -> RecommendationMap { + RecommendationMap( + mapId: mapId, + score: score, + iconUrl: iconUrl, + nameKr: nameKr, + bookmarkId: bookmarkId + ) + } +} + +extension Array where Element == RecommendationMapDTO { + func toDomain() -> [RecommendationMap] { + map { $0.toDomain() } + } +} diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/DTOs/RecommendationResponseDTO.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/DTOs/RecommendationResponseDTO.swift new file mode 100644 index 00000000..151a5e74 --- /dev/null +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/DTOs/RecommendationResponseDTO.swift @@ -0,0 +1,6 @@ +struct RecommendationResponseDTO: Decodable { + let success: Bool + let code: String? + let message: String? + let data: [RecommendationMapDTO]? +} diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/DTOs/UserProfileDTO.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/DTOs/UserProfileDTO.swift new file mode 100644 index 00000000..2e87b83e --- /dev/null +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/DTOs/UserProfileDTO.swift @@ -0,0 +1,33 @@ +import MLSRecommendationFeatureInterface + +struct UserProfileResponseDTO: Decodable { + let success: Bool + let code: String? + let message: String? + let data: UserProfileDTO? +} + +struct UserProfileDTO: Decodable { + let id: String? + let provider: String? + let nickname: String? + let fcmToken: String? + let marketingAgreement: Bool? + let noticeAgreement: Bool? + let patchNoteAgreement: Bool? + let eventAgreement: Bool? + let jobId: Int? + let level: Int? + let profileImageUrl: String? +} + +extension UserProfileDTO { + func toDomain() -> UserProfile { + UserProfile( + nickname: nickname ?? "", + jobId: jobId, + level: level, + profileImageUrl: profileImageUrl ?? "" + ) + } +} diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/Endpoints/RecommendationEndPoint.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/Endpoints/RecommendationEndPoint.swift new file mode 100644 index 00000000..db59aa51 --- /dev/null +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/Endpoints/RecommendationEndPoint.swift @@ -0,0 +1,38 @@ +import MLSCore + +enum RecommendationEndPoint { + static let base = "https://mapleland.2megabytes.me" + + static func fetchRecommendations(level: Int, jobId: Int, limit: Int?) -> ResponsableEndPoint { + .init( + baseURL: base, + path: "/api/v1/maps/recommendations", + method: .GET, + query: FetchQuery(level: Int32(level), jobId: Int32(jobId), limit: limit.map { Int32($0) }) + ) + } + + static func fetchProfile() -> ResponsableEndPoint { + .init( + baseURL: base, + path: "/api/v1/auth/me", + method: .GET + ) + } + + static func fetchJob(jobId: Int) -> ResponsableEndPoint { + .init( + baseURL: base, + path: "/api/v1/jobs/\(jobId)", + method: .GET + ) + } +} + +private extension RecommendationEndPoint { + struct FetchQuery: Encodable { + let level: Int32 + let jobId: Int32 + let limit: Int32? + } +} diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/Repositories/RecommendationRepositoryImpl.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/Repositories/RecommendationRepositoryImpl.swift new file mode 100644 index 00000000..4b5b3c7d --- /dev/null +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Data/Repositories/RecommendationRepositoryImpl.swift @@ -0,0 +1,31 @@ +import MLSCore +import MLSRecommendationFeatureInterface +import RxSwift + +public final class RecommendationRepositoryImpl: RecommendationRepository { + private let provider: NetworkProvider + private let interceptor: Interceptor? + + public init() { + self.provider = NetworkProviderImpl() + self.interceptor = TokenInterceptor() + } + + public func fetchProfile() -> Observable { + let endpoint = RecommendationEndPoint.fetchProfile() + return provider.requestData(endPoint: endpoint, interceptor: interceptor) + .compactMap { $0.data?.toDomain() } + } + + public func fetchJobName(jobId: Int) -> Observable { + let endpoint = RecommendationEndPoint.fetchJob(jobId: jobId) + return provider.requestData(endPoint: endpoint, interceptor: interceptor) + .compactMap { $0.data?.jobName } + } + + public func fetchRecommendations(level: Int, jobId: Int, limit: Int?) -> Observable<[RecommendationMap]> { + let endpoint = RecommendationEndPoint.fetchRecommendations(level: level, jobId: jobId, limit: limit) + return provider.requestData(endPoint: endpoint, interceptor: interceptor) + .map { $0.data?.toDomain() ?? [] } + } +} diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainFactoryImpl.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainFactoryImpl.swift index c34af806..15ccbaea 100644 --- a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainFactoryImpl.swift +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainFactoryImpl.swift @@ -2,10 +2,15 @@ import MLSCore import MLSRecommendationFeatureInterface public struct RecommendationMainFactoryImpl: RecommendationMainFactory { + private let repository: RecommendationRepository - public init() {} + public init(repository: RecommendationRepository) { + self.repository = repository + } public func make() -> BaseViewController { - return RecommendationMainViewController() + let vc = RecommendationMainViewController() + vc.reactor = RecommendationMainReactor(repository: repository) + return vc } } diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainReactor.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainReactor.swift index 4731fcd6..683b3131 100644 --- a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainReactor.swift +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainReactor.swift @@ -1,3 +1,5 @@ +import MLSRecommendationFeatureInterface + import ReactorKit import RxCocoa import RxSwift @@ -5,31 +7,94 @@ import RxSwift final class RecommendationMainReactor: Reactor { // MARK: - Reactor - enum Action { } + enum Action { + case viewWillAppear + case informationButtonTapped + } - enum Mutation { } + enum Mutation { + case setProfile(UserProfile) + case setJobName(String) + case setRecommendations([RecommendationMap]) + case setLoading(Bool) + case informationButtonToggle + } - struct State { } + struct State { + var profile: UserProfile? + var jobName: String = "" + var recommendations: [RecommendationMap] = [] + var isLoading: Bool = false + var informationButtonIsOn: Bool = false + } - // MARK: - properties + // MARK: - Properties var initialState: State var disposeBag = DisposeBag() - // MARK: - init - init() { + private let repository: RecommendationRepository + + // MARK: - Init + init(repository: RecommendationRepository) { + self.repository = repository self.initialState = State() } // MARK: - Reactor Methods func mutate(action: Action) -> Observable { - switch action { } + switch action { + case .viewWillAppear: + let fetchAll = repository.fetchProfile() + .flatMap { [weak self] profile -> Observable in + guard let self else { return .empty() } + let setProfile = Observable.just(Mutation.setProfile(profile)) + let setJobName: Observable + if let jobId = profile.jobId { + setJobName = repository.fetchJobName(jobId: jobId) + .map { Mutation.setJobName($0) } + .catch { _ in .empty() } + } else { + setJobName = .empty() + } + let setRecommendations: Observable + if let level = profile.level, level >= 1, let jobId = profile.jobId { + setRecommendations = repository.fetchRecommendations(level: level, jobId: jobId, limit: 5) + .map { Mutation.setRecommendations($0) } + .catch { _ in .empty() } + } else { + setRecommendations = .empty() + } + let parallelRequests = Observable.merge([setJobName, setRecommendations]) + return Observable.concat([setProfile, parallelRequests]) + } + .catch { _ in .empty() } + + return Observable.concat([ + .just(.setLoading(true)), + fetchAll, + .just(.setLoading(false)) + ]) + + case .informationButtonTapped: + return .just(.informationButtonToggle) + } } func reduce(state: State, mutation: Mutation) -> State { var newState = state - switch mutation { } - + switch mutation { + case .setProfile(let profile): + newState.profile = profile + case .setJobName(let jobName): + newState.jobName = jobName + case .setRecommendations(let maps): + newState.recommendations = maps + case .setLoading(let isLoading): + newState.isLoading = isLoading + case .informationButtonToggle: + newState.informationButtonIsOn.toggle() + } return newState } } diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainViewController.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainViewController.swift index 7af69f72..5046ef77 100644 --- a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainViewController.swift +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainViewController.swift @@ -2,6 +2,7 @@ import UIKit import MLSCore import MLSDesignSystem +import MLSRecommendationFeatureInterface import ReactorKit import RxCocoa @@ -37,18 +38,18 @@ private extension RecommendationMainViewController { func setupConstraints() { mainView.snp.makeConstraints { make in - make.edges.equalToSuperview() + make.edges.equalTo(view.safeAreaLayoutGuide) } } func configureUI() { - mainView.profileView.configure(imageURL: nil, nickName: "익명의 판타지", job: "도적", level: 275) mainView.collectionView.delegate = self mainView.collectionView.dataSource = self mainView.collectionView.register(CardListCell.self, forCellWithReuseIdentifier: CardListCell.identifier) } } +// MARK: - Bind extension RecommendationMainViewController { func bind(reactor: Reactor) { bindUserActions(reactor: reactor) @@ -56,24 +57,99 @@ extension RecommendationMainViewController { } func bindUserActions(reactor: Reactor) { + mainView.informationButton.rx.tap + .map { Reactor.Action.informationButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + rx.viewWillAppear + .map { Reactor.Action.viewWillAppear } + .bind(to: reactor.action) + .disposed(by: disposeBag) + } + + func bindProfile(reactor: Reactor) { + let profileStream = reactor.state + .observe(on: MainScheduler.instance) + .compactMap { $0.profile } + + let jobNameStream = reactor.state + .observe(on: MainScheduler.instance) + .map { $0.jobName } + + Observable.combineLatest(profileStream, jobNameStream) + .withUnretained(self) + .subscribe { owner, pair in + let (profile, jobName) = pair + owner.mainView.profileView.configure( + imageURL: profile.profileImageUrl, + nickName: profile.nickname, + job: jobName, + level: profile.level ?? 0 + ) + } + .disposed(by: disposeBag) } func bindViewState(reactor: Reactor) { + reactor.state + .observe(on: MainScheduler.instance) + .map { $0.informationButtonIsOn } + .distinctUntilChanged() + .withUnretained(self) + .subscribe { owner, toolTipIsOn in + if toolTipIsOn { + TooltipFactory.show( + text: "같은 레벨·직업 유저들이 자주 언급한 \n사냥터를 기반으로 추천해요.", + anchorView: owner.mainView.informationButton, + tooltipPosition: .topTrailing + ) + } else { + TooltipFactory.dismiss() + } + } + .disposed(by: disposeBag) + + bindProfile(reactor: reactor) + + reactor.state + .observe(on: MainScheduler.instance) + .map { $0.recommendations } + .distinctUntilChanged { $0.map(\.mapId) == $1.map(\.mapId) } + .withUnretained(self) + .subscribe { owner, _ in + owner.mainView.collectionView.reloadData() + } + .disposed(by: disposeBag) } } +// MARK: - UICollectionViewDelegate, UICollectionViewDataSource extension RecommendationMainViewController: UICollectionViewDelegate, UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return 5 + return reactor?.currentState.recommendations.count ?? 0 } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CardListCell.identifier, for: indexPath) as? CardListCell else { + guard + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CardListCell.identifier, for: indexPath) as? CardListCell, + let map = reactor?.currentState.recommendations[indexPath.item] + else { return UICollectionViewCell() } - cell.cardView.setMainText(text: "최대 줄은 두 줄입니다.\n넘어갈시 말줄임 처리 합니다.") - cell.cardView.setImage(image: UIImage(systemName: "person")!, backgroundColor: .green) - cell.cardView.setType(type: .recommended(rank: 1)) + + cell.cardView.setMainText(text: map.nameKr) + cell.cardView.setType(type: .recommended(rank: indexPath.row + 1)) + cell.cardView.isIconSelected = map.isBookmarked + + ImageLoader.shared.loadImage(stringURL: map.iconUrl) { [weak self] image in + guard let self, let image else { return } + DispatchQueue.main.async { + guard let currentCell = self.mainView.collectionView.cellForItem(at: indexPath) as? CardListCell else { return } + currentCell.cardView.setImage(image: image, backgroundColor: .clear) + } + } + return cell } } diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/Views/RecommendationMainView.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/Views/RecommendationMainView.swift index 31dea5e0..d9f881b9 100644 --- a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/Views/RecommendationMainView.swift +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/Views/RecommendationMainView.swift @@ -19,6 +19,7 @@ internal final class RecommendationMainView: UIView { static let collectionViewHorizontalInset: CGFloat = 16 static let cellHeight: CGFloat = 104 static let cellSpacing: CGFloat = 8 + static let bottomTabHeight: CGFloat = 64 } // MARK: - Properties @@ -98,7 +99,8 @@ private extension RecommendationMainView { grayBackgroundView.snp.makeConstraints { make in make.top.equalTo(profileView.snp.bottom).offset(Constant.grayViewTopOffset) - make.horizontalEdges.bottom.equalToSuperview() + make.horizontalEdges.equalToSuperview() + make.bottom.equalToSuperview().inset(Constant.bottomTabHeight) } informationButton.snp.makeConstraints { make in diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/Views/RecommendationProfileView.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/Views/RecommendationProfileView.swift index a7a622ea..e86c9b2c 100644 --- a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/Views/RecommendationProfileView.swift +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/Views/RecommendationProfileView.swift @@ -65,7 +65,6 @@ private extension RecommendationProfileView { } func configureUI() { - profileImageView.backgroundColor = .red alignment = .center spacing = Constant.outerStackViewSpacing diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeatureInterface/Entities/RecommendationMap.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeatureInterface/Entities/RecommendationMap.swift new file mode 100644 index 00000000..c066a3a2 --- /dev/null +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeatureInterface/Entities/RecommendationMap.swift @@ -0,0 +1,17 @@ +public struct RecommendationMap { + public let mapId: Int + public let score: Double + public let iconUrl: String + public let nameKr: String + public let bookmarkId: Int? + + public var isBookmarked: Bool { bookmarkId != nil } + + public init(mapId: Int, score: Double, iconUrl: String, nameKr: String, bookmarkId: Int?) { + self.mapId = mapId + self.score = score + self.iconUrl = iconUrl + self.nameKr = nameKr + self.bookmarkId = bookmarkId + } +} diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeatureInterface/Entities/UserProfile.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeatureInterface/Entities/UserProfile.swift new file mode 100644 index 00000000..8e8e0c1b --- /dev/null +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeatureInterface/Entities/UserProfile.swift @@ -0,0 +1,13 @@ +public struct UserProfile { + public let nickname: String + public let jobId: Int? + public let level: Int? + public let profileImageUrl: String + + public init(nickname: String, jobId: Int?, level: Int?, profileImageUrl: String) { + self.nickname = nickname + self.jobId = jobId + self.level = level + self.profileImageUrl = profileImageUrl + } +} diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeatureInterface/Repositories/RecommendationRepository.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeatureInterface/Repositories/RecommendationRepository.swift new file mode 100644 index 00000000..58c67792 --- /dev/null +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeatureInterface/Repositories/RecommendationRepository.swift @@ -0,0 +1,7 @@ +import RxSwift + +public protocol RecommendationRepository { + func fetchProfile() -> Observable + func fetchJobName(jobId: Int) -> Observable + func fetchRecommendations(level: Int, jobId: Int, limit: Int?) -> Observable<[RecommendationMap]> +} diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeatureTesting/FailingMockRecommendationRepository.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeatureTesting/FailingMockRecommendationRepository.swift new file mode 100644 index 00000000..01b83327 --- /dev/null +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeatureTesting/FailingMockRecommendationRepository.swift @@ -0,0 +1,25 @@ +import MLSRecommendationFeatureInterface + +import RxSwift + +/// 모든 요청이 항상 실패하는 Mock +public final class FailingMockRecommendationRepository: RecommendationRepository { + + public init() {} + + public func fetchProfile() -> Observable { + return .error(RecommendationRepositoryError.fetchFailed) + } + + public func fetchJobName(jobId: Int) -> Observable { + return .error(RecommendationRepositoryError.fetchFailed) + } + + public func fetchRecommendations(level: Int, jobId: Int, limit: Int?) -> Observable<[RecommendationMap]> { + return .error(RecommendationRepositoryError.fetchFailed) + } +} + +public enum RecommendationRepositoryError: Error { + case fetchFailed +} diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeatureTesting/MLSRecommendationFeatureTesting.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeatureTesting/MLSRecommendationFeatureTesting.swift deleted file mode 100644 index 0d4fa4b7..00000000 --- a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeatureTesting/MLSRecommendationFeatureTesting.swift +++ /dev/null @@ -1,2 +0,0 @@ -// MLSRecommendationFeatureTesting -// 단위 테스트 및 Example 앱에서 사용할 Mock 객체를 제공합니다. diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeatureTesting/MockRecommendationRepository.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeatureTesting/MockRecommendationRepository.swift new file mode 100644 index 00000000..dca1d630 --- /dev/null +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeatureTesting/MockRecommendationRepository.swift @@ -0,0 +1,31 @@ +import MLSRecommendationFeatureInterface + +import RxSwift + +public final class MockRecommendationRepository: RecommendationRepository { + + public init() {} + + public func fetchProfile() -> Observable { + return .just(UserProfile( + nickname: "익명의 판타지", + jobId: 4, + level: 275, + profileImageUrl: "" + )) + } + + public func fetchJobName(jobId: Int) -> Observable { + return .just("도적") + } + + public func fetchRecommendations(level: Int, jobId: Int, limit: Int?) -> Observable<[RecommendationMap]> { + return .just([ + RecommendationMap(mapId: 100000000, score: 10, iconUrl: "", nameKr: "헤네시스", bookmarkId: nil), + RecommendationMap(mapId: 100000001, score: 9, iconUrl: "", nameKr: "엘리니아", bookmarkId: 1), + RecommendationMap(mapId: 100000002, score: 8, iconUrl: "", nameKr: "페리온", bookmarkId: nil), + RecommendationMap(mapId: 100000003, score: 7, iconUrl: "", nameKr: "슬리피우드", bookmarkId: 2), + RecommendationMap(mapId: 100000004, score: 6, iconUrl: "", nameKr: "노틸러스 항구", bookmarkId: nil) + ]) + } +} diff --git a/MLS/MLSRecommendationFeatureExample/SceneDelegate.swift b/MLS/MLSRecommendationFeatureExample/SceneDelegate.swift index 64e1cd19..3b80d692 100644 --- a/MLS/MLSRecommendationFeatureExample/SceneDelegate.swift +++ b/MLS/MLSRecommendationFeatureExample/SceneDelegate.swift @@ -3,6 +3,7 @@ import UIKit import MLSCore import MLSRecommendationFeature import MLSRecommendationFeatureInterface +import MLSRecommendationFeatureTesting class SceneDelegate: UIResponder, UIWindowSceneDelegate { @@ -28,7 +29,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { private func registerDependencies() { DIContainer.register(type: RecommendationMainFactory.self) { - RecommendationMainFactoryImpl() + RecommendationMainFactoryImpl( + repository: MockRecommendationRepository() + ) } } } diff --git a/MLS/Presentation/DesignSystem/DesignSystem/Components/Tabbar/BottomTabBar.swift b/MLS/Presentation/DesignSystem/DesignSystem/Components/Tabbar/BottomTabBar.swift index bf1c2a71..ab2a2a50 100644 --- a/MLS/Presentation/DesignSystem/DesignSystem/Components/Tabbar/BottomTabBar.swift +++ b/MLS/Presentation/DesignSystem/DesignSystem/Components/Tabbar/BottomTabBar.swift @@ -4,8 +4,13 @@ import SnapKit // MARK: - Model public struct TabItem { - var title: String - var icon: UIImage + public var title: String + public var icon: UIImage + + public init(title: String, icon: UIImage) { + self.title = title + self.icon = icon + } } public final class BottomTabBar: UIStackView { diff --git a/MLS/Presentation/DesignSystem/DesignSystem/Components/Tabbar/BottomTabBarController.swift b/MLS/Presentation/DesignSystem/DesignSystem/Components/Tabbar/BottomTabBarController.swift index 1a89a6b0..5dc4a601 100644 --- a/MLS/Presentation/DesignSystem/DesignSystem/Components/Tabbar/BottomTabBarController.swift +++ b/MLS/Presentation/DesignSystem/DesignSystem/Components/Tabbar/BottomTabBarController.swift @@ -14,13 +14,14 @@ public final class BottomTabBarController: UITabBarController { private let customTabBar: BottomTabBar // MARK: - Init - public init(viewControllers: [UIViewController], initialIndex: Int = 0) { - tabItems = [ + public init(viewControllers: [UIViewController], tabItems: [TabItem]? = nil, initialIndex: Int = 0) { + let resolvedItems = tabItems ?? [ TabItem(title: "도감", icon: .dictionary), TabItem(title: "북마크", icon: .bookmarkList), TabItem(title: "MY", icon: .mypage) ] - customTabBar = BottomTabBar(tabItems: tabItems, selectedIndex: initialIndex) + self.tabItems = resolvedItems + customTabBar = BottomTabBar(tabItems: resolvedItems, selectedIndex: initialIndex) super.init(nibName: nil, bundle: nil) configureUI(controllers: viewControllers) selectedIndex = initialIndex