diff --git a/MLS/MLS.xcodeproj/project.pbxproj b/MLS/MLS.xcodeproj/project.pbxproj index 2de28dda..95e87f02 100644 --- a/MLS/MLS.xcodeproj/project.pbxproj +++ b/MLS/MLS.xcodeproj/project.pbxproj @@ -33,6 +33,9 @@ 08F7AA012F86745C00EF5C06 /* MLSAuthFeature in Frameworks */ = {isa = PBXBuildFile; productRef = 08F7AA022F86745C00EF5C06 /* MLSAuthFeature */; }; 08F7AA032F86745C00EF5C06 /* MLSAuthFeatureInterface in Frameworks */ = {isa = PBXBuildFile; productRef = 08F7AA042F86745C00EF5C06 /* MLSAuthFeatureInterface */; }; 08F7AA052F86745C00EF5C06 /* MLSAuthFeatureTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 08F7AA062F86745C00EF5C06 /* MLSAuthFeatureTesting */; }; + 08F7DC802F9DEA8100EF5C06 /* MLSRecommendationFeature in Frameworks */ = {isa = PBXBuildFile; productRef = 08F7DC812F9DEA8100EF5C06 /* MLSRecommendationFeature */; }; + 08F7DC822F9DEA8100EF5C06 /* MLSRecommendationFeatureInterface in Frameworks */ = {isa = PBXBuildFile; productRef = 08F7DC832F9DEA8100EF5C06 /* MLSRecommendationFeatureInterface */; }; + 08F7DC842F9DEA8100EF5C06 /* MLSCore in Frameworks */ = {isa = PBXBuildFile; productRef = 08F7DC852F9DEA8100EF5C06 /* MLSCore */; }; 770ADB1F2E433EDA00270506 /* RxKeyboard in Frameworks */ = {isa = PBXBuildFile; productRef = 770ADB1E2E433EDA00270506 /* RxKeyboard */; }; 772199F22E0E7EC800A7B58C /* AuthFeatureInterface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 772199F12E0E7EC800A7B58C /* AuthFeatureInterface.framework */; }; 772199F32E0E7EC800A7B58C /* AuthFeatureInterface.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 772199F12E0E7EC800A7B58C /* AuthFeatureInterface.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -139,6 +142,7 @@ 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; }; + 08F7DC612F9DEA8000EF5C06 /* MLSRecommendationFeatureExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MLSRecommendationFeatureExample.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 = ""; }; @@ -170,6 +174,13 @@ ); target = 08F7A9222F86745C00EF5C06 /* MLSAuthFeatureExample */; }; + 08F7DC722F9DEA8100EF5C06 /* Exceptions for "MLSRecommendationFeatureExample" folder in "MLSRecommendationFeatureExample" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 08F7DC602F9DEA8000EF5C06 /* MLSRecommendationFeatureExample */; + }; 77FA688B2F72C7380064B6EB /* Exceptions for "MLSDesignSystemExample" folder in "MLSDesignSystemExample" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( @@ -196,6 +207,14 @@ path = MLSAuthFeatureExample; sourceTree = ""; }; + 08F7DC622F9DEA8000EF5C06 /* MLSRecommendationFeatureExample */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 08F7DC722F9DEA8100EF5C06 /* Exceptions for "MLSRecommendationFeatureExample" folder in "MLSRecommendationFeatureExample" target */, + ); + path = MLSRecommendationFeatureExample; + sourceTree = ""; + }; 77BEB0412DBA84B0002FFCFC /* MLSTests */ = { isa = PBXFileSystemSynchronizedRootGroup; path = MLSTests; @@ -257,6 +276,16 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 08F7DC5E2F9DEA8000EF5C06 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 08F7DC802F9DEA8100EF5C06 /* MLSRecommendationFeature in Frameworks */, + 08F7DC822F9DEA8100EF5C06 /* MLSRecommendationFeatureInterface in Frameworks */, + 08F7DC842F9DEA8100EF5C06 /* MLSCore in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 77BEB03D2DBA84B0002FFCFC /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -360,6 +389,7 @@ 77BEB0412DBA84B0002FFCFC /* MLSTests */, 77FA687B2F72C7360064B6EB /* MLSDesignSystemExample */, 08F7A9242F86745C00EF5C06 /* MLSAuthFeatureExample */, + 08F7DC622F9DEA8000EF5C06 /* MLSRecommendationFeatureExample */, 084A25312DB93A5400C395C0 /* Frameworks */, 087D3EE92DA7972C002F924D /* Products */, ); @@ -372,6 +402,7 @@ 77BEB0402DBA84B0002FFCFC /* MLSTests.xctest */, 77FA687A2F72C7360064B6EB /* MLSDesignSystemExample.app */, 08F7A9232F86745C00EF5C06 /* MLSAuthFeatureExample.app */, + 08F7DC612F9DEA8000EF5C06 /* MLSRecommendationFeatureExample.app */, ); name = Products; sourceTree = ""; @@ -439,6 +470,31 @@ productReference = 08F7A9232F86745C00EF5C06 /* MLSAuthFeatureExample.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"; + }; 77BEB03F2DBA84B0002FFCFC /* MLSTests */ = { isa = PBXNativeTarget; buildConfigurationList = 77BEB0482DBA84B0002FFCFC /* Build configuration list for PBXNativeTarget "MLSTests" */; @@ -505,6 +561,9 @@ 08F7A9222F86745C00EF5C06 = { CreatedOnToolsVersion = 26.1.1; }; + 08F7DC602F9DEA8000EF5C06 = { + CreatedOnToolsVersion = 26.1.1; + }; 77BEB03F2DBA84B0002FFCFC = { CreatedOnToolsVersion = 16.2; TestTargetID = 087D3EE72DA7972C002F924D; @@ -567,6 +626,7 @@ 77BEB03F2DBA84B0002FFCFC /* MLSTests */, 77FA68792F72C7360064B6EB /* MLSDesignSystemExample */, 08F7A9222F86745C00EF5C06 /* MLSAuthFeatureExample */, + 08F7DC602F9DEA8000EF5C06 /* MLSRecommendationFeatureExample */, ); }; /* End PBXProject section */ @@ -589,6 +649,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 08F7DC5F2F9DEA8000EF5C06 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 77BEB03E2DBA84B0002FFCFC /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -641,6 +708,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 08F7DC5D2F9DEA8000EF5C06 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 77BEB03C2DBA84B0002FFCFC /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -937,6 +1011,82 @@ }; name = Release; }; + 08F7DC742F9DEA8100EF5C06 /* 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; + 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_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; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + 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; + 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"; + }; + name = Debug; + }; + 08F7DC752F9DEA8100EF5C06 /* 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; + 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_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; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + 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; + 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"; + }; + name = Release; + }; 77BEB0462DBA84B0002FFCFC /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1081,6 +1231,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 08F7DC732F9DEA8100EF5C06 /* Build configuration list for PBXNativeTarget "MLSRecommendationFeatureExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 08F7DC742F9DEA8100EF5C06 /* Debug */, + 08F7DC752F9DEA8100EF5C06 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 77BEB0482DBA84B0002FFCFC /* Build configuration list for PBXNativeTarget "MLSTests" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -1203,6 +1362,18 @@ isa = XCSwiftPackageProductDependency; productName = MLSAuthFeatureTesting; }; + 08F7DC812F9DEA8100EF5C06 /* MLSRecommendationFeature */ = { + isa = XCSwiftPackageProductDependency; + productName = MLSRecommendationFeature; + }; + 08F7DC832F9DEA8100EF5C06 /* MLSRecommendationFeatureInterface */ = { + isa = XCSwiftPackageProductDependency; + productName = MLSRecommendationFeatureInterface; + }; + 08F7DC852F9DEA8100EF5C06 /* MLSCore */ = { + isa = XCSwiftPackageProductDependency; + productName = MLSCore; + }; 770ADB1E2E433EDA00270506 /* RxKeyboard */ = { isa = XCSwiftPackageProductDependency; package = 770ADB1D2E433EDA00270506 /* XCRemoteSwiftPackageReference "RxKeyboard" */; diff --git a/MLS/MLS.xcworkspace/contents.xcworkspacedata b/MLS/MLS.xcworkspace/contents.xcworkspacedata index 99fde6a0..f7ebda0c 100644 --- a/MLS/MLS.xcworkspace/contents.xcworkspacedata +++ b/MLS/MLS.xcworkspace/contents.xcworkspacedata @@ -44,6 +44,9 @@ + + diff --git a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/CardList.swift b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/CardList.swift index f461d117..16101abf 100644 --- a/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/CardList.swift +++ b/MLS/MLSDesignSystem/Sources/MLSDesignSystem/Components/CardList.swift @@ -263,7 +263,7 @@ public extension CardList { rankTag.isHidden = true badge.update(style: type) case .recommended(let rank): - iconButton.isHidden = true + iconButton.isHidden = false dropInfoStack.isHidden = true subTextLabel.isHidden = true badge.isHidden = true diff --git a/MLS/MLSRecommendationFeature/.gitignore b/MLS/MLSRecommendationFeature/.gitignore new file mode 100644 index 00000000..0023a534 --- /dev/null +++ b/MLS/MLSRecommendationFeature/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/MLS/MLSRecommendationFeature/Package.swift b/MLS/MLSRecommendationFeature/Package.swift new file mode 100644 index 00000000..0878963e --- /dev/null +++ b/MLS/MLSRecommendationFeature/Package.swift @@ -0,0 +1,80 @@ +// swift-tools-version: 6.2 +import PackageDescription + +let package = Package( + name: "MLSRecommendationFeature", + platforms: [.iOS(.v15)], + products: [ + // Interface: Presentation 팩토리 프로토콜 + .library( + name: "MLSRecommendationFeatureInterface", + targets: ["MLSRecommendationFeatureInterface"] + ), + // Feature: Presentation + Domain + Data 구현체 + .library( + name: "MLSRecommendationFeature", + targets: ["MLSRecommendationFeature"] + ), + // Testing: 단위 테스트나 Example 앱에서 사용될 Mock 데이터를 제공하는 모듈 + .library( + name: "MLSRecommendationFeatureTesting", + targets: ["MLSRecommendationFeatureTesting"] + ) + ], + dependencies: [ + .package(path: "../MLSCore"), + .package(path: "../MLSDesignSystem"), + .package(url: "https://github.com/ReactorKit/ReactorKit.git", from: "3.2.0"), + .package(url: "https://github.com/ReactiveX/RxSwift.git", from: "6.7.0"), + .package(url: "https://github.com/RxSwiftCommunity/RxKeyboard.git", from: "2.0.0"), + .package(url: "https://github.com/SnapKit/SnapKit.git", from: "5.7.1") + ], + targets: [ + // Interface 모듈 (Presentation 팩토리 프로토콜) + .target( + name: "MLSRecommendationFeatureInterface", + dependencies: [ + .product(name: "MLSCore", package: "MLSCore"), + .product(name: "MLSDesignSystem", package: "MLSDesignSystem"), + .product(name: "RxSwift", package: "RxSwift") + ], + swiftSettings: [.swiftLanguageMode(.v5)] + ), + // Feature 모듈 (Presentation + Domain + Data 구현체) + .target( + name: "MLSRecommendationFeature", + dependencies: [ + "MLSRecommendationFeatureInterface", + .product(name: "MLSCore", package: "MLSCore"), + .product(name: "MLSDesignSystem", package: "MLSDesignSystem"), + .product(name: "ReactorKit", package: "ReactorKit"), + .product(name: "RxSwift", package: "RxSwift"), + .product(name: "RxCocoa", package: "RxSwift"), + .product(name: "RxRelay", package: "RxSwift"), + .product(name: "RxKeyboard", package: "RxKeyboard"), + .product(name: "SnapKit", package: "SnapKit") + ], + swiftSettings: [.swiftLanguageMode(.v5)] + ), + // Testing 모듈 (Mock 객체) + .target( + name: "MLSRecommendationFeatureTesting", + dependencies: [ + "MLSRecommendationFeatureInterface", + .product(name: "RxSwift", package: "RxSwift") + ], + swiftSettings: [.swiftLanguageMode(.v5)] + ), + // Tests 모듈 + .testTarget( + name: "MLSRecommendationFeatureTests", + dependencies: [ + "MLSRecommendationFeature", + "MLSRecommendationFeatureInterface", + "MLSRecommendationFeatureTesting", + .product(name: "RxBlocking", package: "RxSwift") + ], + swiftSettings: [.swiftLanguageMode(.v5)] + ) + ] +) diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainFactoryImpl.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainFactoryImpl.swift new file mode 100644 index 00000000..c34af806 --- /dev/null +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainFactoryImpl.swift @@ -0,0 +1,11 @@ +import MLSCore +import MLSRecommendationFeatureInterface + +public struct RecommendationMainFactoryImpl: RecommendationMainFactory { + + public init() {} + + public func make() -> BaseViewController { + return RecommendationMainViewController() + } +} diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainReactor.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainReactor.swift new file mode 100644 index 00000000..4731fcd6 --- /dev/null +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainReactor.swift @@ -0,0 +1,35 @@ +import ReactorKit +import RxCocoa +import RxSwift + +final class RecommendationMainReactor: Reactor { + + // MARK: - Reactor + enum Action { } + + enum Mutation { } + + struct State { } + + // MARK: - properties + var initialState: State + var disposeBag = DisposeBag() + + // MARK: - init + init() { + self.initialState = State() + } + + // MARK: - Reactor Methods + func mutate(action: Action) -> Observable { + switch action { } + } + + func reduce(state: State, mutation: Mutation) -> State { + var newState = state + + switch mutation { } + + return newState + } +} diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainViewController.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainViewController.swift new file mode 100644 index 00000000..7af69f72 --- /dev/null +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/RecommendationMainViewController.swift @@ -0,0 +1,79 @@ +import UIKit + +import MLSCore +import MLSDesignSystem + +import ReactorKit +import RxCocoa +import RxSwift +import SnapKit + +final class RecommendationMainViewController: BaseViewController, View { + + typealias Reactor = RecommendationMainReactor + + // MARK: - Properties + var disposeBag = DisposeBag() + + private var mainView = RecommendationMainView() +} + +// MARK: - Life Cycle +extension RecommendationMainViewController { + override func viewDidLoad() { + super.viewDidLoad() + + addViews() + setupConstraints() + configureUI() + } +} + +// MARK: - SetUp +private extension RecommendationMainViewController { + func addViews() { + view.addSubview(mainView) + } + + func setupConstraints() { + mainView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } + + 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) + } +} + +extension RecommendationMainViewController { + func bind(reactor: Reactor) { + bindUserActions(reactor: reactor) + bindViewState(reactor: reactor) + } + + func bindUserActions(reactor: Reactor) { + } + + func bindViewState(reactor: Reactor) { + } +} + +extension RecommendationMainViewController: UICollectionViewDelegate, UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return 5 + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CardListCell.identifier, for: indexPath) as? CardListCell else { + return UICollectionViewCell() + } + cell.cardView.setMainText(text: "최대 줄은 두 줄입니다.\n넘어갈시 말줄임 처리 합니다.") + cell.cardView.setImage(image: UIImage(systemName: "person")!, backgroundColor: .green) + cell.cardView.setType(type: .recommended(rank: 1)) + return cell + } +} diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/Views/CardListCell.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/Views/CardListCell.swift new file mode 100644 index 00000000..f3364133 --- /dev/null +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/Views/CardListCell.swift @@ -0,0 +1,36 @@ +import UIKit + +import MLSDesignSystem + +import SnapKit + +final class CardListCell: UICollectionViewCell { + + // MARK: - Properties + let cardView = CardList() + + // MARK: - init + override init(frame: CGRect) { + super.init(frame: frame) + + addViews() + setupConstraints() + } + + required init?(coder: NSCoder) { + fatalError("\(#file), \(#function) Error") + } +} + +// MARK: - SetUp +private extension CardListCell { + func addViews() { + contentView.addSubview(cardView) + } + + func setupConstraints() { + cardView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } +} diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/Views/RecommendationMainView.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/Views/RecommendationMainView.swift new file mode 100644 index 00000000..31dea5e0 --- /dev/null +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/Views/RecommendationMainView.swift @@ -0,0 +1,125 @@ +import UIKit + +import MLSCore +import MLSDesignSystem + +import SnapKit + +internal final class RecommendationMainView: UIView { + // MARK: - Type + private enum Constant { + static let profileTopOffset: CGFloat = 21 + static let profileHorizontalInset: CGFloat = 40 + static let grayViewTopOffset: CGFloat = 30 + static let informationButtonTopInset: CGFloat = 14 + static let informationButtonTrailingInset: CGFloat = 16 + static let informationIconSize: CGFloat = 24 + static let informationLabelIconSpacing: CGFloat = 3 + static let collectionViewTopOffset: CGFloat = 14 + static let collectionViewHorizontalInset: CGFloat = 16 + static let cellHeight: CGFloat = 104 + static let cellSpacing: CGFloat = 8 + } + + // MARK: - Properties + internal let header = Header(style: .main, title: "추천") + internal let profileView = RecommendationProfileView() + + internal let grayBackgroundView: UIView = { + let view = UIView() + view.backgroundColor = .neutral100 + return view + }() + + internal let collectionView: UICollectionView = { + let layout = CompositionalLayoutBuilder() + .section { + $0.item(width: .fractionalWidth(1), height: .fractionalHeight(1)) + .group(.vertical, width: .fractionalWidth(1), height: .absolute(Constant.cellHeight)) + .buildSection() + .interGroupSpacing(Constant.cellSpacing) + } + .build() + let view = UICollectionView(frame: .zero, collectionViewLayout: layout) + view.backgroundColor = .clear + view.showsVerticalScrollIndicator = false + return view + }() + + internal let informationButton = UIButton() + private let informationLabel: UILabel = { + let label = UILabel() + label.font = .korFont(style: .regular, size: 16) + label.text = "추천" + return label + }() + + private let informationIconView: UIImageView = { + let view = UIImageView() + view.image = DesignSystemAsset.image(named: "errorBlack").withTintColor(.primary700) + return view + }() + + // MARK: - init + init() { + super.init(frame: .zero) + + addViews() + setupConstraints() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("\(#file), \(#function) Error") + } +} + +// MARK: - SetUp +private extension RecommendationMainView { + func addViews() { + addSubview(header) + addSubview(profileView) + addSubview(grayBackgroundView) + grayBackgroundView.addSubview(informationButton) + informationButton.addSubview(informationLabel) + informationButton.addSubview(informationIconView) + grayBackgroundView.addSubview(collectionView) + } + + func setupConstraints() { + header.snp.makeConstraints { make in + make.top.horizontalEdges.equalTo(safeAreaLayoutGuide) + } + + profileView.snp.makeConstraints { make in + make.top.equalTo(header.snp.bottom).offset(Constant.profileTopOffset) + make.horizontalEdges.equalToSuperview().inset(Constant.profileHorizontalInset) + } + + grayBackgroundView.snp.makeConstraints { make in + make.top.equalTo(profileView.snp.bottom).offset(Constant.grayViewTopOffset) + make.horizontalEdges.bottom.equalToSuperview() + } + + informationButton.snp.makeConstraints { make in + make.top.equalToSuperview().inset(Constant.informationButtonTopInset) + make.trailing.equalToSuperview().inset(Constant.informationButtonTrailingInset) + } + + informationIconView.snp.makeConstraints { make in + make.size.equalTo(Constant.informationIconSize) + make.verticalEdges.trailing.equalToSuperview() + } + + informationLabel.snp.makeConstraints { make in + make.centerY.leading.equalToSuperview() + make.trailing.equalTo(informationIconView.snp.leading).offset(-Constant.informationLabelIconSpacing) + } + + collectionView.snp.makeConstraints { make in + make.top.equalTo(informationButton.snp.bottom).offset(Constant.collectionViewTopOffset) + make.horizontalEdges.equalToSuperview().inset(Constant.collectionViewHorizontalInset) + make.bottom.equalToSuperview() + } + } +} diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/Views/RecommendationProfileView.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/Views/RecommendationProfileView.swift new file mode 100644 index 00000000..a7a622ea --- /dev/null +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeature/Presentation/RecommendationMain/Views/RecommendationProfileView.swift @@ -0,0 +1,91 @@ +import UIKit + +import MLSCore +import MLSDesignSystem + +import SnapKit + +internal final class RecommendationProfileView: UIStackView { + // MARK: - Type + private enum Constant { + static let profileImageViewSize: CGFloat = 100 + static let outerStackViewSpacing: CGFloat = 38 + static let verticalStackViewSpacing: CGFloat = 18 + static let horizontalStackViewSpacing: CGFloat = 12 + } + + // MARK: - Properties + internal let profileImageView: UIImageView = .init() + internal let nicknameLabel: UILabel = .init() + internal let jobLabel: UILabel = .init() + internal let levelBadge = Badge(style: .element("")) + internal let editButton: UIButton = { + let button = UIButton() + button.setAttributedTitle(.makeStyledUnderlinedString(font: .korFont(style: .regular, size: 14), text: "수정하기"), for: .normal) + return button + }() + + // MARK: - StackView + private let verticalStackView: UIStackView = .init() + private let horizontalStackView: UIStackView = .init() + + // MARK: - init + internal init() { + super.init(frame: .zero) + + addViews() + setupConstraints() + configureUI() + } + + @available(*, unavailable) + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - SetUp +private extension RecommendationProfileView { + func addViews() { + horizontalStackView.addArrangedSubview(jobLabel) + horizontalStackView.addArrangedSubview(levelBadge) + horizontalStackView.addArrangedSubview(editButton) + + verticalStackView.addArrangedSubview(nicknameLabel) + verticalStackView.addArrangedSubview(horizontalStackView) + + addArrangedSubview(profileImageView) + addArrangedSubview(verticalStackView) + } + + func setupConstraints() { + profileImageView.snp.makeConstraints { make in + make.size.equalTo(Constant.profileImageViewSize) + } + } + + func configureUI() { + profileImageView.backgroundColor = .red + alignment = .center + spacing = Constant.outerStackViewSpacing + + verticalStackView.axis = .vertical + verticalStackView.alignment = .leading + verticalStackView.spacing = Constant.verticalStackViewSpacing + + horizontalStackView.alignment = .center + horizontalStackView.spacing = Constant.horizontalStackViewSpacing + } +} + +// MARK: - Configure +internal extension RecommendationProfileView { + func configure(imageURL: String?, nickName: String, job: String, level: Int) { + ImageLoader.shared.loadImage(stringURL: imageURL) { [weak self] image in + self?.profileImageView.image = image + } + nicknameLabel.attributedText = .makeStyledString(font: .korFont(style: .bold, size: 18), text: nickName) + jobLabel.attributedText = .makeStyledString(font: .korFont(style: .regular, size: 16), text: job) + levelBadge.update(style: .element("Lv.\(level)")) + } +} diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeatureInterface/Factories/RecommendationMainFactory.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeatureInterface/Factories/RecommendationMainFactory.swift new file mode 100644 index 00000000..7272689b --- /dev/null +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeatureInterface/Factories/RecommendationMainFactory.swift @@ -0,0 +1,5 @@ +import MLSCore + +public protocol RecommendationMainFactory { + func make() -> BaseViewController +} diff --git a/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeatureTesting/MLSRecommendationFeatureTesting.swift b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeatureTesting/MLSRecommendationFeatureTesting.swift new file mode 100644 index 00000000..0d4fa4b7 --- /dev/null +++ b/MLS/MLSRecommendationFeature/Sources/MLSRecommendationFeatureTesting/MLSRecommendationFeatureTesting.swift @@ -0,0 +1,2 @@ +// MLSRecommendationFeatureTesting +// 단위 테스트 및 Example 앱에서 사용할 Mock 객체를 제공합니다. diff --git a/MLS/MLSRecommendationFeature/Tests/MLSRecommendationFeatureTests/MLSRecommendationFeatureTests.swift b/MLS/MLSRecommendationFeature/Tests/MLSRecommendationFeatureTests/MLSRecommendationFeatureTests.swift new file mode 100644 index 00000000..ca06a023 --- /dev/null +++ b/MLS/MLSRecommendationFeature/Tests/MLSRecommendationFeatureTests/MLSRecommendationFeatureTests.swift @@ -0,0 +1,6 @@ +@testable import MLSRecommendationFeature +import Testing + +@Test func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. +} diff --git a/MLS/MLSRecommendationFeatureExample/AppDelegate.swift b/MLS/MLSRecommendationFeatureExample/AppDelegate.swift new file mode 100644 index 00000000..d179aeef --- /dev/null +++ b/MLS/MLSRecommendationFeatureExample/AppDelegate.swift @@ -0,0 +1,25 @@ +import UIKit + +import MLSDesignSystem + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + FontManager.registerFonts() + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } +} diff --git a/MLS/MLSRecommendationFeatureExample/Assets.xcassets/AccentColor.colorset/Contents.json b/MLS/MLSRecommendationFeatureExample/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/MLS/MLSRecommendationFeatureExample/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MLS/MLSRecommendationFeatureExample/Assets.xcassets/AppIcon.appiconset/AppIcon_1024 (1).png b/MLS/MLSRecommendationFeatureExample/Assets.xcassets/AppIcon.appiconset/AppIcon_1024 (1).png new file mode 100644 index 00000000..68f3c890 Binary files /dev/null and b/MLS/MLSRecommendationFeatureExample/Assets.xcassets/AppIcon.appiconset/AppIcon_1024 (1).png differ diff --git a/MLS/MLSRecommendationFeatureExample/Assets.xcassets/AppIcon.appiconset/Contents.json b/MLS/MLSRecommendationFeatureExample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..f0d60e38 --- /dev/null +++ b/MLS/MLSRecommendationFeatureExample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,36 @@ +{ + "images" : [ + { + "filename" : "AppIcon_1024 (1).png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MLS/MLSRecommendationFeatureExample/Assets.xcassets/Contents.json b/MLS/MLSRecommendationFeatureExample/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/MLS/MLSRecommendationFeatureExample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MLS/MLSRecommendationFeatureExample/Base.lproj/LaunchScreen.storyboard b/MLS/MLSRecommendationFeatureExample/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..865e9329 --- /dev/null +++ b/MLS/MLSRecommendationFeatureExample/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MLS/MLSRecommendationFeatureExample/Base.lproj/Main.storyboard b/MLS/MLSRecommendationFeatureExample/Base.lproj/Main.storyboard new file mode 100644 index 00000000..25a76385 --- /dev/null +++ b/MLS/MLSRecommendationFeatureExample/Base.lproj/Main.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MLS/MLSRecommendationFeatureExample/Info.plist b/MLS/MLSRecommendationFeatureExample/Info.plist new file mode 100644 index 00000000..dd3c9afd --- /dev/null +++ b/MLS/MLSRecommendationFeatureExample/Info.plist @@ -0,0 +1,25 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + + diff --git a/MLS/MLSRecommendationFeatureExample/SceneDelegate.swift b/MLS/MLSRecommendationFeatureExample/SceneDelegate.swift new file mode 100644 index 00000000..64e1cd19 --- /dev/null +++ b/MLS/MLSRecommendationFeatureExample/SceneDelegate.swift @@ -0,0 +1,34 @@ +import UIKit + +import MLSCore +import MLSRecommendationFeature +import MLSRecommendationFeatureInterface + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let windowScene = (scene as? UIWindowScene) else { return } + + registerDependencies() + + let window = UIWindow(windowScene: windowScene) + self.window = window + + let factory = DIContainer.resolve(type: RecommendationMainFactory.self) + let recommendationVC = factory.make() + let nav = UINavigationController(rootViewController: recommendationVC) + nav.navigationBar.isHidden = true + window.rootViewController = nav + window.makeKeyAndVisible() + } + + // MARK: - Private + + private func registerDependencies() { + DIContainer.register(type: RecommendationMainFactory.self) { + RecommendationMainFactoryImpl() + } + } +} diff --git a/MLS/MLSRecommendationFeatureExample/ViewController.swift b/MLS/MLSRecommendationFeatureExample/ViewController.swift new file mode 100644 index 00000000..0bcd93ff --- /dev/null +++ b/MLS/MLSRecommendationFeatureExample/ViewController.swift @@ -0,0 +1,12 @@ +// +// ViewController.swift +// MLSRecommendationFeatureExample +// +// Created by SeoJunYoung on 4/26/26. +// + +import UIKit + +class ViewController: UIViewController { + +}