diff --git a/.gitignore b/.gitignore index 330d167..8a9974a 100644 --- a/.gitignore +++ b/.gitignore @@ -38,13 +38,13 @@ playground.xcworkspace # # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. # Packages/ -# Package.pins -# Package.resolved +Package.pins +Package.resolved # *.xcodeproj # # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata # hence it is not needed unless you have added a package configuration file to your project -# .swiftpm +.swiftpm .build/ diff --git a/KuringLite.xcodeproj/project.pbxproj b/KuringLite.xcodeproj/project.pbxproj new file mode 100644 index 0000000..1bba3fa --- /dev/null +++ b/KuringLite.xcodeproj/project.pbxproj @@ -0,0 +1,578 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + A95125A8284C973A0072EB8E /* FeedbackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A95125A7284C973A0072EB8E /* FeedbackView.swift */; }; + A95125AB284CA6770072EB8E /* KuringSDK in Frameworks */ = {isa = PBXBuildFile; productRef = A95125AA284CA6770072EB8E /* KuringSDK */; }; + A983BAC2284BB08F00B96FF1 /* KuringLiteApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A983BAC1284BB08F00B96FF1 /* KuringLiteApp.swift */; }; + A983BAC4284BB08F00B96FF1 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A983BAC3284BB08F00B96FF1 /* ContentView.swift */; }; + A983BAC6284BB08F00B96FF1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A983BAC5284BB08F00B96FF1 /* Assets.xcassets */; }; + A983BAC9284BB08F00B96FF1 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A983BAC8284BB08F00B96FF1 /* Preview Assets.xcassets */; }; + A983BAD1284BB0C800B96FF1 /* KuringCommons in Frameworks */ = {isa = PBXBuildFile; productRef = A983BAD0284BB0C800B96FF1 /* KuringCommons */; }; + A983BAD6284BB53800B96FF1 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A983BAD5284BB53800B96FF1 /* SearchView.swift */; }; + A983BAD9284BB54600B96FF1 /* NoticeList.swift in Sources */ = {isa = PBXBuildFile; fileRef = A983BAD8284BB54600B96FF1 /* NoticeList.swift */; }; + A983BADC284BB57B00B96FF1 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A983BADB284BB57B00B96FF1 /* SettingsView.swift */; }; + A983BADF284BB78700B96FF1 /* SubscriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A983BADE284BB78700B96FF1 /* SubscriptionView.swift */; }; + A983BAE1284BB7B100B96FF1 /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = A983BAE0284BB7B100B96FF1 /* Subscription.swift */; }; + A983BAE4284BBDDA00B96FF1 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = A983BAE3284BBDDA00B96FF1 /* Starscream */; }; + A983BAE6284BC16D00B96FF1 /* SubscriptionSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A983BAE5284BC16D00B96FF1 /* SubscriptionSelection.swift */; }; + A983BAE9284BC9CF00B96FF1 /* NoticeRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A983BAE8284BC9CF00B96FF1 /* NoticeRow.swift */; }; + A983BAEB284BCABC00B96FF1 /* NoticeListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A983BAEA284BCABC00B96FF1 /* NoticeListModel.swift */; }; + A983BAED284BCB1F00B96FF1 /* NoticeTypeColumn.swift in Sources */ = {isa = PBXBuildFile; fileRef = A983BAEC284BCB1F00B96FF1 /* NoticeTypeColumn.swift */; }; + A983BAEF284BCB9B00B96FF1 /* LottieView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A983BAEE284BCB9B00B96FF1 /* LottieView.swift */; }; + A983BAF2284BCBCC00B96FF1 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = A983BAF1284BCBCC00B96FF1 /* Lottie */; }; + A983BAF4284BCC7700B96FF1 /* lottieLoading.json in Resources */ = {isa = PBXBuildFile; fileRef = A983BAF3284BCC7700B96FF1 /* lottieLoading.json */; }; + A983BAF7284BCD3D00B96FF1 /* Notice.KuringLite.swift in Sources */ = {isa = PBXBuildFile; fileRef = A983BAF6284BCD3D00B96FF1 /* Notice.KuringLite.swift */; }; + A983BAFA284BCDB700B96FF1 /* NoticeType.KuringLite.swift in Sources */ = {isa = PBXBuildFile; fileRef = A983BAF9284BCDB700B96FF1 /* NoticeType.KuringLite.swift */; }; + A983BAFC284BD10B00B96FF1 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A983BAFB284BD10B00B96FF1 /* Launch Screen.storyboard */; }; + A9F8376F284C87980045FDBD /* SearchEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F8376E284C87980045FDBD /* SearchEngine.swift */; }; + A9F83771284C881C0045FDBD /* SearchTypeColumn.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F83770284C881C0045FDBD /* SearchTypeColumn.swift */; }; + A9F83773284C8B730045FDBD /* SearchedResultList.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F83772284C8B730045FDBD /* SearchedResultList.swift */; }; + A9F83775284C8B870045FDBD /* SearchedNoticeRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F83774284C8B870045FDBD /* SearchedNoticeRow.swift */; }; + A9F83777284C8B930045FDBD /* SearchedStaffRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F83776284C8B930045FDBD /* SearchedStaffRow.swift */; }; + A9F83779284C93A60045FDBD /* SwiftPackageRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F83778284C93A60045FDBD /* SwiftPackageRow.swift */; }; + A9F83783284C96880045FDBD /* FeedbackState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F83781284C96880045FDBD /* FeedbackState.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + A95125A7284C973A0072EB8E /* FeedbackView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedbackView.swift; sourceTree = ""; }; + A983BABE284BB08F00B96FF1 /* KuringLite.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KuringLite.app; sourceTree = BUILT_PRODUCTS_DIR; }; + A983BAC1284BB08F00B96FF1 /* KuringLiteApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KuringLiteApp.swift; sourceTree = ""; }; + A983BAC3284BB08F00B96FF1 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + A983BAC5284BB08F00B96FF1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + A983BAC8284BB08F00B96FF1 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + A983BAD5284BB53800B96FF1 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; }; + A983BAD8284BB54600B96FF1 /* NoticeList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeList.swift; sourceTree = ""; }; + A983BADB284BB57B00B96FF1 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + A983BADE284BB78700B96FF1 /* SubscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionView.swift; sourceTree = ""; }; + A983BAE0284BB7B100B96FF1 /* Subscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Subscription.swift; sourceTree = ""; }; + A983BAE5284BC16D00B96FF1 /* SubscriptionSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionSelection.swift; sourceTree = ""; }; + A983BAE8284BC9CF00B96FF1 /* NoticeRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRow.swift; sourceTree = ""; }; + A983BAEA284BCABC00B96FF1 /* NoticeListModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeListModel.swift; sourceTree = ""; }; + A983BAEC284BCB1F00B96FF1 /* NoticeTypeColumn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeTypeColumn.swift; sourceTree = ""; }; + A983BAEE284BCB9B00B96FF1 /* LottieView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieView.swift; sourceTree = ""; }; + A983BAF3284BCC7700B96FF1 /* lottieLoading.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = lottieLoading.json; sourceTree = ""; }; + A983BAF6284BCD3D00B96FF1 /* Notice.KuringLite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notice.KuringLite.swift; sourceTree = ""; }; + A983BAF9284BCDB700B96FF1 /* NoticeType.KuringLite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeType.KuringLite.swift; sourceTree = ""; }; + A983BAFB284BD10B00B96FF1 /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; }; + A983BAFD284BD2B800B96FF1 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + A9F8376E284C87980045FDBD /* SearchEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchEngine.swift; sourceTree = ""; }; + A9F83770284C881C0045FDBD /* SearchTypeColumn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTypeColumn.swift; sourceTree = ""; }; + A9F83772284C8B730045FDBD /* SearchedResultList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchedResultList.swift; sourceTree = ""; }; + A9F83774284C8B870045FDBD /* SearchedNoticeRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchedNoticeRow.swift; sourceTree = ""; }; + A9F83776284C8B930045FDBD /* SearchedStaffRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchedStaffRow.swift; sourceTree = ""; }; + A9F83778284C93A60045FDBD /* SwiftPackageRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftPackageRow.swift; sourceTree = ""; }; + A9F83781284C96880045FDBD /* FeedbackState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedbackState.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + A983BABB284BB08F00B96FF1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A983BAD1284BB0C800B96FF1 /* KuringCommons in Frameworks */, + A983BAE4284BBDDA00B96FF1 /* Starscream in Frameworks */, + A983BAF2284BCBCC00B96FF1 /* Lottie in Frameworks */, + A95125AB284CA6770072EB8E /* KuringSDK in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + A983BAB5284BB08F00B96FF1 = { + isa = PBXGroup; + children = ( + A983BAFD284BD2B800B96FF1 /* README.md */, + A983BAC0284BB08F00B96FF1 /* KuringLite */, + A983BABF284BB08F00B96FF1 /* Products */, + A9F8376B284C86DA0045FDBD /* Frameworks */, + ); + sourceTree = ""; + }; + A983BABF284BB08F00B96FF1 /* Products */ = { + isa = PBXGroup; + children = ( + A983BABE284BB08F00B96FF1 /* KuringLite.app */, + ); + name = Products; + sourceTree = ""; + }; + A983BAC0284BB08F00B96FF1 /* KuringLite */ = { + isa = PBXGroup; + children = ( + A983BAC1284BB08F00B96FF1 /* KuringLiteApp.swift */, + A983BAC3284BB08F00B96FF1 /* ContentView.swift */, + A9F8377F284C96880045FDBD /* Feedback */, + A983BADA284BB54A00B96FF1 /* NoticeList */, + A983BAE7284BC2ED00B96FF1 /* Subscription */, + A983BAD7284BB53C00B96FF1 /* Search */, + A983BADD284BB58200B96FF1 /* Settings */, + A983BAF5284BCC8300B96FF1 /* Commons */, + A983BAF8284BCD4100B96FF1 /* Extensions */, + A983BAC5284BB08F00B96FF1 /* Assets.xcassets */, + A983BAFB284BD10B00B96FF1 /* Launch Screen.storyboard */, + A983BAC7284BB08F00B96FF1 /* Preview Content */, + ); + path = KuringLite; + sourceTree = ""; + }; + A983BAC7284BB08F00B96FF1 /* Preview Content */ = { + isa = PBXGroup; + children = ( + A983BAC8284BB08F00B96FF1 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + A983BAD7284BB53C00B96FF1 /* Search */ = { + isa = PBXGroup; + children = ( + A983BAD5284BB53800B96FF1 /* SearchView.swift */, + A9F83772284C8B730045FDBD /* SearchedResultList.swift */, + A9F83774284C8B870045FDBD /* SearchedNoticeRow.swift */, + A9F83776284C8B930045FDBD /* SearchedStaffRow.swift */, + A9F83770284C881C0045FDBD /* SearchTypeColumn.swift */, + A9F8376E284C87980045FDBD /* SearchEngine.swift */, + ); + path = Search; + sourceTree = ""; + }; + A983BADA284BB54A00B96FF1 /* NoticeList */ = { + isa = PBXGroup; + children = ( + A983BAD8284BB54600B96FF1 /* NoticeList.swift */, + A983BAEA284BCABC00B96FF1 /* NoticeListModel.swift */, + A983BAEC284BCB1F00B96FF1 /* NoticeTypeColumn.swift */, + A983BAE8284BC9CF00B96FF1 /* NoticeRow.swift */, + ); + path = NoticeList; + sourceTree = ""; + }; + A983BADD284BB58200B96FF1 /* Settings */ = { + isa = PBXGroup; + children = ( + A983BADB284BB57B00B96FF1 /* SettingsView.swift */, + A9F83778284C93A60045FDBD /* SwiftPackageRow.swift */, + ); + path = Settings; + sourceTree = ""; + }; + A983BAE7284BC2ED00B96FF1 /* Subscription */ = { + isa = PBXGroup; + children = ( + A983BADE284BB78700B96FF1 /* SubscriptionView.swift */, + A983BAE5284BC16D00B96FF1 /* SubscriptionSelection.swift */, + A983BAE0284BB7B100B96FF1 /* Subscription.swift */, + ); + path = Subscription; + sourceTree = ""; + }; + A983BAF5284BCC8300B96FF1 /* Commons */ = { + isa = PBXGroup; + children = ( + A983BAEE284BCB9B00B96FF1 /* LottieView.swift */, + A983BAF3284BCC7700B96FF1 /* lottieLoading.json */, + ); + path = Commons; + sourceTree = ""; + }; + A983BAF8284BCD4100B96FF1 /* Extensions */ = { + isa = PBXGroup; + children = ( + A983BAF6284BCD3D00B96FF1 /* Notice.KuringLite.swift */, + A983BAF9284BCDB700B96FF1 /* NoticeType.KuringLite.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + A9F8376B284C86DA0045FDBD /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + A9F8377F284C96880045FDBD /* Feedback */ = { + isa = PBXGroup; + children = ( + A9F83781284C96880045FDBD /* FeedbackState.swift */, + A95125A7284C973A0072EB8E /* FeedbackView.swift */, + ); + path = Feedback; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + A983BABD284BB08F00B96FF1 /* KuringLite */ = { + isa = PBXNativeTarget; + buildConfigurationList = A983BACC284BB08F00B96FF1 /* Build configuration list for PBXNativeTarget "KuringLite" */; + buildPhases = ( + A983BABA284BB08F00B96FF1 /* Sources */, + A983BABB284BB08F00B96FF1 /* Frameworks */, + A983BABC284BB08F00B96FF1 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = KuringLite; + packageProductDependencies = ( + A983BAD0284BB0C800B96FF1 /* KuringCommons */, + A983BAE3284BBDDA00B96FF1 /* Starscream */, + A983BAF1284BCBCC00B96FF1 /* Lottie */, + A95125AA284CA6770072EB8E /* KuringSDK */, + ); + productName = KuringLite; + productReference = A983BABE284BB08F00B96FF1 /* KuringLite.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A983BAB6284BB08F00B96FF1 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1330; + LastUpgradeCheck = 1330; + TargetAttributes = { + A983BABD284BB08F00B96FF1 = { + CreatedOnToolsVersion = 13.3.1; + }; + }; + }; + buildConfigurationList = A983BAB9284BB08F00B96FF1 /* Build configuration list for PBXProject "KuringLite" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = A983BAB5284BB08F00B96FF1; + packageReferences = ( + A983BACF284BB0C800B96FF1 /* XCRemoteSwiftPackageReference "kuring-ios-commons" */, + A983BAE2284BBDDA00B96FF1 /* XCRemoteSwiftPackageReference "Starscream" */, + A983BAF0284BCBCC00B96FF1 /* XCRemoteSwiftPackageReference "lottie-ios" */, + A95125A9284CA6770072EB8E /* XCRemoteSwiftPackageReference "kuring-sdk-ios-spm" */, + ); + productRefGroup = A983BABF284BB08F00B96FF1 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A983BABD284BB08F00B96FF1 /* KuringLite */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + A983BABC284BB08F00B96FF1 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A983BAF4284BCC7700B96FF1 /* lottieLoading.json in Resources */, + A983BAC9284BB08F00B96FF1 /* Preview Assets.xcassets in Resources */, + A983BAFC284BD10B00B96FF1 /* Launch Screen.storyboard in Resources */, + A983BAC6284BB08F00B96FF1 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + A983BABA284BB08F00B96FF1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A9F8376F284C87980045FDBD /* SearchEngine.swift in Sources */, + A983BADC284BB57B00B96FF1 /* SettingsView.swift in Sources */, + A983BAE6284BC16D00B96FF1 /* SubscriptionSelection.swift in Sources */, + A983BAFA284BCDB700B96FF1 /* NoticeType.KuringLite.swift in Sources */, + A983BAED284BCB1F00B96FF1 /* NoticeTypeColumn.swift in Sources */, + A983BAC4284BB08F00B96FF1 /* ContentView.swift in Sources */, + A983BAE9284BC9CF00B96FF1 /* NoticeRow.swift in Sources */, + A9F83773284C8B730045FDBD /* SearchedResultList.swift in Sources */, + A9F83771284C881C0045FDBD /* SearchTypeColumn.swift in Sources */, + A95125A8284C973A0072EB8E /* FeedbackView.swift in Sources */, + A983BAD9284BB54600B96FF1 /* NoticeList.swift in Sources */, + A9F83777284C8B930045FDBD /* SearchedStaffRow.swift in Sources */, + A983BAEB284BCABC00B96FF1 /* NoticeListModel.swift in Sources */, + A983BAD6284BB53800B96FF1 /* SearchView.swift in Sources */, + A9F83779284C93A60045FDBD /* SwiftPackageRow.swift in Sources */, + A983BAE1284BB7B100B96FF1 /* Subscription.swift in Sources */, + A983BAC2284BB08F00B96FF1 /* KuringLiteApp.swift in Sources */, + A983BAF7284BCD3D00B96FF1 /* Notice.KuringLite.swift in Sources */, + A9F83783284C96880045FDBD /* FeedbackState.swift in Sources */, + A983BADF284BB78700B96FF1 /* SubscriptionView.swift in Sources */, + A983BAEF284BCB9B00B96FF1 /* LottieView.swift in Sources */, + A9F83775284C8B870045FDBD /* SearchedNoticeRow.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + A983BACA284BB08F00B96FF1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + 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; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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 = 14.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + A983BACB284BB08F00B96FF1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + 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; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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 = 14.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + A983BACD284BB08F00B96FF1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"KuringLite/Preview Content\""; + DEVELOPMENT_TEAM = 77CD4KLN3Y; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; + 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.0; + PRODUCT_BUNDLE_IDENTIFIER = com.kuring.lite; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + A983BACE284BB08F00B96FF1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"KuringLite/Preview Content\""; + DEVELOPMENT_TEAM = 77CD4KLN3Y; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen"; + 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.0; + PRODUCT_BUNDLE_IDENTIFIER = com.kuring.lite; + 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 */ + A983BAB9284BB08F00B96FF1 /* Build configuration list for PBXProject "KuringLite" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A983BACA284BB08F00B96FF1 /* Debug */, + A983BACB284BB08F00B96FF1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + A983BACC284BB08F00B96FF1 /* Build configuration list for PBXNativeTarget "KuringLite" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A983BACD284BB08F00B96FF1 /* Debug */, + A983BACE284BB08F00B96FF1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + A95125A9284CA6770072EB8E /* XCRemoteSwiftPackageReference "kuring-sdk-ios-spm" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/KU-Stacks/kuring-sdk-ios-spm"; + requirement = { + branch = main; + kind = branch; + }; + }; + A983BACF284BB0C800B96FF1 /* XCRemoteSwiftPackageReference "kuring-ios-commons" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/KU-Stacks/kuring-ios-commons"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; + }; + A983BAE2284BBDDA00B96FF1 /* XCRemoteSwiftPackageReference "Starscream" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/daltoniam/Starscream"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 4.0.0; + }; + }; + A983BAF0284BCBCC00B96FF1 /* XCRemoteSwiftPackageReference "lottie-ios" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/airbnb/lottie-ios"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 3.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + A95125AA284CA6770072EB8E /* KuringSDK */ = { + isa = XCSwiftPackageProductDependency; + package = A95125A9284CA6770072EB8E /* XCRemoteSwiftPackageReference "kuring-sdk-ios-spm" */; + productName = KuringSDK; + }; + A983BAD0284BB0C800B96FF1 /* KuringCommons */ = { + isa = XCSwiftPackageProductDependency; + package = A983BACF284BB0C800B96FF1 /* XCRemoteSwiftPackageReference "kuring-ios-commons" */; + productName = KuringCommons; + }; + A983BAE3284BBDDA00B96FF1 /* Starscream */ = { + isa = XCSwiftPackageProductDependency; + package = A983BAE2284BBDDA00B96FF1 /* XCRemoteSwiftPackageReference "Starscream" */; + productName = Starscream; + }; + A983BAF1284BCBCC00B96FF1 /* Lottie */ = { + isa = XCSwiftPackageProductDependency; + package = A983BAF0284BCBCC00B96FF1 /* XCRemoteSwiftPackageReference "lottie-ios" */; + productName = Lottie; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = A983BAB6284BB08F00B96FF1 /* Project object */; +} diff --git a/KuringLite.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/KuringLite.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/KuringLite.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/KuringLite.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/KuringLite.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/KuringLite.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/KuringLite.xcodeproj/xcshareddata/xcschemes/KuringLite.xcscheme b/KuringLite.xcodeproj/xcshareddata/xcschemes/KuringLite.xcscheme new file mode 100644 index 0000000..7f9252c --- /dev/null +++ b/KuringLite.xcodeproj/xcshareddata/xcschemes/KuringLite.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/KuringLite/Assets.xcassets/AccentColor.colorset/Contents.json b/KuringLite/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/KuringLite/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/KuringLite/Assets.xcassets/AppIcon.appiconset/Contents.json b/KuringLite/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..9221b9b --- /dev/null +++ b/KuringLite/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/KuringLite/Assets.xcassets/Contents.json b/KuringLite/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/KuringLite/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/KuringLite/Assets.xcassets/app.label.horizontal.imageset/Contents.json b/KuringLite/Assets.xcassets/app.label.horizontal.imageset/Contents.json new file mode 100644 index 0000000..f2e4ecb --- /dev/null +++ b/KuringLite/Assets.xcassets/app.label.horizontal.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "app.label.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "app.label@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "app.label@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/KuringLite/Assets.xcassets/app.label.horizontal.imageset/app.label.png b/KuringLite/Assets.xcassets/app.label.horizontal.imageset/app.label.png new file mode 100644 index 0000000..845981d Binary files /dev/null and b/KuringLite/Assets.xcassets/app.label.horizontal.imageset/app.label.png differ diff --git a/KuringLite/Assets.xcassets/app.label.horizontal.imageset/app.label@2x.png b/KuringLite/Assets.xcassets/app.label.horizontal.imageset/app.label@2x.png new file mode 100644 index 0000000..3319788 Binary files /dev/null and b/KuringLite/Assets.xcassets/app.label.horizontal.imageset/app.label@2x.png differ diff --git a/KuringLite/Assets.xcassets/app.label.horizontal.imageset/app.label@3x.png b/KuringLite/Assets.xcassets/app.label.horizontal.imageset/app.label@3x.png new file mode 100644 index 0000000..6f406aa Binary files /dev/null and b/KuringLite/Assets.xcassets/app.label.horizontal.imageset/app.label@3x.png differ diff --git a/KuringLite/Assets.xcassets/app.label.vertical.white.imageset/Contents.json b/KuringLite/Assets.xcassets/app.label.vertical.white.imageset/Contents.json new file mode 100644 index 0000000..e1bdb54 --- /dev/null +++ b/KuringLite/Assets.xcassets/app.label.vertical.white.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "app.white.label.vertical.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "app.white.label.vertical@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "app.white.label.vertical@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/KuringLite/Assets.xcassets/app.label.vertical.white.imageset/app.white.label.vertical.png b/KuringLite/Assets.xcassets/app.label.vertical.white.imageset/app.white.label.vertical.png new file mode 100644 index 0000000..c721629 Binary files /dev/null and b/KuringLite/Assets.xcassets/app.label.vertical.white.imageset/app.white.label.vertical.png differ diff --git a/KuringLite/Assets.xcassets/app.label.vertical.white.imageset/app.white.label.vertical@2x.png b/KuringLite/Assets.xcassets/app.label.vertical.white.imageset/app.white.label.vertical@2x.png new file mode 100644 index 0000000..a89edb6 Binary files /dev/null and b/KuringLite/Assets.xcassets/app.label.vertical.white.imageset/app.white.label.vertical@2x.png differ diff --git a/KuringLite/Assets.xcassets/app.label.vertical.white.imageset/app.white.label.vertical@3x.png b/KuringLite/Assets.xcassets/app.label.vertical.white.imageset/app.white.label.vertical@3x.png new file mode 100644 index 0000000..c6ecaf9 Binary files /dev/null and b/KuringLite/Assets.xcassets/app.label.vertical.white.imageset/app.white.label.vertical@3x.png differ diff --git a/KuringLite/Commons/LottieView.swift b/KuringLite/Commons/LottieView.swift new file mode 100644 index 0000000..aa1d9b0 --- /dev/null +++ b/KuringLite/Commons/LottieView.swift @@ -0,0 +1,58 @@ +// +// LottieView.swift +// KuringLite +// +// Created by Jaesung Lee on 2022/06/05. +// + +import SwiftUI +import Lottie + +struct LottieView: UIViewRepresentable { + + // makeCoordinator를 구현하여 제약사항을 구현합니다. + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + // json파일명을 받을 프로퍼티 + var filename: String + + // lottie View + var animationView = AnimationView() + + + class Coordinator: NSObject { + var parent: LottieView + + init(_ animationView: LottieView) { + // frame을 LottieView로 할당합니다. + self.parent = animationView + super.init() + } + } + + func makeUIView(context: UIViewRepresentableContext) -> UIView { + let view = UIView() + + // lottie 구현뷰 + animationView.animation = Animation.named(filename) + animationView.contentMode = .scaleAspectFit + animationView.translatesAutoresizingMaskIntoConstraints = false + + view.addSubview(animationView) + + NSLayoutConstraint.activate([ + animationView.widthAnchor.constraint(equalTo: view.widthAnchor), + animationView.heightAnchor.constraint(equalTo: view.heightAnchor) + ]) + + // 애니메이션이 계속 반복되게합니다. + animationView.loopMode = .loop + animationView.play() + return view + } + + func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext) { } +} + diff --git a/KuringLite/Commons/lottieLoading.json b/KuringLite/Commons/lottieLoading.json new file mode 100644 index 0000000..3cec72a --- /dev/null +++ b/KuringLite/Commons/lottieLoading.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 1.0.0","a":"","k":"","d":"","tc":"none"},"fr":60,"ip":9,"op":41,"w":300,"h":300,"nm":"Comp 1","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"ball4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":0,"k":223,"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.465],"y":[1]},"o":{"x":[0.534],"y":[0]},"t":25,"s":[169.5]},{"i":{"x":[0.424],"y":[1]},"o":{"x":[0.514],"y":[0]},"t":40,"s":[129.5]},{"i":{"x":[0.465],"y":[1]},"o":{"x":[0.054],"y":[0]},"t":55,"s":[169.5]},{"t":70,"s":[129.5]}],"ix":4}},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-14.359],[14.359,0],[0,14.359],[-14.359,0]],"o":[[0,14.359],[-14.359,0],[0,-14.359],[14.359,0]],"v":[[26,0],[0,26],[-26,0],[0,-26]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23921568627450981,0.7411764705882353,0.5019607843137255,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":40,"op":71,"st":25,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"ball 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":0,"k":150,"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.465],"y":[1]},"o":{"x":[0.534],"y":[0]},"t":20,"s":[169.5]},{"i":{"x":[0.424],"y":[1]},"o":{"x":[0.514],"y":[0]},"t":35,"s":[129.5]},{"i":{"x":[0.465],"y":[1]},"o":{"x":[0.054],"y":[0]},"t":50,"s":[169.5]},{"t":65,"s":[129.5]}],"ix":4}},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-14.359],[14.359,0],[0,14.359],[-14.359,0]],"o":[[0,14.359],[-14.359,0],[0,-14.359],[14.359,0]],"v":[[26,0],[0,26],[-26,0],[0,-26]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23921568627450981,0.7411764705882353,0.5019607843137255,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":35,"op":66,"st":20,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"ball 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":0,"k":77,"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.465],"y":[1]},"o":{"x":[0.534],"y":[0]},"t":15,"s":[169.5]},{"i":{"x":[0.424],"y":[1]},"o":{"x":[0.514],"y":[0]},"t":30,"s":[129.5]},{"i":{"x":[0.465],"y":[1]},"o":{"x":[0.054],"y":[0]},"t":45,"s":[169.5]},{"t":60,"s":[129.5]}],"ix":4}},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-14.359],[14.359,0],[0,14.359],[-14.359,0]],"o":[[0,14.359],[-14.359,0],[0,-14.359],[14.359,0]],"v":[[26,0],[0,26],[-26,0],[0,-26]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23921568627450981,0.7411764705882353,0.5019607843137255,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":30,"op":61,"st":15,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"ball3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":0,"k":223,"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.465],"y":[1]},"o":{"x":[0.534],"y":[0]},"t":-6,"s":[169.5]},{"i":{"x":[0.424],"y":[1]},"o":{"x":[0.514],"y":[0]},"t":9,"s":[129.5]},{"i":{"x":[0.465],"y":[1]},"o":{"x":[0.054],"y":[0]},"t":24,"s":[169.5]},{"t":39,"s":[129.5]}],"ix":4}},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-14.359],[14.359,0],[0,14.359],[-14.359,0]],"o":[[0,14.359],[-14.359,0],[0,-14.359],[14.359,0]],"v":[[26,0],[0,26],[-26,0],[0,-26]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23921568627450981,0.7411764705882353,0.5019607843137255,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":9,"op":40,"st":-6,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"ball 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":0,"k":150,"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.465],"y":[1]},"o":{"x":[0.534],"y":[0]},"t":-11,"s":[169.5]},{"i":{"x":[0.424],"y":[1]},"o":{"x":[0.514],"y":[0]},"t":4,"s":[129.5]},{"i":{"x":[0.465],"y":[1]},"o":{"x":[0.054],"y":[0]},"t":19,"s":[169.5]},{"t":34,"s":[129.5]}],"ix":4}},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-14.359],[14.359,0],[0,14.359],[-14.359,0]],"o":[[0,14.359],[-14.359,0],[0,-14.359],[14.359,0]],"v":[[26,0],[0,26],[-26,0],[0,-26]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23921568627450981,0.7411764705882353,0.5019607843137255,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":4,"op":35,"st":-11,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"ball 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":0,"k":77,"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.465],"y":[1]},"o":{"x":[0.534],"y":[0]},"t":-16,"s":[169.5]},{"i":{"x":[0.424],"y":[1]},"o":{"x":[0.514],"y":[0]},"t":-1,"s":[129.5]},{"i":{"x":[0.465],"y":[1]},"o":{"x":[0.054],"y":[0]},"t":14,"s":[169.5]},{"t":29,"s":[129.5]}],"ix":4}},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-14.359],[14.359,0],[0,14.359],[-14.359,0]],"o":[[0,14.359],[-14.359,0],[0,-14.359],[14.359,0]],"v":[[26,0],[0,26],[-26,0],[0,-26]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23921568627450981,0.7411764705882353,0.5019607843137255,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-1,"op":30,"st":-16,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Pre-comp 1","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[150,150,0],"ix":2},"a":{"a":0,"k":[150,150,0],"ix":1},"s":{"a":0,"k":[41,41,100],"ix":6}},"ao":0,"w":300,"h":300,"ip":0,"op":211,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/KuringLite/ContentView.swift b/KuringLite/ContentView.swift new file mode 100644 index 0000000..e218f16 --- /dev/null +++ b/KuringLite/ContentView.swift @@ -0,0 +1,46 @@ +// +// ContentView.swift +// KuringLite +// +// Created by Jaesung Lee on 2022/06/05. +// + +import SwiftUI +import KuringCommons + +struct ContentView: View { + @State private var showsSubscriptionView: Bool = false + + var body: some View { + NavigationView { + NoticeList() + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItemGroup(placement: .navigationBarLeading) { + Image("app.label.horizontal") + } + + ToolbarItemGroup(placement: .navigationBarTrailing) { + Button(action: { showsSubscriptionView.toggle() }) { + Image(systemName: "checklist") + } + + NavigationLink { + SearchView() + } label: { + Image(systemName: "magnifyingglass") + } + + NavigationLink { + SettingsView() + } label: { + Image(systemName: "ellipsis") + } + } + } + .sheet(isPresented: $showsSubscriptionView) { + SubscriptionView() + } + } + } +} diff --git a/KuringLite/Extensions/Notice.KuringLite.swift b/KuringLite/Extensions/Notice.KuringLite.swift new file mode 100644 index 0000000..6bcb729 --- /dev/null +++ b/KuringLite/Extensions/Notice.KuringLite.swift @@ -0,0 +1,35 @@ +// +// Notice.KuringLite.swift +// KuringLite +// +// Created by Jaesung Lee on 2022/06/05. +// Developed by Hamlit Jason on 2021/12/01. +// + +import KuringSDK +import UIKit + +extension Notice { + enum NoticeURL { + case original(_ articleID: String) + case library(_ articleID: String) + + var urlString: String { + switch self { + case .original(let articleID): + return "https://www.konkuk.ac.kr/do/MessageBoard/ArticleRead.do?id=\(articleID)" + case .library(let articleID): + return "https://library.konkuk.ac.kr/#/bbs/notice/\(articleID)" + } + } + } + + var urlString: String { + switch category { + case .도서관 : + return NoticeURL.library(articleID).urlString + default: + return NoticeURL.original(articleID).urlString + } + } +} diff --git a/KuringLite/Extensions/NoticeType.KuringLite.swift b/KuringLite/Extensions/NoticeType.KuringLite.swift new file mode 100644 index 0000000..e9251d0 --- /dev/null +++ b/KuringLite/Extensions/NoticeType.KuringLite.swift @@ -0,0 +1,14 @@ +// +// NoticeType.KuringLite.swift +// KuringLite +// +// Created by Jaesung Lee on 2022/06/05. +// + +import KuringSDK + +extension NoticeType { + var isSubscribed: Bool { + Kuring.subscribedCategories.contains(self) + } +} diff --git a/KuringLite/Feedback/FeedbackState.swift b/KuringLite/Feedback/FeedbackState.swift new file mode 100644 index 0000000..2b273e8 --- /dev/null +++ b/KuringLite/Feedback/FeedbackState.swift @@ -0,0 +1,73 @@ +// +// FeedbackState.swift +// KuringLite +// +// Created by Jaesung Lee on 2022/06/05. +// + +import SwiftUI +import KuringSDK +import KuringCommons + +class FeedbackState: ObservableObject { + @Published var feedback: String { + didSet { updateStates() } + } + @Published private(set) var textLimitGuide: String + @Published private(set) var isSendable: Bool = false + @Published private(set) var isOverTextLimit: Bool = false + @Published private(set) var textEditorColor: Color = ColorSet.Label.tertiary.color + @Published private(set) var textLimitColor: Color = ColorSet.Label.secondary.color + + let placeholder = "피드백을 남겨주세요" + let textLimit: (min: Int, max: Int) = (min: 5, max: 256) + + init() { + feedback = placeholder + textLimitGuide = "\(textLimit.min)글자 이상 입력해주세요" + } + + func send(onComplete: @escaping () -> Void) { + guard !feedback.isEmpty, feedback != placeholder else { return } + guard isSendable else { return } + Kuring.sendFeedback(feedback) { [weak self] result in + guard let self = self else { return } + // 전송 완료 시 결과와 상관 없이 `feedback`를 활성화 시킵니다. + self.feedback = self.placeholder + + switch result { + case .success: break + case .failure(let error): Logger.debug(error.localizedDescription) + } + onComplete() + } + isSendable = false + } + + private func updateStates() { + isSendable = feedback != placeholder + && feedback.count >= textLimit.min + && feedback.count <= textLimit.max + textLimitGuide = feedback.count < textLimit.min || feedback == placeholder + ? "\(textLimit.min)글자 이상 입력해주세요" + : "글자수: \(feedback.count) / \(textLimit.max)" + isOverTextLimit = feedback.count > textLimit.max + textLimitColor = isOverTextLimit + ? ColorSet.pink.color + : ColorSet.Label.secondary.color + } + + func endEditing() { + if feedback.isEmpty { + feedback = placeholder + textEditorColor = ColorSet.Label.tertiary.color + } + } + + func startEditing() { + if feedback == placeholder { + feedback = "" + textEditorColor = ColorSet.Label.primary.color + } + } +} diff --git a/KuringLite/Feedback/FeedbackView.swift b/KuringLite/Feedback/FeedbackView.swift new file mode 100644 index 0000000..1dc5f73 --- /dev/null +++ b/KuringLite/Feedback/FeedbackView.swift @@ -0,0 +1,96 @@ +// +// FeedbackView.swift +// KuringLite +// +// Created by Jaesung Lee on 2022/06/05. +// + +import SwiftUI +import KuringCommons + +struct FeedbackView: View { + @Environment(\.presentationMode) var presentationMode + @StateObject private var stateModel = FeedbackState() + + var body: some View { + NavigationView { + ZStack { + ColorSet.Background.primary.color + .edgesIgnoringSafeArea(.all) + + VStack { + Text("피드백을 남겨서 앱이 성장 하는데에\n큰 기여를 해주세요 🙂") + .foregroundColor(ColorSet.Label.primary.color) + .lineLimit(2) + .multilineTextAlignment(.center) + .padding(.top, 24) + .padding(.bottom, 32) + + TextEditor(text: $stateModel.feedback) + .foregroundColor(stateModel.textEditorColor) + .font(.footnote) + .frame(maxHeight: 164) + .padding(8) + .background( + RoundedRectangle(cornerRadius: 12) + .stroke(ColorSet.green.color, lineWidth: 1) + .foregroundColor(.clear) + .frame(maxHeight: 180) + + ) + .onTapGesture { + stateModel.startEditing() + } + + HStack { + Spacer() + + Text(stateModel.textLimitGuide) + .font(.caption) + .foregroundColor(stateModel.textLimitColor) + } + + .padding(.bottom, 24) + + Button(action: send) { + Text("피드백 보내기") + .foregroundColor(ColorSet.Background.primary.color) + .padding(.horizontal) + .background( + RoundedRectangle(cornerRadius: 26) + .foregroundColor(ColorSet.green.color) + .frame(height: 52) + .frame(minWidth: 232) + ) + .padding() + } + .opacity(stateModel.isSendable ? 1.0 : 0.5) + .disabled(!stateModel.isSendable) + + Spacer() + } + .padding(.horizontal, 16) + } + .navigationTitle("💬 피드백") + .navigationBarTitleDisplayMode(.inline) + .onTapGesture { + hideKeyboard() + stateModel.endEditing() + } + } + } + + init() { + UITextView.appearance().backgroundColor = .clear + } + + func send() { + stateModel.send { + presentationMode.wrappedValue.dismiss() + } + } + + func hideKeyboard() { + UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + } +} diff --git a/KuringLite/KuringLiteApp.swift b/KuringLite/KuringLiteApp.swift new file mode 100644 index 0000000..0ae66d8 --- /dev/null +++ b/KuringLite/KuringLiteApp.swift @@ -0,0 +1,19 @@ +// +// KuringLiteApp.swift +// KuringLite +// +// Created by Jaesung Lee on 2022/06/05. +// + +import SwiftUI +import KuringCommons + +@main +struct KuringLiteApp: App { + var body: some Scene { + WindowGroup { + ContentView() + .accentColor(ColorSet.Label.primary.color) + } + } +} diff --git a/KuringLite/Launch Screen.storyboard b/KuringLite/Launch Screen.storyboard new file mode 100644 index 0000000..63792e1 --- /dev/null +++ b/KuringLite/Launch Screen.storyboard @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/KuringLite/NoticeList/NoticeList.swift b/KuringLite/NoticeList/NoticeList.swift new file mode 100644 index 0000000..9a73456 --- /dev/null +++ b/KuringLite/NoticeList/NoticeList.swift @@ -0,0 +1,59 @@ +// +// NoticeList.swift +// KuringLite +// +// Created by Jaesung Lee on 2022/06/05. +// + +import SwiftUI +import KuringSDK +import KuringCommons + +struct NoticeList: View { + @StateObject private var model = NoticeListModel() + + var body: some View { + VStack { + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack(spacing: 10) { + ForEach(NoticeType.allCases, id: \.self) { + NoticeTypeColumn( + model: model, + noticeType: $0 + ) + } + } + .padding(.leading, 16) + } + .frame(height: 48) + + ZStack { + List { + ForEach(model.currentNotices) { notice in + ZStack(alignment: .leading) { + Link(destination: URL(string: notice.urlString)!) { + EmptyView() + } + .opacity(0) + + NoticeRow(notice: notice) + } + .onAppear { + // 마지막에 도달했을 때 공지 더 가져오기 + if model.currentNotices.last == notice { + model.load() + } + } + } + } + .listStyle(.plain) + .onAppear { model.load() } + + if model.isLoading { + LottieView(filename: "lottieLoading") + .frame(width: 100, height: 100, alignment: .center) + } + } + } + } +} diff --git a/KuringLite/NoticeList/NoticeListModel.swift b/KuringLite/NoticeList/NoticeListModel.swift new file mode 100644 index 0000000..8f5d62c --- /dev/null +++ b/KuringLite/NoticeList/NoticeListModel.swift @@ -0,0 +1,109 @@ +// +// NoticeListDataModel.swift +// KuringLite +// +// Created by Jaesung Lee on 2022/06/05. +// + +import SwiftUI +import KuringSDK +import KuringCommons + +class NoticeListModel: ObservableObject { + @Published var currentType: NoticeType = .학사 { + didSet { + if noticeList[currentType] == nil { load() } + currentNotices = noticeList[currentType] ?? [] + } + } + + @Published var currentNotices: [Notice] = [] + /// 각 타입별로 가져온 공지사항을 저장 + var noticeList: [NoticeType: [Notice]] = [:] { + didSet { + currentNotices = noticeList[currentType] ?? [] + } + } + + @Published var isLoading = false + + var query: NoticeListQuery? + + // MARK: State + /// 각 타입별로 어느 오프셋부터 공지사항을 가져오면 되는지 기록 + var offsetList: [NoticeType: Int] = [:] + /// 각 타입별로 불러올 수 있는 공지사항이 더 존재하는지 여부 기록 + var hasNextList: [NoticeType: Bool] = [:] + /// 한번 요청 시 가져올 수 있는 공지 사항 개수 최댓값 + let loadLimit = 20 + + func refresh() { + isLoading = true + // 오프셋0부터 데이터 가져오기 + let params = NoticeListQuery.Params( + type: currentType, + offset: 0, + max: UInt(loadLimit) + ) + query = Kuring.createNoticeListQuery(with: params) + query?.load { [weak self] result in + guard let self = self else { return } + switch result { + case .success(let notices): + let noticeType = notices.first?.category ?? self.currentType + var newNotices: [Notice] = [] + for notice in notices { + if notice.id == self.noticeList[noticeType]?.first?.id { return } + newNotices.append(notice) + } + guard !newNotices.isEmpty else { return } + + // 가져온 데이터 수 만큼 오프셋 값 추가 + self.offsetList[noticeType, default: 0] += newNotices.count + + // 가져온 데이터 array 가장 앞에 삽입 + // Update notices + self.noticeList[noticeType, default: []].insert(contentsOf: newNotices, at: 0) + case .failure(let error): + Logger.error(error.localizedDescription) + } + self.isLoading = false + } + } + + func load() { + if hasNextList[currentType] == false { return } + isLoading = true + let currentOffset = offsetList[currentType] ?? 0 + let params = NoticeListQuery.Params( + type: currentType, + offset: UInt(currentOffset), + max: UInt(loadLimit) + ) + query = Kuring.createNoticeListQuery(with: params) + query?.load { [weak self] result in + guard let self = self else { return } + switch result { + case .success(let notices): + // Update hasNext + let noticeType = notices.first?.category ?? self.currentType + let hasNext = notices.count >= self.loadLimit + self.hasNextList.updateValue(hasNext, forKey: noticeType) + + // Update offset + let prevOffset = self.offsetList[noticeType] ?? 0 + let currentOffset = prevOffset + notices.count + self.offsetList.updateValue(currentOffset, forKey: noticeType) + + // Update notices + var currentNotices = self.noticeList[noticeType] ?? [] + notices.forEach { currentNotices.append($0) } + self.noticeList.updateValue(currentNotices, forKey: noticeType) + case .failure(let error): + Logger.error(error.localizedDescription) + } + self.isLoading = false + } + } +} + diff --git a/KuringLite/NoticeList/NoticeRow.swift b/KuringLite/NoticeList/NoticeRow.swift new file mode 100644 index 0000000..75e3575 --- /dev/null +++ b/KuringLite/NoticeList/NoticeRow.swift @@ -0,0 +1,65 @@ +// +// NoticeRow.swift +// KuringLite +// +// Created by Jaesung Lee on 2022/06/05. +// + +import SwiftUI +import KuringSDK +import KuringCommons + +struct NoticeRow: View { + let notice: Notice + + var dateText: String { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy.MM.dd" + return formatter.string(from: Date(timeIntervalSince1970: notice.postedAt)) + } + + var body: some View { + HStack { + // MARK: New notice marker + Image(systemName: "circle.fill") + .resizable() + .frame(width: 8, height: 8) + .foregroundColor( + notice.isNew + ? notice.isSubscribed ? ColorSet.pink.color : ColorSet.gray.color + : .clear + ) + .padding(.leading, 8) + + VStack(alignment: .leading, spacing: 8) { + // MARK: Title + Text(notice.subject) + .font(.subheadline) + .lineLimit(2) + + HStack { + // MARK: Date + Text(dateText) + .foregroundColor(ColorSet.Label.secondary.color) + .font(.caption) + + // MARK: Tag + ForEach(notice.tags, id: \.self) { + Text($0) + .frame(height: 16) + .foregroundColor(ColorSet.Background.primary.color) + .font(.caption) + .padding(.horizontal, 4) + .background( + RoundedRectangle(cornerRadius: 8) + .foregroundColor(ColorSet.secondaryGray.color) + ) + + } + } + } + .padding(.trailing, 24) + } + .frame(minHeight: 56) + } +} diff --git a/KuringLite/NoticeList/NoticeTypeColumn.swift b/KuringLite/NoticeList/NoticeTypeColumn.swift new file mode 100644 index 0000000..300f8d0 --- /dev/null +++ b/KuringLite/NoticeList/NoticeTypeColumn.swift @@ -0,0 +1,38 @@ +// +// NoticeTypeColumn.swift +// KuringLite +// +// Created by Jaesung Lee on 2022/06/05. +// + +import SwiftUI +import KuringSDK +import KuringCommons + +struct NoticeTypeColumn: View { + @ObservedObject var model: NoticeListModel + let noticeType: NoticeType + + var body: some View { + Text(noticeType.koreanValue) + .font(.subheadline) + .foregroundColor( + noticeType == model.currentType + ? ColorSet.Label.green.color + : ColorSet.Label.primary.color + ) + .padding(.horizontal, 16) + .frame(height: 36) + .background( + RoundedRectangle(cornerRadius: 18) + .foregroundColor( + noticeType == model.currentType + ? ColorSet.secondaryGreen.color + : Color.clear + ) + ) + .onTapGesture { + model.currentType = noticeType + } + } +} diff --git a/KuringLite/Preview Content/Preview Assets.xcassets/Contents.json b/KuringLite/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/KuringLite/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/KuringLite/Search/SearchEngine.swift b/KuringLite/Search/SearchEngine.swift new file mode 100644 index 0000000..ec83aba --- /dev/null +++ b/KuringLite/Search/SearchEngine.swift @@ -0,0 +1,72 @@ +// +// SearchEngine.swift +// KuringLite +// +// Created by Jaesung Lee on 2022/06/05. +// + +import SwiftUI +import KuringSDK +import KuringCommons + +class SearchEngine: ObservableObject { + @Published var searchText: String = "" + + @Published var currentType: Searcher.SearchType = .notice { + didSet { + resetResult() + search() + } + } + + @Published var staffResult: [Staff] = [] + @Published var noticeResult: [Notice] = [] + + var searcher: Searcher? + + func start() { + searcher = Kuring.createSearcher(delegate: self) + searcher?.connect() + } + + func resetResult() { + staffResult = [] + noticeResult = [] + } + + func search() { + guard !searchText.isEmpty else { + return resetResult() + } + searcher?.search(searchText, forType: currentType) + } +} + +// MARK: SearcherDelegate +extension SearchEngine: SearcherDelegate { + /// 서쳐의 연결 상태가 변경될 때마다 호출되는 이벤트 입니다. + func searcher(_ searcher: Searcher, didChangeState state: Searcher.ConnectionState) { + // NOTE: disconnected 일 때 연결상태가 불안정하다는 시스템 메세지를 보여줘야 하나? + } + + /// 서쳐에서 에러가 발생할 때 호출되는 이벤트 입니다. + func searcher(_ searcher: Searcher, didReceiveError error: Error?) { + Logger.debug(error?.localizedDescription ?? "") + } + + /// 서쳐에서 교직원 정보(`[Staff]`)를 가져왔을 때 호출 되는 이벤트 입니다. + func searcher(_ searcher: Searcher, didReceiveStaffList staffs: [Staff]) { + self.staffResult = staffs + } + + /// 서쳐에서 공지 정보(`[Notice]`)를 가져왔을 때 호출 되는 이벤트 입니다. + func searcher(_ searcher: Searcher, didReceiveNoticeList notices: [Notice]) { + self.noticeResult = notices + } + + /// 서처에서 웹소켓 연결 유지를 위해 30초마다 보내는 **heart beat** 가 서버로 전송 되었을 때 호출되는 이벤트 입니다. + func searcherDidSendHeartbeat(_ searcher: Searcher) { + // TODO: 주기적으로 핑 보내는지 확인 필요 + Logger.debug("didSendHeartbeat") + } +} diff --git a/KuringLite/Search/SearchTypeColumn.swift b/KuringLite/Search/SearchTypeColumn.swift new file mode 100644 index 0000000..db27685 --- /dev/null +++ b/KuringLite/Search/SearchTypeColumn.swift @@ -0,0 +1,38 @@ +// +// SearchTypeColumn.swift +// KuringLite +// +// Created by Jaesung Lee on 2022/06/05. +// + +import SwiftUI +import KuringSDK +import KuringCommons + +struct SearchTypeColumn: View { + @ObservedObject var engine: SearchEngine + let searchType: Searcher.SearchType + + var body: some View { + Text(searchType.koreanValue) + .font(.subheadline) + .foregroundColor( + searchType == engine.currentType + ? ColorSet.Label.green.color + : ColorSet.Label.primary.color + ) + .padding(.horizontal, 16) + .frame(height: 36) + .background( + RoundedRectangle(cornerRadius: 18) + .foregroundColor( + searchType == engine.currentType + ? ColorSet.secondaryGreen.color + : Color.clear + ) + ) + .onTapGesture { + engine.currentType = searchType + } + } +} diff --git a/KuringLite/Search/SearchView.swift b/KuringLite/Search/SearchView.swift new file mode 100644 index 0000000..0340781 --- /dev/null +++ b/KuringLite/Search/SearchView.swift @@ -0,0 +1,57 @@ +// +// SearchView.swift +// KuringLite +// +// Created by Jaesung Lee on 2022/06/05. +// + +import SwiftUI +import KuringSDK +import KuringCommons + +struct SearchView: View { + @StateObject private var engine = SearchEngine() + + var body: some View { + VStack { + HStack { + Image(systemName: "magnifyingglass") + .foregroundColor(ColorSet.green.color) + + TextField("검색어를 입력해주세요", text: $engine.searchText) + } + .padding(.horizontal, 20) + .frame(height: 40) + .background( + RoundedRectangle(cornerRadius: 20) + .stroke(ColorSet.green.color, lineWidth: 1) + ) + .padding(16) + + ScrollView(showsIndicators: false) { + LazyVStack { + HStack(spacing: 10) { + ForEach(Searcher.SearchType.allCases, id: \.self) { + SearchTypeColumn( + engine: engine, + searchType: $0 + ) + } + } + + SearchedResultList(engine: engine) + } + } + } + .onAppear { engine.start() } + .onChange(of: engine.searchText) { newValue in + engine.search() + } + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .principal) { + Text("🔍 검색하기") + } + } + } +} diff --git a/KuringLite/Search/SearchedNoticeRow.swift b/KuringLite/Search/SearchedNoticeRow.swift new file mode 100644 index 0000000..cb3d589 --- /dev/null +++ b/KuringLite/Search/SearchedNoticeRow.swift @@ -0,0 +1,52 @@ +// +// SearchedNoticeRow.swift +// KuringLite +// +// Created by Jaesung Lee on 2022/06/05. +// + +import SwiftUI +import KuringSDK +import KuringCommons + +struct SearchedNoticeRow: View { + let notice: Notice + + var dateText: String { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy.MM.dd" + return formatter.string(from: Date(timeIntervalSince1970: notice.postedAt)) + } + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + // MARK: Title + Text(notice.subject) + .font(.subheadline) + .lineLimit(2) + + HStack { + // MARK: Date + Text(dateText) + .foregroundColor(ColorSet.Label.secondary.color) + .font(.caption) + + // MARK: Tag + ForEach(notice.tags, id: \.self) { + Text($0) + .frame(height: 16) + .foregroundColor(ColorSet.Background.primary.color) + .font(.caption) + .padding(.horizontal, 4) + .background( + RoundedRectangle(cornerRadius: 8) + .foregroundColor(ColorSet.secondaryGray.color) + ) + + } + } + } + .padding(.vertical, 8) + .padding(.horizontal, 20) + } +} diff --git a/KuringLite/Search/SearchedResultList.swift b/KuringLite/Search/SearchedResultList.swift new file mode 100644 index 0000000..bee04a2 --- /dev/null +++ b/KuringLite/Search/SearchedResultList.swift @@ -0,0 +1,29 @@ +// +// SearchedResultList.swift +// KuringLite +// +// Created by Jaesung Lee on 2022/06/05. +// + +import SwiftUI + +struct SearchedResultList: View { + @ObservedObject var engine: SearchEngine + + var body: some View { + LazyVStack(alignment: .leading) { + switch engine.currentType { + case .notice: + ForEach(engine.noticeResult) { notice in + SearchedNoticeRow(notice: notice) + } + case .staff: + ForEach(engine.staffResult, id: \.email) { staff in + SearchedStaffRow(staff: staff) + } + default: + EmptyView() + } + } + } +} diff --git a/KuringLite/Search/SearchedStaffRow.swift b/KuringLite/Search/SearchedStaffRow.swift new file mode 100644 index 0000000..cdd77d9 --- /dev/null +++ b/KuringLite/Search/SearchedStaffRow.swift @@ -0,0 +1,28 @@ +// +// SearchedStaffRow.swift +// KuringLite +// +// Created by Jaesung Lee on 2022/06/05. +// + +import SwiftUI +import KuringSDK +import KuringCommons + +struct SearchedStaffRow: View { + let staff: Staff + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text(staff.name) + .font(.subheadline) + .foregroundColor(ColorSet.Label.primary.color) + + Text("\(staff.deptName) · \(staff.collegeName)") + .font(.caption) + .foregroundColor(ColorSet.Label.secondary.color) + } + .padding(.vertical, 8) + .padding(.horizontal, 20) + } +} diff --git a/KuringLite/Settings/SettingsView.swift b/KuringLite/Settings/SettingsView.swift new file mode 100644 index 0000000..8bc09ed --- /dev/null +++ b/KuringLite/Settings/SettingsView.swift @@ -0,0 +1,71 @@ +// +// SettingsView.swift +// KuringLite +// +// Created by Jaesung Lee on 2022/06/05. +// + +import SwiftUI +import KuringCommons + +struct SettingsView: View { + @State private var showsSubscription: Bool = false + @State private var showsFeedbackView: Bool = false + + var body: some View { + List { + Section { + Button(action: { showsSubscription.toggle() }) { + HStack { + Text("🗞 공지 구독하기") + .font(.subheadline) + + Spacer() + + Image(systemName: "chevron.right") + .foregroundColor(ColorSet.Label.tertiary.color) + } + } + .foregroundColor(ColorSet.Label.primary.color) + } header: { + Text("공지구독") + } + + Section { + Button(action: { showsFeedbackView.toggle() }) { + HStack { + Text("💬 피드백 보내기") + .font(.subheadline) + + Spacer() + + Image(systemName: "chevron.right") + .foregroundColor(ColorSet.Label.tertiary.color) + } + } + .foregroundColor(ColorSet.Label.primary.color) + } header: { + Text("피드백") + } + + Section { + ForEach(SwiftPackage.allUsed) { + SwiftPackageRow(package: $0) + } + } header: { + Text("사용된 스위프트 패키지") + } + } + .sheet(isPresented: $showsSubscription) { + SubscriptionView() + } + .sheet(isPresented: $showsFeedbackView) { + FeedbackView() + } + .toolbar { + ToolbarItem(placement: .principal) { + Text("👋 더보기") + } + } + } +} diff --git a/KuringLite/Settings/SwiftPackageRow.swift b/KuringLite/Settings/SwiftPackageRow.swift new file mode 100644 index 0000000..d4e717d --- /dev/null +++ b/KuringLite/Settings/SwiftPackageRow.swift @@ -0,0 +1,48 @@ +// +// SwiftPackageRow.swift +// KuringLite +// +// Created by Jaesung Lee on 2022/06/05. +// + +import SwiftUI +import KuringCommons + +struct SwiftPackage: Identifiable { + var id: String { name } + let name: String + let urlString: String + let version: String + + static let allUsed: [SwiftPackage] = [ + .init(name: "KuringSDK", urlString: "https://github.com/KU-Stacks/kuring-sdk-ios-spm", version: "1.2.1"), + .init(name: "KuringCommons", urlString: "https://github.com/KU-Stacks/kuring-ios-commons", version: "1.0.2"), + .init(name: "Lottie", urlString: "https://github.com/airbnb/lottie-ios", version: "3.3.0"), + .init(name: "Starscream", urlString: "https://github.com/daltoniam/Starscream", version: "4.0.4"), + ] +} + + +struct SwiftPackageRow: View { + let package: SwiftPackage + var body: some View { + ZStack { + Link(destination: URL(string: package.urlString)!) { + EmptyView() + } + .opacity(0) + + HStack { + Text("🏛 \(package.name)") + .font(.subheadline) + .foregroundColor(ColorSet.Label.primary.color) + + Spacer() + + Text(package.version) + .font(.caption) + .foregroundColor(ColorSet.Label.secondary.color) + } + } + } +} diff --git a/KuringLite/Subscription/Subscription.swift b/KuringLite/Subscription/Subscription.swift new file mode 100644 index 0000000..a793351 --- /dev/null +++ b/KuringLite/Subscription/Subscription.swift @@ -0,0 +1,50 @@ +// +// Subscription.swift +// KuringLite +// +// Created by Jaesung Lee on 2022/06/05. +// + +import SwiftUI +import KuringSDK +import KuringCommons + +public class Subscription: ObservableObject { + @Published var selectedNoticeTypes: [NoticeType] = NoticeType.allCases.filter { $0.isSubscribed } + @Published var noticeTypes: [NoticeType] = NoticeType.allCases + @Published var isUpdatable: Bool = false + @Published var isSaved: Bool = false + + + public func select(noticeType: NoticeType) { + + if isSelecting(noticeType) { + selectedNoticeTypes.removeAll { $0 == noticeType } + } else { + selectedNoticeTypes.append(noticeType) + } + isUpdatable = true + } + + public func isSelecting(_ noticeType: NoticeType) -> Bool { + selectedNoticeTypes.contains(noticeType) + } + + public func save() { + let subscriptionList = selectedNoticeTypes.compactMap { $0.stringValue } + // Remote + Kuring.updateSubscription(categories: subscriptionList) { _ in } + // Local + Kuring.categoryStrings = subscriptionList + + isSaved = true + isUpdatable = false + } + + public func reset() { + self.selectedNoticeTypes = NoticeType.allCases.filter { $0.isSubscribed } + + isUpdatable = false + } +} + diff --git a/KuringLite/Subscription/SubscriptionSelection.swift b/KuringLite/Subscription/SubscriptionSelection.swift new file mode 100644 index 0000000..495f51a --- /dev/null +++ b/KuringLite/Subscription/SubscriptionSelection.swift @@ -0,0 +1,64 @@ +// +// SubscriptionSelection.swift +// KuringLite +// +// Created by Jaesung Lee on 2022/06/05. +// + +import SwiftUI + +struct SubscriptionSelection: View { + @ObservedObject var subscription: Subscription + + let columns = [ + GridItem(.adaptive(minimum: 60)), + GridItem(.adaptive(minimum: 60)), + GridItem(.adaptive(minimum: 60)) + ] + + var body: some View { + VStack { + // 카테고리 그룹 버튼 + HStack { + Text("대학 공지 카테고리") + .font(.subheadline.weight(.semibold)) + + Spacer() + } + + // 구독 가능 카테고리 목록 + LazyVGrid(columns: columns) { + ForEach(subscription.noticeTypes, id: \.self) { noticeType in + Button { + subscription.select(noticeType: noticeType) + } label: { + RoundedRectangle(cornerRadius: 8) + .stroke(Color.white, lineWidth: 1) + .frame(height: 32) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(subscription.isSelecting(noticeType) ? .white : .clear) + ) + .overlay( + Text(noticeType.koreanValue) + .foregroundColor( + subscription.isSelecting(noticeType) + ? Color(red: 61 / 255, green: 189 / 255, blue: 128 / 255) + : .white + ) + ) + } + } + } + } + .padding(.vertical, 15) + .foregroundColor(.white) + .padding(.horizontal, 16) + .background( + Color.white.opacity(0.2) + .clipShape(RoundedRectangle(cornerRadius: 16)) + ) + .padding(.horizontal, 38) + .clipped() + } +} diff --git a/KuringLite/Subscription/SubscriptionView.swift b/KuringLite/Subscription/SubscriptionView.swift new file mode 100644 index 0000000..d695599 --- /dev/null +++ b/KuringLite/Subscription/SubscriptionView.swift @@ -0,0 +1,67 @@ +// +// SubscriptionView.swift +// KuringLite +// +// Created by Jaesung Lee on 2022/06/05. +// + +import SwiftUI + +struct SubscriptionView: View { + @Environment(\.presentationMode) var presentationMode + @StateObject private var subscription = Subscription() + + var body: some View { + NavigationView { + ZStack(alignment: .top) { + Color(red: 61 / 255, green: 189 / 255, blue: 128 / 255) + .ignoresSafeArea(.all) + + LazyVStack { + Image(systemName: "bell") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 48, height: 48) + .clipped() + .padding(.bottom, 24) + + Text("어떤 알림들을 받아보시겠습니까?\n알림 받고 싶은 카테고리를 선택해주세요.") + .font(.body.weight(.semibold)) + .padding(.bottom, 20) + + SubscriptionSelection(subscription: subscription) + } + + } + .foregroundColor(.white) + .toolbar { + ToolbarItem(placement: .principal) { + Text("푸시 알림 설정") + .font(.body.weight(.semibold)) + .foregroundColor(.white) + } + + ToolbarItemGroup(placement: .navigationBarTrailing) { + Button(action: subscription.reset) { + Image(systemName: "arrow.uturn.left") + } + .foregroundColor(.white) + .opacity(subscription.isUpdatable ? 1 : 0.5) + .disabled(!subscription.isUpdatable) + + Button(action: save) { + Image(systemName: "checkmark") + } + .foregroundColor(.white) + .opacity(subscription.isUpdatable ? 1 : 0.5) + .disabled(!subscription.isUpdatable) + } + } + } + } + + func save() { + subscription.save() + presentationMode.wrappedValue.dismiss() + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..6ae105f --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# 쿠링Lite + +쿠링 Lite 는 모두가 자유롭게 쿠링 기능을 체험하고 기능 개발에 참여해볼 수 있는 오픈 프로젝트 입니다. + +## 개요 + +쿠링 Lite 는 실제 서비스 되고 있는 **쿠링 - 건국대학교 공지앱** 의 주요 기능 일부만을 모아둔 오픈된 앱 프로젝트 입니다. 모두가 소스코드를 확인할 수 있으며, Xcode 및 연결된 실제 디바이스에서 앱을 구동시켜볼 수 있습니다. + +앱 프로젝트의 개발환경은 다음과 같습니다. + +- SwiftUI 기반 +- iOS 14 이상 +- Xcode 13.1 이상 +- Swift 5.6 이상 + +앱 프로젝트에 사용되는 스위프트 패키지는 다음과 같습니다. + +- KuringSDK 1.2.1 이상 +- KuringCommons 1.0.2 이상 +- Lottie 3.3.0 +- Starscream 4.0.4 + +앱 프로젝트에서 제공하는 기능은 다음과 같습니다. + +- 공지 카테고리 목록 +- 공지 리스트 가져오기 +- 공지 구독하기 +- 공지 / 교직원 검색하기 +- 피드백 전송하기 + +### KuringSDK + +**KuringSDK**(이하 쿠링SDK)는 쿠링 앱 기능을 모아둔 쿠링 iOS 만의 소프트웨어 개발 키트 입니다. 로컬 저장소 관리, API 통신 관리, 모델 정의, 앱 기능 구현에 필요한 다양한 퍼블릭 인터페이스 등 여러가지 개발 도구를 제공합니다. + +[👉🏼 깃헙 링크 바로가기](https://github.com/KU-Stacks/kuring-sdk-ios-spm) + +### KuringCommons + +**KuringCommons**는 쿠링 앱에 사용되는 공통 UI 요소와 로깅에 대한 가이드를 제공하는 쿠링 iOS 만의 소프트웨어 개발 키트 입니다. + +[👉🏼 깃헙 링크 바로가기](https://github.com/KU-Stacks/kuring-ios-commons) + + +## 이 프로젝트의 개발자 + +이 오픈 프로젝트는 쿠링 iOS 팀에 의해 운영관리 됩니다.