diff --git a/Combine-Study/Combine-Study.xcodeproj/project.pbxproj b/Combine-Study/Combine-Study.xcodeproj/project.pbxproj new file mode 100644 index 0000000..a973b44 --- /dev/null +++ b/Combine-Study/Combine-Study.xcodeproj/project.pbxproj @@ -0,0 +1,348 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXFileReference section */ + 513CE29C2EF065E1006C1062 /* Combine-Study.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Combine-Study.app"; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 513CE2D02EF079C8006C1062 /* Exceptions for "Combine-Study" folder in "Combine-Study" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 513CE29B2EF065E1006C1062 /* Combine-Study */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 513CE29E2EF065E1006C1062 /* Combine-Study */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 513CE2D02EF079C8006C1062 /* Exceptions for "Combine-Study" folder in "Combine-Study" target */, + ); + path = "Combine-Study"; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 513CE2992EF065E1006C1062 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 513CE2932EF065E1006C1062 = { + isa = PBXGroup; + children = ( + 513CE29E2EF065E1006C1062 /* Combine-Study */, + 513CE29D2EF065E1006C1062 /* Products */, + ); + sourceTree = ""; + }; + 513CE29D2EF065E1006C1062 /* Products */ = { + isa = PBXGroup; + children = ( + 513CE29C2EF065E1006C1062 /* Combine-Study.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 513CE29B2EF065E1006C1062 /* Combine-Study */ = { + isa = PBXNativeTarget; + buildConfigurationList = 513CE2AA2EF065E3006C1062 /* Build configuration list for PBXNativeTarget "Combine-Study" */; + buildPhases = ( + 513CE2982EF065E1006C1062 /* Sources */, + 513CE2992EF065E1006C1062 /* Frameworks */, + 513CE29A2EF065E1006C1062 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 513CE29E2EF065E1006C1062 /* Combine-Study */, + ); + name = "Combine-Study"; + packageProductDependencies = ( + ); + productName = "Combine-Study"; + productReference = 513CE29C2EF065E1006C1062 /* Combine-Study.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 513CE2942EF065E1006C1062 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1620; + LastUpgradeCheck = 1620; + TargetAttributes = { + 513CE29B2EF065E1006C1062 = { + CreatedOnToolsVersion = 16.2; + }; + }; + }; + buildConfigurationList = 513CE2972EF065E1006C1062 /* Build configuration list for PBXProject "Combine-Study" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 513CE2932EF065E1006C1062; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 513CE29D2EF065E1006C1062 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 513CE29B2EF065E1006C1062 /* Combine-Study */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 513CE29A2EF065E1006C1062 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 513CE2982EF065E1006C1062 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 513CE2A82EF065E3006C1062 /* 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.2; + 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; + }; + 513CE2A92EF065E3006C1062 /* 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.2; + 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; + }; + 513CE2AB2EF065E3006C1062 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = 513CE29E2EF065E1006C1062 /* Combine-Study */; + baseConfigurationReferenceRelativePath = Config.xcconfig; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Combine-Study/Preview Content\""; + DEVELOPMENT_TEAM = CG37U6CMKP; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "Combine-Study/Info.plist"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + 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.nayeon.Combine-Study"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 513CE2AC2EF065E3006C1062 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = 513CE29E2EF065E1006C1062 /* Combine-Study */; + baseConfigurationReferenceRelativePath = Config.xcconfig; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Combine-Study/Preview Content\""; + DEVELOPMENT_TEAM = CG37U6CMKP; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "Combine-Study/Info.plist"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + 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.nayeon.Combine-Study"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 513CE2972EF065E1006C1062 /* Build configuration list for PBXProject "Combine-Study" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 513CE2A82EF065E3006C1062 /* Debug */, + 513CE2A92EF065E3006C1062 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 513CE2AA2EF065E3006C1062 /* Build configuration list for PBXNativeTarget "Combine-Study" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 513CE2AB2EF065E3006C1062 /* Debug */, + 513CE2AC2EF065E3006C1062 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 513CE2942EF065E1006C1062 /* Project object */; +} diff --git a/Combine-Study/Combine-Study.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Combine-Study/Combine-Study.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Combine-Study/Combine-Study.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Combine-Study/Combine-Study/Assets.xcassets/AccentColor.colorset/Contents.json b/Combine-Study/Combine-Study/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Combine-Study/Combine-Study/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Combine-Study/Combine-Study/Assets.xcassets/AppIcon.appiconset/Contents.json b/Combine-Study/Combine-Study/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..2305880 --- /dev/null +++ b/Combine-Study/Combine-Study/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/Combine-Study/Combine-Study/Assets.xcassets/Contents.json b/Combine-Study/Combine-Study/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Combine-Study/Combine-Study/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Combine-Study/Combine-Study/Combine_StudyApp.swift b/Combine-Study/Combine-Study/Combine_StudyApp.swift new file mode 100644 index 0000000..7aaa28a --- /dev/null +++ b/Combine-Study/Combine-Study/Combine_StudyApp.swift @@ -0,0 +1,19 @@ +// +// Combine_StudyApp.swift +// Combine-Study +// +// Created by 이나연 on 12/16/25. +// + +import SwiftUI + +@main +struct Combine_StudyApp: App { + @StateObject private var viewModel = DailyBoxOfficeViewModel_Combine() + + var body: some Scene { + WindowGroup { + DailyBoxOfficeView(viewModel: viewModel) + } + } +} diff --git a/Combine-Study/Combine-Study/DailyBoxOfficeModel.swift b/Combine-Study/Combine-Study/DailyBoxOfficeModel.swift new file mode 100644 index 0000000..2d42ba5 --- /dev/null +++ b/Combine-Study/Combine-Study/DailyBoxOfficeModel.swift @@ -0,0 +1,16 @@ +// +// DailyBoxOfficeModel.swift +// Combine-Study +// +// Created by 이나연 on 12/16/25. +// + +import Foundation + +struct DailyBoxOfficeModel { + let rank: String + let movieNm: String + let movieCd: String + let openDt: String +} + diff --git a/Combine-Study/Combine-Study/DailyBoxOfficeView.swift b/Combine-Study/Combine-Study/DailyBoxOfficeView.swift new file mode 100644 index 0000000..b251650 --- /dev/null +++ b/Combine-Study/Combine-Study/DailyBoxOfficeView.swift @@ -0,0 +1,35 @@ +// +// ContentView.swift +// Combine-Study +// +// Created by 이나연 on 12/16/25. +// + +import SwiftUI + +struct DailyBoxOfficeView: View { + @StateObject var viewModel: DailyBoxOfficeViewModel_Combine + + var body: some View { + ScrollView(.vertical, showsIndicators: false) { + Text("박스 오피스 영화 순위") + .font(.headline) + + VStack(alignment: .leading) { + ForEach(viewModel.dailyBoxOfficeList, id: \.movieCd) { item in + VStack(alignment: .leading) { + Text("\(item.rank)위") + .fontWeight(.bold) + Text(item.movieNm) + .foregroundStyle(.blue) + Text("\(item.openDt) 개봉") + } + Spacer() + } + } + }.onAppear { + self.viewModel.action(.viewDidLoad) + } + + } +} diff --git a/Combine-Study/Combine-Study/DailyBoxOfficeViewModel.swift b/Combine-Study/Combine-Study/DailyBoxOfficeViewModel.swift new file mode 100644 index 0000000..6c10bbb --- /dev/null +++ b/Combine-Study/Combine-Study/DailyBoxOfficeViewModel.swift @@ -0,0 +1,37 @@ +// +// DailyBoxOfficeViewModel.swift +// Combine-Study +// +// Created by 이나연 on 12/16/25. +// + +import Foundation + +class DailyBoxOfficeViewModel: ViewModelType { + private let networkService = DefaultBoxOfficeService() + @Published private(set) var dailyBoxOfficeList: [DailyBoxOfficeModel] = [] + + enum Input { + case viewDidLoad + } + + func action(_ trigger: Input) { + switch trigger { + case .viewDidLoad: + fetchDailyBoxOffice() + } + } + + private func fetchDailyBoxOffice() { + Task { @MainActor in + do { + let result = try await networkService.fetchDailyBoxOffice() + self.dailyBoxOfficeList = result + } + catch { + print(error) + } + } + } + +} diff --git a/Combine-Study/Combine-Study/DailyBoxOfficeViewModel_Combine.swift b/Combine-Study/Combine-Study/DailyBoxOfficeViewModel_Combine.swift new file mode 100644 index 0000000..a96cee3 --- /dev/null +++ b/Combine-Study/Combine-Study/DailyBoxOfficeViewModel_Combine.swift @@ -0,0 +1,43 @@ +// +// DailyBoxOfficeViewModel.swift +// Combine-Study +// +// Created by 이나연 on 12/16/25. +// + +import Combine +import Foundation + +class DailyBoxOfficeViewModel_Combine: ViewModelType { + private let networkService = CombineBoxOfficeService() + private var cancellables: Set = [] + @Published private(set) var dailyBoxOfficeList: [DailyBoxOfficeModel] = [] + + enum Input { + case viewDidLoad + } + + func action(_ trigger: Input) { + switch trigger { + case .viewDidLoad: + fetchDailyBoxOffice() + } + } + + private func fetchDailyBoxOffice() { + networkService.fetchDailyBoxOffice() + .receive(on: DispatchQueue.main) + .sink( + receiveCompletion: { completion in + if case let .failure(error) = completion { + print(error) + } + }, + receiveValue: { [weak self] list in + self?.dailyBoxOfficeList = list + } + ) + .store(in: &cancellables) + } + +} diff --git a/Combine-Study/Combine-Study/Info.plist b/Combine-Study/Combine-Study/Info.plist new file mode 100644 index 0000000..f40d215 --- /dev/null +++ b/Combine-Study/Combine-Study/Info.plist @@ -0,0 +1,13 @@ + + + + + API_KEY + $(API_KEY) + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + + diff --git a/Combine-Study/Combine-Study/Network/BoxOfficeService.swift b/Combine-Study/Combine-Study/Network/BoxOfficeService.swift new file mode 100644 index 0000000..3f1ced2 --- /dev/null +++ b/Combine-Study/Combine-Study/Network/BoxOfficeService.swift @@ -0,0 +1,51 @@ +// +// BoxOfficeService.swift +// Combine-Study +// +// Created by 이나연 on 12/16/25. +// + +import Combine + +protocol BoxOfficeService { + func fetchDailyBoxOffice() async throws -> [DailyBoxOfficeModel] +} + +struct DefaultBoxOfficeService: BoxOfficeService { + let networkService = NetworkService.shared + + func fetchDailyBoxOffice() async throws -> [DailyBoxOfficeModel] { + do { + let response: DailyBoxOfficeResponseDTO = try await networkService.request(endPoint: .dailyBoxOffice) + + return response.boxOfficeResult.dailyBoxOfficeList.map { $0.toEntity() } + + } + } +} + +protocol BoxOfficeService_Combine { + func fetchDailyBoxOffice()-> AnyPublisher<[DailyBoxOfficeModel], Error> +} + +struct CombineBoxOfficeService: BoxOfficeService_Combine { + let networkService = NetworkService.shared + + func fetchDailyBoxOffice() -> AnyPublisher<[DailyBoxOfficeModel], Error> { + return Future { promise in + Task { + do { + let response: DailyBoxOfficeResponseDTO = + try await networkService.request(endPoint: .dailyBoxOffice) + + let result = response.boxOfficeResult.dailyBoxOfficeList + .map { $0.toEntity() } + + promise(.success(result)) + } catch { + promise(.failure(error)) + } + } + }.eraseToAnyPublisher() + } +} diff --git a/Combine-Study/Combine-Study/Network/DTO/BoxOfficeResultDTO.swift b/Combine-Study/Combine-Study/Network/DTO/BoxOfficeResultDTO.swift new file mode 100644 index 0000000..adea685 --- /dev/null +++ b/Combine-Study/Combine-Study/Network/DTO/BoxOfficeResultDTO.swift @@ -0,0 +1,48 @@ +// +// BoxOfficeResultDTO.swift +// Combine-Study +// +// Created by 이나연 on 12/16/25. +// + +import Foundation + +struct DailyBoxOfficeResponseDTO: Decodable { + let boxOfficeResult: BoxOfficeResultDTO +} + +struct BoxOfficeResultDTO: Decodable { + let boxofficeType: String + let showRange: String + let dailyBoxOfficeList: [DailyBoxOfficeDTO] +} + +struct DailyBoxOfficeDTO: Decodable { + let rnum: String + let rank: String + let rankInten: String + let rankOldAndNew: String + let movieCd: String + let movieNm: String + let openDt: String + let salesAmt: String + let salesShare: String + let salesInten: String + let salesChange: String + let salesAcc: String + let audiCnt: String + let audiInten: String + let audiChange: String + let audiAcc: String + let scrnCnt: String + let showCnt: String + + func toEntity() -> DailyBoxOfficeModel { + return .init( + rank: self.rank, + movieNm: self.movieNm, + movieCd: self.movieCd, + openDt: self.openDt + ) + } +} diff --git a/Combine-Study/Combine-Study/Network/EndPoint.swift b/Combine-Study/Combine-Study/Network/EndPoint.swift new file mode 100644 index 0000000..7cc22fe --- /dev/null +++ b/Combine-Study/Combine-Study/Network/EndPoint.swift @@ -0,0 +1,43 @@ +// +// EndPoint.swift +// Combine-Study +// +// Created by 이나연 on 12/16/25. +// + +import Foundation + +enum EndPoint { + case dailyBoxOffice + + var requestType: HTTPMethodType { + switch self { + case .dailyBoxOffice: + return .get + } + } + + var url: String { + switch self { + case .dailyBoxOffice: + return "/searchDailyBoxOfficeList.json" + } + } + + var header: [String: String] { + switch self { + case .dailyBoxOffice: + return HeaderType.basic.value + } + } + + var queryParams: [String: String]? { + switch self { + case .dailyBoxOffice: + return [ + "key": Bundle.main.infoDictionary?["API_KEY"] as! String, + "targetDt": "20251215" + ] + } + } +} diff --git a/Combine-Study/Combine-Study/Network/HTTPMethodType.swift b/Combine-Study/Combine-Study/Network/HTTPMethodType.swift new file mode 100644 index 0000000..105c16c --- /dev/null +++ b/Combine-Study/Combine-Study/Network/HTTPMethodType.swift @@ -0,0 +1,40 @@ +// +// HTTPMethodType.swift +// Combine-Study +// +// Created by 이나연 on 12/16/25. +// + +enum HTTPMethodType { + case get + case post + case patch + + var key: String { + switch self { + case .get: + return "GET" + case .post: + return "POST" + case .patch: + return "PATCH" + } + } +} + +enum HeaderType { + case auth + case basic + + var value: [String: String] { + switch self { + case .auth: + return [ + "Content-Type": "application/json", + "userID": "1" + ] + case .basic: + return ["Content-Type": "application/json"] + } + } +} diff --git a/Combine-Study/Combine-Study/Network/NetworkError.swift b/Combine-Study/Combine-Study/Network/NetworkError.swift new file mode 100644 index 0000000..b480bb2 --- /dev/null +++ b/Combine-Study/Combine-Study/Network/NetworkError.swift @@ -0,0 +1,37 @@ +// +// NetworkError.swift +// Combine-Study +// +// Created by 이나연 on 12/16/25. +// + +import Foundation + +enum NetworkError: Error { + case urlError + case httpURLResponseError + case responseDecodingError + case noData + case requestEncodingError + case serverErrorMessage(String) + case unknownError + + var errorDescription: String { + switch self { + case .urlError: + "사용할 수 없는 URL" + case .httpURLResponseError: + "HTTPURLResponse로 타입 캐스팅 불가" + case .responseDecodingError: + "디코딩 실패" + case .noData: + "데이터 없음" + case .requestEncodingError: + "인코딩 실패" + case .serverErrorMessage(let message): + message + case .unknownError: + "알 수 없는 에러" + } + } +} diff --git a/Combine-Study/Combine-Study/Network/NetworkLogger.swift b/Combine-Study/Combine-Study/Network/NetworkLogger.swift new file mode 100644 index 0000000..1685d4e --- /dev/null +++ b/Combine-Study/Combine-Study/Network/NetworkLogger.swift @@ -0,0 +1,56 @@ +// +// NetworkLogger.swift +// Combine-Study +// +// Created by 이나연 on 12/16/25. +// + +import Foundation + +final class NetworkLogger { + + static let shared = NetworkLogger() + private init() { } + + static func requestLog(request: URLRequest) { + print("\n🌀 Request 🌀") + + let url = request.url?.absoluteString ?? "URL None" + let method = request.httpMethod + + var output = """ + url: \(url) + method: \(String(describing: method)) + """ + + for (key, value) in request.allHTTPHeaderFields ?? [:] { + output += "\(key): \(value) \n" + } + + if let body = request.httpBody { + output += "\n \(String(data: body, encoding: .utf8) ?? "")" + } + + print(output) + + defer { + print("\n") + } + } + + static func responseLog(response: HTTPURLResponse?, data: Data?) { + print("\n☄️ Response ☄️ ") + + var output = "" + + if let statusCode = response?.statusCode { + output += "✅ StatusCode: \(statusCode)" + } + + if let body = data { + output += "\n✅ Body: \(String(data: body, encoding: .utf8) ?? "")" + } + + print(output) + } +} diff --git a/Combine-Study/Combine-Study/Network/NetworkService.swift b/Combine-Study/Combine-Study/Network/NetworkService.swift new file mode 100644 index 0000000..9ca8da2 --- /dev/null +++ b/Combine-Study/Combine-Study/Network/NetworkService.swift @@ -0,0 +1,91 @@ +// +// Untitled.swift +// Combine-Study +// +// Created by 이나연 on 12/16/25. +// + +import Foundation + +struct NetworkService { + + static let shared = NetworkService() + private init() {} + + func request( + endPoint: EndPoint, + body: Encodable? = nil + ) async throws -> Response { + + let url = makeRequestURL(endPoint: endPoint) + var request = URLRequest(url: url) + + request.httpMethod = endPoint.requestType.key + endPoint.header.forEach { + request.addValue($0.value, forHTTPHeaderField: $0.key) + } + + if let body { + let requestBody = try makeRequestBody(data: body) + request.httpBody = requestBody + } + + NetworkLogger.requestLog(request: request) + + return try await requestToResponse(request: request) + } +} + +extension NetworkService { + private func makeRequestURL(endPoint: EndPoint) -> URL { + let baseURL = "http://www.kobis.or.kr/kobisopenapi/webservice/rest/boxoffice" + endPoint.url + + guard var urlComponents = URLComponents(string: baseURL) else { + print(NetworkError.httpURLResponseError) + return URL(string: "")! + } + + if let queryParams = endPoint.queryParams { + urlComponents.queryItems = queryParams.map{ + URLQueryItem(name: $0, value: $1) + } + } + + guard let url = urlComponents.url else { + print(NetworkError.httpURLResponseError) + return URL(string: "")! + } + + return url + } + private func makeRequestBody(data: Body) throws -> Data { + do { + let jsonEncoder = JSONEncoder() + let requestBody = try jsonEncoder.encode(data) + + return requestBody + } catch { + throw NetworkError.requestEncodingError + } + } + + private func requestToResponse(request: URLRequest) async throws -> Response { + let (data, response) = try await URLSession.shared.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse else { + throw NetworkError.httpURLResponseError + } + + NetworkLogger.responseLog(response: httpResponse, data: data) + + do { + print("type\(Response.self)") + let decoded = try JSONDecoder().decode(DailyBoxOfficeResponseDTO.self, from: data) + + return decoded as! Response + + } catch { + throw NetworkError.responseDecodingError + } + } +} diff --git a/Combine-Study/Combine-Study/Preview Content/Preview Assets.xcassets/Contents.json b/Combine-Study/Combine-Study/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Combine-Study/Combine-Study/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Combine-Study/Combine-Study/ViewModelType.swift b/Combine-Study/Combine-Study/ViewModelType.swift new file mode 100644 index 0000000..0643164 --- /dev/null +++ b/Combine-Study/Combine-Study/ViewModelType.swift @@ -0,0 +1,14 @@ +// +// ViewModelType.swift +// Combine-Study +// +// Created by 이나연 on 12/16/25. +// + +import SwiftUI + +protocol ViewModelType: ObservableObject { + associatedtype Input + + func action(_ trigger: Input) +}