diff --git a/week8/UMC_KREAM.xcodeproj/project.pbxproj b/week8/UMC_KREAM.xcodeproj/project.pbxproj new file mode 100644 index 0000000..2c43d42 --- /dev/null +++ b/week8/UMC_KREAM.xcodeproj/project.pbxproj @@ -0,0 +1,628 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 48566FB32CECBFBA00846862 /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = 48566FB22CECBFBA00846862 /* SnapKit */; }; + 48735EA62CED1B46006680BC /* Then in Frameworks */ = {isa = PBXBuildFile; productRef = 48735EA52CED1B46006680BC /* Then */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 48566F8A2CECB68E00846862 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 48566F6B2CECB68C00846862 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 48566F722CECB68C00846862; + remoteInfo = UMC_KREAM; + }; + 48566F942CECB68E00846862 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 48566F6B2CECB68C00846862 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 48566F722CECB68C00846862; + remoteInfo = UMC_KREAM; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 48566F732CECB68C00846862 /* UMC_KREAM.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = UMC_KREAM.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 48566F892CECB68E00846862 /* UMC_KREAMTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UMC_KREAMTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 48566F932CECB68E00846862 /* UMC_KREAMUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UMC_KREAMUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 48566F9B2CECB68E00846862 /* Exceptions for "UMC_KREAM" folder in "UMC_KREAM" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 48566F722CECB68C00846862 /* UMC_KREAM */; + }; + 48735E9E2CED0922006680BC /* Exceptions for "UMC_KREAM" folder in "UMC_KREAMTests" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Presentation/Home/Controller/HomeViewController.swift, + ); + target = 48566F882CECB68E00846862 /* UMC_KREAMTests */; + }; + 48735E9F2CED0922006680BC /* Exceptions for "UMC_KREAM" folder in "UMC_KREAMUITests" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Presentation/Home/Controller/HomeViewController.swift, + ); + target = 48566F922CECB68E00846862 /* UMC_KREAMUITests */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 48566F752CECB68C00846862 /* UMC_KREAM */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 48566F9B2CECB68E00846862 /* Exceptions for "UMC_KREAM" folder in "UMC_KREAM" target */, + 48735E9E2CED0922006680BC /* Exceptions for "UMC_KREAM" folder in "UMC_KREAMTests" target */, + 48735E9F2CED0922006680BC /* Exceptions for "UMC_KREAM" folder in "UMC_KREAMUITests" target */, + ); + path = UMC_KREAM; + sourceTree = ""; + }; + 48566F8C2CECB68E00846862 /* UMC_KREAMTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = UMC_KREAMTests; + sourceTree = ""; + }; + 48566F962CECB68E00846862 /* UMC_KREAMUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = UMC_KREAMUITests; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 48566F702CECB68C00846862 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 48566FB32CECBFBA00846862 /* SnapKit in Frameworks */, + 48735EA62CED1B46006680BC /* Then in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 48566F862CECB68E00846862 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 48566F902CECB68E00846862 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 48566F6A2CECB68C00846862 = { + isa = PBXGroup; + children = ( + 48566F752CECB68C00846862 /* UMC_KREAM */, + 48566F8C2CECB68E00846862 /* UMC_KREAMTests */, + 48566F962CECB68E00846862 /* UMC_KREAMUITests */, + 48566F742CECB68C00846862 /* Products */, + ); + sourceTree = ""; + }; + 48566F742CECB68C00846862 /* Products */ = { + isa = PBXGroup; + children = ( + 48566F732CECB68C00846862 /* UMC_KREAM.app */, + 48566F892CECB68E00846862 /* UMC_KREAMTests.xctest */, + 48566F932CECB68E00846862 /* UMC_KREAMUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 48566F722CECB68C00846862 /* UMC_KREAM */ = { + isa = PBXNativeTarget; + buildConfigurationList = 48566F9C2CECB68E00846862 /* Build configuration list for PBXNativeTarget "UMC_KREAM" */; + buildPhases = ( + 48566F6F2CECB68C00846862 /* Sources */, + 48566F702CECB68C00846862 /* Frameworks */, + 48566F712CECB68C00846862 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 48566F752CECB68C00846862 /* UMC_KREAM */, + ); + name = UMC_KREAM; + packageProductDependencies = ( + 48566FB22CECBFBA00846862 /* SnapKit */, + 48735EA52CED1B46006680BC /* Then */, + ); + productName = UMC_KREAM; + productReference = 48566F732CECB68C00846862 /* UMC_KREAM.app */; + productType = "com.apple.product-type.application"; + }; + 48566F882CECB68E00846862 /* UMC_KREAMTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 48566FA12CECB68E00846862 /* Build configuration list for PBXNativeTarget "UMC_KREAMTests" */; + buildPhases = ( + 48566F852CECB68E00846862 /* Sources */, + 48566F862CECB68E00846862 /* Frameworks */, + 48566F872CECB68E00846862 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 48566F8B2CECB68E00846862 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 48566F8C2CECB68E00846862 /* UMC_KREAMTests */, + ); + name = UMC_KREAMTests; + packageProductDependencies = ( + ); + productName = UMC_KREAMTests; + productReference = 48566F892CECB68E00846862 /* UMC_KREAMTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 48566F922CECB68E00846862 /* UMC_KREAMUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 48566FA42CECB68E00846862 /* Build configuration list for PBXNativeTarget "UMC_KREAMUITests" */; + buildPhases = ( + 48566F8F2CECB68E00846862 /* Sources */, + 48566F902CECB68E00846862 /* Frameworks */, + 48566F912CECB68E00846862 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 48566F952CECB68E00846862 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 48566F962CECB68E00846862 /* UMC_KREAMUITests */, + ); + name = UMC_KREAMUITests; + packageProductDependencies = ( + ); + productName = UMC_KREAMUITests; + productReference = 48566F932CECB68E00846862 /* UMC_KREAMUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 48566F6B2CECB68C00846862 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1610; + LastUpgradeCheck = 1610; + TargetAttributes = { + 48566F722CECB68C00846862 = { + CreatedOnToolsVersion = 16.1; + }; + 48566F882CECB68E00846862 = { + CreatedOnToolsVersion = 16.1; + TestTargetID = 48566F722CECB68C00846862; + }; + 48566F922CECB68E00846862 = { + CreatedOnToolsVersion = 16.1; + TestTargetID = 48566F722CECB68C00846862; + }; + }; + }; + buildConfigurationList = 48566F6E2CECB68C00846862 /* Build configuration list for PBXProject "UMC_KREAM" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 48566F6A2CECB68C00846862; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + 48566FB12CECBFBA00846862 /* XCRemoteSwiftPackageReference "SnapKit" */, + 48735EA42CED1B45006680BC /* XCRemoteSwiftPackageReference "Then" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = 48566F742CECB68C00846862 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 48566F722CECB68C00846862 /* UMC_KREAM */, + 48566F882CECB68E00846862 /* UMC_KREAMTests */, + 48566F922CECB68E00846862 /* UMC_KREAMUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 48566F712CECB68C00846862 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 48566F872CECB68E00846862 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 48566F912CECB68E00846862 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 48566F6F2CECB68C00846862 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 48566F852CECB68E00846862 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 48566F8F2CECB68E00846862 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 48566F8B2CECB68E00846862 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 48566F722CECB68C00846862 /* UMC_KREAM */; + targetProxy = 48566F8A2CECB68E00846862 /* PBXContainerItemProxy */; + }; + 48566F952CECB68E00846862 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 48566F722CECB68C00846862 /* UMC_KREAM */; + targetProxy = 48566F942CECB68E00846862 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 48566F9D2CECB68E00846862 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + "EXCLUDED_ARCHS[sdk=*]" = ""; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = UMC_KREAM/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.sominjun.UMC-KREAM"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 48566F9E2CECB68E00846862 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = UMC_KREAM/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.sominjun.UMC-KREAM"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 48566F9F2CECB68E00846862 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.1; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 48566FA02CECB68E00846862 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.1; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 48566FA22CECB68E00846862 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.sominjun.UMC-KREAMTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/UMC_KREAM.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/UMC_KREAM"; + }; + name = Debug; + }; + 48566FA32CECB68E00846862 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.sominjun.UMC-KREAMTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/UMC_KREAM.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/UMC_KREAM"; + }; + name = Release; + }; + 48566FA52CECB68E00846862 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.sominjun.UMC-KREAMUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = UMC_KREAM; + }; + name = Debug; + }; + 48566FA62CECB68E00846862 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.sominjun.UMC-KREAMUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = UMC_KREAM; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 48566F6E2CECB68C00846862 /* Build configuration list for PBXProject "UMC_KREAM" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 48566F9F2CECB68E00846862 /* Debug */, + 48566FA02CECB68E00846862 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 48566F9C2CECB68E00846862 /* Build configuration list for PBXNativeTarget "UMC_KREAM" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 48566F9D2CECB68E00846862 /* Debug */, + 48566F9E2CECB68E00846862 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 48566FA12CECB68E00846862 /* Build configuration list for PBXNativeTarget "UMC_KREAMTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 48566FA22CECB68E00846862 /* Debug */, + 48566FA32CECB68E00846862 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 48566FA42CECB68E00846862 /* Build configuration list for PBXNativeTarget "UMC_KREAMUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 48566FA52CECB68E00846862 /* Debug */, + 48566FA62CECB68E00846862 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 48566FB12CECBFBA00846862 /* XCRemoteSwiftPackageReference "SnapKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/SnapKit/SnapKit"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 5.7.1; + }; + }; + 48735EA42CED1B45006680BC /* XCRemoteSwiftPackageReference "Then" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/devxoul/Then"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 3.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 48566FB22CECBFBA00846862 /* SnapKit */ = { + isa = XCSwiftPackageProductDependency; + package = 48566FB12CECBFBA00846862 /* XCRemoteSwiftPackageReference "SnapKit" */; + productName = SnapKit; + }; + 48735EA52CED1B46006680BC /* Then */ = { + isa = XCSwiftPackageProductDependency; + package = 48735EA42CED1B45006680BC /* XCRemoteSwiftPackageReference "Then" */; + productName = Then; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 48566F6B2CECB68C00846862 /* Project object */; +} diff --git a/week8/UMC_KREAM/Assets.xcassets/AccentColor.colorset/Contents.json b/week8/UMC_KREAM/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/week8/UMC_KREAM/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/week8/UMC_KREAM/Assets.xcassets/AppIcon.appiconset/Contents.json b/week8/UMC_KREAM/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..2305880 --- /dev/null +++ b/week8/UMC_KREAM/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "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/week8/UMC_KREAM/Assets.xcassets/Contents.json b/week8/UMC_KREAM/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/week8/UMC_KREAM/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/week8/UMC_KREAM/Common/CustomNavagationTitle.swift b/week8/UMC_KREAM/Common/CustomNavagationTitle.swift new file mode 100644 index 0000000..3c9e2d0 --- /dev/null +++ b/week8/UMC_KREAM/Common/CustomNavagationTitle.swift @@ -0,0 +1,76 @@ +// +// CustomNavagationTitle.swift +// UMC_KREAM +// +// Created by 소민준 on 11/19/24. +// + +import UIKit +import SnapKit + +class CustomNavagationTitle: UIView { + + let titleText: String + let subTitleText: String? + + + init(frame: CGRect, titleText: String, subTitleText: String?) { + self.titleText = titleText + self.subTitleText = subTitleText + + super.init(frame: frame) + + setTitle(title: titleText, subTitle: subTitleText) + constraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + /// 네비게이션 타이틀뷰의 메인 타이틀 라벨 + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.textColor = .black + label.font = UIFont.systemFont(ofSize: 15, weight: .bold) + return label + }() + + /// 네비게이션 타이틀뷰의 서브 타이틀 라벨 + private lazy var subTitleLabel: UILabel = { + let label = UILabel() + label.textColor = .gray + label.font = UIFont.systemFont(ofSize: 10, weight: .regular) + return label + }() + + /// 네비게이션 타이틀뷰의 메인 타이틀 + 서브 타이틀 스택 + private lazy var titleStack: UIStackView = { + let stack = UIStackView(arrangedSubviews: [titleLabel, subTitleLabel]) + stack.axis = .vertical + stack.spacing = 2 + stack.alignment = .center + return stack + }() + + // MARK: - Function + + /// 초기화를 통해 전달 받은 메인 타이틀 값, 서브 타이틀 값을 전달하여 해당 라벨에 적용 + /// - Parameters: + /// - title: 초기화를 통해 설정된 메인 타이틀 값 + /// - subTitle: 초기화를 통해 설정된 서브 타이틀 값 + private func setTitle(title: String, subTitle: String?) { + self.titleLabel.text = title + self.subTitleLabel.text = subTitle ?? nil + } + + /// 제약 조건 설정 + private func constraints() { + self.addSubview(titleStack) + titleStack.snp.makeConstraints { + $0.centerY.centerX.equalToSuperview() + } + } + + + } + diff --git a/week8/UMC_KREAM/Common/PurchaseBottomBackground.swift b/week8/UMC_KREAM/Common/PurchaseBottomBackground.swift new file mode 100644 index 0000000..f75cddf --- /dev/null +++ b/week8/UMC_KREAM/Common/PurchaseBottomBackground.swift @@ -0,0 +1,45 @@ +// +// PurchaseBottomBackground.swift +// UMC_KREAM +// +// Created by 소민준 on 11/19/24. +// + +import UIKit + +class PurchaseBottomBackground: UIView { + + // MARK: - Init + override init(frame: CGRect) { + super.init(frame: frame) + + self.backgroundColor = .white + self.addSubview(topLine) + constraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Property + + /// 상단 구분선, 피그마를 통해 확대해서 보면 상단 선이 있음을 확인 가능 + /// 피그마의 라인 색을 적용 안하고 연한색 값을 그냥 넣어서 작성함 + private lazy var topLine: UIView = { + let view = UIView() + view.backgroundColor = .systemGray5 + return view + }() + + // MARK: - Function + + /// 제약 조건 설정 + private func constraints() { + topLine.snp.makeConstraints { + $0.top.left.equalToSuperview() + $0.width.equalToSuperview() + $0.height.equalTo(1) + } + } + } diff --git a/week8/UMC_KREAM/Common/PurchaseButton.swift b/week8/UMC_KREAM/Common/PurchaseButton.swift new file mode 100644 index 0000000..bac5eee --- /dev/null +++ b/week8/UMC_KREAM/Common/PurchaseButton.swift @@ -0,0 +1,149 @@ +// +// PurchaseButton.swift +// UMC_KREAM +// +// Created by 소민준 on 11/19/24. +// + +import UIKit + +class PurchaseButton: UIView, PurchaseButtonProtocol { + + let btnType: PurchaseButtonType + init(frame: CGRect, btnType: PurchaseButtonType) { + self.btnType = btnType + super.init(frame: frame) + + setConfigure(type: btnType) + setBackgroundColor(type: btnType) + addStackView() + addComponents() + constraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + // MARK: - Property + + /// 커스텀 버튼에서 사용할 가격 표시 라벨 + var priceLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 13, weight: .semibold) + label.textColor = UIColor.white + return label + }() + + /// 커스텀 버튼에서 사용할 가격 밑에 보이는 텍스트, "즉시 판매가", "즉시 구매가" + var subLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 10, weight: .regular) + return label + }() + + /// 버튼의 왼쪽에 구매 타입의 버튼인지, 판매 타입의 버튼인지 텍스트를 보일 라벨 + var buttonLeftLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 16, weight: .semibold) + label.textColor = .white + return label + }() + + /// priceLabel + subLabel을 담을 스택 + var priceInfoStack: UIStackView = { + let stack = UIStackView() + stack.axis = .vertical + stack.spacing = 2 + stack.distribution = .equalSpacing + stack.alignment = .leading + return stack + }() + + // MARK: - Function + + /// 컴포넌트 스택에 추가 + private func addStackView() { + [priceLabel, subLabel].forEach{ self.priceInfoStack.addArrangedSubview($0) } + } + + /// 버튼 내부에 추가할 컴포넌트 + private func addComponents() { + [buttonLeftLabel, priceInfoStack].forEach{ self.addSubview($0) } + } + + /// 제약 조건 설정 + private func constraints() { + buttonLeftLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(15) + $0.bottom.equalToSuperview().offset(-15) + $0.left.equalToSuperview().offset(10) + $0.width.equalTo(28) + } + + priceInfoStack.snp.makeConstraints { + $0.top.equalToSuperview().offset(8) + $0.left.equalTo(buttonLeftLabel.snp.right).offset(21) + $0.width.greaterThanOrEqualTo(53) + $0.height.equalTo(30) + } + + priceLabel.snp.makeConstraints { + $0.width.greaterThanOrEqualTo(30) + $0.height.equalTo(16) + } + + subLabel.snp.makeConstraints { + $0.width.greaterThanOrEqualTo(30) + $0.height.equalTo(12) + } + } + + // MARK: - SetupButton Custom + + /// 버튼 타입을 통해 버튼 내부 텍스트 커스텀 및 버튼 자체 커스텀 + /// - Parameter type: 구매 버튼 or 판매 버튼 + private func setConfigure(type: PurchaseButtonType) { + + self.clipsToBounds = true + self.layer.cornerRadius = 10 + + buttonLeftLabel.text = buttonLeftText(type: type) + subLabel.textColor = setTextColor(type: type) + } + + + /// 버튼 타입에 따른 서브라벨 텍스트 컬러 반환 + /// - Parameter type: 구매 버튼 or 판매 버튼 + /// - Returns: 버튼 타입에 맞는 컬러 반환 + private func setTextColor(type: PurchaseButtonType) -> UIColor { + switch type { + case .purchase: + return UIColor(red: 0.639, green: 0.216, blue: 0.137, alpha: 1) + case .sales: + return UIColor(red: 0.122, green: 0.467, blue: 0.271, alpha: 1) + } + } + + /// 버튼 타입에 따른 버튼 배경색 반환 + /// - Parameter type: 구매 버튼 or 판매 버튼 + private func setBackgroundColor(type: PurchaseButtonType) { + switch type { + case .purchase: + self.backgroundColor = UIColor(red: 0.937, green: 0.384, blue: 0.329, alpha: 1) + case .sales: + self.backgroundColor = UIColor(red: 0.255, green: 0.725, blue: 0.478, alpha: 1) + } + } + + /// 버튼 타입에 따른 왼쪽 타입을 나타내는 텍스트 반환 + /// - Parameter type: 구매 버튼 or 판매 버튼 + /// - Returns: 타입을 스트링으로 반환 + private func buttonLeftText(type: PurchaseButtonType) -> String { + switch type { + case .purchase: + return "구매" + case .sales: + return "판매" + } + } +} diff --git a/week8/UMC_KREAM/Common/ShippingButton.swift b/week8/UMC_KREAM/Common/ShippingButton.swift new file mode 100644 index 0000000..a02ab82 --- /dev/null +++ b/week8/UMC_KREAM/Common/ShippingButton.swift @@ -0,0 +1,138 @@ +// +// ShippingButton.swift +// UMC_KREAM +// +// Created by 소민준 on 11/19/24. +// + +import UIKit + +class ShippingButton: UIView, PurchaseButtonProtocol { + + let btnType: ShippingButtonType + + /// 빠른 배송 또는 일반 배송 버튼의 초기화 부분 + /// - Parameters: + /// - frame: 버튼의 프레임 + /// - btnType: 빠른 배송 타입 or 일반 배송 타입 선택 + /// + /* + init() 내부 코드 작성 순서는 아래와 같습니다. 아래 순서를 지키지 않으면 컴파일 초기화 오류가 생길 것입니다. + 1. init()을 override 하지 않기 때문에 초기화 값을 먼저 넣고, + 2. super init으로 프레임을 지정한다 + 3. 초기화 값을 사용하여 버튼을 커스텀한다. + */ + init(frame: CGRect, btnType: ShippingButtonType) { + self.btnType = btnType + super.init(frame: frame) + + setConfigure(type: btnType) + setBackgroundColor(type: btnType) + addStackView() + addComponents() + constraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Property + + /// 커스텀 버튼에서 사용할 가격 표시 라벨 + var priceLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 14, weight: .semibold) + label.textColor = UIColor.white + return label + }() + + /// 커스텀 버튼에서 사용할 가격 밑에 보이는 텍스트, "빠른 배송", "일반 배송" + var subLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 10, weight: .regular) + return label + }() + + /// 버튼의 왼쪽에 구매 타입의 버튼인지, 판매 타입의 버튼인지 텍스트를 보일 라벨 + var priceInfoStack: UIStackView = { + let stack = UIStackView() + stack.axis = .vertical + stack.spacing = 2 + stack.distribution = .equalSpacing + stack.alignment = .center + return stack + }() + + // MARK: - Function + + /// 컴포넌트 스택에 추가 + private func addStackView() { + [priceLabel, subLabel].forEach{ self.priceInfoStack.addArrangedSubview($0) } + } + + /// 버튼 내부에 추가할 컴포넌트 + private func addComponents() { + [priceInfoStack].forEach{ self.addSubview($0) } + } + + /// 제약 조건 설정 + private func constraints() { + priceInfoStack.snp.makeConstraints { + $0.top.equalToSuperview().offset(8) + $0.centerX.equalToSuperview() + $0.width.greaterThanOrEqualTo(53) + $0.height.equalTo(30) + } + + priceLabel.snp.makeConstraints { + $0.width.greaterThanOrEqualTo(30) + } + + subLabel.snp.makeConstraints { + $0.width.greaterThanOrEqualTo(30) + } + } + + // MARK: - SetupButton Custom + + /// 버튼 타입을 통해 버튼 내부 텍스트 커스텀 및 버튼 자체 커스텀 + /// - Parameter type: 빠른 배송 or 일반 배송 + private func setConfigure(type: ShippingButtonType) { + + self.clipsToBounds = true + self.layer.cornerRadius = 10 + + subLabel.textColor = setTextColor(type: type) + subLabel.text = setSubText(type: type) + } + + private func setTextColor(type: ShippingButtonType) -> UIColor { + switch type { + case .speed: + return UIColor(red: 1, green: 0.792, blue: 0.725, alpha: 1) + case .normal: + return UIColor(red: 0.886, green: 0.886, blue: 0.886, alpha: 1) + } + } + + private func setBackgroundColor(type: ShippingButtonType) { + switch type { + case .speed: + self.backgroundColor = UIColor(red: 0.937, green: 0.384, blue: 0.329, alpha: 1) + case .normal: + self.backgroundColor = UIColor(red: 0.133, green: 0.133, blue: 0.133, alpha: 1) + } + } + + private func setSubText(type: ShippingButtonType) -> String { + switch type { + case .speed: + return "빠른배송(1~2일 소요)" + case .normal: + return "일반배송(5~7일 소요)" + } + } + + } + diff --git a/week8/UMC_KREAM/Data/Enum/PurchaseButtonType.swift b/week8/UMC_KREAM/Data/Enum/PurchaseButtonType.swift new file mode 100644 index 0000000..d1b25fe --- /dev/null +++ b/week8/UMC_KREAM/Data/Enum/PurchaseButtonType.swift @@ -0,0 +1,16 @@ +// +// PurchaseButtonType.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import Foundation + +import Foundation + +/// 구매하기 버튼 두개의 타입 재사용 +enum PurchaseButtonType { + case purchase + case sales +} diff --git a/week8/UMC_KREAM/Data/Enum/ShippingButtonType.swift b/week8/UMC_KREAM/Data/Enum/ShippingButtonType.swift new file mode 100644 index 0000000..6a448ed --- /dev/null +++ b/week8/UMC_KREAM/Data/Enum/ShippingButtonType.swift @@ -0,0 +1,13 @@ +// +// ShippingButtonType.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import Foundation +/// 빠른배송 또는 일반배송 타입 +enum ShippingButtonType { + case speed + case normal +} diff --git a/week8/UMC_KREAM/Data/Models/HomeCellModel.swift b/week8/UMC_KREAM/Data/Models/HomeCellModel.swift new file mode 100644 index 0000000..b3cab44 --- /dev/null +++ b/week8/UMC_KREAM/Data/Models/HomeCellModel.swift @@ -0,0 +1,77 @@ +// +// HomeCellModel.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import Foundation + +/// 홈 탭의 광고 배너, 1번째 섹션 +struct BannerModel: RequiredCellProtocol { + var imageView: String +} + +/// 홈 탭의 추천 상품 데이터, 2번째 섹션 +struct RecommendProductModel: RequiredCellProtocol { + var imageView: String + var titleText: String +} + +/// 홈 탭의 판매 상품 데이터, 3번째 섹션 +struct ProductGridModel: RequiredCellProtocol { + var imageView: String + var purchaseCnt: String + var selectedTag: Bool + var titleText: String + var subTitleText: String + var priceText: String + var priceSubText: String +} + +/// 홈 탭의 유저 스토리형 데이터, 4번째 섹션 +struct UserStoryModel: RequiredCellProtocol { + var imageView: String + var userName: String +} + +// MARK: - Dummy Data + +/// 홈 탭의 섹션셜 더미데이터 +final class HomeCellModel { + /* 유저 스토리형 데이터, 4번째 섹션 데이터 */ + static let userStoryData: [UserStoryModel] = [ + .init(imageView: "userCell1", userName: "@kakain^.^"), + .init(imageView: "userCell2", userName: "@UMC-7th"), + .init(imageView: "userCell3", userName: "@thosan"), + .init(imageView: "userCell4", userName: "@londonHuman"), + .init(imageView: "userCell5", userName: "@Faker") + ] + + /* 판매 상품 데이터, 3번째 섹션 데이터 */ + static let productGridData: [ProductGridModel] = [ + .init(imageView: "GridCell1", purchaseCnt: "거래 12.8만", selectedTag: false, titleText: "MLB", subTitleText: "청키라이너 뉴욕양키스", priceText: "139,000원", priceSubText: "즉시 구매가"), + .init(imageView: "GridCell2", purchaseCnt: "거래 1.8만", selectedTag: true, titleText: "나이키", subTitleText: "Jordan 1 Retro High OG Yellow", priceText: "139,000원", priceSubText: "즉시 구매가"), + .init(imageView: "GridCell3", purchaseCnt: "거래 11.8만", selectedTag: true, titleText: "아디다스", subTitleText: "Human MASEW", priceText: "228,000원", priceSubText: "즉시 구매가"), + .init(imageView: "GridCell4", purchaseCnt: "거래 89.8만", selectedTag: false, titleText: "뉴발란스", subTitleText: "그냥 어떤 물품 유명함..", priceText: "2,000,000원", priceSubText: "즉시 구매가"), + .init(imageView: "GridCell5", purchaseCnt: "거래 54.8만", selectedTag: false, titleText: "어딘가 브랜드", subTitleText: "설명 뭐 넣을까요..!", priceText: "421,991,000원", priceSubText: "즉시 구매가"), + .init(imageView: "GridCell6", purchaseCnt: "거래 3.8만", selectedTag: true, titleText: "브랜드 또 모있지..", subTitleText: "하하하하 그냥 안 쓸래요", priceText: "1,000원", priceSubText: "즉시 구매가") + ] + + /* 추천 상품 뎅터, 2번째 섹션 데이터 */ + static let recommendProductData: [RecommendProductModel] = [ + .init(imageView: "homeCell1", titleText: "크림 드로우"), + .init(imageView: "homeCell2", titleText: "실시간 차트"), + .init(imageView: "homeCell3", titleText: "남성 추천"), + .init(imageView: "homeCell4", titleText: "여성 추천"), + .init(imageView: "homeCell5", titleText: "색다른 추천"), + .init(imageView: "homeCell6", titleText: "정가 아래"), + .init(imageView: "homeCell7", titleText: "윤세 24AW"), + .init(imageView: "homeCell8", titleText: "올해의 베스트"), + .init(imageView: "homeCell9", titleText: "10월 베네핏"), + .init(imageView: "homeCell10", titleText: "아크네 선물") + ] + + /* 광고 배너 데이터, 1번째 섹션 데이터 */ + static let bannerData: [BannerModel] = [.init(imageView: "adverImage")] +} diff --git a/week8/UMC_KREAM/Data/Models/PuchaseModel.swift b/week8/UMC_KREAM/Data/Models/PuchaseModel.swift new file mode 100644 index 0000000..7562d4a --- /dev/null +++ b/week8/UMC_KREAM/Data/Models/PuchaseModel.swift @@ -0,0 +1,28 @@ +// +// PuchaseModel.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import Foundation + +/// 상품 구매 뷰, 점퍼 데이터 +struct PurchaseModel: RequiredCellProtocol { + var imageView: String + var productName: String +} + +// MARK: - DummyData + +final class PurchaseData { + static let purchaseData: [PurchaseModel] = [ + .init(imageView: "purchase1", productName: "마뗑킴 로고 코팅 점퍼 블루"), + .init(imageView: "purchase2", productName: "마뗑킴 로고 코팅 점퍼 퍼플"), + .init(imageView: "purchase3", productName: "마뗑킴 로고 코팅 점퍼 청록"), + .init(imageView: "purchase4", productName: "마뗑킴 로고 코팅 점퍼 자두"), + .init(imageView: "purchase5", productName: "마뗑킴 로고 코팅 점퍼 빨강"), + .init(imageView: "purchase6", productName: "마뗑킴 로고 코팅 점퍼 갈색"), + .init(imageView: "purchase7", productName: "마뗑킴 로고 코팅 점퍼 블랙") + ] +} diff --git a/week8/UMC_KREAM/Data/Models/SavedProduct.swift b/week8/UMC_KREAM/Data/Models/SavedProduct.swift new file mode 100644 index 0000000..8c8ab18 --- /dev/null +++ b/week8/UMC_KREAM/Data/Models/SavedProduct.swift @@ -0,0 +1,35 @@ +// +// SavedProduct.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import Foundation + +/// Saved 탭 데이터 +struct SavedProduct { + let imageName: String + let description: Description + let price: Int +} + +struct Description { + let title: String + let subTitle: String +} + +// MARK: - Dummy Data + +final class SavedProductData { + static let datalist: [SavedProduct] = [ + SavedProduct(imageName: "1.png", description: Description(title: "에베레스트 다이아 반지", subTitle: "에베레스트 올라가서 다이아 구하고 만든 반지!! 과연 이것을 누가 사갈것인가??"), price: 1292999000), + SavedProduct(imageName: "2.png", description: Description(title: "애기 신발", subTitle: "우리 아기가 신으면 정말 잘 어울리 거 같은 초미니 신발!!"), price: 12000), + SavedProduct(imageName: "3.png", description: Description(title: "남성 헬창복", subTitle: "너 헬창이니?? 그럼 당장 사서 입어 핏 미쳐~"), price: 90000), + SavedProduct(imageName: "4.png", description: Description(title: "코딩 바지", subTitle: "컴퓨터 앞에 앉아서 코딩 하는 너,, 너를 위한 바지야 그냥 입고 코딩해봐 그럼 잘될걸??"), price: 1292999000), + SavedProduct(imageName: "5.png", description: Description(title: "아디다스다 겨울 신발", subTitle: "사실 아디다스가 아닐지도 모르는 신발 하지만 겨울 신발인건 맞아요!! 따뜻해요!!"), price: 120000), + SavedProduct(imageName: "6.png", description: Description(title: "손오공이 잃어버린 머리띠", subTitle: "손오공이 장난치다가 머리띠를 어딘가 숨겨뒀던 역사속 물건!!"), price: 120000000), + SavedProduct(imageName: "7.png", description: Description(title: "커플 반지", subTitle: "이제 슬슬 마련하시죠?? 정말 잘 어울릴거에요!"), price: 111111), + SavedProduct(imageName: "8.png", description: Description(title: "그냥 신발", subTitle: "헐값 떨이요~"), price: 1989) + ] +} diff --git a/week8/UMC_KREAM/Data/Models/SizeModel.swift b/week8/UMC_KREAM/Data/Models/SizeModel.swift new file mode 100644 index 0000000..17e8421 --- /dev/null +++ b/week8/UMC_KREAM/Data/Models/SizeModel.swift @@ -0,0 +1,26 @@ +// +// SizeModel.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import Foundation + +/// 상품 구매 뷰의 사이즈 선택 데이터 +struct SizeModel { + var size: String + var price: String +} + +// MARK: - Dummy Data + +final class SizeData { + static let sizeData: [SizeModel] = [ + .init(size: "S", price: "360,000"), + .init(size: "M", price: "360,000"), + .init(size: "L", price: "360,000"), + .init(size: "XL", price: "360,000"), + .init(size: "XXL", price: "360,000"), + ] +} diff --git a/week8/UMC_KREAM/Data/Models/UserInfo.swift b/week8/UMC_KREAM/Data/Models/UserInfo.swift new file mode 100644 index 0000000..74c9412 --- /dev/null +++ b/week8/UMC_KREAM/Data/Models/UserInfo.swift @@ -0,0 +1,31 @@ +// +// UserInfo.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import Foundation + +struct UserInfo { + let id: String + let pwd: String + + /// UserDefaults에 저장하는 메서드 + public func saveUserDefaults() { + UserDefaults.standard.set(id, forKey: "userId") + UserDefaults.standard.set(pwd, forKey: "userPwd") + } + + /// UserDefaults에서 회원 아이디, 비밀번호 불러오기 + /// - Returns: 아이디, 비밀번호 반환 + static func loadUserDefaults() -> UserInfo? { + guard + let id = UserDefaults.standard.string(forKey: "userId"), + let pwd = UserDefaults.standard.string(forKey: "userPwd") + else { + return nil + } + return UserInfo(id: id, pwd: pwd) + } +} diff --git a/week8/UMC_KREAM/Data/Protocol/CellHeaderProtocol.swift b/week8/UMC_KREAM/Data/Protocol/CellHeaderProtocol.swift new file mode 100644 index 0000000..e6e0948 --- /dev/null +++ b/week8/UMC_KREAM/Data/Protocol/CellHeaderProtocol.swift @@ -0,0 +1,14 @@ +// +// CellHeaderProtocol.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import UIKit + +/// 셀의 헤더가 컴포넌트 정의 프로토콜 +protocol CellHeaderProtocol { + var headerTitle: UILabel { get } + var headerSubTitle: UILabel { get } +} diff --git a/week8/UMC_KREAM/Data/Protocol/ProductCellProtocol.swift b/week8/UMC_KREAM/Data/Protocol/ProductCellProtocol.swift new file mode 100644 index 0000000..486d050 --- /dev/null +++ b/week8/UMC_KREAM/Data/Protocol/ProductCellProtocol.swift @@ -0,0 +1,18 @@ +// +// ProductCellProtocol.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import UIKit + + +/// 크림 상품 셀에 사용할 프로토콜 +protocol ProductCellProtocol { + var imageView: UIImageView { get } + var titleText: UILabel { get } + var priceLabel: UILabel { get } + + func configure(model: RequiredCellProtocol) +} diff --git a/week8/UMC_KREAM/Data/Protocol/PurchaseButtonProtocol.swift b/week8/UMC_KREAM/Data/Protocol/PurchaseButtonProtocol.swift new file mode 100644 index 0000000..640a47d --- /dev/null +++ b/week8/UMC_KREAM/Data/Protocol/PurchaseButtonProtocol.swift @@ -0,0 +1,15 @@ +// +// PurchaseButtonProtocol.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import Foundation +import UIKit + +/// 커스텀 버튼의 공통 속성 지정 +protocol PurchaseButtonProtocol { + var priceLabel: UILabel { get set } + var subLabel: UILabel { get set } +} diff --git a/week8/UMC_KREAM/Data/Protocol/RequiredCellProtocol.swift b/week8/UMC_KREAM/Data/Protocol/RequiredCellProtocol.swift new file mode 100644 index 0000000..a1e3a77 --- /dev/null +++ b/week8/UMC_KREAM/Data/Protocol/RequiredCellProtocol.swift @@ -0,0 +1,12 @@ +// +// RequiredCellProtocol.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import Foundation + +protocol RequiredCellProtocol { + var imageView: String { get } +} diff --git a/week8/UMC_KREAM/MainTabBarController.swift b/week8/UMC_KREAM/MainTabBarController.swift new file mode 100644 index 0000000..b0dcbf2 --- /dev/null +++ b/week8/UMC_KREAM/MainTabBarController.swift @@ -0,0 +1,57 @@ +// +// MainTabBarController.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import UIKit + +class MainTabBarController: UITabBarController { + + override func viewDidLoad() { + super.viewDidLoad() + + setupTabBar() + appearance() + } + + + /// 탭바 설정 함수(피그마 디자인에 맞춰 5개 구성) + private func setupTabBar() { + self.tabBar.backgroundColor = .white + self.tabBar.isTranslucent = false + + let homeVC = UINavigationController(rootViewController: HomeViewController()) + homeVC.tabBarItem = UITabBarItem(title: "", image: UIImage(named: "Home"), tag: 1) + + let StyleVC = UINavigationController(rootViewController: StyleViewController()) + StyleVC.tabBarItem = UITabBarItem(title: "", image: UIImage(named: "Style"), tag: 2) + + let ShopVC = UINavigationController(rootViewController: ShopViewController()) + ShopVC.tabBarItem = UITabBarItem(title: "", image: UIImage(named: "Shop.pdf"), tag: 3) + + let SavedVC = UINavigationController(rootViewController: SavedViewController()) + SavedVC.tabBarItem = UITabBarItem(title: "", image: UIImage(named: "Saved.pdf"), tag: 4) + + let MyVC = UINavigationController(rootViewController: MyPageViewController()) + MyVC.tabBarItem = UITabBarItem(title: "", image: UIImage(named: "My.pdf"), tag: 5) + + self.viewControllers = [homeVC, StyleVC, ShopVC, SavedVC, MyVC] + + } + + /// 클릭 시, 검은색으로 칠해지도록 Aprrearance 조정 함수 + private func appearance() { + let barAppearance = UITabBarAppearance() + barAppearance.stackedLayoutAppearance.selected.iconColor = UIColor.black + barAppearance.stackedLayoutAppearance.selected.titleTextAttributes = [.foregroundColor: UIColor.black] + barAppearance.stackedLayoutAppearance.selected.badgeBackgroundColor = UIColor.black + barAppearance.stackedLayoutAppearance.normal.badgeBackgroundColor = UIColor.black + + self.tabBar.standardAppearance = barAppearance + self.tabBar.backgroundColor = .clear + } + + + } diff --git a/week8/UMC_KREAM/Presentation/Home/Controller/HomeSegmentControl.swift b/week8/UMC_KREAM/Presentation/Home/Controller/HomeSegmentControl.swift new file mode 100644 index 0000000..44008aa --- /dev/null +++ b/week8/UMC_KREAM/Presentation/Home/Controller/HomeSegmentControl.swift @@ -0,0 +1,129 @@ +// +// HomeSegmentControl.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import UIKit +class HomeSegmentControl: UISegmentedControl { + + /// 세그먼트 컨트롤러가 가지는 하단 막대 커스텀을 위한 UIView 생성 + let selectedIndicator = UIView() + + // MARK: - Init + + init(items: [String]) { + super.init(items: items) + configureSegmentControl() + selectedSegmentIndicator() + } + + override func layoutSubviews() { + super.layoutSubviews() + updateIndicatorPosition(animated: true) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - selfSetup Function + + /// 세그먼트 컨트롤 커스텀 + private func configureSegmentControl() { + self.backgroundColor = UIColor.clear + self.selectedSegmentTintColor = UIColor.clear + + + /* 경계선 및 뒷 배경 제거*/ + let clearImage = UIImage() + setBackgroundImage(clearImage, for: .normal, barMetrics: .default) + setBackgroundImage(clearImage, for: .selected, barMetrics: .default) + setDividerImage(clearImage, forLeftSegmentState: .normal, rightSegmentState: .normal, barMetrics: .default) + + /* 세그먼트 선택되었을 때 폰트 값 */ + let selectedAttributes = makeAttributes(color: UIColor.black, weight: .bold) + + /* 세그먼트 선택되지 않은 상태 폰트 값*/ + let normalAttributes = makeAttributes(color: UIColor.darkGray, weight: .regular) + + /* 세그먼트 선택되었을 때 폰트 설정 */ + setTitleTextAttributes(selectedAttributes, for: .selected) + + /* 세그먼트 기본 상태 폰트 설정 */ + setTitleTextAttributes(normalAttributes, for: .normal) + + /* 세그먼트 버튼 클릭 시 변화 */ + addTarget(self, action: #selector(segmentChange), for: .valueChanged) + } + + // MARK: - MakeFunction + + /// 세그먼트 글자 커스텀 지정 + /// - Parameter weight: 굵기 조정 + /// - Returns: 지정된 폰트 스타일 반환 + private func makeAttributes(color: UIColor, weight: UIFont.Weight) -> [NSAttributedString.Key: Any] { + var value = [NSAttributedString.Key: Any]() + value[.foregroundColor] = color + value[.font] = UIFont.systemFont(ofSize: 16, weight: weight) + return value + } +} + +//MARK: - SegmentExtension + +extension HomeSegmentControl { + // MARK: - IndicatorFunction + + /// 세그먼트 막대 추가 + private func selectedSegmentIndicator() { + selectedIndicator.backgroundColor = UIColor.black + selectedSegmentIndex = 0 + addSubview(selectedIndicator) + + /* + 처음 등장하는 시점에서는 애니메이션이 필요가 없습니다. + 뷰 컨트롤러가 바로 띄워지는 시점이기 때문에 세그먼트가 선택된 상태로 바로 등장해야 합니다. + 그래서 animation값은 false로 지정합니다. + */ + updateIndicatorPosition(animated: false) + } + + /// 세그먼트 막대 포지션 이동 설정 + /// - Parameter animated: 애니메이션 유/무 설정 + private func updateIndicatorPosition(animated: Bool) { + let segmentWidth = bounds.width / CGFloat(numberOfSegments) + + /* 세그먼트 인디케이터와 세그먼트 텍스트 중간 정렬을 위한 여백 값 */ + let leftOffset: CGFloat = 1 + + /* 선택된 세그먼트의 텍스트 길이에 맞춰 막대의 가로 길이 설정 */ + let indicatorWidth = calculateLabelWidth(for: selectedSegmentIndex) + + let indicatorPositionX = segmentWidth * CGFloat(selectedSegmentIndex) + (segmentWidth - indicatorWidth) / 2 - leftOffset + let indicatorFrame = CGRect(x: indicatorPositionX, y: bounds.height - 3, width: indicatorWidth, height: 2) + + if animated { + UIView.animate(withDuration: 0.3, animations: { + self.selectedIndicator.frame = indicatorFrame + }) + } else { + self.selectedIndicator.frame = indicatorFrame + } + } + + /// 각 세그먼트 버튼 위치의 텍스트 사이즈 계산 + /// - Parameter index: 현재 선택된 세그먼트 버튼 + /// - Returns: 선택된 세그먼트 텍스트 사이즈 값 반환 + private func calculateLabelWidth(for index: Int) -> CGFloat { + guard let title = titleForSegment(at: index) else { return 0 } + let attributes: [NSAttributedString.Key: Any] = [.font: UIFont.systemFont(ofSize: 16, weight: .bold)] + let textSize = (title as NSString).size(withAttributes: attributes) + return textSize.width + } + + @objc private func segmentChange() { + updateIndicatorPosition(animated: true) + } +} diff --git a/week8/UMC_KREAM/Presentation/Home/Controller/HomeViewController.swift b/week8/UMC_KREAM/Presentation/Home/Controller/HomeViewController.swift new file mode 100644 index 0000000..2ac4a8b --- /dev/null +++ b/week8/UMC_KREAM/Presentation/Home/Controller/HomeViewController.swift @@ -0,0 +1,136 @@ +// +// HomeViewController.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import UIKit + +class HomeViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + } + + + private lazy var homeView: HomeView = { + let homeView = HomeView() + homeView.backgroundColor = .white + homeView.homeCollectionView.delegate = self + homeView.homeCollectionView.dataSource = self + return homeView + }() + } + + // MARK: Extension + + extension HomeViewController: UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { + + /// 섹션 갯수 + func numberOfSections(in collectionView: UICollectionView) -> Int { + return 4 + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + switch section { + case 0: + return HomeCellModel.bannerData.count + case 1: + return HomeCellModel.recommendProductData.count + case 2: + return HomeCellModel.productGridData.count + case 3: + return HomeCellModel.userStoryData.count + default: + return 0 + } + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + switch indexPath.section { + case 0: + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: AdBannerCell.identifier, for: indexPath) as? AdBannerCell else { return UICollectionViewCell() } + + let data = HomeCellModel.bannerData + cell.configure(model: data[indexPath.row]) + + return cell + case 1: + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: RecommendationCell.identifier, for: indexPath) as? RecommendationCell else { return UICollectionViewCell() } + + let data = HomeCellModel.recommendProductData + cell.configure(model: data[indexPath.row]) + + return cell + case 2: + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ProductGridCell.identifier, for: indexPath) as? ProductGridCell else { return UICollectionViewCell() } + + let data = HomeCellModel.productGridData + cell.configure(model: data[indexPath.row]) + + return cell + case 3: + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: UserStoryCell.identifier, for: indexPath) as? UserStoryCell else { return UICollectionViewCell() } + + let data = HomeCellModel.userStoryData + cell.configure(model: data[indexPath.row]) + + return cell + default: + return UICollectionViewCell() + } + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { + return 8 + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { + return 0 + } + } + + + extension HomeViewController { + /// 헤더와 푸터 추가 헤더의 경의 특정 섹션에 대해서만 추가 + func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { + switch kind { + case UICollectionView.elementKindSectionFooter: + let footer = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: SectionSeparatorFooter.identifier, for: indexPath) as! SectionSeparatorFooter + return footer + case UICollectionView.elementKindSectionHeader: + let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: BaseCellHeader.identifier, for: indexPath) as! BaseCellHeader + + if indexPath.section == 2 { + header.configure(title: "Just Dropped", subTitle: "발매 상품") + } else if indexPath.section == 3 { + header.configure(title: "본격 한파대비! 연말 필수템 모음", subTitle:"#해피홀릭룩챌린지") + } + return header + default: + return UICollectionReusableView() + } + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { + if section == 2 || section == 3 { + return CGSize(width: collectionView.bounds.width, height: 50) + } else { + return CGSize.zero + } + } + + /// 약간의 이스터 에그?? 마지막 섹션의 첫번째 셀을 누르면 6주차의 구매 뷰를 불러온다. + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + if indexPath.section == 3 && indexPath.item == 0{ + let newViewController = PurchaseViewController() + let navigationController = UINavigationController(rootViewController: newViewController) + + navigationController.modalPresentationStyle = .fullScreen + present(navigationController, animated: true) + } + } + } diff --git a/week8/UMC_KREAM/Presentation/Home/Views/CollectionView/Cell/AdBannerCell.swift b/week8/UMC_KREAM/Presentation/Home/Views/CollectionView/Cell/AdBannerCell.swift new file mode 100644 index 0000000..aa9c8b6 --- /dev/null +++ b/week8/UMC_KREAM/Presentation/Home/Views/CollectionView/Cell/AdBannerCell.swift @@ -0,0 +1,52 @@ +// +// AdBannerCell.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import UIKit + +class AdBannerCell: BaseCollectionCell { + + static let identifier: String = "AdBannerCell" + + // MARK: - Init + + override init(frame: CGRect) { + super.init(frame: frame) + addComponents() + constraints() + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + /// BaseCollectionCell의 prepareForReuse()를 오버라이드합니다. + override func prepareForReuse() { + self.imageView.image = nil + } + + // MARK: - Function + + /// 컴포넌트 생성 + private func addComponents() { + self.addSubview(imageView) + } + + /// 제약 조건 설정 + private func constraints() { + self.imageView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + } + + /// RequiredCellProtocol 상속을 받는 모델 값을 파라미터로 전달하여 해당 셀의 값 넣어준다. + /// - Parameter model: RequiredCellProtocol을 상속받는 모델 + override func configure(model: any RequiredCellProtocol) { + guard let bannerModel = model as? BannerModel else { return } + + self.imageView.image = UIImage(named: bannerModel.imageView) + } +} diff --git a/week8/UMC_KREAM/Presentation/Home/Views/CollectionView/Cell/BaseCollectionCell.swift b/week8/UMC_KREAM/Presentation/Home/Views/CollectionView/Cell/BaseCollectionCell.swift new file mode 100644 index 0000000..479c7d5 --- /dev/null +++ b/week8/UMC_KREAM/Presentation/Home/Views/CollectionView/Cell/BaseCollectionCell.swift @@ -0,0 +1,86 @@ +// +// BaseCollectionCell.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import UIKit +/// 여러 셀 중 가장 기본이 되는 컴포넌트 셀 +class BaseCollectionCell: UICollectionViewCell, ProductCellProtocol { + + // MARK: - Init + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + imageView.image = nil + + titleText.text = nil + subTitleText.text = nil + + priceLabel.text = nil + } + + //MARK: - Property + + /// 상품 이미지 + lazy var imageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFill + imageView.clipsToBounds = true + return imageView + }() + + /// 상품 타이틀 + lazy var titleText: UILabel = makeLabel(font: UIFont.systemFont(ofSize: 12, weight: .semibold), UIColor.black) + + /// 상품 서브 타이틀 + lazy var subTitleText: UILabel = makeLabel(font: UIFont.systemFont(ofSize: 12, weight: .regular), UIColor.black) + + /// 상품 가격 + lazy var priceLabel: UILabel = makeLabel(font: UIFont.systemFont(ofSize: 13, weight: .semibold), UIColor.black) + + /// 셀 내부 컴포넌트 값 할당 함수 + func configure(model: any RequiredCellProtocol) { + print("상속 받은 셀에서 오버라이드 하여 사용할 것!") + } + + + //MARK: - Function + + /// 중복 라벨 처리 함수 + /// - Parameter font: 텍스트 폰트 지정 + /// - Returns: 지정된 폰트 UILabel 반환 + func makeLabel(font: UIFont, _ color: UIColor) -> UILabel { + let label = UILabel() + label.font = font + label.textColor = color + label.numberOfLines = 2 + label.textAlignment = .left + return label + } + + + /// 스태뷰 생성 + /// - Parameter spacing: 간격 조정 + /// - Returns: UIStackView 반환 + func makeStackView(spacing: CGFloat) -> UIStackView { + let stack = UIStackView() + stack.spacing = spacing + stack.axis = .vertical + stack.distribution = .fill + return stack + } + + /// 이미지의 cornerRadius를 수정할 경우 사용한다 + /// - Parameter cornerRadius: 원하는 값 넣기 + func imageViewCorner(cornerRadius: CGFloat) { + self.imageView.layer.cornerRadius = cornerRadius + } +} diff --git a/week8/UMC_KREAM/Presentation/Home/Views/CollectionView/Cell/ProductGridCell.swift b/week8/UMC_KREAM/Presentation/Home/Views/CollectionView/Cell/ProductGridCell.swift new file mode 100644 index 0000000..6a25138 --- /dev/null +++ b/week8/UMC_KREAM/Presentation/Home/Views/CollectionView/Cell/ProductGridCell.swift @@ -0,0 +1,177 @@ +// +// ProductGridCell.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import UIKit +import SnapKit + +/// 가로로 스크롤 되면서 보이는 상품 그리드 컬렉션 셀 +class ProductGridCell: BaseCollectionCell { + + static let identifier: String = "ProductGridCell" + + // MARK: - Init + + override init(frame: CGRect) { + super.init(frame: frame) + addStackView() + addComponents() + constaints() + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + imageView.image = nil + purchaseCount.text = nil + tagButton.setImage(nil, for: .normal) + + titleText.text = nil + subTitleText.text = nil + + priceLabel.text = nil + priceDescription.text = nil + } + // MARK: - Property + + /// 상품 구매 수 라벨 + private lazy var purchaseCount: UILabel = makeLabel(font: UIFont.systemFont(ofSize: 10, weight: .regular), UIColor(red: 0.251, green: 0.272, blue: 0.294, alpha: 1)) + + + /// 이미지 백그라운드 랜덤 색상 적용 + private lazy var imageBackground: UIView = { + let view = UIView() + view.backgroundColor = UIColor( + red: .random(in: 0...1), + green: .random(in: 0...1), + blue: .random(in: 0...1), + alpha: 0.6 + ) + view.clipsToBounds = true + view.layer.cornerRadius = 12 + return view + }() + + /// 태그 버튼 + private lazy var tagButton: UIButton = { + let btn = UIButton() + return btn + }() + + /// "즉시 구매가" 설명 라벨 + private lazy var priceDescription: UILabel = { + let label = UILabel() + label.text = "즉시 구매가" + label.font = UIFont.systemFont(ofSize: 10, weight: .light) + label.textColor = UIColor(red: 0.635, green: 0.635, blue: 0.635, alpha: 1) + return label + }() + + /// 상품 이름 및 설명 스택 뷰 + private lazy var productTitleStack: UIStackView = makeStackView(spacing: 3) + + /// 상품 가격 및 "즉시 구매가" 스택 뷰 + private lazy var priceTitleStack: UIStackView = makeStackView(spacing: 2) + + + //MARK: - Function + + /// 셀 데이터에서 클릭되었는가 안되었는가 값 전달받아 빈 태그 이미지 or 속이 찬 태그 이미지인지 구분 + /// - Parameter isMark: true flase로 전달 + /// - Returns: 빈 태그 이미지 or 속이 찬 태그 이미지 반환 + private func setTagButtonImage(isMark: Bool) -> UIImage { + if isMark { + return UIImage(named: "notTag.pdf") ?? UIImage() + } else { + return UIImage(named: "tag.pdf") ?? UIImage() + } + } + + /// 스택 뷰에 컴포넌트 추가 + private func addStackView() { + [self.priceLabel, self.priceDescription].forEach{ priceTitleStack.addArrangedSubview($0) } + [self.titleText, self.subTitleText].forEach{ productTitleStack.addArrangedSubview($0) } + } + + /// 컴포넌트 추가 + private func addComponents() { + [self.imageView, self.purchaseCount, self.tagButton].forEach{ self.imageBackground.addSubview($0) } + [productTitleStack, priceTitleStack, imageBackground].forEach{ self.addSubview($0) } + } + + /// 제약 조건 설정 + private func constaints() { + imageBackground.snp.makeConstraints { + $0.top.equalToSuperview().offset(0) + $0.left.right.equalToSuperview().offset(0) + $0.height.equalTo(142) + } + + purchaseCount.snp.makeConstraints { + $0.top.equalToSuperview().offset(8) + $0.right.equalToSuperview().offset(-8) + $0.width.greaterThanOrEqualTo(48) + $0.height.equalTo(12) + } + + tagButton.snp.makeConstraints { + $0.top.equalTo(purchaseCount.snp.bottom).offset(92) + $0.right.bottom.equalToSuperview().offset(-10) + $0.width.equalTo(14.2) + } + + imageView.snp.makeConstraints { + $0.centerX.centerY.equalToSuperview() + $0.width.lessThanOrEqualTo(123) + $0.height.greaterThanOrEqualTo(30) + } + + /* 상품 배경에 속해 있는 데이터 */ + + productTitleStack.snp.makeConstraints { + $0.top.equalTo(imageBackground.snp.bottom).offset(8) + $0.left.equalToSuperview().offset(2) + $0.width.equalToSuperview() + $0.height.greaterThanOrEqualTo(25) + } + + titleText.snp.makeConstraints { + $0.width.equalToSuperview() + $0.height.equalTo(14) + } + + subTitleText.snp.makeConstraints { + $0.width.equalToSuperview() + $0.height.greaterThanOrEqualTo(14) + } + + priceTitleStack.snp.makeConstraints { + $0.bottom.equalToSuperview().offset(0) + $0.left.equalToSuperview().offset(2) + $0.width.equalToSuperview() + $0.height.equalTo(30) + } + } + + /// RequiredCellProtocol 상속을 받는 모델 값을 파라미터로 전달하여 해당 셀의 값 넣어준다. + /// - Parameter model: RequiredCellProtocol을 상속받는 모델 + override func configure(model: any RequiredCellProtocol) { + guard let productGridModel = model as? ProductGridModel else { return } + + self.imageView.image = UIImage(named: productGridModel.imageView)?.downSample(scale: 0.3) + self.purchaseCount.text = productGridModel.purchaseCnt + self.tagButton.setImage(setTagButtonImage(isMark: productGridModel.selectedTag), for: .normal) + + self.titleText.text = productGridModel.titleText + self.subTitleText.text = productGridModel.subTitleText + + self.priceLabel.text = productGridModel.priceText + self.priceDescription.text = productGridModel.priceSubText + } + +} diff --git a/week8/UMC_KREAM/Presentation/Home/Views/CollectionView/Cell/RecommendationCell.swift b/week8/UMC_KREAM/Presentation/Home/Views/CollectionView/Cell/RecommendationCell.swift new file mode 100644 index 0000000..2383da8 --- /dev/null +++ b/week8/UMC_KREAM/Presentation/Home/Views/CollectionView/Cell/RecommendationCell.swift @@ -0,0 +1,69 @@ +// +// RecommendationCell.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import UIKit +import SnapKit + +// 상품 추천 셀 +class RecommendationCell: BaseCollectionCell { + + static let identifier: String = "RecommendationCell" + + // MARK: - Init + + override init(frame: CGRect) { + super.init(frame: frame) + changeFont() + addComponents() + constraints() + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + self.imageView.image = nil + self.titleText.text = nil + } + + //MARK: - Function + + /// BaseCollectionCell에서의 폰트를 재수정 + private func changeFont() { + self.titleText.font = UIFont.systemFont(ofSize: 11.5, weight: .regular) + } + + /// 컴포넌트 추가 + private func addComponents() { + [self.imageView, self.titleText].forEach { self.addSubview($0) } + } + + /// 제약 조건 설정 + private func constraints() { + self.imageView.snp.makeConstraints { + $0.centerX.equalToSuperview() + $0.width.height.equalTo(61) + } + + self.titleText.snp.makeConstraints { + $0.top.equalTo(imageView.snp.bottom).offset(6) + $0.centerX.equalTo(imageView.snp.centerX) + $0.width.lessThanOrEqualTo(65) + $0.height.equalTo(14) + } + } + + /// RequiredCellProtocol 상속을 받는 모델 값을 파라미터로 전달하여 해당 셀의 값 넣어준다. + /// - Parameter model: RequiredCellProtocol을 상속받는 모델 + override func configure(model: any RequiredCellProtocol) { + guard let recommendProductModel = model as? RecommendProductModel else { return } + + self.imageView.image = UIImage(named: recommendProductModel.imageView) + self.titleText.text = recommendProductModel.titleText + } +} diff --git a/week8/UMC_KREAM/Presentation/Home/Views/CollectionView/Cell/UserStoryCell.swift b/week8/UMC_KREAM/Presentation/Home/Views/CollectionView/Cell/UserStoryCell.swift new file mode 100644 index 0000000..05d3c24 --- /dev/null +++ b/week8/UMC_KREAM/Presentation/Home/Views/CollectionView/Cell/UserStoryCell.swift @@ -0,0 +1,62 @@ +// +// UserStoryCell.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import UIKit +import SnapKit + +class UserStoryCell: BaseCollectionCell { + + static let identifier: String = "UserStoryCell" + + // MARK: - Init + + override init(frame: CGRect) { + super.init(frame: frame) + imageViewCorner(cornerRadius: 10) + chageLabelColor() + addComponents() + constraints() + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + //MARK: - Function + + /// 사용자 계정 하얀색으로 변경하는 함수 + private func chageLabelColor() { + self.subTitleText.textColor = UIColor.white + } + + /* subTitleText로 선택한 이유는 유저의 계정을 표시하는 라벨의 폰트와 같기 때문!! */ + private func addComponents() { + [self.imageView, self.subTitleText].forEach{ self.addSubview($0) } + } + + private func constraints() { + imageView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + subTitleText.snp.makeConstraints { + $0.left.equalToSuperview().offset(11) + $0.bottom.equalToSuperview().offset(-10) + $0.width.greaterThanOrEqualTo(60) + $0.height.equalTo(14) + } + } + + /// RequiredCellProtocol 상속을 받는 모델 값을 파라미터로 전달하여 해당 셀의 값 넣어준다. + /// - Parameter model: RequiredCellProtocol을 상속받는 모델 + override func configure(model: any RequiredCellProtocol) { + guard let userStroyModel = model as? UserStoryModel else { return } + + self.imageView.image = UIImage(named: userStroyModel.imageView) + self.subTitleText.text = userStroyModel.userName + } +} diff --git a/week8/UMC_KREAM/Presentation/Home/Views/CollectionView/CollectionLayout/HomeCollectionLayout.swift b/week8/UMC_KREAM/Presentation/Home/Views/CollectionView/CollectionLayout/HomeCollectionLayout.swift new file mode 100644 index 0000000..c00e626 --- /dev/null +++ b/week8/UMC_KREAM/Presentation/Home/Views/CollectionView/CollectionLayout/HomeCollectionLayout.swift @@ -0,0 +1,165 @@ +// +// HomeCollectionLayout.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import UIKit + +/// 홈탭의 커스텀 컬렉션뷰 레이아웃 +class HomeCollectionLayout { + + /// 컬렉션뷰가 가지는 레이아웃 지정 + /// - Returns: 크림앱은 4가지 섹션을 가지고 있으니, 해당 케이스에 맞춰 섹션이 반환되도록 레이아웃 생성한다. + static func createCompositionalLayout() -> UICollectionViewCompositionalLayout { + return UICollectionViewCompositionalLayout { (section, _) -> NSCollectionLayoutSection? in + switch section { + case 0: + return createBannerSection() + case 1: + return createRecommendationSection() + case 2: + return createProductGridSection() + case 3: + return createUserStorySection() + default: + return nil + } + } + } + + /* + -------------------------------------------- + !!!! 코드 한줄 한줄 주석 설명을 꼭 읽어주세요.. !!!!! + -------------------------------------------- + */ + + /// 배너 섹션 생성 + private static func createBannerSection() -> NSCollectionLayoutSection { + + /* + itemSize는 셀 하나의 아이템을 의미합니다. + .fraction은 지정된 셀크기의 퍼센트 비율을 의미합니다. 동적으로 조정이 가능하죠! + 셀 아이템 크기에 맞춰 비율로 조정됩니다. + .absolute는 고정 값입니다. 저 값을 무조건 지키게 됩니다. + */ + let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(336)) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + /* 셀을 담는 전체 그룹을 의미합니다. 배너 사진은 사진 크기만큼 셀 크기를 가지고 그게 곧 그룹 사이즈가 됩니다. 그래서 아이템 사이즈와 동일하게 설정합니다. */ + let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(336)) + let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) + + /* 섹션 크기를 지정합니다. 섹션은 (헤더 + 셀 부분 + 푸터)을 의미합니다. 하지만 배너에서는 헤더와 푸터가 없기 때문에 자체 사진 크기 즉, 그룹 사이즈가 섹션 사이즈로 값을 가지면 됩니다. */ + let section = NSCollectionLayoutSection(group: group) + section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0) + + return section + } + + /// 추천 섹션 반환 + /// - Returns: 추천 섹션의 아이템, 컬렉션 그룹 사이즈 값을 적용하여 섹션 반환 + private static func createRecommendationSection() -> NSCollectionLayoutSection { + + /* + 한줄에 5개의 이미지 총 2줄이 필요합니다. 한줄을 1이라 보고 한줄에 5개의 아이템이 들어가야 합니다. 즉, 아이템의 가로 사이즈는 1/5 = 0.2 입니다. + fraction으로 0.2를 주면 아이템이 한줄에 5개가 최대가 됩니다. + 높이는 당연히 고정크기를 가지면 됩니다. + */ + let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.2), heightDimension: .absolute(81)) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + // 각 아이템의 간격을 설정 + /* + 한줄당 하나의 그룹입니다. + 우리는 두 줄이 생성되야 하니깐 두개의 그룹이 생성된다 생각하면 됩니다. + 아이템 사이즈에 맞춰 그룹을 생성하면 너무 딱 달라붙기 때문에 일부러 heightDimension에 10만큼 더 길게 absolute값을 주었습니다. + */ + let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(91)) + let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) + + /* 그룹간의 간격입니다. 각 그룹 즉, 각 줄끼리의 사이 간격을 주기 위해 값을 넣었습니다. */ + group.interItemSpacing = .fixed(8) + + + let section = NSCollectionLayoutSection(group: group) + + /* 섹션 내부 여백 지정, 추천 섹션은 헤더는 없지만 푸터가 있습니다. 바로 구분선입니다. 셀 그룹이 끝나는 지점으로부터 30 떨어진 지점에 푸터가 생성될 수 있도록 bottom에 30을 넣어줍니다. 다른 부분의 여백 값은 피그마를 통해 수치대로 넣었습니다. */ + section.contentInsets = NSDirectionalEdgeInsets(top: 20, leading: 10, bottom: 30, trailing: 15) + section.boundarySupplementaryItems = [createFooterItem()] + + return section + } + + /// Grid형 상품 섹션 생성 + private static func createProductGridSection() -> NSCollectionLayoutSection { + /* + 피그마의 Just Dropped, 발매 상품 부분입니다. 일반적으로 한줄의 가로형으로 셀들을 펼쳐 놓은 레이아웃입니다. + 이 부분은 한줄에 쭉 나열되기 떄문에 고정된 값의 가로 길이와 세로 길이를 가지면 됩니다. + */ + let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(142), heightDimension: .absolute(237)) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + /* + 한줄로 나열하는 레이아웃이기 때문에 하나의 아이템이 하나의 그룹입니다. 하나의 아이템 옆에 바로 아이템이 달라붙는게 아닌 공백을 가지고 아이템이 옆에 생성되기 때문에 가로 크기를 좀 더 주어 여백을 만들어주었습니다. + */ + let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(152), heightDimension: .absolute(237)) + let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) + group.interItemSpacing = .fixed(8) + + let section = NSCollectionLayoutSection(group: group) + + /* 이 섹션부분은 가로로 나열되는 형태이기 때문에 continuous를 주도록 합니다. */ + section.orthogonalScrollingBehavior = .continuous + section.contentInsets = NSDirectionalEdgeInsets(top: 14, leading: 10, bottom: 30, trailing: 10) + + /* 이 섹션은 헤더(상단 타이틀 라벨)와 푸터(구분선)를 가지고 있기 때문에 바운더리에 헤더와 푸터를 넣어줍니다. */ + section.boundarySupplementaryItems = [createHeaderItem(), createFooterItem()] + return section + } + + /// 유저 스토리 섹션 생성 + /// 위의 Grid형 상품 섹션과 동일합니다. 생략하겠습니다!! + private static func createUserStorySection() -> NSCollectionLayoutSection { + let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(124), heightDimension: .absolute(165)) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(134), heightDimension: .absolute(165)) + let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) + group.interItemSpacing = .fixed(8) + + let section = NSCollectionLayoutSection(group: group) + section.orthogonalScrollingBehavior = .continuous + section.contentInsets = NSDirectionalEdgeInsets(top: 14, leading: 10, bottom: 30, trailing: 10) + section.boundarySupplementaryItems = [createHeaderItem(), createFooterItem()] + + return section + } + + /// 푸터 생성(구분선) + /// - Returns: 구분선을 가로(동적 사이즈), 세로(고정 사이즈)로 조정하여 각 섹션마다 생성할 수 있도록 푸터 반환 + private static func createFooterItem() -> NSCollectionLayoutBoundarySupplementaryItem { + + /* + 구분선이 푸터입니다. 즉 구분선의 길이를 지정합니다. + 구분 선 푸터 뷰를 따로 커스텀으로 만들어두었기 떄문에 전체 가로 길이가 되도록 fraction으로 처리합니다. + */ + let footerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(1)) + let footer = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: footerSize, elementKind: UICollectionView.elementKindSectionFooter, alignment: .bottom) + return footer + } + + /// 헤더 생성(특정 섹션에 대해 타이틀 헤더) + /// - Returns: 고정된 헤더 반환 + private static func createHeaderItem() -> NSCollectionLayoutBoundarySupplementaryItem { + let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(45)) + let header = NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: headerSize, + elementKind: UICollectionView.elementKindSectionHeader, + alignment: .top) + header.pinToVisibleBounds = true + + return header + } +} diff --git a/week8/UMC_KREAM/Presentation/Home/Views/CollectionView/Footer/SectionSeparatorFooter.swift b/week8/UMC_KREAM/Presentation/Home/Views/CollectionView/Footer/SectionSeparatorFooter.swift new file mode 100644 index 0000000..e9358d5 --- /dev/null +++ b/week8/UMC_KREAM/Presentation/Home/Views/CollectionView/Footer/SectionSeparatorFooter.swift @@ -0,0 +1,35 @@ +// +// SectionSeparatorFooter.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import UIKit + +class SectionSeparatorFooter: UICollectionReusableView { + + static let identifier: String = "SectionSeparatorFooter" + + override init(frame: CGRect) { + super.init(frame: frame) + self.addSubview(separator) + separator.snp.makeConstraints { + $0.edges.equalToSuperview() + $0.width.equalToSuperview() + $0.height.equalTo(1) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + /// 구분선 생성 + lazy var separator: UIView = { + let view = UIView() + view.backgroundColor = UIColor(red: 0.949, green: 0.949, blue: 0.949, alpha: 1) + return view + }() + + } diff --git a/week8/UMC_KREAM/Presentation/Home/Views/CollectionView/Header/BaseCellHeader.swift b/week8/UMC_KREAM/Presentation/Home/Views/CollectionView/Header/BaseCellHeader.swift new file mode 100644 index 0000000..1d043b7 --- /dev/null +++ b/week8/UMC_KREAM/Presentation/Home/Views/CollectionView/Header/BaseCellHeader.swift @@ -0,0 +1,86 @@ +// +// BaseCellHeader.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import UIKit + +class BaseCellHeader: UICollectionReusableView, CellHeaderProtocol { + + static let identifier: String = "BaseCellHeader" + + // MARK: - Init + + override init(frame: CGRect) { + super.init(frame: frame) + addComponents() + constraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Init + + /// 헤더의 메인 타이틀 라벨 + lazy var headerTitle: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: 16, weight: .semibold) + label.textColor = UIColor.black + return label + }() + + /// 헤더의 서브 타이틀 라벨 + lazy var headerSubTitle: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: 13, weight: .light) + label.textColor = UIColor.lightGray + return label + }() + + /// 헤더의 타이틀을 담는 스택 + private lazy var headerStack: UIStackView = { + let stack = UIStackView() + stack.axis = .vertical + stack.spacing = 4 + stack.distribution = .equalSpacing + return stack + }() + + /// 컴포넌트 추가 함수 + private func addComponents() { + [headerTitle, headerSubTitle].forEach { self.headerStack.addArrangedSubview($0) } + self.addSubview(headerStack) + } + + /// 제약 조건 설정 + private func constraints() { + + headerStack.snp.makeConstraints { + $0.top.equalToSuperview().offset(20) + $0.left.right.bottom.equalToSuperview() + } + + headerTitle.snp.makeConstraints { + $0.width.greaterThanOrEqualTo(200) + $0.height.equalTo(19) + } + + headerSubTitle.snp.makeConstraints { + $0.width.greaterThanOrEqualTo(50) + $0.height.equalTo(19) + } + } + + /// 섹션 마다 헤더의 값이 다르기 때문에 뷰 컨트롤러에서 헤더를 지정할 때 값을 넣어줄 수 있도록 한다. + /// - Parameters: + /// - title: 헤더의 메인 타이틀 값 + /// - subTitle: 헤더의 서브 타이틀 값 + public func configure(title: String, subTitle: String) { + self.headerTitle.text = title + self.headerSubTitle.text = subTitle + } + } diff --git a/week8/UMC_KREAM/Presentation/Home/Views/CollectionView/HomeHeaderView.swift b/week8/UMC_KREAM/Presentation/Home/Views/CollectionView/HomeHeaderView.swift new file mode 100644 index 0000000..82f05a2 --- /dev/null +++ b/week8/UMC_KREAM/Presentation/Home/Views/CollectionView/HomeHeaderView.swift @@ -0,0 +1,96 @@ +// +// HomeHeaderView.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import UIKit + +class HomeHeaderView: UIView { + + override init(frame: CGRect) { + super.init(frame: frame) + self.backgroundColor = UIColor.white + addComponents() + constraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + //MARK: - Property + + /// 상단 검색 바 + public lazy var searchBar: UISearchBar = { + let searchBar = UISearchBar() + searchBar.placeholder = "브랜드, 상품, 프로필, 태그 등" + searchBar.barTintColor = .white + searchBar.backgroundColor = .clear + searchBar.clipsToBounds = true + searchBar.layer.cornerRadius = 12 + searchBar.backgroundImage = UIImage() + + /* 돋보기 아이콘 및 여백 제거 */ + searchBar.searchTextField.leftView = nil + return searchBar + }() + + /// 상단 벨 아이콘 + private lazy var topBell: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(named: "bell.pdf") + return imageView + }() + + /// 상단 세그먼트 컨트롤 + public lazy var segmentControl: HomeSegmentControl = { + let items = ["추천", "랭킹", "발매정보", "럭셔리", "남성", "여성"] + let segmentControl = HomeSegmentControl(items: items) + segmentControl.selectedSegmentIndex = 0 + return segmentControl + }() + + /// 크림앱을 직접 다운로드해서 자세히보면 구분선을 가지고 있습니다. 그것을 표현하기 위해 작성합니다. + private lazy var bottomBorder: UIView = { + let view = UIView() + view.backgroundColor = UIColor.lightGray.withAlphaComponent(0.5) + return view + }() + + /// 컴포넌트 생성 + private func addComponents() { + [searchBar, topBell, segmentControl,bottomBorder].forEach { self.addSubview($0) } + } + + /// 제약 조건 설정 + private func constraints() { + searchBar.snp.makeConstraints { + $0.top.equalToSuperview().offset(60) + $0.left.equalTo(16) + $0.right.equalTo(topBell.snp.left).offset(-15) + } + + topBell.snp.makeConstraints { + $0.centerY.equalTo(searchBar.snp.centerY) + $0.right.equalToSuperview().offset(-16) + $0.width.height.equalTo(24) + } + + segmentControl.snp.makeConstraints { + $0.top.equalTo(searchBar.snp.bottom).offset(8) + $0.left.equalToSuperview().offset(14) + $0.right.equalToSuperview().offset(-15) + $0.height.equalTo(27) + } + + bottomBorder.snp.makeConstraints { + $0.left.right.bottom.equalToSuperview() + $0.top.equalTo(segmentControl.snp.bottom).offset(1) + $0.height.equalTo(1) + $0.width.equalToSuperview() + } + } + +} diff --git a/week8/UMC_KREAM/Presentation/Home/Views/CollectionView/HomeView.swift b/week8/UMC_KREAM/Presentation/Home/Views/CollectionView/HomeView.swift new file mode 100644 index 0000000..7f8cbc5 --- /dev/null +++ b/week8/UMC_KREAM/Presentation/Home/Views/CollectionView/HomeView.swift @@ -0,0 +1,104 @@ +// +// HomeView.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import UIKit +import SnapKit + +class HomeView: UIView { + + + override init(frame: CGRect) { + super.init(frame: frame) + addComponents() + constraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + //MARK: - Property + public lazy var homeCollectionView: UICollectionView = { + + /* + 크립앱의 홈탭에서 상단 커스텀 네비게이션을 제외한 뷰는 컬렉션 뷰이고 + 이 하나의 컬렉션 뷰가 여러개의 섹션을 가지고 있고, 섹션마다 자신의 셀 패턴을 가지고 있습니다. + 그래서 4주차에서 자세한 언급은 없었지만 레이아웃이라는 놈을 우리가 직접 조정하여 이 하나의 컬렉션뷰가 사용할 수 있도록하면 + 우리가 원하는 부분의 섹션에서 원하는 스타일의 셀을 갖도록 할 수 있습니다. + 즉, 크림앱은 4개의 섹션을 갖고 있고, 각 섹션은 자신만의 셀 패턴을 갖고 있으니, 그 셀 패턴을 갖도록 레이아웃을 구성해주면 됩니다. + */ + let layout = HomeCollectionLayout.createCompositionalLayout() + let collectionView = UICollectionView(frame: self.bounds, collectionViewLayout: layout) + + /* 셀 등록 */ + /* + 크림앱에는 규칙적인 셀 패턴을 가지고 있습니다. + 만약 이걸 하나하나 뷰로 구현해서 했다면 틀렸습니다..ㅎ.... + 중복되는 셀의 종류를 파악하여 셀을 등록 해두고 필요로 하는 섹션에서 셀을 불러와서 사용할 수 있도록 해줍니다. + */ + collectionView.register(AdBannerCell.self, forCellWithReuseIdentifier: AdBannerCell.identifier) + collectionView.register(RecommendationCell.self, forCellWithReuseIdentifier: RecommendationCell.identifier) + collectionView.register(ProductGridCell.self, forCellWithReuseIdentifier: ProductGridCell.identifier) + collectionView.register(UserStoryCell.self, forCellWithReuseIdentifier: UserStoryCell.identifier) + + /* 헤더 및 푸터 등록 */ + + /* 셀이 끝나는 지점마다 공통적으로 같은 간격을 가지고 구분선이 생성되고 있습니다. 즉, 푸터로 구분선을 두어 모든 섹션이 구분선을 보이도록 했습니다. + 대신 모든 섹션은 각자 자신만의 높이를 갖고 있기때문에 섹션의 높이는 다릅니다. 그래서 높이가 다르더라도 셀 아이템이 끝나는 지점부터 일정 거리를 두어 구분선을 두어 자동으로 생성되도록 구성했습니다. + */ + collectionView.register(SectionSeparatorFooter.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: SectionSeparatorFooter.identifier) + + /* + 어느 섹션에 대해서는 헤더를 들고 있고, 공통적인 컴포넌트를 가지고 있습니다. 그래서 하나의 헤더만 등록하고 원하는 섹션에서 헤더를 사용할 수 있도록 지정합니다. + */ + collectionView.register(BaseCellHeader.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: BaseCellHeader.identifier) + + collectionView.backgroundColor = .white + collectionView.refreshControl = refreshControl + return collectionView + }() + + /// 상단 헤더 뷰 + public lazy var homeHeaderView: HomeHeaderView = HomeHeaderView() + + /// 컬렉션 뷰 상단에서 잡아당길 때 리프레시 버튼 생성 + private lazy var refreshControl: UIRefreshControl = { + let refreshControl = UIRefreshControl() + refreshControl.addTarget(self, action: #selector(pullRefresh), for: .valueChanged) + return refreshControl + }() + + // MARK: - Function + + /// 1.0초 동안 리프레시 버튼 재생 + @objc private func pullRefresh() { + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: { + self.refreshControl.endRefreshing() + }) + } + + // MARK: - Constaints & Add Function + + /// 컴포넌트 생성 + private func addComponents() { + [homeHeaderView, homeCollectionView].forEach{ self.addSubview($0) } + } + + /// 제약 조건 설정 + private func constraints() { + + homeHeaderView.snp.makeConstraints { + $0.top.left.right.equalToSuperview() + } + + homeCollectionView.snp.makeConstraints { + $0.top.equalTo(homeHeaderView.snp.bottom).offset(0) + $0.left.right.bottom.equalToSuperview() + } + } + } + diff --git a/week8/UMC_KREAM/Presentation/Login/LoginView.swift b/week8/UMC_KREAM/Presentation/Login/LoginView.swift new file mode 100644 index 0000000..845b8c3 --- /dev/null +++ b/week8/UMC_KREAM/Presentation/Login/LoginView.swift @@ -0,0 +1,213 @@ +// +// LoginView.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import UIKit +import SnapKit + +class LoginView: UIView { + + override init(frame: CGRect) { + super.init(frame: frame) + self.backgroundColor = .white + addStackView() + addComponents() + constraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Property + + /// 아이디 입력 타이틀 라벨 + private lazy var idTitleLabel = makeTitleTextLabel("이메일 주소") + + /// 아이디 입력 텍스트 필드 + public lazy var idTextField = makeTextField("예) kream@kream.co.kr") + + /// 비밀번호 입력 타이틀 라벨 + private lazy var pwdTitleLabel = makeTitleTextLabel("비밀번호") + + /// 비밀번호 입력 텍스트 필드 + public lazy var pwdTextField = makeTextField("비밀번호를 입력해주세요") + + /// 비밀번호 화면 로고 이미지뷰 + private lazy var logoImageView: UIImageView = { + let view = UIImageView() + view.image = UIImage(named: "logo.pdf") + view.contentMode = .scaleAspectFit + return view + }() + + /// 로그인 버튼(아이디 및 비밀번호 입력 할 경우) + public lazy var loginBtn: UIButton = { + let btn = UIButton() + btn.setTitle("로그인", for: .normal) + btn.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .regular) + btn.setTitleColor(UIColor.white, for: .normal) + btn.backgroundColor = UIColor(red: 0.851, green: 0.851, blue: 0.851, alpha: 1) + btn.clipsToBounds = true + btn.layer.cornerRadius = 8 + return btn + }() + + /// 카카오 로그인 버튼 + private lazy var kakaoBtn: UIButton = makeSocialBtn(image: "kakao.pdf", title: "카카오로 로그인") + + /// 애플 로그인 버튼 + private lazy var appleBtn: UIButton = makeSocialBtn(image: "apple.pdf", title: "Apple로 로그인") + + // MARK: - StackView + + /// 아아디 타이틀 + 아이디 텍스트 필드 저장 스택 + private lazy var idStackView: UIStackView = makeStackView(spacing: 8) + + /// 비밀번호 타이틀 + 비밀번호 텍스트 필드 저장 스택 + private lazy var pwdStackView: UIStackView = makeStackView(spacing: 8) + + /// idStackView + pwdStackView + 로그인 버튼 모음 스택 + private lazy var topUserLoginStackView: UIStackView = makeStackView(spacing: 17) + + /// 하단 카카오 로그인 버튼 + 애플 로그인 버튼 + private lazy var bottomSocialStackView: UIStackView = makeStackView(spacing: 22) + + // MARK: - Option + + /// 버튼 타이틀 텍스트 스타일 지정 + private lazy var attributeContainer: AttributeContainer = { + var container = AttributeContainer() + container.font = UIFont.systemFont(ofSize: 13, weight: .semibold) + container.foregroundColor = UIColor.black + return container + }() + + /// TextField placeholder 커스텀 스타일 지정 + private lazy var placeholderContainer: [NSAttributedString.Key: Any] = { + var value = [NSAttributedString.Key: Any]() + value[.foregroundColor] = UIColor.gray + value[.font] = UIFont.systemFont(ofSize: 12) + return value + }() + + + // MARK: - MakeFunction + + /// 아이디 및 비밀번호 중복되는 타이틀 UILabel 생성 + /// - Parameter text: 타이틀로 사용할 텍스트 + /// - Returns: 설정된 스타일의 UILabel 객체 + private func makeTitleTextLabel(_ text: String) -> UILabel { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 12, weight: .regular) + label.textColor = UIColor.black + label.text = text + return label + } + + /// 아이디 및 비밀번호 입력 텍스트 입력 UITextField 생성 + /// - Parameter placeholder: 텍스트 필드 내부에 사용할 placeholder 지정 + /// - Returns: 설정된 스타일의 UITextField 객체 + private func makeTextField(_ placeholder: String) -> UITextField { + let field = UITextField() + + field.attributedPlaceholder = NSAttributedString(string: placeholder, attributes: placeholderContainer) + field.textColor = UIColor.black + field.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 16, height: 1)) + field.leftViewMode = .always + field.layer.borderColor = UIColor(red: 0.635, green: 0.635, blue: 0.635, alpha: 1).cgColor + field.clipsToBounds = true + field.layer.cornerRadius = 15 + field.layer.borderWidth = 1 + field.layer.borderColor = UIColor.gray.cgColor + return field + } + + /// 중복 되는 소셜 버튼 UIButton 샐서 + /// - Parameters: 버튼 속 사용하게 되는 소셜 로고 이미지 + 버튼 타이틀 + /// - image: 로고 이미지 이름 String 값 + /// - title: 버튼 타이틀 String 값 + /// - Returns: 설정된 스타일의 UIButton 객체 + private func makeSocialBtn(image: String, title: String) -> UIButton { + let btn = UIButton() + var configuration = UIButton.Configuration.plain() + + configuration.image = UIImage(named: image) + configuration.imagePlacement = .leading + configuration.imagePadding = 69 + + configuration.attributedTitle = AttributedString(title, attributes: attributeContainer) + configuration.contentInsets = NSDirectionalEdgeInsets(top: 11, leading: 17, bottom: 13, trailing: 102) + btn.configuration = configuration + + btn.clipsToBounds = true + btn.layer.cornerRadius = 10 + btn.layer.borderColor = UIColor.gray.cgColor + btn.layer.borderWidth = 1 + + return btn + } + + /// 중복되는 스택뷰 생성 + /// - Parameter spacing: 스택 내부 간격 조정 + /// - Returns: Vertical 스택 뷰 반환 + private func makeStackView(spacing: CGFloat) -> UIStackView { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.spacing = spacing + stackView.distribution = .equalSpacing + return stackView + } + + // MARK: - Constaints & Add Function + + /// 스택 뷰 내부에 컴포넌트 생성 함수 + private func addStackView() { + [idTitleLabel, idTextField].forEach{ idStackView.addArrangedSubview($0) } + [pwdTitleLabel, pwdTextField].forEach{ pwdStackView.addArrangedSubview($0) } + [idStackView, pwdStackView, loginBtn].forEach{ topUserLoginStackView.addArrangedSubview($0) } + [kakaoBtn, appleBtn].forEach{ bottomSocialStackView.addArrangedSubview($0) } + } + + /// 컴포넌트 생성 + private func addComponents() { + [logoImageView, topUserLoginStackView, bottomSocialStackView].forEach{ self.addSubview($0)} + } + + /// 오토레이아웃 지정 + private func constraints() { + + idTextField.snp.makeConstraints { + $0.height.equalTo(34) + } + + pwdTextField.snp.makeConstraints { + $0.height.equalTo(34) + } + + loginBtn.snp.makeConstraints { + $0.height.equalTo(38) + } + + logoImageView.snp.makeConstraints { + $0.top.equalToSuperview().offset(126) + $0.left.equalToSuperview().offset(53) + $0.right.equalToSuperview().offset(-53) + } + + topUserLoginStackView.snp.makeConstraints { + $0.top.equalTo(logoImageView.snp.bottom).offset(87) + $0.left.equalToSuperview().offset(45) + $0.right.equalToSuperview().offset(-45) + } + + bottomSocialStackView.snp.makeConstraints { + $0.left.equalToSuperview().offset(47.5) + $0.right.equalToSuperview().offset(-47.5) + $0.bottom.equalToSuperview().offset(-189) + } + } + } diff --git a/week8/UMC_KREAM/Presentation/Login/LoginViewController.swift b/week8/UMC_KREAM/Presentation/Login/LoginViewController.swift new file mode 100644 index 0000000..fa3eed1 --- /dev/null +++ b/week8/UMC_KREAM/Presentation/Login/LoginViewController.swift @@ -0,0 +1,65 @@ +// +// LoginViewController.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import UIKit + +class LoginViewController: UIViewController { + + /// 아이디, 비번 지정 변수 + let userInfo: UserInfo = UserInfo(id: "So", pwd: "1234") + + /// 앱 실행 단계 + override func viewDidLoad() { + super.viewDidLoad() + self.view = loginView + } + + // MARK: - Property + + /// 커스텀으로 작성한 로그인 뷰 + private lazy var loginView: LoginView = { + let view = LoginView() + view.loginBtn.addTarget(self, action: #selector(loginFunction), for: .touchUpInside) + return view + }() + + // MARK: - Function + + /// 데이터 모델에 지정한 아이디, 비밀번호에 해당 할 경우 로그인 가능하도록 하는 함수 + @objc private func loginFunction() { + guard let inputId = loginView.idTextField.text, + let inputPwd = loginView.pwdTextField.text, + !inputId.isEmpty, !inputPwd.isEmpty else { + print("아이디와 비밀번호를 입력해주세요") + return + } + + if let storedUserInfo = UserInfo.loadUserDefaults() { + if storedUserInfo.id == inputId && storedUserInfo.pwd == inputPwd { + print("기존 사용자 로그인 성공") + changeRootView() + } else { + print("아이디 또는 비밀번호 불일치") + } + } else { + let newUserInfo = UserInfo(id: inputId, pwd: inputPwd) + newUserInfo.saveUserDefaults() + print("아이디 비밀번호 새롭게 갱신 및 로그인 성공") + changeRootView() + } + } + + /// 로그인 뷰 -> TabBarController 루트 뷰 전환 함수 + private func changeRootView() { + let rootVC = MainTabBarController() + + if let window = UIApplication.shared.connectedScenes.first as? UIWindowScene, let sceneDelegate = window.delegate as? SceneDelegate, let window = sceneDelegate.window { + window.rootViewController = rootVC + UIView.transition(with: window, duration: 0.3, options: .transitionCrossDissolve, animations: nil, completion: nil) + } + } + } diff --git a/week8/UMC_KREAM/Presentation/My/ViewControllers/MyPageManageViewController.swift b/week8/UMC_KREAM/Presentation/My/ViewControllers/MyPageManageViewController.swift new file mode 100644 index 0000000..e65363c --- /dev/null +++ b/week8/UMC_KREAM/Presentation/My/ViewControllers/MyPageManageViewController.swift @@ -0,0 +1,85 @@ +// +// MyPageManageViewController.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import UIKit + +class MyPageManageViewController: UIViewController { + + // MARK: - Init + + override func viewDidLoad() { + super.viewDidLoad() + + self.view = profileManagerView + setNavigation() + } + + // MARK: - Property + + /// 네비게이션 바의 상단 화면으로 나가는 버튼 + 네비게이션 타이틀 지정 + private func setNavigation() { + self.navigationItem.title = "프로필 관리" + + let backBtn = UIBarButtonItem(image: UIImage(systemName: "arrow.left")?.withRenderingMode(.alwaysOriginal).withTintColor(UIColor.black), style: .plain, target: self, action: #selector(didTap)) + navigationItem.leftBarButtonItem = backBtn + } + + /// 프로필 관리 페이지 커스텀 뷰 + private lazy var profileManagerView: ProfileManagerView = { + let view = ProfileManagerView() + view.idCheckBtn.addTarget(self, action: #selector(clickedBtn(_:)), for: .touchUpInside) + view.pwdCheckBtn.addTarget(self, action: #selector(clickedBtn(_:)), for: .touchUpInside) + return view + }() + + // MARK: - Function + + /// 네비게이션 왼쪽 상단 버튼을 통해 이전 화면으로 돌아감 + @objc func didTap() { + navigationController?.popViewController(animated: true) + } + + /// 아이디 및 비밀번호 변경 버튼 + /// - Parameter sender: 현재 클릭된 UIButton + @objc func clickedBtn(_ sender: UIButton) { + let config = getButtonConfig(button: sender) + guard let textField = config.textField, let userDefaultsKey = config.UserDefaultsKey else { return } + + if sender.titleLabel?.text == "변경" { + sender.setTitle("확인", for: .normal) + textField.isUserInteractionEnabled = true + textField.becomeFirstResponder() + } else if sender.titleLabel?.text == "확인" { + if let text = textField.text, !text.isEmpty { + saveUserDefaults(text, forKey: userDefaultsKey) + sender.setTitle("변경", for: .normal) + textField.isUserInteractionEnabled = false + textField.resignFirstResponder() + } + } + } + + /// 변경 버튼이 아이디에 해당하는지 비밀번호에 해당하는지 판단 함수 + /// - Parameter button: 현재 눌린 UIButton + /// - Returns: 눌린 버튼에 해당하는 아이디 또는 비밀번호 텍스트 필드 + UserDefaults Key 값 + private func getButtonConfig(button: UIButton) -> (textField: UITextField?, UserDefaultsKey: String?) { + if button == profileManagerView.idCheckBtn { + return (profileManagerView.idTextField, "userId") + } else if button == profileManagerView.pwdCheckBtn { + return (profileManagerView.pwdTextField, "userPwd") + } + return (nil, nil) + } + + /// 변경된 값 UserDefaults에 저장 + /// - Parameters: + /// - value: 새롭게 입력된 값 + /// - key: UserDefaults 키 + private func saveUserDefaults(_ value: String, forKey key: String) { + UserDefaults.standard.set(value, forKey: key) + } + } diff --git a/week8/UMC_KREAM/Presentation/My/ViewControllers/MyPageViewController.swift b/week8/UMC_KREAM/Presentation/My/ViewControllers/MyPageViewController.swift new file mode 100644 index 0000000..abfdbfb --- /dev/null +++ b/week8/UMC_KREAM/Presentation/My/ViewControllers/MyPageViewController.swift @@ -0,0 +1,30 @@ +// +// MyPageViewController.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import UIKit + +class MyPageViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + self.view = myPageView + } + + /// 커스텀한 마이페이지 뷰 + private lazy var myPageView: MyPageView = { + let view = MyPageView() + view.topView.profileManageBtn.addTarget(self, action: #selector(changePage), for: .touchUpInside) + return view + }() + + /// 프로필 관리 버튼을 통해 프로필 수정 페이지 이동 + @objc func changePage() { + let changeVC = MyPageManageViewController() + navigationController?.pushViewController(changeVC, animated: true) + } + } diff --git a/week8/UMC_KREAM/Presentation/My/Views/MyPageTopView.swift b/week8/UMC_KREAM/Presentation/My/Views/MyPageTopView.swift new file mode 100644 index 0000000..0754f50 --- /dev/null +++ b/week8/UMC_KREAM/Presentation/My/Views/MyPageTopView.swift @@ -0,0 +1,194 @@ +// +// MyPageTopView.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import UIKit +import SnapKit + + +class MyPageTopView: UIView { + // MARK: - Init + override init(frame: CGRect) { + super.init(frame: frame) + addStackView() + addComponents() + constraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Property + + /// 상단 옵션 아이콘 버튼 + private lazy var optionIcon: UIButton = makeIconBtn(image: "option.pdf") + + /// 상단 카메라 아이콘 버튼 + private lazy var cameraIcon: UIButton = makeIconBtn(image: "camera.pdf") + + + /// 유저 프로필 이미지 뷰 + private lazy var userProfile: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(named: "profile.pdf") + return imageView + }() + + /// 유저 이름 라벨 + private lazy var userName: UILabel = { + let label = UILabel() + label.text = "Jeong_iOS" + label.font = UIFont.systemFont(ofSize: 16, weight: .medium) + label.textColor = .black + return label + }() + + /// 유저 팔로워 수 라벨 + private lazy var userFollow: UILabel = makeUserSub(content: "팔로워", count: 326) + + /// 유저 팔로잉 수 라벨 + private lazy var userFollowing: UILabel = makeUserSub(content: "팔로잉", count: 20) + + /// 프로필 관리 버튼 + public lazy var profileManageBtn: UIButton = makeUserBtn(title: "프로필 관리") + + /// 프로필 공유 버튼 + public lazy var profileShareBtn: UIButton = makeUserBtn(title: "프로필 공유") + + // MARK: - StackView + + /// 상단 카메라 버튼 + 옵션 버튼 모음 스택 + private lazy var topOptionStack: UIStackView = makeStackView(axis: .horizontal, spacing: 277) + + /// 유저 팔로워 수 + 팔로잉 수 라벨 모음 스택 + private lazy var userFollowerStack: UIStackView = makeStackView(axis: .horizontal, spacing: 8) + + /// 유저 이름 라벨 + userFollowerStack 모음 스택 + private lazy var userInfoTextStack: UIStackView = makeStackView(axis: .vertical, spacing: 6) + + /// 프로필 관리 버튼 + 프로필 공유 버튼 모음 스택 + private lazy var profileBtnStack: UIStackView = makeStackView(axis: .horizontal, spacing: 14) + + + // MARK: - Make Function + + /// 상단 중복되는 아이콘 버튼 생성 + /// - Parameter image: 아이콘 이미지 String + /// - Returns: 지정된 스타일의 UIButton 객체 + private func makeIconBtn(image: String) -> UIButton { + let btn = UIButton() + btn.setImage(UIImage(named: image), for: .normal) + return btn + } + + /// 팔로워 및 팔로잉 생성 라벨 + /// - Parameters: + /// - content: 팔로워, 팔로잉 중 작성 + /// - count: 팔로워, 팔로잉에 해당하는 숫자 + /// - Returns: 숫자와 한글 스타일 별도 지정된 UILabel 객체 + private func makeUserSub(content: String, count: Int) -> UILabel { + let label = UILabel() + + let textValue = content + " " + String(count) + let attributedString = NSMutableAttributedString(string: textValue) + attributedString.addAttribute(.font, value: UIFont.systemFont(ofSize: 12, weight: .regular), range: NSRange(location: 0, length: content.count)) + attributedString.addAttribute(.font, value: UIFont.systemFont(ofSize: 12, weight: .bold), range: NSRange(location: content.count, length: String(count).count + 1)) + + label.attributedText = attributedString + label.textColor = UIColor.black + return label + } + + /// 프로필 관리 및 프로필 공유 중복되는 버튼 생성 + /// - Parameter title: 원하는 타이틀 생성 + /// - Returns: 지정된 스타일의 UIButton 객체 + private func makeUserBtn(title: String) -> UIButton { + let btn = UIButton() + btn.setTitle(title, for: .normal) + btn.titleLabel?.font = UIFont.systemFont(ofSize: 9, weight: .bold) + btn.setTitleColor(UIColor.black, for: .normal) + btn.clipsToBounds = true + btn.layer.cornerRadius = 8 + btn.layer.borderWidth = 1 + btn.layer.borderColor = UIColor.gray.cgColor + return btn + } + + /// 중복되는 스택뷰 생성 + /// - Parameters: + /// - axis: 스택 뷰 축 지정 (수직, 수평) + /// - spacing: 스택 내부 간격 지정 + /// - Returns: 지정된 스타일의 UIStackView 객체 + private func makeStackView(axis: NSLayoutConstraint.Axis, spacing: CGFloat) -> UIStackView { + let stackView = UIStackView() + stackView.axis = axis + stackView.spacing = spacing + stackView.distribution = .equalSpacing + return stackView + } + + // MARK: - Constaints & Add Function + + /// 스택 븁 내뷰에 컴포넌트 생성 함수 + private func addStackView() { + [optionIcon, cameraIcon].forEach{ topOptionStack.addArrangedSubview($0) } + + [userFollow, userFollowing].forEach{ userFollowerStack.addArrangedSubview($0) } + [userName, userFollowerStack].forEach{ userInfoTextStack.addArrangedSubview($0) } + + [profileManageBtn, profileShareBtn].forEach{ profileBtnStack.addArrangedSubview($0) } + } + + + /// 컴포넌트 생성 + private func addComponents() { + + [topOptionStack, userProfile, userInfoTextStack, profileBtnStack].forEach { self.addSubview($0) } + } + + + /// 오토레이아웃 지정 + private func constraints() { + + topOptionStack.snp.makeConstraints { + $0.top.equalToSuperview().offset(75) + $0.left.equalToSuperview().offset(32.5) + $0.right.equalToSuperview().offset(-32.5) + $0.height.equalTo(25) + } + + userProfile.snp.makeConstraints { + $0.top.equalTo(topOptionStack.snp.bottom).offset(26) + $0.left.equalToSuperview().offset(32.5) + $0.width.height.equalTo(90) + } + + userInfoTextStack.snp.makeConstraints { + $0.left.equalTo(userProfile.snp.right).offset(16) + $0.top.equalTo(topOptionStack.snp.bottom).offset(47) + $0.width.greaterThanOrEqualTo(115) + $0.height.equalTo(48) + } + + profileBtnStack.snp.makeConstraints { + $0.left.equalToSuperview().offset(32.5) + $0.right.equalToSuperview().offset(-32.5) + $0.bottom.equalToSuperview().offset(-29) + $0.height.equalTo(26) + } + + profileManageBtn.snp.makeConstraints { + $0.width.equalTo(157) + $0.height.equalTo(26) + } + + profileShareBtn.snp.makeConstraints { + $0.width.equalTo(157) + $0.height.equalTo(26) + } + } + } diff --git a/week8/UMC_KREAM/Presentation/My/Views/MyPageView.swift b/week8/UMC_KREAM/Presentation/My/Views/MyPageView.swift new file mode 100644 index 0000000..7c652ad --- /dev/null +++ b/week8/UMC_KREAM/Presentation/My/Views/MyPageView.swift @@ -0,0 +1,62 @@ +// +// MyPageView.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import UIKit +import SnapKit + +class MyPageView: UIView { + + // MARK: - Init + + override init(frame: CGRect) { + super.init(frame: frame) + self.backgroundColor = .gray + addComponents() + constraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Property + + /// 마이페이지 내부 상단 뷰 + public lazy var topView: MyPageTopView = { + let view = MyPageTopView() + view.backgroundColor = .white + return view + }() + + /// 마이페이지 하단 뷰 + private lazy var bottomView: UIView = { + let view = UIView() + view.backgroundColor = .white + return view + }() + + // MARK: - Constaints & Add Function + + /// 뷰 내부에 컴포넌트 생성 함수 + private func addComponents() { + [topView, bottomView].forEach{ self.addSubview($0) } + } + + /// 오토레이아웃 지정 + private func constraints() { + topView.snp.makeConstraints { + $0.top.left.right.equalToSuperview() + $0.bottom.equalTo(bottomView.snp.top).offset(-24) + } + + bottomView.snp.makeConstraints { + $0.left.right.bottom.equalToSuperview() + $0.height.equalTo(441) + } + } + + } diff --git a/week8/UMC_KREAM/Presentation/My/Views/ProfileManagerView.swift b/week8/UMC_KREAM/Presentation/My/Views/ProfileManagerView.swift new file mode 100644 index 0000000..6586f38 --- /dev/null +++ b/week8/UMC_KREAM/Presentation/My/Views/ProfileManagerView.swift @@ -0,0 +1,205 @@ +// +// ProfileManagerView.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import UIKit + +class ProfileManagerView: UIView { + + // MARK: - Init + + override init(frame: CGRect) { + super.init(frame: frame) + self.backgroundColor = .white + addComponents() + addStackView() + constraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Property + + /// 프로필 이미지 뷰 + private lazy var profileImage: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(named: "profile.pdf") + return imageView + }() + + /// 타이틀 지정 + private lazy var title: UILabel = makeTitleLabel("프로필 정보", font: UIFont.systemFont(ofSize: 14, weight: .bold)) + + /// 아이디 입력 타이틀 라벨 + public lazy var idTitle: UILabel = makeTitleLabel("유저 이메일", font: UIFont.systemFont(ofSize: 14, weight: .regular)) + + /// 비밀번호 입력 타이틀 라벨 + private lazy var pwdTitle: UILabel = makeTitleLabel("유저 비밀번호", font: UIFont.systemFont(ofSize: 14, weight: .regular)) + + /// 아이디 입력 텍스트 필드 + public lazy var idTextField: UITextField = makeTextField("새로운 이메일을 입력해주세요!", defaultValueKey: "userId") + + /// 비밀번호 입력 텍스트 필드 + public lazy var pwdTextField: UITextField = makeTextField("새로운 비밀번호를 입력해주세요!", defaultValueKey: "userPwd") + + /// 아이디 체크 버튼 + public lazy var idCheckBtn: UIButton = makeCheckBtn(title: "변경") + + /// 비밀번호 체크 버튼 + public lazy var pwdCheckBtn: UIButton = makeCheckBtn(title: "변경") + + // MARK: - Option + + /// placholder 커스텀 스타일 지정 + private lazy var placeholderAttributes: [NSAttributedString.Key: Any] = { + var value = [NSAttributedString.Key: Any]() + value[.foregroundColor] = UIColor.gray + value[.font] = UIFont.systemFont(ofSize: 12) + return value + }() + + // MARK: - StackView + + /// 아이디 텍스트필드 + 아이디 체크 버튼 모음 스택 + private lazy var idCheckStack: UIStackView = makeStackView(axis: .horizontal, spacing: 9) + + /// 비밀번호 텍스트필드 + 비밀번호 체크 버튼 모음 스택 + private lazy var pwdCheckStack: UIStackView = makeStackView(axis: .horizontal, spacing: 9) + + /// 아이디 입력 타이틀 라벨 + idCheckStack 모음 스택 + private lazy var userIdStack: UIStackView = makeStackView(axis: .vertical, spacing: 4) + + + /// 비밀번호 입력 타이틀 라벨 + pwdCheckStack 모음 스택 + private lazy var userPwdStack: UIStackView = makeStackView(axis: .vertical, spacing: 4) + + /// userIdStack + userPwdStack 모음 스택 + private lazy var userInputStack: UIStackView = makeStackView(axis: .vertical, spacing: 23) + + + // MARK: - MakeFunction + + /// 타이틀 생성 + /// - Parameters: + /// - text: 타이틀 값으로 지정할 텍스트 값 + /// - font: 타이틀 폰트 지정 + /// - Returns: 지정된 스타일의 UILabel 객체 + private func makeTitleLabel(_ text: String, font: UIFont) -> UILabel { + let label = UILabel() + label.text = text + label.textColor = .black + label.font = font + return label + } + + /// 텍스트 필드 생성 + /// - Parameter placeholder: 텍스트 필드 내부 placehodle 값 지정 + /// - Returns: 지정된 스타일의 UITextField 객체 + private func makeTextField(_ placeholder: String, defaultValueKey: String) -> UITextField { + let textField = UITextField() + let defaultValue = UserDefaults.standard.string(forKey: defaultValueKey) + + textField.attributedPlaceholder = NSAttributedString(string: placeholder, attributes: placeholderAttributes) + textField.font = UIFont.systemFont(ofSize: 14, weight: .medium) + textField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 11, height: 1)) + textField.leftViewMode = .always + textField.textColor = UIColor.black + textField.clipsToBounds = true + textField.layer.borderColor = UIColor.lightGray.cgColor + textField.layer.borderWidth = 1 + textField.layer.cornerRadius = 8 + + textField.text = defaultValue + textField.isUserInteractionEnabled = false + return textField + } + + /// 중복되는 스택뷰 생성 + /// - Parameters: + /// - axis: 스택 축 조정 + /// - spacing: 스택 내부 간경 조정 + /// - Returns: 지정된 스타일의 UIStackView 객체 + private func makeStackView(axis: NSLayoutConstraint.Axis ,spacing: CGFloat) -> UIStackView { + let stackView = UIStackView() + stackView.axis = axis + stackView.spacing = spacing + stackView.distribution = .equalSpacing + stackView.alignment = .leading + return stackView + } + + /// 변경 및 확인 버튼 + /// - Parameter title: 버튼 내부 타이틀 지정 + /// - Returns: 지정된 스타일의 UIButton 객체 + private func makeCheckBtn(title: String) -> UIButton { + let btn = UIButton() + btn.setTitle("변경", for: .normal) + btn.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium) + btn.setTitleColor(UIColor.black, for: .normal) + + btn.clipsToBounds = true + btn.layer.cornerRadius = 8 + btn.layer.borderColor = UIColor.black.cgColor + btn.layer.borderWidth = 1 + return btn + } + + // MARK: - Constaints & Add Function + + /// 스택 뷰 내부 컴포넌트 생성 + private func addStackView() { + [idTextField, idCheckBtn].forEach{ idCheckStack.addArrangedSubview($0) } + [idTitle, idCheckStack].forEach{ userIdStack.addArrangedSubview($0) } + [pwdTextField, pwdCheckBtn].forEach{ pwdCheckStack.addArrangedSubview($0) } + [pwdTitle, pwdCheckStack].forEach{ userPwdStack.addArrangedSubview($0) } + [title, userIdStack, userPwdStack].forEach{ userInputStack.addArrangedSubview($0) } + } + + /// 컴포넌트 생성 + private func addComponents() { + [profileImage, title, userInputStack].forEach{ self.addSubview($0) } + } + + /// 오토레이아웃 지정 + private func constraints() { + profileImage.snp.makeConstraints { + $0.top.equalToSuperview().offset(144) + $0.left.equalToSuperview().offset(151) + $0.width.height.equalTo(90) + } + + userInputStack.snp.makeConstraints { + $0.top.equalTo(profileImage.snp.bottom).offset(20) + $0.left.equalToSuperview().offset(27) + $0.right.equalToSuperview().offset(-17) + $0.height.equalTo(191) + } + + + idTextField.snp.makeConstraints { + $0.width.equalTo(282) + $0.height.equalTo(32) + } + + pwdTextField.snp.makeConstraints { + $0.width.equalTo(282) + $0.height.equalTo(32) + } + + idCheckBtn.snp.makeConstraints { + $0.width.equalTo(58) + $0.height.equalTo(32) + } + + pwdCheckBtn.snp.makeConstraints { + $0.width.equalTo(58) + $0.height.equalTo(32) + } + } + + } diff --git a/week8/UMC_KREAM/Presentation/Purchase/Cells/PurchaseCell.swift b/week8/UMC_KREAM/Presentation/Purchase/Cells/PurchaseCell.swift new file mode 100644 index 0000000..b348333 --- /dev/null +++ b/week8/UMC_KREAM/Presentation/Purchase/Cells/PurchaseCell.swift @@ -0,0 +1,60 @@ +// +// PurchaseCell.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import UIKit +import SnapKit + +class PurchaseCell: UICollectionViewCell { + + + static let identifier: String = "PurchaseCollectionViewCell" + + //MARK: - Init + + override init(frame: CGRect) { + super.init(frame: frame) + constraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + self.imageView.image = nil + } + + // MARK: - Property + + /// 셀 내부 이미지 버튼 + private lazy var imageView: UIImageView = { + let imageView = UIImageView() + return imageView + }() + + // MARK: - Function + + /// 제약 조건 설정 + private func constraints() { + self.addSubview(imageView) + + imageView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + } + + /// 뷰 컨트롤러에서 셀 접근하여 데이터 할당 + /// - Parameter model: RequiredCellProtocol을 상속 받는 모델 데이터 입력! + public func configure(model: any RequiredCellProtocol) { + /* as를 통해 RequiredCellProtocol을 상속 받는 모델이면서, 해당 셀에서 사용할 실제 모델 할당 */ + guard let purchaseData = model as? PurchaseModel else { return } + self.imageView.image = UIImage(named: purchaseData.imageView) + } + + } + + diff --git a/week8/UMC_KREAM/Presentation/Purchase/Cells/SizeCell.swift b/week8/UMC_KREAM/Presentation/Purchase/Cells/SizeCell.swift new file mode 100644 index 0000000..42a3e43 --- /dev/null +++ b/week8/UMC_KREAM/Presentation/Purchase/Cells/SizeCell.swift @@ -0,0 +1,101 @@ +// +// SizeCell.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import UIKit +import SnapKit + +class SizeCell: UICollectionViewCell { + + static let identifier: String = "SizeCell" + + // MARK: - Init + + override init(frame: CGRect) { + super.init(frame: frame) + selfSetup() + addComponents() + constraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + // MARK: - Property + + /// 사이즈 속성을 나타내는 라벨, 피그마 값으로 넣어서 적용 + private lazy var sizeLabel: UILabel = makeLabel(UIFont.systemFont(ofSize: 14, weight: .regular), color: .black) + + /// 가격 표시 라벨, 피그마 값으로 넣어서 적용 + private lazy var priceLabel: UILabel = makeLabel(UIFont.systemFont(ofSize: 12, weight: .regular), color: .red) + + // MARK: - Function + + /// 중복되는 라벨 생성 함수 + /// - Parameters: + /// - font: 폰트 적용 + /// - color: 컬러 적용 + /// - Returns: UILabel 반환 + private func makeLabel(_ font: UIFont, color: UIColor) -> UILabel { + let label = UILabel() + label.font = font + label.textColor = color + return label + } + + /// 사이즈 선택 셀 자체 옵션 설정 + private func selfSetup() { + self.backgroundColor = .white + self.clipsToBounds = true + self.layer.cornerRadius = 10 + self.layer.borderColor = UIColor.lightGray.cgColor + self.layer.borderWidth = 1 + } + + /// 컴포넌트 추가 + private func addComponents() { + [sizeLabel, priceLabel].forEach{ self.addSubview($0) } + } + + /// 제약 조건 설정 + private func constraints() { + sizeLabel.snp.makeConstraints { + $0.top.equalToSuperview().offset(8) + $0.centerX.equalToSuperview() + $0.width.greaterThanOrEqualTo(9) + $0.height.equalTo(17) + } + + priceLabel.snp.makeConstraints { + $0.top.equalTo(sizeLabel.snp.bottom).offset(1) + $0.centerX.equalTo(sizeLabel.snp.centerX) + $0.width.greaterThanOrEqualTo(9) + $0.height.equalTo(17) + } + } + + /// 뷰 컨트롤러에서 셀 접근하여 데이터 전달 + /// - Parameter model: SizeModel 타입 파라미터 + public func configure(model: SizeModel) { + self.sizeLabel.text = model.size + self.priceLabel.text = model.price + } + + /// 버튼이 선택되었을 때, 선택 해제되었을 때 처리를 위한 함수 + /// - Parameter isSelected: 버튼이 선택되면 true, 해제되면 false + public func changeOption(isSelected: Bool) { + if isSelected { + self.priceLabel.font = UIFont.systemFont(ofSize: 14, weight: .bold) + self.sizeLabel.font = UIFont.systemFont(ofSize: 14, weight: .bold) + self.layer.borderColor = UIColor.black.cgColor + } else { + self.priceLabel.font = UIFont.systemFont(ofSize: 14, weight: .regular) + self.sizeLabel.font = UIFont.systemFont(ofSize: 14, weight: .regular) + self.layer.borderColor = UIColor.lightGray.cgColor + } + } + + } diff --git a/week8/UMC_KREAM/Presentation/Purchase/ViewControllers/PurchaseViewController.swift b/week8/UMC_KREAM/Presentation/Purchase/ViewControllers/PurchaseViewController.swift new file mode 100644 index 0000000..6a55cc9 --- /dev/null +++ b/week8/UMC_KREAM/Presentation/Purchase/ViewControllers/PurchaseViewController.swift @@ -0,0 +1,117 @@ +// +// PurchaseViewController.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import UIKit + +class PurchaseViewController: UIViewController { + + + + let data = PurchaseData.purchaseData + + // MARK: - Init + + /// 네비게이션 적용함수, 메인 사진 클릭시 액션, 구매 버튼 클릭시 뷰 전환 액션 추가 + override func viewDidLoad() { + super.viewDidLoad() + setupNavigationBar() + setDefaultSelection() + addTapGesture() + } + + /// viewDidLoad()와 분리하여, 네비게이션을 부르는게 아닌 네비게이션 영역을 제외한 뷰를 부른다. 커스텀한 뷰를 부르드록 overrride 적용 + override func loadView() { + self.view = purchaseView + } + + // MARK: - Property + + /// 커스텀한 구매 뷰 + private lazy var purchaseView: PurchaseView = { + let view = PurchaseView() + view.productCollectionView.delegate = self + view.productCollectionView.dataSource = self + return view + }() + + // MARK: - Function + + /// 네비게이션 아이템 지정, SFSymbol을 톨해 이미지를 가져오고, 렌더링모드를 통해 해당 이미지를 블랙처리, 또한 액션을 지정하여 이전화면으로 돌아갈 수 있도록 함 + private func setupNavigationBar() { + let item = UIBarButtonItem(image: UIImage(systemName: "arrow.left")?.withRenderingMode(.alwaysOriginal).withTintColor(UIColor.black), style: .plain, target: self, action: #selector(handleBackButton)) + self.navigationItem.leftBarButtonItem = item + } + + /// 커스텀한 구매 또는 판매 버튼은 UIButton 타입이 아닌 UIView 타입이기 때문에 Gesture를 추가하여 사용자의 인터랙션을 감지 할 수 있도록 한다 + /// 구매하기 버튼의 액션 추가 + private func addTapGesture() { + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(buttonTapped)) + purchaseView.leftPurchaseBtn.addGestureRecognizer(tapGesture) + } + + /// 하나의 상품에 대해 다른 색상의 상품 셀을 클릭했을 때, 대표 사진 및 설명 텍스트 부분 바뀌도록 설정하는 함수 + /// - Parameter indexPath: 선택한 셀의 인덱스 + private func updateSelectedImage(for indexPath: IndexPath) { + purchaseView.productImageView.image = UIImage(named: data[indexPath.item].imageView) + purchaseView.productDescription.text = data[indexPath.item].productName + } + + /// 구매 뷰에 처음 접근했을 때, 컬렉션의 첫번째 셀이 대표 사진으로 선택되어 있으면서, 첫번째 셀의 값이 다른 UIComponents의 값으로 사용되도록 지정 + private func setDefaultSelection() { + let firstIndexPath = IndexPath(item: 0, section: 0) + updateSelectedImage(for: firstIndexPath) + } + + // MARK: - Action Function + + /// 좌측 네비게이션 아이템 클릭 시, 화면 닫기 + @objc private func handleBackButton() { + self.dismiss(animated: true, completion: nil) + } + + /// 구매 버튼 클릭 시, 사이즈 선택 화면을 전환 + @objc func buttonTapped() { + let newViewController = SelectSizeViewController() + /* + 3주차 워크북에서 데이터 전달 개념 + 구매 버튼에서 원하는 색상의 점퍼를 클릭하고, 구매하기 버튼을 클릭하면 선택한 점퍼의 사진과 이름, 설명을 전달한다. + */ + newViewController.receiveData = ReceivePurchaseData(image:purchaseView.productImageView.image ?? UIImage(), + productName: purchaseView.productName.text ?? "전달 못했음", + prodcutDescription: purchaseView.productDescription.text ?? "전달 못했음") + let navigationController = UINavigationController(rootViewController: newViewController) + + navigationController.modalPresentationStyle = .pageSheet + present(navigationController, animated: true, completion: nil) + } + } + + // MARK: - Extension + + extension PurchaseViewController: UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return data.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PurchaseCell.identifier, for: indexPath) as? PurchaseCell else { return UICollectionViewCell() } + + cell.configure(model: data[indexPath.row]) + + return cell + } + + /// 컬렉션 뷰 섹션 내부 여백 조정 + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { + return UIEdgeInsets(top: 15, left: 10, bottom: 15, right: 10) + } + + /// 컬렉션 뷰의 아이템(하나의 셀)을 클릭했을 때, 처리되어야 할 것 지정 + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + updateSelectedImage(for: indexPath) + } + } diff --git a/week8/UMC_KREAM/Presentation/Purchase/ViewControllers/SelectSizeViewController.swift b/week8/UMC_KREAM/Presentation/Purchase/ViewControllers/SelectSizeViewController.swift new file mode 100644 index 0000000..1b6265a --- /dev/null +++ b/week8/UMC_KREAM/Presentation/Purchase/ViewControllers/SelectSizeViewController.swift @@ -0,0 +1,112 @@ +// +// SelectSizeViewController.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import UIKit + +class SelectSizeViewController: UIViewController { + + struct ReceivePurchaseData { + let image: UIImage + let productName: String + let prodcutDescription: String + } + + /// 사이즈 선택 뷰 컨트롤러 + /// 점퍼 구매 뷰에서 구매 버튼 클릭 후 넘어오는 뷰 컨트롤러 + /// 네비게이션 컨트롤러가 아니기 때문에, 해당 뷰를 부르기 위해 NavigationController를 통해 불러야함! 그래야 상단 네비게이션 타이틀뷰와 아이템을 불러올 수 있음 + class SelectSizeViewController: UIViewController { + + let data = SizeData.sizeData + var receiveData: ReceivePurchaseData? + + // MARK: - Init + override func viewDidLoad() { + super.viewDidLoad() + setNavigation() + changeValue() + } + + /// viewDidLoad()와 분리하여, 네비게이션을 부르는게 아닌 네비게이션 영역을 제외한 뷰를 부른다. 커스텀한 뷰를 부르드록 overrride 적용 + override func loadView() { + self.view = purchaseSelectSizeView + } + + // MARK: - Property + + /// MARK: - 커스텀한 사이즈 선택 뷰 + private lazy var purchaseSelectSizeView: PurchaseSelectSizeView = { + let view = PurchaseSelectSizeView() + view.collectionView.delegate = self + view.collectionView.dataSource = self + return view + }() + + /// 구매 뷰에서 원하는 점퍼의 색상을 선택 후, 구매버튼을 클릭하여 사이즈 선택 뷰 컨트롤러로 넘어오면서 전달받은 값이 적용되어 보이도록 처리 + private func changeValue() { + if let data = receiveData { + purchaseSelectSizeView.changeValue(data: data) + } + } + + // MARK: - Function + + /// 커스텀 네비게이션 타이틀 뷰 및 우측 닫기 버튼 생서 + private func setNavigation() { + self.navigationItem.titleView = CustomNavigationTitle(frame: .zero, titleText: "구매하기", subTitleText: "(가격 단위:원)") + + let closeButton = UIBarButtonItem(image: UIImage(systemName: "xmark"), + style: .plain, + target: self, + action: #selector(closeButtonTapped)) + closeButton.tintColor = UIColor.black + self.navigationItem.rightBarButtonItem = closeButton + } + + /// 우측 네비게이션 아이템 클릭 시 닫기 액션 + @objc private func closeButtonTapped() { + dismiss(animated: true, completion: nil) + } + } + + // MARK: - Extension + extension SelectSizeViewController: UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + data.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: SizeCell.identifier, for: indexPath) as? SizeCell else { return UICollectionViewCell() } + + cell.configure(model: data[indexPath.row]) + + return cell + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + return CGSize(width: 110, height: 47) + } + + /// 셀 내부 여백 처리 + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { + return UIEdgeInsets(top: 5, left: 0, bottom: 5, right: 0) + } + + /// 셀 아이템 선택 시 처리 + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + if let cell = collectionView.cellForItem(at: indexPath) as? SizeCell { + cell.changeOption(isSelected: true) + } + } + /// 셀 아이템 선택 해제 시 처리 + func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { + if let cell = collectionView.cellForItem(at: indexPath) as? SizeCell { + cell.changeOption(isSelected: false) + } + } + + } +} diff --git a/week8/UMC_KREAM/Presentation/Purchase/Views/PurchaseSelectSizeView.swift b/week8/UMC_KREAM/Presentation/Purchase/Views/PurchaseSelectSizeView.swift new file mode 100644 index 0000000..edb2f30 --- /dev/null +++ b/week8/UMC_KREAM/Presentation/Purchase/Views/PurchaseSelectSizeView.swift @@ -0,0 +1,176 @@ +// +// PurchaseSelectSizeView.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import UIKit +import SnapKit + +class PurchaseSelectSizeView: UIView { + + // MARK: - Init + + override init(frame: CGRect) { + super.init(frame: frame) + self.backgroundColor = .white + addStackView() + addComponents() + constraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Property + + /// 사이즈 선택 화면에서 사이즈 선택하고자 하는 상품 이미지 + private lazy var productImageView: UIImageView = { + let imageView = UIImageView() + imageView.clipsToBounds = true + imageView.layer.cornerRadius = 8 + return imageView + }() + + /// 구매 상품 이름 라벨 + private lazy var productNameLabel: UILabel = makeLabel("aaaaaa", UIFont.systemFont(ofSize: 14, weight: .regular), color: .black) + + /// 구매 상품 설명 라벨 + private lazy var productDescription: UILabel = makeLabel("bbbbb", UIFont.systemFont(ofSize: 12, weight: .regular), color: .lightGray) + + /// 상품 사이즈 버튼 컬렉션 뷰 -> 사이즈를 컬렉션 뷰로 처리하여 간편하게 생성 + public lazy var collectionView: UICollectionView = { + let layout = UICollectionViewFlowLayout() + + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.register(SizeCell.self, forCellWithReuseIdentifier: SizeCell.identifier) + + return collectionView + }() + + /// 커스텀한 왼쪽 빠른 배송 버튼 + public lazy var leftSpeedBtn: ShippingButton = makeBottomBtn(price: "345,000", type: .speed) + + /// 커스텀한 우측 일반 배송 버튼 + public lazy var rightNormalBtn: ShippingButton = makeBottomBtn(price: "407,000", type: .normal) + + /// 빠른 배송 및 일반 배송 버튼을 담고 있는 하단 백그라운드 뷰 + private lazy var bottomBackgroundView: PurchaseBottomBackground = PurchaseBottomBackground() + + /// 상품 이름과 상품 설명을 담는 스택 + private lazy var productInfoStack: UIStackView = makeStackView(spacing: 4, axis: .vertical) + + /// 하단 빠른 배송 및 일반 배송 버튼을 담은 스택뷰 + private lazy var bottomButtonStack: UIStackView = makeStackView(spacing: 6, axis: .horizontal) + + + // MARK: - Function + + /// 뷰 컨트롤러에서 전달 받은 데이터를 뷰에 넘겨주어 값 적용되도록 하는 함수 + /// - Parameter data: 전달 받은 데이터 구조체 + public func changeValue(data: ReceivePurchaseData) { + self.productImageView.image = data.image + self.productNameLabel.text = data.productName + self.productDescription.text = data.prodcutDescription + } + + /// 스택 뷰에 컴포넌트 추가 + private func addStackView() { + [productNameLabel, productDescription].forEach{ self.productInfoStack.addArrangedSubview($0) } + [leftSpeedBtn, rightNormalBtn].forEach{ self.bottomButtonStack.addArrangedSubview($0) } + } + + /// 컴포넌트 추가 함수 + private func addComponents() { + [productImageView, productInfoStack, collectionView, bottomBackgroundView].forEach{ self.addSubview($0) } + [bottomButtonStack].forEach{ self.bottomBackgroundView.addSubview($0) } + } + + /// 오토레이아웃 지정 + private func constraints() { + productImageView.snp.makeConstraints { + $0.top.equalTo(self.safeAreaLayoutGuide).offset(5) + $0.left.equalToSuperview().offset(16) + $0.width.height.equalTo(91) + } + + productInfoStack.snp.makeConstraints { + $0.centerY.equalTo(productImageView.snp.centerY) + $0.left.equalTo(productImageView.snp.right).offset(15) + $0.right.equalToSuperview().offset(-20) + $0.height.equalTo(35) + } + + collectionView.snp.makeConstraints { + $0.top.equalTo(productImageView.snp.bottom).offset(35) + $0.left.equalToSuperview().offset(15) + $0.right.equalToSuperview().offset(-15) + $0.height.greaterThanOrEqualTo(115) + } + + bottomBackgroundView.snp.makeConstraints { + $0.bottom.left.right.equalToSuperview() + $0.height.equalTo(95) + } + + bottomButtonStack.snp.makeConstraints { + $0.top.equalToSuperview().offset(8) + $0.centerX.equalToSuperview() + $0.width.equalTo(342) + $0.height.equalTo(52) + } + + leftSpeedBtn.snp.makeConstraints { + $0.width.equalTo(168) + $0.height.equalTo(52) + } + + rightNormalBtn.snp.makeConstraints { + $0.width.equalTo(168) + $0.height.equalTo(52) + } + } + + // MARK: - MakeFunction + + /// 중복되는 라벨 생성 함수 + /// - Parameters: + /// - text: 텍스트 값 + /// - font: 텍스트 폰트 + /// - color: 텍스트 생상 + /// - Returns: UILabel 반환 + private func makeLabel(_ text: String, _ font: UIFont, color: UIColor) -> UILabel { + let label = UILabel() + label.text = text + label.font = font + label.textColor = color + return label + } + + /// 하단 배송 관련 버튼 생성 함수 + /// - Parameters: + /// - price: 버튼에 작성될 가격 + /// - type: 배송 타입 + /// - Returns: 커스텀한 UIView 타입의 버튼 생성 + private func makeBottomBtn(price: String, type: ShippingButtonType) -> ShippingButton { + let btn = ShippingButton(frame: .zero, btnType: type) + btn.priceLabel.text = price + btn.isUserInteractionEnabled = true + return btn + } + + /// 중복되는 스택 뷰 생성 + /// - Parameters: + /// - spacing: 스택 뷰 내부 간격 + /// - axis: 스택 뷰 내부 축 + /// - Returns: UIStackView 반환 + private func makeStackView(spacing: CGFloat, axis: NSLayoutConstraint.Axis) -> UIStackView { + let stack = UIStackView() + stack.axis = axis + stack.spacing = spacing + stack.distribution = .equalSpacing + return stack + } + } diff --git a/week8/UMC_KREAM/Presentation/Purchase/Views/PurchaseView.swift b/week8/UMC_KREAM/Presentation/Purchase/Views/PurchaseView.swift new file mode 100644 index 0000000..af2143f --- /dev/null +++ b/week8/UMC_KREAM/Presentation/Purchase/Views/PurchaseView.swift @@ -0,0 +1,217 @@ +// +// PurchaseView.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import UIKit + +class PurchaseView: UIView { + + // MARK: - Init + + override init(frame: CGRect) { + super.init(frame: frame) + self.backgroundColor = UIColor.white + addStackView() + addComponents() + constraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Property + + /// 셀에서 선택한 이미지 뷰 + public lazy var productImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFill + return imageView + }() + + /// 하낭의 상품에 대해 다른 색상의 상품 이미지 처리 컬렉션 뷰 + public lazy var productCollectionView: UICollectionView = { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .horizontal + layout.itemSize = CGSize(width: 53, height: 53) + layout.minimumInteritemSpacing = 8 + + let collection = UICollectionView(frame: .zero, collectionViewLayout: layout) + collection.register(PurchaseCell.self, forCellWithReuseIdentifier: PurchaseCell.identifier) + + collection.backgroundColor = .white + return collection + }() + + /// 상품의 가격 위에 위치한 라벨, 피그마 디자인에서 "즉시 구매가"로 위치함 + private lazy var priceTitleLabel: UILabel = makeLabel("즉시 구매가", UIFont.systemFont(ofSize: 12, weight: .light), color: UIColor.black) + + /// 상품의 가격 라벨 + private lazy var priceLabel: UILabel = makeLabel("228,000원", UIFont.systemFont(ofSize: 20, weight: .semibold), color: UIColor.black) + + /// 상품의 이름 라벨 + public lazy var productName: UILabel = makeLabel("Matin Kim Logo Coating Jumprt", UIFont.systemFont(ofSize: 16, weight: .regular), color: UIColor.black) + + /// 상품의 설명, 피그마 디자인상 점퍼 색상을 보이는 라벨 + public lazy var productDescription: UILabel = makeLabel("마뗑킴 로고 코팅 점퍼 블랙", UIFont.systemFont(ofSize: 12, weight: .regular), color: UIColor(red: 0.612, green: 0.612, blue: 0.612, alpha: 1)) + + /// 하단 백그라운드 뷰 내부의 태그 버튼 + private lazy var tagBtn: UIButton = { + let btn = UIButton() + var configuration = UIButton.Configuration.plain() + configuration.image = UIImage(named: "notTag") + configuration.imagePlacement = .top + configuration.imagePadding = 1 + + /* + 내부 영역 지정 안하면 기본값으로 지정들어가서 버튼 하나가 차지하는 공간이 커집니다. + 그래서 지정 안하면, 옆에 위치한 커스텀 버튼이 차지하는 공간의 수치를 잘 입력해도 피그마에 보이는 것처럼 안나타날 수 있습니다! + */ + configuration.contentInsets = NSDirectionalEdgeInsets(top: 2, leading: 2, bottom: 2, trailing: 2) + + configuration.attributedTitle = AttributedString("2,122", attributes: AttributeContainer([.foregroundColor: UIColor.black, .font: UIFont.systemFont(ofSize: 12, weight: .regular)])) + + btn.configuration = configuration + return btn + }() + + /// 하단 버튼 왼쪽 즉시 구매 버튼 + public lazy var leftPurchaseBtn: PurchaseButton = makeBottomBtn(price: "345,000", sub: "즉시 구매가", type: .purchase) + + /// 하단 버튼 오른쪽 즉시 판매 버튼 + public lazy var rightSalesBtn: PurchaseButton = makeBottomBtn(price: "396,000", sub: "즉시 판매가", type: .sales) + + /// 하단의 태그 및 버튼들을 담는 백그라운드 뷰 + private lazy var bottomBackgroundView: PurchaseBottomBackground = PurchaseBottomBackground() + + /// priceTitleLabel + priceLabel 담는 스택 + private lazy var priceStack: UIStackView = makeStack(spacing: 4, axis: .vertical) + + /// productName + productDescription 담는 스택 + private lazy var productStack: UIStackView = makeStack(spacing: 6, axis: .vertical) + + /// 하단 커스텀 버튼 2개 담는 스택 + private lazy var bottomButtonStack: UIStackView = makeStack(spacing: 6, axis: .horizontal) + + // MARK: - MakeFunction + + /// 즉시 판매가 또는 즉시 구매가 버튼 생성 + /// - Parameters: + /// - price: 버튼 내부 가격 + /// - sub: 즉시 판매가 또는 즉시 구매가 내용 입력 + /// - type: 판매 or 구매 + /// - Returns: 커스텀한 버튼 반환 + private func makeBottomBtn(price: String, sub: String, type: PurchaseButtonType) -> PurchaseButton { + let btn = PurchaseButton(frame: .zero, btnType: type) + btn.priceLabel.text = price + btn.subLabel.text = sub + btn.isUserInteractionEnabled = true + return btn + } + + /// 중복되는 라벨 생성 + /// - Parameters: + /// - text: 텍스트 값 + /// - font: 텍스트 폰트 + /// - color: 텍스트 색상 + /// - Returns: UILabel 반환 + private func makeLabel(_ text: String, _ font: UIFont, color: UIColor) -> UILabel { + let label = UILabel() + label.text = text + label.font = font + label.textColor = color + return label + } + + /// 중복되는 스택 반환 + /// - Parameters: + /// - spacing: 스택 내부 간격 조정 + /// - axis: 스택 축 + /// - Returns: UIStackView 반환 + private func makeStack(spacing: CGFloat, axis: NSLayoutConstraint.Axis) -> UIStackView { + let stack = UIStackView() + stack.axis = axis + stack.spacing = spacing + stack.distribution = .equalSpacing + stack.alignment = .leading + return stack + } + + // MARK: - Constaints & Add Function + + /// 스택뷰에 컴포넌트 추가 + private func addStackView() { + [priceTitleLabel, priceLabel].forEach{ self.priceStack.addArrangedSubview($0) } + [productName, productDescription].forEach{ self.productStack.addArrangedSubview($0) } + [leftPurchaseBtn, rightSalesBtn].forEach{ self.bottomButtonStack.addArrangedSubview($0) } + } + + /// 컴포넌트 추가 함수 + private func addComponents() { + [productImageView, productCollectionView].forEach{ self.addSubview($0) } + [priceStack, productStack, bottomBackgroundView].forEach{ self.addSubview($0) } + [tagBtn, bottomButtonStack].forEach{ self.bottomBackgroundView.addSubview($0) } + } + + /// 오토레이아웃 지정 + private func constraints() { + productImageView.snp.makeConstraints { + $0.top.equalTo(self.safeAreaLayoutGuide.snp.top).offset(5) + $0.left.right.equalToSuperview() + $0.height.equalTo(373) + } + + productCollectionView.snp.makeConstraints { + $0.top.equalTo(productImageView.snp.bottom).offset(20) + $0.left.right.equalToSuperview() + $0.height.equalTo(60) + } + + priceStack.snp.makeConstraints { + $0.top.equalTo(productCollectionView.snp.bottom).offset(23) + $0.left.equalToSuperview().offset(16) + $0.width.greaterThanOrEqualTo(50) + $0.height.equalTo(42) + } + + productStack.snp.makeConstraints { + $0.top.equalTo(priceStack.snp.bottom).offset(18) + $0.left.equalToSuperview().offset(16) + $0.width.greaterThanOrEqualTo(80) + $0.height.equalTo(40) + } + + bottomBackgroundView.snp.makeConstraints { + $0.bottom.left.right.equalToSuperview() + $0.height.equalTo(95) + } + + tagBtn.snp.makeConstraints { + $0.left.equalToSuperview().offset(19) + $0.top.equalToSuperview().offset(12) + $0.height.greaterThanOrEqualTo(30) + $0.width.greaterThanOrEqualTo(20) + } + + bottomButtonStack.snp.makeConstraints { + $0.top.equalToSuperview().offset(8) + $0.left.equalTo(tagBtn.snp.right).offset(19) + $0.width.equalTo(300) + $0.height.equalTo(49) + } + + leftPurchaseBtn.snp.makeConstraints { + $0.width.equalTo(147) + $0.height.equalTo(49) + } + + rightSalesBtn.snp.makeConstraints { + $0.width.equalTo(147) + $0.height.equalTo(49) + } + } + + } diff --git a/week8/UMC_KREAM/Presentation/Saved/ViewControllers/SavedViewController.swift b/week8/UMC_KREAM/Presentation/Saved/ViewControllers/SavedViewController.swift new file mode 100644 index 0000000..8e19795 --- /dev/null +++ b/week8/UMC_KREAM/Presentation/Saved/ViewControllers/SavedViewController.swift @@ -0,0 +1,51 @@ +// +// SavedViewController.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import UIKit + +class SavedViewController: UIViewController { + + let data = SavedProductData.datalist + + override func viewDidLoad() { + super.viewDidLoad() + self.view = savedView + } + + private lazy var savedView: SavedView = { + let savedView = SavedView(productCount: data.count) + savedView.tableView.delegate = self + savedView.tableView.dataSource = self + return savedView + }() + } + + //MARK: - Extension + + /* 테이블 뷰 내부 속성 조정 */ + extension SavedViewController: UITableViewDelegate, UITableViewDataSource { + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + data.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: SavedTableViewCell.identifier, for: indexPath) as? SavedTableViewCell else { + return UITableViewCell() + } + + cell.configure(model: data[indexPath.row]) + + return cell + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return 100 + } + + + } diff --git a/week8/UMC_KREAM/Presentation/Saved/Views/SavedTableViewCell.swift b/week8/UMC_KREAM/Presentation/Saved/Views/SavedTableViewCell.swift new file mode 100644 index 0000000..7e07ff4 --- /dev/null +++ b/week8/UMC_KREAM/Presentation/Saved/Views/SavedTableViewCell.swift @@ -0,0 +1,155 @@ +// +// SavedTableViewCell.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import UIKit + +class SavedTableViewCell: UITableViewCell { + + /// Saved에 사용하는 테이블 뷰 셀 + class SavedTableViewCell: UITableViewCell { + + static let identifier: String = "SavedCell" + + // MARK: - Init + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + override func prepareForReuse() { + super.prepareForReuse() + productImageView.image = nil + titleLabel.text = nil + subTitleLabel.text = nil + priceLabel.text = nil + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + addStackView() + addComponents() + constraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Property + + /// 셀에 들어가는 상품 이미지 + private lazy var productImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + imageView.clipsToBounds = true + imageView.layer.cornerRadius = 10 + return imageView + }() + + /// 셀의 라벨을 담는 스택 뷰 + private lazy var labelStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.spacing = 0 + stackView.distribution = .fill + return stackView + }() + + /// 셀에 들어가는 태그 이미지 + private lazy var tagImage: UIImageView = UIImageView.init(image: UIImage(named: "tag.pdf")) + + /// 셀에 들어가는 타이틀 라벨 + private lazy var titleLabel: UILabel = makeLabel(font: UIFont.systemFont(ofSize: 12, weight: .semibold), color: UIColor.black) + + /// 셀에 들어가는 서브 타이틀 라벨 + private lazy var subTitleLabel: UILabel = makeLabel(font: UIFont.systemFont(ofSize: 9, weight: .medium), color: UIColor.lightGray) + + private lazy var priceLabel: UILabel = makeLabel(font: UIFont.systemFont(ofSize: 14, weight: .semibold), color: UIColor.black) + // MARK: - MakeFunction + + /// 셀에 들어가는 라벨 재사용 + /// - Parameters: + /// - font: 텍스트 폰트 지정 + /// - color: 텍스트 칼라 지정 + /// - Returns: 지정된 스타일의 UILabel 객체 + private func makeLabel(font: UIFont, color: UIColor) -> UILabel { + let label = UILabel() + label.font = font + label.textColor = color + label.numberOfLines = 2 + return label + } + + // MARK: - Constraints & Add Function + /// 라벨 타이틀에 추가 + private func addStackView() { + [titleLabel, subTitleLabel].forEach{ labelStackView.addArrangedSubview($0) } + } + + /// 캄퍼넌트 추가 + private func addComponents() { + [productImageView, labelStackView, tagImage, priceLabel].forEach{ self.addSubview($0) } + } + + /// 오토레이아웃 설정 + private func constraints() { + productImageView.snp.makeConstraints { + $0.left.top.equalToSuperview().offset(13) + $0.height.width.equalTo(72) + } + + labelStackView.snp.makeConstraints { + $0.left.equalTo(productImageView.snp.right).offset(13) + $0.top.equalToSuperview().offset(13) + $0.width.equalTo(153) + $0.height.equalTo(54) + } + + tagImage.snp.makeConstraints { + $0.right.equalToSuperview().offset(-17) + $0.top.equalToSuperview().offset(18) + $0.width.equalTo(14) + $0.height.equalTo(18) + } + + priceLabel.snp.makeConstraints { + $0.right.equalToSuperview().offset(-16) + $0.top.equalToSuperview().offset(67) + $0.width.lessThanOrEqualTo(120) + } + + subTitleLabel.snp.makeConstraints { + $0.height.equalTo(33) + } + } + + /// Cell 내부 속성 조정 함수, 뷰 컨트롤러 접근 + /// - Parameter model: Cell에 사용하는 더미 데이터 모델 + public func configure(model: SavedProduct) { + self.productImageView.image = UIImage(named: model.imageName) + self.titleLabel.text = model.description.title + self.subTitleLabel.text = model.description.subTitle + self.priceLabel.text = "\(formatPrice(model.price))원" + } + + /// 가격을 3자리마다 쉼표를 찍는 형식으로 변환 + /// - Parameter price: 변환할 가격 (Int) + /// - Returns: 3자리마다 쉼표가 찍힌 문자열 + private func formatPrice(_ price: Int) -> String { + let numberFormatter = NumberFormatter() + numberFormatter.numberStyle = .decimal + return numberFormatter.string(from: NSNumber(value: price)) ?? "\(price)" + } + } +} diff --git a/week8/UMC_KREAM/Presentation/Saved/Views/SavedView.swift b/week8/UMC_KREAM/Presentation/Saved/Views/SavedView.swift new file mode 100644 index 0000000..f6d177d --- /dev/null +++ b/week8/UMC_KREAM/Presentation/Saved/Views/SavedView.swift @@ -0,0 +1,87 @@ +// +// SavedView.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import UIKit +import SnapKit + +class SavedView: UIView { + + let productCount: Int + + // MARK: - Init + init(frame: CGRect = .zero, productCount: Int) { + self.productCount = productCount + super.init(frame: frame) + addComponents() + constraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Property + /// 테이블 뷰 상단 타이틀 + private lazy var topTitle: UILabel = makeLabel("Saved", font: UIFont.systemFont(ofSize: 28, weight: .semibold)) + + /// 테이블 뷰 셀 갯수 표시 + private lazy var countLabel: UILabel = makeLabel("전체 \(productCount)개", font: UIFont.systemFont(ofSize: 14, weight: .regular)) + + /// 테이블 뷰 + public lazy var tableView: UITableView = { + let tableView = UITableView() + tableView.register(SavedTableViewCell.self, forCellReuseIdentifier: SavedTableViewCell.identifier) + tableView.separatorStyle = .singleLine + return tableView + }() + + + // MARK: - Function + /// 공통 특성의 라벨 생성하기 + /// - Parameters: + /// - text: 원하는 텍스트 값 입력 + /// - font: 폰트 지정 + /// - Returns: 지정된 스타일의 Label 생성 + private func makeLabel(_ text: String, font: UIFont) -> UILabel { + let label = UILabel() + label.font = font + label.text = text + label.textColor = UIColor.black + return label + } + + /// 컴포넌트 추가 함수 + private func addComponents() { + [topTitle, countLabel, tableView].forEach{ self.addSubview($0)} + } + + /// 오토레이아웃 지정 + private func constraints() { + topTitle.snp.makeConstraints { + $0.top.equalToSuperview().offset(61) + $0.left.equalToSuperview().offset(10) + $0.width.equalTo(81) + $0.height.equalTo(45) + } + + countLabel.snp.makeConstraints { + $0.top.equalTo(topTitle.snp.bottom).offset(16) + $0.left.equalToSuperview().offset(10) + $0.width.greaterThanOrEqualTo(55) + $0.height.equalTo(22) + } + + tableView.snp.makeConstraints { + $0.top.equalTo(countLabel.snp.bottom).offset(12) + $0.left.right.equalToSuperview() + $0.bottom.equalTo(self.safeAreaLayoutGuide) + } + } + + + + } diff --git a/week8/UMC_KREAM/Presentation/Shop/ShopViewController.swift b/week8/UMC_KREAM/Presentation/Shop/ShopViewController.swift new file mode 100644 index 0000000..04bb848 --- /dev/null +++ b/week8/UMC_KREAM/Presentation/Shop/ShopViewController.swift @@ -0,0 +1,29 @@ +// +// ShopViewController.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import UIKit + +class ShopViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + } + + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destination. + // Pass the selected object to the new view controller. + } + */ + +} diff --git a/week8/UMC_KREAM/Presentation/Style/StyleViewController.swift b/week8/UMC_KREAM/Presentation/Style/StyleViewController.swift new file mode 100644 index 0000000..0745ef6 --- /dev/null +++ b/week8/UMC_KREAM/Presentation/Style/StyleViewController.swift @@ -0,0 +1,29 @@ +// +// StyleViewController.swift +// UMC_KREAM +// +// Created by 소민준 on 11/20/24. +// + +import UIKit + +class StyleViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + } + + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destination. + // Pass the selected object to the new view controller. + } + */ + +} diff --git a/week8/UMC_KREAM/ViewController.swift b/week8/UMC_KREAM/ViewController.swift new file mode 100644 index 0000000..1df3286 --- /dev/null +++ b/week8/UMC_KREAM/ViewController.swift @@ -0,0 +1,19 @@ +// +// ViewController.swift +// UMC_KREAM +// +// Created by 소민준 on 11/19/24. +// + +import UIKit + +class ViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + } + + +} + diff --git a/week8/UMC_KREAM/app/AppDelegate.swift b/week8/UMC_KREAM/app/AppDelegate.swift new file mode 100644 index 0000000..550474f --- /dev/null +++ b/week8/UMC_KREAM/app/AppDelegate.swift @@ -0,0 +1,37 @@ +// +// AppDelegate.swift +// UMC_KREAM +// +// Created by 소민준 on 11/19/24. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + 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/week8/UMC_KREAM/app/SceneDelegate.swift b/week8/UMC_KREAM/app/SceneDelegate.swift new file mode 100644 index 0000000..b3b4135 --- /dev/null +++ b/week8/UMC_KREAM/app/SceneDelegate.swift @@ -0,0 +1,53 @@ +// +// SceneDelegate.swift +// UMC_KREAM +// +// Created by 소민준 on 11/19/24. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let windowsScene = (scene as? UIWindowScene) else { return } + window = UIWindow(frame: windowsScene.coordinateSpace.bounds) + window?.windowScene = windowsScene + window?.rootViewController = LoginViewController() + window?.makeKeyAndVisible() + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/week8/UMC_KREAMTests/UMC_KREAMTests.swift b/week8/UMC_KREAMTests/UMC_KREAMTests.swift new file mode 100644 index 0000000..fbbc14c --- /dev/null +++ b/week8/UMC_KREAMTests/UMC_KREAMTests.swift @@ -0,0 +1,17 @@ +// +// UMC_KREAMTests.swift +// UMC_KREAMTests +// +// Created by 소민준 on 11/19/24. +// + +import Testing +@testable import UMC_KREAM + +struct UMC_KREAMTests { + + @Test func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. + } + +} diff --git a/week8/UMC_KREAMUITests/UMC_KREAMUITests.swift b/week8/UMC_KREAMUITests/UMC_KREAMUITests.swift new file mode 100644 index 0000000..3941106 --- /dev/null +++ b/week8/UMC_KREAMUITests/UMC_KREAMUITests.swift @@ -0,0 +1,43 @@ +// +// UMC_KREAMUITests.swift +// UMC_KREAMUITests +// +// Created by 소민준 on 11/19/24. +// + +import XCTest + +final class UMC_KREAMUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + @MainActor + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + @MainActor + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/week8/UMC_KREAMUITests/UMC_KREAMUITestsLaunchTests.swift b/week8/UMC_KREAMUITests/UMC_KREAMUITestsLaunchTests.swift new file mode 100644 index 0000000..d622fa6 --- /dev/null +++ b/week8/UMC_KREAMUITests/UMC_KREAMUITestsLaunchTests.swift @@ -0,0 +1,33 @@ +// +// UMC_KREAMUITestsLaunchTests.swift +// UMC_KREAMUITests +// +// Created by 소민준 on 11/19/24. +// + +import XCTest + +final class UMC_KREAMUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + @MainActor + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +}