├── burstcamp ├── Modules │ └── .gitkeep ├── burstcamp │ ├── Domain │ │ ├── Model │ │ │ ├── .gitkeep │ │ │ ├── User │ │ │ │ ├── ScrapUser.swift │ │ │ │ ├── Login │ │ │ │ │ └── LoginProvider.swift │ │ │ │ ├── Domain.swift │ │ │ │ └── SignUpUser.swift │ │ │ ├── Feed │ │ │ │ ├── DiffableFeed.swift │ │ │ │ ├── FeedCellType.swift │ │ │ │ └── FeedWriter.swift │ │ │ ├── Network │ │ │ │ ├── HTTPMethod.swift │ │ │ │ ├── GithubAPI.swift │ │ │ │ └── HTTPHeader.swift │ │ │ ├── CellIndexPath.swift │ │ │ └── Appearance.swift │ │ ├── Interfaces │ │ │ ├── BlogRepository │ │ │ │ └── BlogRepository.swift │ │ │ ├── ImageRepository │ │ │ │ └── ImageRepository.swift │ │ │ ├── NotificationRepository │ │ │ │ └── NotificationRepository.swift │ │ │ ├── LoginRepository │ │ │ │ └── LoginRepository.swift │ │ │ ├── UserRepository │ │ │ │ └── UserRepository.swift │ │ │ └── FeedRepository │ │ │ │ └── FeedRepository.swift │ │ └── UseCase │ │ │ ├── Tab │ │ │ ├── ScrapPage │ │ │ │ ├── ScrapPageUseCase.swift │ │ │ │ └── DefaultScrapPageUseCase.swift │ │ │ ├── Home │ │ │ │ └── HomeUseCase.swift │ │ │ ├── Detail │ │ │ │ ├── FeedDetailUseCase.swift │ │ │ │ └── DefaultFeedDetailUseCase.swift │ │ │ └── MyPage │ │ │ │ ├── MyPageEdit │ │ │ │ ├── MyPageEditUseCase.swift │ │ │ │ └── DefaultMyPageEditUseCase.swift │ │ │ │ └── MyPageUseCase.swift │ │ │ ├── Notification │ │ │ ├── NotificationUseCase.swift │ │ │ └── DefaultNotificationUseCase.swift │ │ │ └── Auth │ │ │ ├── Login │ │ │ ├── LoginUseCase.swift │ │ │ └── DefaultLoginUseCase.swift │ │ │ └── SignUp │ │ │ ├── SignUpUseCase.swift │ │ │ └── DefaultSignUpUseCase.swift │ ├── Presentation │ │ ├── Common │ │ │ ├── View │ │ │ │ ├── .gitkeep │ │ │ │ ├── Label │ │ │ │ │ ├── DefaultMultilLineLabel.swift │ │ │ │ │ └── DefaultPaddingLabel.swift │ │ │ │ ├── Button │ │ │ │ │ ├── DefaultButton.swift │ │ │ │ │ ├── DefaultToggleButton.swift │ │ │ │ │ └── AuthButton.swift │ │ │ │ ├── ImageView │ │ │ │ │ └── DefaultProfileImageView.swift │ │ │ │ ├── LoadingVIew │ │ │ │ │ └── LoadingView.swift │ │ │ │ ├── NormalFeedCell │ │ │ │ │ └── Header │ │ │ │ │ │ ├── NormalFeedCellBadgeStackView.swift │ │ │ │ │ │ └── NormalFeedCellHeader.swift │ │ │ │ ├── TextField │ │ │ │ │ └── DefaultTextField.swift │ │ │ │ ├── Badge │ │ │ │ │ ├── DefaultBadgeLabel.swift │ │ │ │ │ └── DefaultBadgeView.swift │ │ │ │ └── RecommendCell │ │ │ │ │ ├── RecommendFeedHeader.swift │ │ │ │ │ └── RecommendFeedUserView.swift │ │ │ └── Base │ │ │ │ └── AppleAuthViewController.swift │ │ ├── Tab │ │ │ ├── Detail │ │ │ │ ├── ViewModel │ │ │ │ │ └── ActionSheetEvent.swift │ │ │ │ └── View │ │ │ │ │ ├── EmptyView │ │ │ │ │ ├── EmptyFeedView.swift │ │ │ │ │ └── LoadingFeedView.swift │ │ │ │ │ └── FeedInfoStackView.swift │ │ │ ├── MyPage │ │ │ │ ├── Enum │ │ │ │ │ └── SettingSection.swift │ │ │ │ ├── Edit │ │ │ │ │ └── Enum │ │ │ │ │ │ └── MyPageEditValidation.swift │ │ │ │ └── OpenSource │ │ │ │ │ └── ViewController │ │ │ │ │ └── OpenSourceLicenseViewController.swift │ │ │ ├── ScrapPage │ │ │ │ └── View │ │ │ │ │ └── ScrapPageView.swift │ │ │ └── Home │ │ │ │ └── ViewController │ │ │ │ └── DataSource │ │ │ │ └── HomeFeedListSkeletonDiffableDatasource.swift │ │ ├── Coordinator │ │ │ ├── TabBar │ │ │ │ └── TabBarPage.swift │ │ │ ├── Base │ │ │ │ ├── Coordinator.swift │ │ │ │ ├── GithubLogInCoordinator.swift │ │ │ │ └── CoordinatorEvent.swift │ │ │ ├── ScrapPage │ │ │ │ └── ScrapPageCoordinator.swift │ │ │ └── Home │ │ │ │ └── HomeCoordinator.swift │ │ ├── Factory │ │ │ └── DependencyFactoryProtocol.swift │ │ └── Auth │ │ │ ├── SignUp │ │ │ └── CamperID │ │ │ │ └── SignUpCamperIDViewModel.swift │ │ │ └── LogIn │ │ │ └── ViewModel │ │ │ └── LogInViewModel.swift │ ├── Resource │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── AppIcon.png │ │ │ │ └── Contents.json │ │ │ ├── LaunchScreen.imageset │ │ │ │ ├── LaunchScreen_Dark 1.png │ │ │ │ ├── LaunchScreen_Dark.png │ │ │ │ ├── LaunchScreen_Dark@2x.png │ │ │ │ ├── LaunchScreen_Dark@3x.png │ │ │ │ ├── LaunchScreen_Dark@2x 1.png │ │ │ │ ├── LaunchScreen_Dark@3x 1.png │ │ │ │ └── Contents.json │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ └── LaunchScreenBackground.colorset │ │ │ │ └── Contents.json │ │ └── gif │ │ │ └── LoadingBurstcamper.gif │ ├── Util │ │ ├── Factory │ │ │ └── Factory.swift │ │ ├── Error │ │ │ ├── Service │ │ │ │ ├── UserManagerError.swift │ │ │ │ └── URLSessionServiceError.swift │ │ │ ├── UseCase │ │ │ │ ├── LoginUseCaseError.swift │ │ │ │ ├── MyPageUseCaseError.swift │ │ │ │ └── SignUpUseCaseError.swift │ │ │ ├── ViewModel │ │ │ │ ├── LoginViewModelError.swift │ │ │ │ ├── SignUpBlogViewModelError.swift │ │ │ │ ├── FeedDetailViewModelError.swift │ │ │ │ ├── ScrapViewModelError.swift │ │ │ │ └── HomeViewModelError.swift │ │ │ ├── Repository │ │ │ │ ├── FeedRepository │ │ │ │ │ ├── MockUpFeedRepositoryError.swift │ │ │ │ │ └── FeedRepositoryError.swift │ │ │ │ ├── UserRepository │ │ │ │ │ └── UserRepositoryError.swift │ │ │ │ └── NotificationRepository │ │ │ │ │ └── NotificationRepositoryError.swift │ │ │ ├── DataSource │ │ │ │ └── GithubLoginDataSourceError.swift │ │ │ ├── ConvertError.swift │ │ │ ├── ImageCacheError.swift │ │ │ ├── GithubError.swift │ │ │ └── NetworkError.swift │ │ ├── Protocol │ │ │ ├── ContainScrollViewController.swift │ │ │ ├── ReusableView.swift │ │ │ ├── ContainFeedDetailViewController.swift │ │ │ └── ContainCollectionView.swift │ │ ├── Extension │ │ │ ├── UI │ │ │ │ ├── View │ │ │ │ │ ├── UI+ReusableView.swift │ │ │ │ │ ├── UIView+addSubViews.swift │ │ │ │ │ ├── UIScrollView+.swift │ │ │ │ │ ├── UIStackView+addArrangedSubviews.swift │ │ │ │ │ ├── UIImageView+Cache.swift │ │ │ │ │ ├── UILabel+LineHeight.swift │ │ │ │ │ └── UICollectionView+emptyView.swift │ │ │ │ └── ViewController │ │ │ │ │ └── UINavigationController+configure.swift │ │ │ ├── Type │ │ │ │ ├── TypeConversion.swift │ │ │ │ ├── Notification+.swift │ │ │ │ ├── Encodable+dictionary.swift │ │ │ │ ├── TimeInterval+.swift │ │ │ │ └── Date+FormatString.swift │ │ │ ├── AsyncCompatible │ │ │ │ ├── Sequence+Async.swift │ │ │ │ ├── Future+Async.swift │ │ │ │ └── Publisher+Async.swift │ │ │ └── Combine │ │ │ │ └── Publisher+Extension.swift │ │ ├── Constant │ │ │ ├── Alert.swift │ │ │ └── UserDefaultsKey.swift │ │ ├── Validator │ │ │ └── Validator.swift │ │ └── BCDateFormatter.swift │ ├── Service │ │ ├── Test │ │ │ └── TestCounter.swift │ │ ├── UserDefaultsService │ │ │ ├── UserDefaultsService.swift │ │ │ └── DefaultUserDefaultsService.swift │ │ ├── GithubAPIKey │ │ │ └── GithubAPIKeyManager.swift │ │ └── Singleton │ │ │ ├── DarkmodeManager.swift │ │ │ ├── KeyChainManager.swift │ │ │ ├── UserManager.swift │ │ │ └── UserDefaultsManager.swift │ ├── burstcamp.entitlements │ ├── Data │ │ ├── Repositories │ │ │ ├── BlogRepository │ │ │ │ └── DefaultBlogRepository.swift │ │ │ ├── ImageRepository │ │ │ │ └── DefaultImageRepository.swift │ │ │ └── NotificationRepository │ │ │ │ └── DefaultNotificationRepository.swift │ │ ├── Network │ │ │ └── GithubLogin │ │ │ │ └── GithubLoginModel.swift │ │ └── Local │ │ │ └── FeedMockUpDatasource.swift │ └── App │ │ └── Info.plist ├── .swiftlint.auto.yml ├── burstcamp.xcodeproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── .swiftlint.yml ├── Firebase-Functions ├── functions │ ├── .gitignore │ ├── service │ │ ├── test │ │ │ ├── mockUpHTMLData │ │ │ │ └── mockUpHTMLData_Algorithm.js │ │ │ ├── testRSSParsing.js │ │ │ ├── mockUpService.js │ │ │ └── testAlgorithmFeed.js │ │ └── withdrawalManager.js │ ├── .eslintrc.js │ ├── model.js │ ├── package.json │ └── util.js ├── .firebaserc ├── firebase.json └── .gitignore ├── modules ├── BCResource │ ├── BCResource │ │ ├── Resource │ │ │ ├── Color.xcassets │ │ │ │ ├── Contents.json │ │ │ │ ├── brown.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── main.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── white.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── brightGreen.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── brightOrange.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── brightYellow.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── red.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── background.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── black.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── blue.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── green.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── indigo.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── orange.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── pink.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── purple.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── teal.colorset │ │ │ │ │ └── Contents.json │ │ │ │ └── yellow.colorset │ │ │ │ │ └── Contents.json │ │ │ ├── Image.xcassets │ │ │ │ ├── Contents.json │ │ │ │ ├── Github.imageset │ │ │ │ │ ├── Github.png │ │ │ │ │ ├── Github@2x.png │ │ │ │ │ ├── Github@3x.png │ │ │ │ │ ├── Github_dark.png │ │ │ │ │ ├── Github_dark@2x.png │ │ │ │ │ ├── Github_dark@3x.png │ │ │ │ │ └── Contents.json │ │ │ │ ├── burstcamper100.imageset │ │ │ │ │ ├── burstcamper100.png │ │ │ │ │ ├── burstcamper100@2x.png │ │ │ │ │ ├── burstcamper100@3x.png │ │ │ │ │ ├── burstcamper100_Dark.png │ │ │ │ │ ├── burstcamper100_Dark@2x.png │ │ │ │ │ ├── burstcamper100_Dark@3x.png │ │ │ │ │ └── Contents.json │ │ │ │ └── burstcamperStun100.imageset │ │ │ │ │ ├── burstcamperStun100.png │ │ │ │ │ ├── burstcamperStun100@2x.png │ │ │ │ │ ├── burstcamperStun100@3x.png │ │ │ │ │ ├── burstcamperStunDark100.png │ │ │ │ │ ├── burstcamperStunDark100@2x.png │ │ │ │ │ ├── burstcamperStunDark100@3x.png │ │ │ │ │ └── Contents.json │ │ │ └── Fonts │ │ │ │ ├── NanumSquareB.otf │ │ │ │ ├── NanumSquareEB.otf │ │ │ │ └── NanumSquareR.otf │ │ ├── README_resource │ │ │ ├── Structure.png │ │ │ └── Asset_Generated.png │ │ ├── ImageSet.swift │ │ ├── BCResource.h │ │ ├── ColorSet.swift │ │ ├── README.md │ │ └── FontSet.swift │ ├── BCResource.xcodeproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── swiftgen.yml ├── BCFirebase │ ├── BCFirebase.xcodeproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── BCFirebase │ │ ├── Model │ │ └── FCMToken.swift │ │ ├── FireFunction │ │ └── FirebaseFunctionError.swift │ │ ├── App │ │ └── BCFirebaseApp.swift │ │ ├── BCFirebase.docc │ │ └── BCFirebase.md │ │ ├── Firestore │ │ ├── FirestoreServiceError.swift │ │ ├── FirestoreCollection.swift │ │ └── BCFirestoreUserListener.swift │ │ ├── Util │ │ └── Encodable+.swift │ │ ├── BCFirebase.h │ │ ├── FireStorage │ │ ├── FireStorageError.swift │ │ └── BCFireStorageService.swift │ │ ├── Auth │ │ └── FirebaseAuthError.swift │ │ └── Messaging │ │ └── BCFirebaseMessaging.swift ├── Manager │ └── RealmManager │ │ └── RealmManager │ │ ├── README.md │ │ ├── Model │ │ ├── AutoIncrementable.swift │ │ ├── QuerySupportable.swift │ │ ├── SortSupportable.swift │ │ └── RealmCompatible.swift │ │ ├── SortingPolicy.swift │ │ ├── RealmManager.h │ │ ├── FetchedResults.swift │ │ └── WriteTransaction.swift └── BCFetcher │ ├── BCFetcher │ ├── FetchingState.swift │ ├── BCFetcher.h │ └── Fetchable.swift │ └── Extension │ └── AnyCancellable+store.swift └── .github ├── ISSUE_TEMPLATE └── ✅-feature-template.md ├── PULL_REQUEST_TEMPLATE.md └── workflows └── build.yml /burstcamp/Modules/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Domain/Model/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Firebase-Functions/functions/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /burstcamp/burstcamp/Presentation/Common/View/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Firebase-Functions/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "eoljuga-9b868" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Firebase-Functions/functions/service/test/mockUpHTMLData/mockUpHTMLData_Algorithm.js: -------------------------------------------------------------------------------- 1 | export const mockUpHTMLData_Algorithm = ` 2 | 3 | ` -------------------------------------------------------------------------------- /burstcamp/burstcamp/Resource/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Color.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Image.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Resource/gif/LoadingBurstcamper.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS09-burstcamp/HEAD/burstcamp/burstcamp/Resource/gif/LoadingBurstcamper.gif -------------------------------------------------------------------------------- /modules/BCResource/BCResource/README_resource/Structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS09-burstcamp/HEAD/modules/BCResource/BCResource/README_resource/Structure.png -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Fonts/NanumSquareB.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS09-burstcamp/HEAD/modules/BCResource/BCResource/Resource/Fonts/NanumSquareB.otf -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Fonts/NanumSquareEB.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS09-burstcamp/HEAD/modules/BCResource/BCResource/Resource/Fonts/NanumSquareEB.otf -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Fonts/NanumSquareR.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS09-burstcamp/HEAD/modules/BCResource/BCResource/Resource/Fonts/NanumSquareR.otf -------------------------------------------------------------------------------- /modules/BCResource/BCResource/README_resource/Asset_Generated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS09-burstcamp/HEAD/modules/BCResource/BCResource/README_resource/Asset_Generated.png -------------------------------------------------------------------------------- /burstcamp/.swiftlint.auto.yml: -------------------------------------------------------------------------------- 1 | whitelist_rules: 2 | - leading_whitespace 3 | - trailing_whitespace 4 | - vertical_whitespace 5 | - vertical_whitespace_closing_braces 6 | - trailing_newline 7 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Resource/Assets.xcassets/AppIcon.appiconset/AppIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS09-burstcamp/HEAD/burstcamp/burstcamp/Resource/Assets.xcassets/AppIcon.appiconset/AppIcon.png -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Factory/Factory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Factory.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2022/12/06. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol Factory { 11 | } 12 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Image.xcassets/Github.imageset/Github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS09-burstcamp/HEAD/modules/BCResource/BCResource/Resource/Image.xcassets/Github.imageset/Github.png -------------------------------------------------------------------------------- /burstcamp/burstcamp.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Image.xcassets/Github.imageset/Github@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS09-burstcamp/HEAD/modules/BCResource/BCResource/Resource/Image.xcassets/Github.imageset/Github@2x.png -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Image.xcassets/Github.imageset/Github@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS09-burstcamp/HEAD/modules/BCResource/BCResource/Resource/Image.xcassets/Github.imageset/Github@3x.png -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Image.xcassets/Github.imageset/Github_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS09-burstcamp/HEAD/modules/BCResource/BCResource/Resource/Image.xcassets/Github.imageset/Github_dark.png -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/✅-feature-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "✅ Feature Template" 3 | about: Feature 작업 사항을 입력해주세요. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## 할 일 11 | 12 | - [ ] 작업사항 13 | - [ ] 작업사항 14 | 15 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Resource/Assets.xcassets/LaunchScreen.imageset/LaunchScreen_Dark 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS09-burstcamp/HEAD/burstcamp/burstcamp/Resource/Assets.xcassets/LaunchScreen.imageset/LaunchScreen_Dark 1.png -------------------------------------------------------------------------------- /burstcamp/burstcamp/Resource/Assets.xcassets/LaunchScreen.imageset/LaunchScreen_Dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS09-burstcamp/HEAD/burstcamp/burstcamp/Resource/Assets.xcassets/LaunchScreen.imageset/LaunchScreen_Dark.png -------------------------------------------------------------------------------- /modules/BCFirebase/BCFirebase.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Image.xcassets/Github.imageset/Github_dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS09-burstcamp/HEAD/modules/BCResource/BCResource/Resource/Image.xcassets/Github.imageset/Github_dark@2x.png -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Image.xcassets/Github.imageset/Github_dark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS09-burstcamp/HEAD/modules/BCResource/BCResource/Resource/Image.xcassets/Github.imageset/Github_dark@3x.png -------------------------------------------------------------------------------- /burstcamp/burstcamp/Resource/Assets.xcassets/LaunchScreen.imageset/LaunchScreen_Dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS09-burstcamp/HEAD/burstcamp/burstcamp/Resource/Assets.xcassets/LaunchScreen.imageset/LaunchScreen_Dark@2x.png -------------------------------------------------------------------------------- /burstcamp/burstcamp/Resource/Assets.xcassets/LaunchScreen.imageset/LaunchScreen_Dark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS09-burstcamp/HEAD/burstcamp/burstcamp/Resource/Assets.xcassets/LaunchScreen.imageset/LaunchScreen_Dark@3x.png -------------------------------------------------------------------------------- /burstcamp/burstcamp/Resource/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Resource/Assets.xcassets/LaunchScreen.imageset/LaunchScreen_Dark@2x 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS09-burstcamp/HEAD/burstcamp/burstcamp/Resource/Assets.xcassets/LaunchScreen.imageset/LaunchScreen_Dark@2x 1.png -------------------------------------------------------------------------------- /burstcamp/burstcamp/Resource/Assets.xcassets/LaunchScreen.imageset/LaunchScreen_Dark@3x 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS09-burstcamp/HEAD/burstcamp/burstcamp/Resource/Assets.xcassets/LaunchScreen.imageset/LaunchScreen_Dark@3x 1.png -------------------------------------------------------------------------------- /modules/BCFirebase/BCFirebase/Model/FCMToken.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FCMToken.swift 3 | // burstcamp 4 | // 5 | // Created by neuli on 2022/11/29. 6 | // 7 | 8 | import Foundation 9 | 10 | struct FCMToken: Codable { 11 | let fcmToken: String 12 | } 13 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Image.xcassets/burstcamper100.imageset/burstcamper100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS09-burstcamp/HEAD/modules/BCResource/BCResource/Resource/Image.xcassets/burstcamper100.imageset/burstcamper100.png -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Image.xcassets/burstcamper100.imageset/burstcamper100@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS09-burstcamp/HEAD/modules/BCResource/BCResource/Resource/Image.xcassets/burstcamper100.imageset/burstcamper100@2x.png -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Image.xcassets/burstcamper100.imageset/burstcamper100@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS09-burstcamp/HEAD/modules/BCResource/BCResource/Resource/Image.xcassets/burstcamper100.imageset/burstcamper100@3x.png -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Image.xcassets/burstcamper100.imageset/burstcamper100_Dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS09-burstcamp/HEAD/modules/BCResource/BCResource/Resource/Image.xcassets/burstcamper100.imageset/burstcamper100_Dark.png -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Image.xcassets/burstcamper100.imageset/burstcamper100_Dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS09-burstcamp/HEAD/modules/BCResource/BCResource/Resource/Image.xcassets/burstcamper100.imageset/burstcamper100_Dark@2x.png -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Image.xcassets/burstcamper100.imageset/burstcamper100_Dark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS09-burstcamp/HEAD/modules/BCResource/BCResource/Resource/Image.xcassets/burstcamper100.imageset/burstcamper100_Dark@3x.png -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Image.xcassets/burstcamperStun100.imageset/burstcamperStun100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS09-burstcamp/HEAD/modules/BCResource/BCResource/Resource/Image.xcassets/burstcamperStun100.imageset/burstcamperStun100.png -------------------------------------------------------------------------------- /modules/Manager/RealmManager/RealmManager/README.md: -------------------------------------------------------------------------------- 1 | # RealmManager 2 | 3 | RealmManager는 RealmSwift를 쉽게 사용할 수 있도록 래핑한 모듈입니다. 4 | 5 | RealmManager는 [Using Realm with Value Types](https://medium.com/@gonzalezreal/using-realm-with-value-types-b69947741e8b)를 기반으로 만들어졌습니다. 6 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Image.xcassets/burstcamperStun100.imageset/burstcamperStun100@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS09-burstcamp/HEAD/modules/BCResource/BCResource/Resource/Image.xcassets/burstcamperStun100.imageset/burstcamperStun100@2x.png -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Image.xcassets/burstcamperStun100.imageset/burstcamperStun100@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS09-burstcamp/HEAD/modules/BCResource/BCResource/Resource/Image.xcassets/burstcamperStun100.imageset/burstcamperStun100@3x.png -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Error/Service/UserManagerError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserManagerError.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/17. 6 | // 7 | 8 | import Foundation 9 | 10 | enum UserManagerError: Error { 11 | case weakSelfIsNil 12 | } 13 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Error/UseCase/LoginUseCaseError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginUseCaseError.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/26. 6 | // 7 | 8 | import Foundation 9 | 10 | enum LoginUseCaseError: Error { 11 | case fetchUser 12 | } 13 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Error/UseCase/MyPageUseCaseError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyPageUseCaseError.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/16. 6 | // 7 | 8 | import Foundation 9 | 10 | enum MyPageUseCaseError: Error { 11 | case withdrawal 12 | } 13 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Error/UseCase/SignUpUseCaseError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignUpUseCaseError.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/15. 6 | // 7 | 8 | import Foundation 9 | 10 | enum SignUpUseCaseError: Error { 11 | case createUser 12 | } 13 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Error/ViewModel/LoginViewModelError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginViewModelError.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/16. 6 | // 7 | 8 | import Foundation 9 | 10 | enum LoginViewModelError: Error { 11 | case login 12 | } 13 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Image.xcassets/burstcamperStun100.imageset/burstcamperStunDark100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS09-burstcamp/HEAD/modules/BCResource/BCResource/Resource/Image.xcassets/burstcamperStun100.imageset/burstcamperStunDark100.png -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Image.xcassets/burstcamperStun100.imageset/burstcamperStunDark100@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS09-burstcamp/HEAD/modules/BCResource/BCResource/Resource/Image.xcassets/burstcamperStun100.imageset/burstcamperStunDark100@2x.png -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Image.xcassets/burstcamperStun100.imageset/burstcamperStunDark100@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS09-burstcamp/HEAD/modules/BCResource/BCResource/Resource/Image.xcassets/burstcamperStun100.imageset/burstcamperStunDark100@3x.png -------------------------------------------------------------------------------- /burstcamp/burstcamp/Domain/Model/User/ScrapUser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScrapUser.swift 3 | // burstcamp 4 | // 5 | // Created by neuli on 2022/11/29. 6 | // 7 | 8 | import Foundation 9 | 10 | struct ScrapUser: Codable { 11 | let userUUID: String 12 | let scrapDate: Date 13 | } 14 | -------------------------------------------------------------------------------- /Firebase-Functions/functions/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | es6: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | "eslint:recommended", 9 | "google", 10 | ], 11 | rules: { 12 | quotes: ["error", "double"], 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Domain/Model/Feed/DiffableFeed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiffableFeed.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/24. 6 | // 7 | 8 | import Foundation 9 | 10 | enum DiffableFeed: Hashable { 11 | case normal(Feed) 12 | case recommend(Feed) 13 | } 14 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Presentation/Tab/Detail/ViewModel/ActionSheetEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActionSheetEvent.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/02/08. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ActionSheetEvent { 11 | case report 12 | case block 13 | } 14 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Protocol/ContainScrollViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContainScrollViewController.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/02/05. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol ContainScrollViewController { 11 | func scrollToTop() 12 | } 13 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Domain/Model/Network/HTTPMethod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPMethod.swift 3 | // FireStoreTest 4 | // 5 | // Created by neuli on 2022/11/20. 6 | // 7 | 8 | import Foundation 9 | 10 | enum HttpMethod: String { 11 | case GET 12 | case POST 13 | case PATCH 14 | case DELETE 15 | } 16 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Error/Service/URLSessionServiceError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLSessionServiceError.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/16. 6 | // 7 | 8 | import Foundation 9 | 10 | enum URLSessionServiceError: Error { 11 | case makeRequest 12 | case responseCode 13 | } 14 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Error/Repository/FeedRepository/MockUpFeedRepositoryError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockUpFeedRepositoryError.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/27. 6 | // 7 | 8 | import Foundation 9 | 10 | enum MockUpFeedRepositoryError: Error { 11 | case noImplementation 12 | } 13 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Error/ViewModel/SignUpBlogViewModelError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignUpViewModelError.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/16. 6 | // 7 | 8 | import Foundation 9 | 10 | enum SignUpBlogViewModelError: Error { 11 | case createUser 12 | case getBlogTitle 13 | } 14 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Error/DataSource/GithubLoginDataSourceError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GithubLoginDataSourceError.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/16. 6 | // 7 | 8 | import Foundation 9 | 10 | enum GithubLoginDataSourceError: Error { 11 | case noAPIKey 12 | case bodyEncode 13 | } 14 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Extension/UI/View/UI+ReusableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UICollectionReusableView.swift 3 | // Eoljuga 4 | // 5 | // Created by youtak on 2022/11/20. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UICollectionViewCell: ReusableView {} 11 | 12 | extension UICollectionReusableView: ReusableView {} 13 | -------------------------------------------------------------------------------- /modules/BCFirebase/BCFirebase/FireFunction/FirebaseFunctionError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FirebaseFunctionError.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/16. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum FirebaseFunctionError: Error { 11 | case getBlogTitle 12 | case deleteUser 13 | } 14 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Error/Repository/UserRepository/UserRepositoryError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserRepositoryError.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/26. 6 | // 7 | 8 | import Foundation 9 | 10 | enum UserRepositoryError: Error { 11 | case userNotExist 12 | case createGuestID 13 | } 14 | -------------------------------------------------------------------------------- /modules/BCFirebase/BCFirebase/App/BCFirebaseApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BCFirebaseApp.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/02/03. 6 | // 7 | 8 | import Firebase 9 | 10 | public final class BCFirebaseApp { 11 | 12 | public static func startApp() { 13 | FirebaseApp.configure() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## 관련 이슈 2 | 6 | 7 | ## 내용 8 | 12 | 13 | ## 리뷰어가 확인할 사항 14 | 18 | 19 | ## 기타 20 | 23 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Error/Repository/NotificationRepository/NotificationRepositoryError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationRepositoryError.swift 3 | // burstcamp 4 | // 5 | // Created by neuli on 2023/01/31. 6 | // 7 | 8 | import Foundation 9 | 10 | enum NotificationRepositoryError: Error { 11 | case failedToSaveFCMToken 12 | } 13 | -------------------------------------------------------------------------------- /burstcamp/burstcamp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Resource/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Extension/UI/View/UIView+addSubViews.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+addSubViews.swift 3 | // Eoljuga 4 | // 5 | // Created by youtak on 2022/11/17. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIView { 11 | 12 | func addSubViews(_ subViews: [UIView]) { 13 | subViews.forEach { self.addSubview($0) } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /modules/BCFirebase/BCFirebase.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Domain/Model/Network/GithubAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GithubAPI.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/15. 6 | // 7 | 8 | import Foundation 9 | 10 | struct APIKey: Codable { 11 | let github: Github 12 | } 13 | 14 | struct Github: Codable { 15 | let clientID: String 16 | let clientSecret: String 17 | } 18 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Domain/Interfaces/BlogRepository/BlogRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlogRepository.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/15. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol BlogRepository { 11 | func checkBlogTitle(link: String) async throws -> String 12 | func isValidateLink(_ link: String) -> Bool 13 | } 14 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Service/Test/TestCounter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestCounter.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/27. 6 | // 7 | 8 | import Foundation 9 | 10 | struct TestCounter { 11 | static var count = 0 12 | 13 | static func up() { 14 | count += 1 15 | print("Constraints 업데이트 수 : ", count) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /modules/BCFetcher/BCFetcher/FetchingState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FetchingState.swift 3 | // BCFetcher 4 | // 5 | // Created by SEUNGMIN OH on 2022/12/08. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum Status { 11 | case loading 12 | case failure(_ error: FetchingError) 13 | case success 14 | // case alreadyLatest 15 | } 16 | -------------------------------------------------------------------------------- /modules/Manager/RealmManager/RealmManager/Model/AutoIncrementable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AutoIncrementable.swift 3 | // RealmManager 4 | // 5 | // Created by SEUNGMIN OH on 2022/12/13. 6 | // 7 | 8 | import class RealmSwift.Object 9 | 10 | /// 자동증가를 지원하기 위한 프로토콜 11 | public protocol AutoIncrementable: RealmSwift.Object { 12 | var autoIndex: Int { get set } 13 | } 14 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Extension/Type/TypeConversion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TypeConversion.swift 3 | // Eoljuga 4 | // 5 | // Created by youtak on 2022/11/18. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Int { 11 | var cgFloat: CGFloat { 12 | return CGFloat(self) 13 | } 14 | 15 | var float: Float { 16 | return Float(self) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /modules/BCFirebase/BCFirebase/BCFirebase.docc/BCFirebase.md: -------------------------------------------------------------------------------- 1 | # ``BCFirebase`` 2 | 3 | Summary 4 | 5 | ## Overview 6 | 7 | Text 8 | 9 | ## Topics 10 | 11 | ### Group 12 | 13 | - ``Symbol`` -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Constant/Alert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Alert.swift 3 | // burstcamp 4 | // 5 | // Created by neuli on 2022/12/03. 6 | // 7 | 8 | import Foundation 9 | 10 | enum Alert { 11 | static let yes = "네" 12 | static let no = "아니오" 13 | static let withdrawalTitleMessage = "정말 탈퇴하시겠어요?" 14 | static let withdrawalMessage = "탈퇴시 모아둔 스크랩 정보가 모두 사라져요." 15 | } 16 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Extension/Type/Notification+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Notification+Name.swift 3 | // burstcamp 4 | // 5 | // Created by neuli on 2022/11/29. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Notification.Name { 11 | static let Push = Notification.Name(rawValue: "push") 12 | } 13 | 14 | enum NotificationKey { 15 | static let feedUUID = "feedUUID" 16 | } 17 | -------------------------------------------------------------------------------- /Firebase-Functions/functions/model.js: -------------------------------------------------------------------------------- 1 | /* 2 | { 3 | status: String 4 | feed: { 5 | url: 블로그 rss 주소 6 | title: 블로그 이름 7 | link: 블로그 주소 8 | author: 9 | description: 블로그 소개 10 | image: 11 | } 12 | items: [{ 13 | title: 글 제목 14 | pubDate: 글 작성 시간 15 | link: 글 주소 16 | author: 작성자 이름 17 | thumbnail: 썸네일 주소 18 | description: 글 19 | }] 20 | } 21 | */ -------------------------------------------------------------------------------- /Firebase-Functions/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": [ 3 | { 4 | "source": "functions", 5 | "codebase": "default", 6 | "ignore": [ 7 | "node_modules", 8 | ".git", 9 | "firebase-debug.log", 10 | "firebase-debug.*.log" 11 | ], 12 | "predeploy": [ 13 | "npm --prefix \"$RESOURCE_DIR\" run lint" 14 | ] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Domain/Interfaces/ImageRepository/ImageRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageRepository.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/19. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol ImageRepository { 11 | func saveProfileImage(imageData: Data, userUUID: String) async throws -> String 12 | func deleteProfileImage(userUUID: String) async throws 13 | } 14 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Protocol/ReusableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReusableView.swift 3 | // Eoljuga 4 | // 5 | // Created by youtak on 2022/11/17. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol ReusableView { 11 | static var identifier: String { get } 12 | } 13 | 14 | extension ReusableView { 15 | static var identifier: String { 16 | return String(describing: self) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Error/Repository/FeedRepository/FeedRepositoryError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FeedRepositoryError.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/31. 6 | // 7 | 8 | import Foundation 9 | 10 | enum FeedRepositoryError: Error { 11 | case fetchRecentHomeFeedList 12 | case fetchMoreNormalFeed 13 | case fetchRecentScrapFeed 14 | case fetchMoreScrapFeed 15 | } 16 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource/ImageSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageSet.swift 3 | // BCResource 4 | // 5 | // Created by SEUNGMIN OH on 2022/12/07. 6 | // 7 | 8 | import UIKit 9 | 10 | public extension UIImage { 11 | static let burstcamper = Assets.Image.burstcamper100.image 12 | static let burstcamperStun = Assets.Image.burstcamperStun100.image 13 | static let github = Assets.Image.github.image 14 | } 15 | -------------------------------------------------------------------------------- /modules/Manager/RealmManager/RealmManager/Model/QuerySupportable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuerySupportable.swift 3 | // RealmManager 4 | // 5 | // Created by SEUNGMIN OH on 2022/12/13. 6 | // 7 | 8 | public protocol QuerySupportable: RealmCompatible { 9 | associatedtype Query: QueryType 10 | } 11 | 12 | /// Query를 사용할 수 있도록 도와주는 protocol 13 | public protocol QueryType { 14 | var predicate: NSPredicate? { get } 15 | } 16 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/burstcamp.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | com.apple.developer.applesignin 8 | 9 | Default 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Domain/UseCase/Tab/ScrapPage/ScrapPageUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScrapPageUseCase.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/14. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol ScrapPageUseCase { 11 | func fetchRecentScrapFeed() async throws -> [Feed] 12 | func fetchMoreScrapFeed() async throws -> [Feed] 13 | func scrapFeed(_ feed: Feed, userUUID: String) async throws -> Feed 14 | } 15 | -------------------------------------------------------------------------------- /modules/BCFetcher/Extension/AnyCancellable+store.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyCancellable + store.swift 3 | // BCFetcher 4 | // 5 | // Created by SEUNGMIN OH on 2022/12/11. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | public extension Set where Element == AnyCancellable { 12 | func store(in set: inout Self) { 13 | self.forEach { cancellable in 14 | cancellable.store(in: &set) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Presentation/Tab/MyPage/Enum/SettingSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingSection.swift 3 | // Eoljuga 4 | // 5 | // Created by neuli on 2022/11/22. 6 | // 7 | 8 | import Foundation 9 | 10 | enum SettingSection: CaseIterable { 11 | case setting 12 | case appInfo 13 | 14 | var index: Int { 15 | switch self { 16 | case .setting: return 0 17 | case .appInfo: return 1 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Protocol/ContainFeedDetailViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContainFeedDetailViewController.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/21. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | protocol ContainFeedDetailViewController { 12 | func configure( 13 | scrapUpdatePublisher: AnyPublisher, 14 | deletePublisher: AnyPublisher 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Constant/UserDefaultsKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultsKey.swift 3 | // burstcamp 4 | // 5 | // Created by neuli on 2023/01/31. 6 | // 7 | 8 | import Foundation 9 | 10 | enum UserDefaultsKey { 11 | static let appearanceKey = "AppearanceKey" 12 | static let fcmTokenKey = "fcmTokenKey" 13 | static let isForegroundKey = "isForegroundKey" 14 | static let notificationFeedUUIDKey = "notificationFeedUUIDKey" 15 | } 16 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Domain/Model/CellIndexPath.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CellIndexPath.swift 3 | // Eoljuga 4 | // 5 | // Created by neuli on 2022/11/23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct CellIndexPath: Equatable { 11 | let indexPath: (Int, Int) 12 | 13 | static func == (lhs: CellIndexPath, rhs: CellIndexPath) -> Bool { 14 | return lhs.indexPath.0 == rhs.indexPath.0 && 15 | lhs.indexPath.1 == rhs.indexPath.1 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Extension/UI/ViewController/UINavigationController+configure.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UINavigationController+configure.swift 3 | // Eoljuga 4 | // 5 | // Created by youtak on 2022/11/21. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UINavigationController { 11 | 12 | convenience init(backgroundColor: UIColor = .background) { 13 | self.init() 14 | self.navigationBar.backgroundColor = backgroundColor 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Extension/UI/View/UIScrollView+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIScrollView+.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2022/11/30. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIScrollView { 11 | 12 | func isOverTarget(ratio: CGFloat = 0.8) -> Bool { 13 | let offset = contentOffset.y 14 | let targetOffset = (contentSize.height - frame.height) * ratio 15 | return offset > targetOffset 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Service/UserDefaultsService/UserDefaultsService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultsService.swift 3 | // burstcamp 4 | // 5 | // Created by neuli on 2023/01/31. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol UserDefaultsService { 11 | func save(value: T, forKey key: String) 12 | func value(valueType: T.Type, forKey key: String) -> T? 13 | func stringValue(forKey key: String) -> String? 14 | func delete(forKey key: String) 15 | } 16 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Domain/Model/User/Login/LoginProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginProviderID.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/30. 6 | // 7 | 8 | import Foundation 9 | 10 | enum LoginProvider { 11 | case github 12 | case apple 13 | } 14 | 15 | extension LoginProvider { 16 | var id: String { 17 | switch self { 18 | case .github: return "github.com" 19 | case .apple: return "apple.com" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Domain/UseCase/Notification/NotificationUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationUseCase.swift 3 | // burstcamp 4 | // 5 | // Created by neuli on 2023/01/31. 6 | // 7 | 8 | import UserNotifications 9 | import Foundation 10 | 11 | protocol NotificationUseCase { 12 | func didReceiveNotification(response: UNNotificationResponse) 13 | func saveIfDifferentFromTheStoredToken(fcmToken: String?) async throws 14 | func refresh(fcmToken: String?) async throws 15 | } 16 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Presentation/Tab/MyPage/Edit/Enum/MyPageEditValidation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ValidationResult.swift 3 | // burstcamp 4 | // 5 | // Created by neuli on 2022/11/23. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - 닉네임 유효성 11 | enum MyPageEditNicknameValidation { 12 | case success 13 | case regexError 14 | case duplicateError 15 | } 16 | 17 | // MARK: - 최종 유효성 18 | 19 | enum MyPageEditBlogValidation { 20 | case success 21 | case regexError 22 | } 23 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Domain/UseCase/Tab/Home/HomeUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeUseCase.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/03. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol HomeUseCase { 11 | func fetchRecentHomeFeedList() async throws -> HomeFeedList 12 | func fetchMoreNormalFeed() async throws -> [Feed] 13 | func scrapFeed(_ feed: Feed, userUUID: String) async throws -> Feed 14 | func updateUserPushState(to pushState: Bool) async throws 15 | } 16 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Error/ViewModel/FeedDetailViewModelError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FeedDetailViewModel.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/21. 6 | // 7 | 8 | import Foundation 9 | 10 | enum FeedDetailViewModelError: LocalizedError { 11 | case feedIsNil 12 | } 13 | 14 | extension FeedDetailViewModelError { 15 | var errorDescription: String? { 16 | switch self { 17 | case .feedIsNil: return "해당하는 Feed가 없습니다." 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Error/ViewModel/ScrapViewModelError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScrapViewModelError.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/23. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ScrapPageViewModelError: LocalizedError { 11 | case scrapFeed 12 | } 13 | 14 | extension ScrapPageViewModelError { 15 | var errorDescription: String? { 16 | switch self { 17 | case .scrapFeed: return "피드를 스크랩 하는 중 오류가 발생했습니다." 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Domain/UseCase/Tab/Detail/FeedDetailUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FeedDetailUseCase.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/14. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol FeedDetailUseCase { 11 | func fetchFeed(by feedUUID: String) async throws -> Feed 12 | 13 | func scrapFeed(_ feed: Feed, userUUID: String) async throws -> Feed 14 | 15 | func blockFeed(_ feed: Feed) async throws 16 | func reportFeed(_ feed: Feed) async throws 17 | } 18 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Domain/UseCase/Tab/MyPage/MyPageEdit/MyPageEditUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyPageEditUseCase.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/14. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol MyPageEditUseCase { 11 | func isValidNickname(_ nickname: String) async throws -> MyPageEditNicknameValidation 12 | func isValidBlogURL(_ blogURL: String) -> MyPageEditBlogValidation 13 | 14 | func updateUser(user: User, imageData: Data?) async throws 15 | } 16 | -------------------------------------------------------------------------------- /modules/BCFirebase/BCFirebase/Firestore/FirestoreServiceError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FirestoreServiceError.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/15. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum FirestoreServiceError: Error, Equatable { 11 | case getCollection 12 | case getDocument 13 | case lastCollection 14 | case addListenerFail 15 | case errorCastingFail(message: String) 16 | case batch 17 | case lastFetch 18 | case userListener 19 | case scrapIsEmpty 20 | } 21 | -------------------------------------------------------------------------------- /Firebase-Functions/functions/service/test/testRSSParsing.js: -------------------------------------------------------------------------------- 1 | import { logger } from "firebase-functions/v1"; 2 | import { fetchContent, fetchParsedRSS } from "../feedAPI.js"; 3 | 4 | 5 | export async function testYouTakBlog() { 6 | const blogURL = "https://malchafrappuccino.tistory.com/" 7 | await testUpdateRSS(blogURL) 8 | } 9 | 10 | async function testUpdateRSS(blogURL) { 11 | const feedInfo = await fetchContent("https://malchafrappuccino.tistory.com/148") 12 | logger.log(feedInfo.content) 13 | logger.log(feedInfo.thumbnailURL) 14 | } 15 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Extension/AsyncCompatible/Sequence+Async.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Sequence+Async.swift 3 | // burstcamp 4 | // 5 | // Created by SEUNGMIN OH on 2022/12/11. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Sequence { 11 | func asyncMap ( 12 | _ transform: (Element) async throws -> T 13 | ) async rethrows -> [T] { 14 | var values = [T]() 15 | 16 | for element in self { 17 | try await values.append(transform(element)) 18 | } 19 | 20 | return values 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /modules/BCFirebase/BCFirebase/Util/Encodable+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Encodable+.swift 3 | // BCFirebase 4 | // 5 | // Created by youtak on 2023/02/03. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Encodable { 11 | var asDictionary: [String: Any]? { 12 | guard let object = try? JSONEncoder().encode(self), 13 | let dictinoary = try? JSONSerialization.jsonObject( 14 | with: object, options: [] 15 | ) as? [String: Any] 16 | else { return nil } 17 | return dictinoary 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /modules/BCFetcher/BCFetcher/BCFetcher.h: -------------------------------------------------------------------------------- 1 | // 2 | // BCFetcher.h 3 | // BCFetcher 4 | // 5 | // Created by SEUNGMIN OH on 2022/12/11. 6 | // 7 | 8 | #import 9 | 10 | //! Project version number for BCFetcher. 11 | FOUNDATION_EXPORT double BCFetcherVersionNumber; 12 | 13 | //! Project version string for BCFetcher. 14 | FOUNDATION_EXPORT const unsigned char BCFetcherVersionString[]; 15 | 16 | // In this header, you should import all the public headers of your framework using statements like #import 17 | 18 | 19 | -------------------------------------------------------------------------------- /modules/BCFirebase/BCFirebase/BCFirebase.h: -------------------------------------------------------------------------------- 1 | // 2 | // BCFirebase.h 3 | // BCFirebase 4 | // 5 | // Created by youtak on 2023/02/03. 6 | // 7 | 8 | #import 9 | 10 | //! Project version number for BCFirebase. 11 | FOUNDATION_EXPORT double BCFirebaseVersionNumber; 12 | 13 | //! Project version string for BCFirebase. 14 | FOUNDATION_EXPORT const unsigned char BCFirebaseVersionString[]; 15 | 16 | // In this header, you should import all the public headers of your framework using statements like #import 17 | 18 | 19 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource/BCResource.h: -------------------------------------------------------------------------------- 1 | // 2 | // BCResource.h 3 | // BCResource 4 | // 5 | // Created by SEUNGMIN OH on 2022/12/07. 6 | // 7 | 8 | #import 9 | 10 | //! Project version number for BCResource. 11 | FOUNDATION_EXPORT double BCResourceVersionNumber; 12 | 13 | //! Project version string for BCResource. 14 | FOUNDATION_EXPORT const unsigned char BCResourceVersionString[]; 15 | 16 | // In this header, you should import all the public headers of your framework using statements like #import 17 | 18 | 19 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Extension/Type/Encodable+dictionary.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Encodable+dictionary.swift 3 | // burstcamp 4 | // 5 | // Created by neuli on 2022/11/29. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Encodable { 11 | var asDictionary: [String: Any]? { 12 | guard let object = try? JSONEncoder().encode(self), 13 | let dictinoary = try? JSONSerialization.jsonObject( 14 | with: object, options: [] 15 | ) as? [String: Any] 16 | else { return nil } 17 | return dictinoary 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Error/ConvertError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConvertError.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2022/12/07. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ConvertError: LocalizedError { 11 | case dictionaryUnwrappingError 12 | case invalidImageConvert 13 | } 14 | 15 | extension ConvertError { 16 | var errorDescription: String? { 17 | switch self { 18 | case .dictionaryUnwrappingError: return "딕셔너리 언래핑 중 에러가 발생했습니다." 19 | case .invalidImageConvert: return "이미지를 데이터로 변환 중 에러가 발생했습니다." 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /modules/Manager/RealmManager/RealmManager/SortingPolicy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SortingPolicy.swift 3 | // RealmManager 4 | // 5 | // Created by SEUNGMIN OH on 2022/12/16. 6 | // 7 | 8 | import RealmSwift 9 | 10 | public typealias SortingPolicy = (keyPath: KeyPath, ascending: Bool) 11 | 12 | public extension RealmCollection { 13 | func sorted(using policy: SortingPolicy) -> Results 14 | where T.PersistedType: SortableType, Element: ObjectBase { 15 | sorted(by: policy.keyPath, ascending: policy.ascending) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Domain/UseCase/Auth/Login/LoginUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginUseCase.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/14. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol LoginUseCase { 11 | func checkIsExist(userUUID: String) async throws -> Bool 12 | func isLoggedIn() -> Bool 13 | 14 | func loginWithGithub(code: String) async throws -> (userNickname: String, userUUID: String) 15 | 16 | func loginWithApple(idTokenString: String, nonce: String) async throws -> String 17 | 18 | func createGuest(userUUID: String) async throws 19 | } 20 | -------------------------------------------------------------------------------- /modules/Manager/RealmManager/RealmManager/RealmManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // RealmManager.h 3 | // RealmManager 4 | // 5 | // Created by SEUNGMIN OH on 2022/12/13. 6 | // 7 | 8 | #import 9 | 10 | //! Project version number for RealmManager. 11 | FOUNDATION_EXPORT double RealmManagerVersionNumber; 12 | 13 | //! Project version string for RealmManager. 14 | FOUNDATION_EXPORT const unsigned char RealmManagerVersionString[]; 15 | 16 | // In this header, you should import all the public headers of your framework using statements like #import 17 | 18 | 19 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Domain/Interfaces/NotificationRepository/NotificationRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationRepository.swift 3 | // burstcamp 4 | // 5 | // Created by neuli on 2023/01/31. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol NotificationRepository { 11 | func saveToUserDefaults(fcmToken: String) 12 | func fcmTokenInUserDefaults() -> String? 13 | func saveFCMTokenToFirestore(_ fcmToken: String, to userUUID: String) async throws 14 | 15 | func saveToUserDefaults(notificationFeedUUID: String) 16 | func notificationFeedUUIDInUserDefaults() -> String? 17 | func removeNotificationFeedUUID() 18 | } 19 | -------------------------------------------------------------------------------- /modules/Manager/RealmManager/RealmManager/Model/SortSupportable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SortSupportable.swift 3 | // RealmManager 4 | // 5 | // Created by SEUNGMIN OH on 2022/12/13. 6 | // 7 | 8 | import struct RealmSwift.SortDescriptor 9 | 10 | public protocol SortSupportable: RealmCompatible { 11 | associatedtype Sort: SortingType 12 | } 13 | 14 | /// Foundation의 SortDescriptor과 구별하기 위한 `typealias` 15 | /// 16 | /// @discussion 17 | /// 이를 통해, 상위 모듈에서 "렐름"을 Import하지 않아도 된다. 18 | public typealias RealmSortDescriptor = RealmSwift.SortDescriptor 19 | 20 | public protocol SortingType { 21 | var sortDescriptors: [RealmSortDescriptor] { get } 22 | } 23 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Domain/UseCase/Tab/MyPage/MyPageUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyPageUseCase.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/14. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol MyPageUseCase { 11 | func withdrawalWithGithub(code: String) async throws 12 | func withdrawalWithApple(idTokenString: String, nonce: String) async throws 13 | 14 | func canUpdateMyInfo() -> Bool 15 | func getNextUpdateDate() -> Date 16 | 17 | func updateUserPushState(userUUID: String, isPushOn: Bool) async throws 18 | func updateUserDarkModeState(appearance: Appearance) 19 | func updateLocalUser(_ user: User) 20 | } 21 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Domain/Interfaces/LoginRepository/LoginRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginRepository.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/15. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol LoginRepository { 11 | func isLoggedIn() -> Bool 12 | 13 | func loginWithGithub(code: String) async throws -> (userNickname: String, userUUID: String) 14 | func withdrawalWithGithub(code: String, userUUID: String) async throws -> Bool 15 | 16 | func loginWithApple(idTokenString: String, nonce: String) async throws -> String 17 | func withdrawalWithApple(idTokenString: String, nonce: String, userUUID: String) async throws -> Bool 18 | } 19 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Service/GithubAPIKey/GithubAPIKeyManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GithubAPIKeyManager.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/15. 6 | // 7 | 8 | import Foundation 9 | 10 | final class GithubAPIKeyManager { 11 | var githubAPIKey: Github? { 12 | guard let serviceInfoURL = Bundle.main.url( 13 | forResource: "Service-Info", 14 | withExtension: "plist" 15 | ), 16 | let data = try? Data(contentsOf: serviceInfoURL), 17 | let apiKey = try? PropertyListDecoder().decode(APIKey.self, from: data) 18 | else { return nil } 19 | return apiKey.github 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /modules/Manager/RealmManager/RealmManager/FetchedResults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FetchedResults.swift 3 | // RealmManager 4 | // 5 | // Created by SEUNGMIN OH on 2022/12/13. 6 | // 7 | 8 | import struct RealmSwift.Results 9 | 10 | public final class FetchedResults { 11 | 12 | internal let results: Results 13 | 14 | public var count: Int { 15 | return results.count 16 | } 17 | 18 | internal init(results: Results) { 19 | self.results = results 20 | } 21 | 22 | public func value(at index: Int) -> T? { 23 | guard index < count else { return nil } 24 | return T(realmModel: results[index]) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Error/ViewModel/HomeViewModelError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeViewModelError.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/21. 6 | // 7 | 8 | import Foundation 9 | 10 | enum HomeViewModelError: LocalizedError { 11 | case fetchHomeFeedList 12 | case feedIndex 13 | case feedUpdate 14 | case pushState 15 | } 16 | 17 | extension HomeViewModelError { 18 | var errorDescription: String? { 19 | switch self { 20 | case .fetchHomeFeedList: return "최신 피드를 불러오는데 에러가 발생했습니다." 21 | case .feedIndex: return "피드에 접근하는데 오류가 발생했습니다.(index)" 22 | case .feedUpdate: return "피드 업데이트하는데 오류가 발생했습니다." 23 | case .pushState: return "푸시 상태를 업데이트하는데 오류가 발생했습니다." 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /modules/BCFirebase/BCFirebase/FireStorage/FireStorageError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FireStorageError.swift 3 | // burstcamp 4 | // 5 | // Created by neuli on 2022/12/02. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum FireStorageError: LocalizedError { 11 | case userSignUp 12 | case dataUpload 13 | case URLDownload 14 | case deleteError 15 | } 16 | 17 | extension FireStorageError { 18 | public var errorDescription: String? { 19 | switch self { 20 | case .userSignUp: return "DB에 블로그 업데이트 중 에러가 발생했습니다" 21 | case .dataUpload: return "데이터 업로드 중 에러가 발생했습니다." 22 | case .URLDownload: return "서버에서 URL을 받아오던 중 에러가 발생했습니다." 23 | case .deleteError: return "서버에서 데이터 삭제 중 에러가 발생했습니다." 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Domain/Model/Feed/FeedCellType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FeedCellType.swift 3 | // Eoljuga 4 | // 5 | // Created by youtak on 2022/11/19. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Home에서 CellType을 분류할 때 사용되는 `enum` 11 | enum FeedCellType: Int, CaseIterable { 12 | case recommend 13 | case normal 14 | } 15 | 16 | extension FeedCellType { 17 | init?(index: Int) { 18 | self.init(rawValue: index) 19 | } 20 | 21 | var columnCount: Int { 22 | switch self { 23 | case .recommend: return 1 24 | case .normal: return 1 25 | } 26 | } 27 | 28 | var index: Int { 29 | return self.rawValue 30 | } 31 | 32 | static var count: Int { 33 | return Self.allCases.count 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Error/ImageCacheError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageCacheError.swift 3 | // burstcamp 4 | // 5 | // Created by neuli on 2022/11/27. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ImageCacheError: LocalizedError { 11 | case imageURLErrror 12 | case notModifiedImage 13 | case network(error: NetworkError) 14 | case unKnownError 15 | } 16 | 17 | extension ImageCacheError { 18 | var errorDescription: String? { 19 | switch self { 20 | case .imageURLErrror: return "이미지 URL 변환 중 에러가 발생했습니다" 21 | case .notModifiedImage: return "이미지가 동일합니다 (etag 동일)" 22 | case .network(let error): return "\(error.errorDescription)" 23 | case .unKnownError: return "알 수 없는 네트워크 에러가 발생했습니다." 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /burstcamp/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - nesting 3 | - file_types_order 4 | 5 | opt_in_rules: 6 | - empty_count 7 | - empty_string 8 | - file_name_no_space 9 | - file_types_order 10 | - force_unwrapping 11 | - multiline_arguments 12 | - multiline_function_chains 13 | - multiline_parameters_brackets 14 | - unused_import 15 | - sorted_imports 16 | - vertical_parameter_alignment_on_call 17 | - vertical_whitespace_closing_braces 18 | - weak_delegate 19 | 20 | excluded: 21 | - ${TARGETNAME}/App 22 | - ${TARGETNAME}/Resource 23 | - ${TARGETNAME}Tests/ 24 | - ${TARGETNAME}UITests/ 25 | 26 | line_length: 130 27 | 28 | file_length: 29 | warning: 700 30 | error: 700 31 | 32 | identifier_name: 33 | excluded: 34 | - id 35 | - URL 36 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Domain/Model/Appearance.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Appearance.swift 3 | // Eoljuga 4 | // 5 | // Created by neuli on 2022/11/21. 6 | // 7 | 8 | import UIKit 9 | 10 | enum Appearance: String { 11 | case light 12 | case dark 13 | 14 | var theme: String { 15 | return self.rawValue 16 | } 17 | 18 | var userInterfaceStyle: UIUserInterfaceStyle { 19 | switch self { 20 | case .light: return .light 21 | case .dark: return .dark 22 | } 23 | } 24 | 25 | var switchMode: Bool { 26 | switch self { 27 | case .light: return false 28 | case .dark: return true 29 | } 30 | } 31 | 32 | static func appearance(isOn: Bool) -> Appearance { 33 | return isOn ? .dark : .light 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /modules/Manager/RealmManager/RealmManager/Model/RealmCompatible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RealmCompatible.swift 3 | // RealmManager 4 | // 5 | // Created by SEUNGMIN OH on 2022/12/13. 6 | // 7 | 8 | import class RealmSwift.Object 9 | 10 | /// 구조체를 RealmSwift에서 사용할 수 있도록 래핑해주는 `protocol` 11 | public protocol RealmCompatible { 12 | associatedtype RealmModel: RealmSwift.Object 13 | associatedtype PropertyValue: PropertyValueType 14 | 15 | init(realmModel: RealmModel) 16 | func realmModel() -> RealmModel 17 | } 18 | 19 | /// 데이터를 type-safe 하게 사용할 수 있도록 도와주는 `typealias` 20 | public typealias PropertyValuePair = (name: String, value: Any) 21 | 22 | /// 데이터를 type-safe 하게 사용할 수 있도록 도와주는 `protocol` 23 | public protocol PropertyValueType { 24 | var propertyValuePair: PropertyValuePair { get } 25 | } 26 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Presentation/Common/View/Label/DefaultMultilLineLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultMultilLineLabel.swift 3 | // burstcamp 4 | // 5 | // Created by SEUNGMIN OH on 2022/11/23. 6 | // 7 | 8 | import UIKit 9 | 10 | class DefaultMultiLineLabel: UILabel { 11 | override var text: String? { 12 | didSet { 13 | setLineHeight160() 14 | } 15 | } 16 | 17 | override init(frame: CGRect) { 18 | super.init(frame: frame) 19 | configureMultiLineSetting() 20 | } 21 | 22 | required init?(coder: NSCoder) { 23 | fatalError("init(coder:) has not been implemented") 24 | } 25 | 26 | private func configureMultiLineSetting() { 27 | lineBreakMode = .byWordWrapping 28 | lineBreakStrategy = .hangulWordPriority 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Presentation/Common/View/Button/DefaultButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultButton.swift 3 | // Eoljuga 4 | // 5 | // Created by neuli on 2022/11/18. 6 | // 7 | 8 | import UIKit 9 | 10 | final class DefaultButton: UIButton { 11 | 12 | init( 13 | title: String, 14 | font: UIFont = .extraBold16, 15 | backgroundColor: UIColor = .main 16 | ) { 17 | super.init(frame: .zero) 18 | setTitle(title, for: .normal) 19 | setTitleColor(.white, for: .normal) 20 | self.backgroundColor = backgroundColor 21 | titleLabel?.font = font 22 | layer.cornerRadius = Constant.CornerRadius.radius8.cgFloat 23 | } 24 | 25 | required init?(coder: NSCoder) { 26 | fatalError("init(coder:) has not been implemented") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Presentation/Tab/MyPage/OpenSource/ViewController/OpenSourceLicenseViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OpenSourceLicenseViewController.swift 3 | // Eoljuga 4 | // 5 | // Created by neuli on 2022/11/21. 6 | // 7 | 8 | import UIKit 9 | 10 | final class OpenSourceLicenseViewController: UIViewController { 11 | 12 | // MARK: - Properties 13 | 14 | private var openSourceLicenseView: OpenSourceLicenseView { 15 | guard let view = view as? OpenSourceLicenseView else { 16 | return OpenSourceLicenseView() 17 | } 18 | return view 19 | } 20 | 21 | // MARK: - Life Cycle 22 | 23 | override func loadView() { 24 | view = OpenSourceLicenseView() 25 | } 26 | 27 | override func viewDidLoad() { 28 | } 29 | 30 | // MARK: - Methods 31 | } 32 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Data/Repositories/BlogRepository/DefaultBlogRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultBlogRepository.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/15. 6 | // 7 | 8 | import Foundation 9 | 10 | import BCFirebase 11 | 12 | final class DefaultBlogRepository: BlogRepository { 13 | 14 | private let bcFirebaseFunctionService: BCFirebaseFunctionService 15 | 16 | init(bcFirebaseFunctionService: BCFirebaseFunctionService) { 17 | self.bcFirebaseFunctionService = bcFirebaseFunctionService 18 | } 19 | 20 | func checkBlogTitle(link: String) async throws -> String { 21 | return try await bcFirebaseFunctionService.getBlogTitle(link: link) 22 | } 23 | 24 | func isValidateLink(_ link: String) -> Bool { 25 | return Validator.validate(blogLink: link) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Domain/UseCase/Auth/SignUp/SignUpUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignUpUseCase.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/14. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol SignUpUseCase { 11 | func setUserNickname(_ nickname: String) 12 | func setUserDomain(_ domain: Domain) 13 | func setUserCamperID(_ camperID: String) 14 | func setUserBlogURL(_ blogURL: String) 15 | func getUserDomain() -> Domain 16 | func getUserBlogURL() -> String 17 | 18 | func isValidateBlogURL(_ blogURL: String) -> Bool 19 | func getBlogTitle(blogURL: String) async throws -> String 20 | func getUser(userUUID: String, blogTitle: String) throws -> User 21 | 22 | func signUp(_ user: User) async throws 23 | func saveFCMToken(_ token: String, to userUUID: String) async throws 24 | } 25 | -------------------------------------------------------------------------------- /modules/BCResource/swiftgen.yml: -------------------------------------------------------------------------------- 1 | input_dir: ${TARGETNAME}/Resource/ 2 | output_dir: ${TARGETNAME}/Generated/ 3 | 4 | xcassets: 5 | inputs: 6 | - Color.xcassets 7 | - Image.xcassets 8 | outputs: 9 | - templateName: swift5 10 | params: 11 | forceProvidesNamespaces: true 12 | enumName: Assets 13 | publicAccess: public 14 | output: Assets+Generated.swift 15 | 16 | 17 | fonts: 18 | inputs: 19 | - Fonts 20 | outputs: 21 | - templateName: swift5 22 | params: 23 | forceProvidesNamespaces: true 24 | enumName: Fonts 25 | publicAccess: public 26 | output: Fonts+Generated.swift 27 | 28 | ## For more info, use `swiftgen config doc` to open the full documentation on GitHub. 29 | ## https://github.com/SwiftGen/SwiftGen/tree/6.5.1/Documentation/ 30 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Domain/Interfaces/UserRepository/UserRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserRepository.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/15. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol UserRepository { 11 | func fetchUser(_ userUUID: String) async throws -> User 12 | func saveUser(_ user: User) async throws 13 | func updateUser(_ user: User) async throws 14 | func updateUserPushState(userUUID: String, isPushOn: Bool) async throws 15 | func removeUser(_ user: User) async throws 16 | func saveFCMToken(_ token: String, to userUUID: String) async throws 17 | 18 | func updateBlog(with signUpUserUUID: String, blogURL: String) async throws 19 | 20 | func saveGuest(userUUID: String) async throws -> User 21 | func isNicknameExist(_ nickname: String) async throws -> Bool 22 | } 23 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Presentation/Common/View/ImageView/DefaultProfileImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultProfileImageView.swift 3 | // Eoljuga 4 | // 5 | // Created by neuli on 2022/11/18. 6 | // 7 | 8 | import UIKit 9 | 10 | final class DefaultProfileImageView: UIImageView { 11 | 12 | let imageSize: Int 13 | 14 | init(imageSize: Int) { 15 | self.imageSize = imageSize 16 | super.init(frame: .zero) 17 | layer.cornerRadius = imageSize.cgFloat / 2 18 | clipsToBounds = true 19 | image = UIImage(systemName: "person.fill")?.withTintColor(.white, renderingMode: .alwaysOriginal) 20 | contentMode = .scaleAspectFill 21 | backgroundColor = .systemGray5 22 | } 23 | 24 | required init?(coder: NSCoder) { 25 | fatalError("init(coder:) has not been implemented") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Service/Singleton/DarkmodeManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DarkmodeManager.swift 3 | // Eoljuga 4 | // 5 | // Created by neuli on 2022/11/21. 6 | // 7 | 8 | import UIKit 9 | 10 | struct DarkModeManager { 11 | static private(set) var currentAppearance: Appearance = UserDefaultsManager.currentAppearance() 12 | 13 | static func setAppearance(_ appearance: Appearance) { 14 | UserDefaultsManager.saveAppearance(appearance: appearance) 15 | setWindowAppearance(appearance: appearance) 16 | } 17 | 18 | private static func setWindowAppearance(appearance: Appearance) { 19 | if let window = UIApplication.shared.connectedScenes.first as? UIWindowScene { 20 | let windows = window.windows.first 21 | windows?.overrideUserInterfaceStyle = appearance.userInterfaceStyle 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Extension/UI/View/UIStackView+addArrangedSubviews.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIStackView+addArrangedSubviews.swift 3 | // Eoljuga 4 | // 5 | // Created by youtak on 2022/11/17. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIStackView { 11 | func addArrangedSubViews(_ subViews: [UIView]) { 12 | subViews.forEach { self.addArrangedSubview($0) } 13 | } 14 | 15 | convenience init( 16 | views: [UIView], 17 | axis: NSLayoutConstraint.Axis = .vertical, 18 | distribution: UIStackView.Distribution = .equalSpacing, 19 | alignment: UIStackView.Alignment = .fill, 20 | spacing: Int 21 | ) { 22 | self.init(arrangedSubviews: views) 23 | self.axis = axis 24 | self.distribution = distribution 25 | self.alignment = alignment 26 | self.spacing = spacing.cgFloat 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Data/Repositories/ImageRepository/DefaultImageRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultImageRepository.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/19. 6 | // 7 | 8 | import Foundation 9 | 10 | import BCFirebase 11 | 12 | final class DefaultImageRepository: ImageRepository { 13 | 14 | private let bcFirestorageService: BCFireStorageService 15 | 16 | init(bcFirestorageService: BCFireStorageService) { 17 | self.bcFirestorageService = bcFirestorageService 18 | } 19 | 20 | func saveProfileImage(imageData: Data, userUUID: String) async throws -> String { 21 | return try await bcFirestorageService.saveProfileImage(imageData: imageData, to: userUUID) 22 | } 23 | 24 | func deleteProfileImage(userUUID: String) async throws { 25 | try await bcFirestorageService.deleteProfileImage(userUUID: userUUID) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Firebase-Functions/functions/service/test/mockUpService.js: -------------------------------------------------------------------------------- 1 | import { getFirestore, Timestamp } from "firebase-admin/firestore" 2 | 3 | 4 | const db = getFirestore() 5 | const userRef = db.collection('user') 6 | 7 | export async function createMockUpUser() { 8 | const mockUpUser = { 9 | blogTitle: "", 10 | blogURL: "", 11 | camperID: "S999", 12 | domain: "iOS", 13 | isPushOn: true, 14 | nickname: 'mockup', 15 | ordinalNumber: 7, 16 | profileImageURL: "https://w.namu.la/s/62223555ff374704aa337bb299929204693c936dc4cf8d45ec0844b189605b317667a6956e0c50c46c69600a18b652f53f85e3358a66865b8d57b8d7a00ad19c732c11df86798ab7a83de831010b920f26eb7b45736cb858aa2bdc5b8a9770c3", 17 | signupDate: Timestamp.now(), 18 | userUUID: "hello2burstcamp" 19 | } 20 | 21 | const res = await userRef.doc(mockUpUser.userUUID).set(mockUpUser) 22 | console.log('유저 생성 - ', res.nickname); 23 | } -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Color.xcassets/brown.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x70", 9 | "green" : "0x92", 10 | "red" : "0xAC" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x94", 27 | "green" : "0xB1", 28 | "red" : "0xC9" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Color.xcassets/main.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xF3", 9 | "green" : "0x73", 10 | "red" : "0x00" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0xFF", 27 | "green" : "0x84", 28 | "red" : "0x00" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Presentation/Coordinator/TabBar/TabBarPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabBarPage.swift 3 | // Eoljuga 4 | // 5 | // Created by youtak on 2022/11/16. 6 | // 7 | 8 | import Foundation 9 | 10 | enum TabBarPage: Int { 11 | case home 12 | case scrapPage 13 | case myPage 14 | } 15 | 16 | extension TabBarPage { 17 | init?(index: Int) { 18 | self.init(rawValue: index) 19 | } 20 | 21 | var pageTitle: String { 22 | switch self { 23 | case .home: return "홈" 24 | case .scrapPage: return "모아보기" 25 | case .myPage: return "마이페이지" 26 | } 27 | } 28 | 29 | var pageIconTitle: String { 30 | switch self { 31 | case .home: return "house.fill" 32 | case .scrapPage: return "bookmark.fill" 33 | case .myPage: return "person.fill" 34 | } 35 | } 36 | 37 | var index: Int { 38 | return self.rawValue 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Color.xcassets/white.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "1.000", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.000", 27 | "green" : "0.000", 28 | "red" : "0.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Resource/Assets.xcassets/LaunchScreenBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFF", 9 | "green" : "0xFF", 10 | "red" : "0xFF" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x1E", 27 | "green" : "0x1C", 28 | "red" : "0x1C" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Color.xcassets/brightGreen.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.814", 9 | "green" : "0.947", 10 | "red" : "0.747" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.858", 27 | "green" : "0.960", 28 | "red" : "0.804" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Color.xcassets/brightOrange.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.708", 9 | "green" : "0.880", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.772", 27 | "green" : "0.904", 28 | "red" : "1.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Color.xcassets/brightYellow.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.704", 9 | "green" : "0.942", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.765", 27 | "green" : "0.956", 28 | "red" : "1.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Color.xcassets/red.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.314", 9 | "green" : "0.369", 10 | "red" : "0.925" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.447", 27 | "green" : "0.475", 28 | "red" : "0.894" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Color.xcassets/background.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "1.000", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.118", 27 | "green" : "0.110", 28 | "red" : "0.110" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Color.xcassets/black.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.000", 9 | "green" : "0.000", 10 | "red" : "0.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "1.000", 27 | "green" : "1.000", 28 | "red" : "1.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Color.xcassets/blue.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.969", 9 | "green" : "0.529", 10 | "red" : "0.259" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.969", 27 | "green" : "0.588", 28 | "red" : "0.275" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Color.xcassets/green.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.459", 9 | "green" : "0.792", 10 | "red" : "0.443" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.557", 27 | "green" : "0.835", 28 | "red" : "0.545" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Color.xcassets/indigo.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.831", 9 | "green" : "0.412", 10 | "red" : "0.420" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.863", 27 | "green" : "0.525", 28 | "red" : "0.522" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Color.xcassets/orange.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.255", 9 | "green" : "0.647", 10 | "red" : "0.949" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.275", 27 | "green" : "0.714", 28 | "red" : "0.957" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Color.xcassets/pink.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.420", 9 | "green" : "0.329", 10 | "red" : "0.922" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.522", 27 | "green" : "0.435", 28 | "red" : "0.929" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Color.xcassets/purple.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.859", 9 | "green" : "0.412", 10 | "red" : "0.678" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.898", 27 | "green" : "0.584", 28 | "red" : "0.769" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Color.xcassets/teal.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.969", 9 | "green" : "0.800", 10 | "red" : "0.522" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.980", 27 | "green" : "0.875", 28 | "red" : "0.710" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Color.xcassets/yellow.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.302", 9 | "green" : "0.831", 10 | "red" : "0.973" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.318", 27 | "green" : "0.890", 28 | "red" : "0.976" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Error/GithubError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GithubError.swift 3 | // burstcamp 4 | // 5 | // Created by 김기훈 on 2022/12/05. 6 | // 7 | 8 | import Foundation 9 | 10 | enum GithubError: LocalizedError { 11 | case requestAccessTokenError 12 | case requestUserInfoError 13 | case checkOrganizationError 14 | case APIKeyError 15 | case encodingError 16 | } 17 | 18 | extension GithubError { 19 | var errorDescription: String? { 20 | switch self { 21 | case .requestAccessTokenError: 22 | return "Github에서 AccessToken을 불러올 수 없습니다" 23 | case .requestUserInfoError: 24 | return "Github 유저 정보를 불러올 수 없습니다" 25 | case .checkOrganizationError: 26 | return "부스트캠퍼가 아닙니다" 27 | case .APIKeyError: 28 | return "관리자에게 문의해주세요 (APIKey)" 29 | case .encodingError: 30 | return "관리자에게 문의해주세요 (Github Request body)" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Service/UserDefaultsService/DefaultUserDefaultsService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultUserDefaultsService.swift 3 | // burstcamp 4 | // 5 | // Created by neuli on 2023/01/31. 6 | // 7 | 8 | import Foundation 9 | 10 | final class DefaultUserDefaultsService: UserDefaultsService { 11 | 12 | // MARK: - Properties 13 | 14 | private let standard = UserDefaults.standard 15 | 16 | // MARK: - Methods 17 | 18 | func save(value: T, forKey key: String) { 19 | standard.set(value, forKey: key) 20 | } 21 | 22 | func value(valueType: T.Type, forKey key: String) -> T? { 23 | guard let value = standard.object(forKey: key) as? T else { return nil } 24 | return value 25 | } 26 | 27 | func stringValue(forKey key: String) -> String? { 28 | return standard.string(forKey: key) 29 | } 30 | 31 | func delete(forKey key: String) { 32 | standard.removeObject(forKey: key) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Presentation/Coordinator/Base/Coordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Coordinator.swift 3 | // Eoljuga 4 | // 5 | // Created by youtak on 2022/11/15. 6 | // 7 | 8 | import Combine 9 | import UIKit 10 | 11 | protocol Coordinator: AnyObject { 12 | var childCoordinators: [Coordinator] { get set } 13 | var navigationController: UINavigationController { get set } 14 | var cancelBag: Set { get set } 15 | var dependencyFactory: DependencyFactoryProtocol { get set } 16 | } 17 | 18 | extension Coordinator { 19 | func finish() { 20 | childCoordinators.removeAll() 21 | } 22 | 23 | func remove(childCoordinator: Coordinator) { 24 | childCoordinators = childCoordinators.filter({ $0 !== childCoordinator }) 25 | } 26 | } 27 | 28 | protocol NormalCoordinator: Coordinator { 29 | func start() 30 | } 31 | 32 | protocol TabBarChildCoordinator: Coordinator { 33 | func start(viewController: UIViewController) 34 | } 35 | -------------------------------------------------------------------------------- /Firebase-Functions/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "name": "functions", 4 | "description": "Cloud Functions for Firebase", 5 | "scripts": { 6 | "lint": "eslint", 7 | "serve": "firebase emulators:start --only functions", 8 | "shell": "firebase functions:shell", 9 | "start": "npm run shell", 10 | "deploy": "firebase deploy --only functions", 11 | "logs": "firebase functions:log" 12 | }, 13 | "engines": { 14 | "node": "16" 15 | }, 16 | "main": "index.js", 17 | "dependencies": { 18 | "@mozilla/readability": "^0.4.2", 19 | "crypto-js": "^4.1.1", 20 | "firebase-admin": "^11.3.0", 21 | "firebase-functions": "^4.1.0", 22 | "firebase-messaging": "^1.0.6", 23 | "jsdom": "^20.0.3", 24 | "node-fetch": "^3.3.0" 25 | }, 26 | "devDependencies": { 27 | "eslint": "^8.9.0", 28 | "eslint-config-google": "^0.14.0", 29 | "firebase-functions-test": "^0.2.0" 30 | }, 31 | "private": true 32 | } 33 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Domain/Interfaces/FeedRepository/FeedRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FeedRepository.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/03. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol FeedRepository { 11 | func fetchRecentHomeFeedList() async throws -> HomeFeedList 12 | func fetchMoreNormalFeed() async throws -> [Feed] 13 | 14 | func fetchRecentScrapFeed(userUUID: String) async throws -> [Feed] 15 | func fetchMoreScrapFeed(userUUID: String) async throws -> [Feed] 16 | 17 | func fetchFeed(by feedUUID: String) async throws -> Feed 18 | 19 | func scrapFeed(_ feed: Feed, userUUID: String) async throws -> Feed 20 | func unScrapFeed(_ feed: Feed, userUUID: String) async throws -> Feed 21 | 22 | func createMockUpRecommendFeedList(count: Int) -> [Feed] 23 | 24 | func blockFeed(_ feed: Feed, userUUID: String, wasScraped: Bool) async throws 25 | func reportFeed(_ feed: Feed, userUUID: String) async throws 26 | } 27 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Presentation/Factory/DependencyFactoryProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DependencyFactoryProtocol.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/14. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol DependencyFactoryProtocol { 11 | func createLoginUseCase() -> LoginUseCase 12 | 13 | func createLoginViewModel() -> LogInViewModel 14 | func createSignUpDomainViewModel(userNickname: String) -> SignUpDomainViewModel 15 | func createSignUpCamperIDViewModel() -> SignUpCamperIDViewModel 16 | func createSignUpBlogViewModel() -> SignUpBlogViewModel 17 | func createHomeViewModel() -> HomeViewModel 18 | func createScrapPageViewModel() -> ScrapPageViewModel 19 | func createFeedDetailViewModel(feed: Feed) -> FeedDetailViewModel 20 | func createFeedDetailViewModel(feedUUID: String) -> FeedDetailViewModel 21 | func createMyPageViewModel() -> MyPageViewModel 22 | func createMyPageEditViewModel() -> MyPageEditViewModel 23 | } 24 | -------------------------------------------------------------------------------- /modules/BCFirebase/BCFirebase/Auth/FirebaseAuthError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FirebaseAuthError.swift 3 | // burstcamp 4 | // 5 | // Created by 김기훈 on 2022/12/09. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum FirebaseAuthError: LocalizedError { 11 | case currentUserNil 12 | case failSignIn 13 | case readToken 14 | case userReAuth 15 | case userDelete 16 | case authSignOut 17 | case fetchUUID 18 | } 19 | 20 | extension FirebaseAuthError { 21 | public var errorDescription: String? { 22 | switch self { 23 | case .currentUserNil: return "현재 유저가 없습니다." 24 | case .failSignIn: return "Fail to firebase auth signIn" 25 | case .readToken: return "토큰을 불러올 수 없습니다" 26 | case .userReAuth: return "재인증을 하던 중 에러가 발생했습니다." 27 | case .userDelete: return "유저를 삭제하던 중 에러가 발생했습니다." 28 | case .authSignOut: return "Fail to auth sign out" 29 | case .fetchUUID: return "UUID에 접근 하던 중 에러가 발생했습니다" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Extension/AsyncCompatible/Future+Async.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Future+Async.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2022/12/10. 6 | // 7 | 8 | import class Combine.Future 9 | 10 | extension Future where Failure == Error { 11 | convenience init( 12 | _ operation: @escaping () async -> Output 13 | ) { 14 | self.init { promise in 15 | Task { 16 | let output = await operation() 17 | promise(.success(output)) 18 | } 19 | } 20 | } 21 | 22 | convenience init( 23 | _ operation: @escaping () async throws -> Output 24 | ) { 25 | self.init { promise in 26 | Task { 27 | do { 28 | let output = try await operation() 29 | promise(.success(output)) 30 | } catch { 31 | promise(.failure(error)) 32 | } 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Extension/UI/View/UIImageView+Cache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImageView+Cache.swift 3 | // burstcamp 4 | // 5 | // Created by neuli on 2022/11/27. 6 | // 7 | 8 | import Combine 9 | import UIKit 10 | 11 | extension UIImageView { 12 | 13 | func setImage( 14 | urlString: String, 15 | isDiskCaching: Bool = false, 16 | defaultImage: UIImage? = UIImage(named: "burstcamper100"), 17 | imagePublisher: PassthroughSubject = 18 | PassthroughSubject() 19 | ) { 20 | imagePublisher 21 | .map { image in image == nil ? defaultImage : image } 22 | .receive(on: DispatchQueue.main) 23 | .assign(to: \.image, on: self) 24 | .store(in: &ImageCacheManager.shared.cancelBag) 25 | 26 | ImageCacheManager.shared.image( 27 | urlString: urlString, 28 | isDiskCaching: isDiskCaching, 29 | imagePublisher: imagePublisher 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Presentation/Common/View/Label/DefaultPaddingLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultPaddingLabel.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/02/02. 6 | // 7 | 8 | import UIKit 9 | 10 | final class DefaultPaddingLabel: UILabel { 11 | private var padding = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0) 12 | 13 | convenience init(padding: UIEdgeInsets) { 14 | self.init() 15 | self.padding = padding 16 | } 17 | 18 | convenience init(horizontalPadding: CGFloat) { 19 | self.init() 20 | self.padding = UIEdgeInsets(top: 0, left: horizontalPadding, bottom: 0, right: horizontalPadding) 21 | } 22 | 23 | override func drawText(in rect: CGRect) { 24 | super.drawText(in: rect.inset(by: padding)) 25 | } 26 | 27 | override var intrinsicContentSize: CGSize { 28 | var contentSize = super.intrinsicContentSize 29 | contentSize.height += padding.top + padding.bottom 30 | contentSize.width += padding.left + padding.right 31 | 32 | return contentSize 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /modules/BCFetcher/BCFetcher/Fetchable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Fetchable.swift 3 | // BCFetcher 4 | // 5 | // Created by SEUNGMIN OH on 2022/12/08. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | public protocol Fetchable { 12 | associatedtype Data 13 | associatedtype FetchingError: Error 14 | 15 | var queue: DispatchQueue { get } 16 | 17 | // Remote 18 | var onRemoteCombine: (() -> AnyPublisher) { get } 19 | 20 | // Local 21 | var onLocalCombine: (() -> AnyPublisher) { get } 22 | var onLocal: (() -> Data) { get } 23 | var onUpdateLocal: ((Data) -> Void) { get } 24 | 25 | init( 26 | onRemoteCombine: @escaping () -> AnyPublisher, 27 | onLocalCombine: @escaping () -> AnyPublisher, 28 | onLocal: @escaping () -> Data, 29 | onUpdateLocal: @escaping (Data) -> Void, 30 | queue: DispatchQueue 31 | ) 32 | 33 | func fetch(_ onNext: @escaping (Status, Data) -> Void) -> Set 34 | } 35 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Domain/Model/Network/HTTPHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPHeader.swift 3 | // burstcamp 4 | // 5 | // Created by neuli on 2022/11/25. 6 | // 7 | 8 | import Foundation 9 | 10 | enum HTTPHeader { 11 | case contentTypeApplicationJSON 12 | case acceptApplicationJSON 13 | case acceptApplicationVNDGithubJSON 14 | case authorizationBearer(token: String) 15 | case contentTypeTextPlain 16 | 17 | var keyValue: (key: String, value: String) { 18 | switch self { 19 | case .contentTypeApplicationJSON: 20 | return (key: "Content-Type", value: "application/json") 21 | case .acceptApplicationJSON: 22 | return (key: "Accept", value: "application/json") 23 | case .acceptApplicationVNDGithubJSON: 24 | return (key: "Accept", value: "application/vnd.github+json") 25 | case .authorizationBearer(let token): 26 | return (key: "Authorization", value: "Bearer \(token)") 27 | case .contentTypeTextPlain: 28 | return (key: "Content-Type", value: "text/plain") 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Presentation/Common/Base/AppleAuthViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppleAuthViewController.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/30. 6 | // 7 | 8 | import AuthenticationServices 9 | import UIKit 10 | 11 | class AppleAuthViewController: UIViewController, 12 | ASAuthorizationControllerPresentationContextProviding { 13 | 14 | private(set) var currentNonce: String? 15 | 16 | func getAppleLoginRequest() -> ASAuthorizationAppleIDRequest { 17 | let nonce = String.randomNonceString() 18 | currentNonce = nonce 19 | let appleIDProvider = ASAuthorizationAppleIDProvider() 20 | let request = appleIDProvider.createRequest() 21 | request.requestedScopes = [.fullName, .email] 22 | request.nonce = nonce.sha256() 23 | 24 | return request 25 | } 26 | 27 | func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { 28 | guard let window = self.view.window else { fatalError("애플 로그인 ASPresentationAnchor 에러")} 29 | return window 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Presentation/Coordinator/Base/GithubLogInCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GithubLogInCoordinator.swift 3 | // burstcamp 4 | // 5 | // Created by 김기훈 on 2022/12/12. 6 | // 7 | 8 | import Foundation 9 | import SafariServices 10 | 11 | protocol GithubLogInCoordinator: Coordinator { } 12 | 13 | extension GithubLogInCoordinator { 14 | func moveToGithubLogIn() { 15 | let urlString = "https://github.com/login/oauth/authorize" 16 | let githubAPIKeyManager = GithubAPIKeyManager() 17 | 18 | guard var urlComponent = URLComponents(string: urlString), 19 | let clientID = githubAPIKeyManager.githubAPIKey?.clientID 20 | else { 21 | return 22 | } 23 | 24 | urlComponent.queryItems = [ 25 | URLQueryItem(name: "client_id", value: clientID), 26 | URLQueryItem(name: "scope", value: "admin:org") 27 | ] 28 | 29 | guard let url = urlComponent.url else { return } 30 | let safariViewController = SFSafariViewController(url: url) 31 | navigationController.present(safariViewController, animated: true) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Validator/Validator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Validator.swift 3 | // burstcamp 4 | // 5 | // Created by neuli on 2022/11/29. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | struct Validator { 12 | static let nicknameRegex = "^[가-힣a-zA-Z0-9_-]{2,10}$" 13 | 14 | static let tistoryRegex = #"^https://?[a-z0-9-]{4,32}.tistory.com[/]{0,1}$"# 15 | static let velogRegex = #"^https://velog.io/@?[A-Za-z0-9-_]{3,16}$"# 16 | 17 | static func validate(nickname: String) -> Bool { 18 | return nickname.isValidRegex(regex: nicknameRegex) 19 | } 20 | 21 | static func validate(blogLink: String) -> Bool { 22 | if blogLink.isValidRegex(regex: tistoryRegex) || blogLink.isValidRegex(regex: velogRegex) { 23 | return true 24 | } 25 | return false 26 | } 27 | 28 | static func validateIsEmpty(blogLink: String) -> Bool { 29 | if blogLink.isEmpty { return true } 30 | if blogLink.isValidRegex(regex: tistoryRegex) || blogLink.isValidRegex(regex: velogRegex) { 31 | return true 32 | } 33 | return false 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Data/Network/GithubLogin/GithubLoginModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIKey.swift 3 | // Eoljuga 4 | // 5 | // Created by 김기훈 on 2022/11/18. 6 | // 7 | 8 | import Foundation 9 | 10 | struct GithubToken: Codable { 11 | let accessToken: String 12 | let scope: String 13 | let tokenType: String 14 | 15 | enum CodingKeys: String, CodingKey { 16 | case accessToken = "access_token" 17 | case scope 18 | case tokenType = "token_type" 19 | } 20 | } 21 | 22 | struct GithubUser: Codable { 23 | let login: String 24 | } 25 | 26 | struct GithubMembership: Codable { 27 | let role: String 28 | let user: MembershipUser 29 | 30 | enum CodingKeys: String, CodingKey { 31 | case role 32 | case user 33 | } 34 | } 35 | 36 | struct MembershipUser: Codable { 37 | let login: String 38 | let id: Int 39 | let nodeID: String 40 | let htmlURL: String 41 | let type: String 42 | 43 | enum CodingKeys: String, CodingKey { 44 | case login, id 45 | case nodeID = "node_id" 46 | case htmlURL = "html_url" 47 | case type 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Domain/Model/User/Domain.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Domain.swift 3 | // Eoljuga 4 | // 5 | // Created by neuli on 2022/11/18. 6 | // 7 | 8 | import UIKit 9 | 10 | enum Domain: String, Codable { 11 | case iOS = "iOS" 12 | case android = "Android" 13 | case web = "Web" 14 | case guest = "Guest" 15 | 16 | var color: UIColor { 17 | switch self { 18 | case .iOS: return UIColor.customOrange 19 | case .android: return UIColor.customGreen 20 | case .web: return UIColor.customYellow 21 | case .guest: return UIColor.systemGray 22 | } 23 | } 24 | 25 | var brightColor: UIColor { 26 | switch self { 27 | case .iOS: return UIColor.brightOrange 28 | case .android: return UIColor.brightGreen 29 | case .web: return UIColor.brightYellow 30 | case .guest: return UIColor.systemGray 31 | } 32 | } 33 | 34 | var representing: String { 35 | switch self { 36 | case .iOS: return "S" 37 | case .android: return "K" 38 | case .web: return "J" 39 | case .guest: return "Guest" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Domain/UseCase/Tab/ScrapPage/DefaultScrapPageUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultScrapPageUseCase.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/14. 6 | // 7 | 8 | import Foundation 9 | 10 | final class DefaultScrapPageUseCase: ScrapPageUseCase { 11 | 12 | private let feedRepository: FeedRepository 13 | 14 | init(feedRepository: FeedRepository) { 15 | self.feedRepository = feedRepository 16 | } 17 | 18 | func fetchRecentScrapFeed() async throws -> [Feed] { 19 | let userUUID = UserManager.shared.user.userUUID 20 | return try await feedRepository.fetchRecentScrapFeed(userUUID: userUUID) 21 | } 22 | 23 | func fetchMoreScrapFeed() async throws -> [Feed] { 24 | let userUUID = UserManager.shared.user.userUUID 25 | return try await feedRepository.fetchMoreScrapFeed(userUUID: userUUID) 26 | } 27 | 28 | func scrapFeed(_ feed: Feed, userUUID: String) async throws -> Feed { 29 | return feed.isScraped 30 | ? try await feedRepository.unScrapFeed(feed, userUUID: userUUID) 31 | : try await feedRepository.scrapFeed(feed, userUUID: userUUID) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Firebase-Functions/functions/service/test/testAlgorithmFeed.js: -------------------------------------------------------------------------------- 1 | import { logger } from "firebase-functions/v1"; 2 | import { fetchContent } from "../feedAPI.js"; 3 | import { isSolvingAlgorithm, isContainBaekJoonLink } from "../../util.js"; 4 | 5 | export async function testIsAlgorithmFeed() { 6 | 7 | const mockUpURL = [ 8 | { 9 | blogTitle : "크기가 큰 배열에서의 탐색 & 캐시 히트", 10 | blogURL : "https://minios.tistory.com/75" 11 | }, 12 | { 13 | blogTitle : "[백준] 18258 큐 2 - Swift", 14 | blogURL : "https://minios.tistory.com/55" 15 | }, 16 | { 17 | blogTitle : "[실험실] - JPEG 압축률에 따른 품질 비교 (10% ~ 100%)", 18 | blogURL : "https://malchafrappuccino.tistory.com/144" 19 | } 20 | ] 21 | 22 | mockUpURL.forEach( (feed) => { 23 | checkIsAlgorithm(feed.blogTitle, feed.blogURL) 24 | }) 25 | } 26 | 27 | async function checkIsAlgorithm(title, blogURL) { 28 | const feedInfo = await fetchContent(blogURL) 29 | const content = feedInfo.content 30 | let titleResult = isSolvingAlgorithm(title) 31 | let contentResult = isContainBaekJoonLink(content) 32 | logger.log(title, "결과 - 제목에 포함", titleResult, "내용에 링크 포함",contentResult) 33 | } -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Extension/Combine/Publisher+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Publisher+Extension.swift 3 | // burstcamp 4 | // 5 | // Created by SEUNGMIN OH on 2022/12/11. 6 | // 7 | 8 | import Combine 9 | 10 | extension Publisher { 11 | func mapToVoid() -> Publishers.Map { 12 | return self.map { _ in Void() } 13 | } 14 | 15 | func unwrap() -> Publishers.CompactMap 16 | where Output == Result? { 17 | return self.compactMap { $0 } 18 | } 19 | } 20 | 21 | extension Publisher where Self.Failure == Never { 22 | 23 | /// publisher에서 방출된 각 element를 object의 property에 할당한다. 24 | /// 25 | /// `assign(to:)`와는 다르게, object를 weak capture한다. 26 | /// - Note: [Does 'assign(to:)' produce memory leaks?](https://forums.swift.org/t/does-assign-to-produce-memory-leaks/29546/9) 27 | /// 28 | 29 | func weakAssign( 30 | to keyPath: ReferenceWritableKeyPath, 31 | on object: Root 32 | ) -> AnyCancellable 33 | where Root: AnyObject { 34 | sink { [weak object] (value) in 35 | object?[keyPath: keyPath] = value 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /modules/BCFirebase/BCFirebase/Firestore/FirestoreCollection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FirestoreCollection.swift 3 | // Eoljuga 4 | // 5 | // Created by neuli on 2022/11/17. 6 | // 7 | 8 | import Foundation 9 | 10 | import FirebaseFirestore 11 | 12 | enum FirestoreCollection { 13 | case normalFeed 14 | case recommendFeed 15 | case user 16 | case scrapUsers(feedUUID: String) 17 | case scrapFeeds(userUUID: String) 18 | case admin 19 | case fcmToken 20 | case reportFeed 21 | 22 | static let scrapFeedUUIDs = "scrapFeedUUIDs" 23 | static let reportFeedUUIDs = "reportFeedUUIDs" 24 | } 25 | 26 | extension FirestoreCollection { 27 | 28 | var path: String { 29 | switch self { 30 | case .normalFeed: return "feed" 31 | case .recommendFeed: return "recommendFeed" 32 | case .user: return "user" 33 | case .scrapUsers(let feedUUID): return "feed/\(feedUUID)/scrapUsers" 34 | case .scrapFeeds(let userUUID): return "user/\(userUUID)/scrapFeeds" 35 | case .admin: return "admin" 36 | case .fcmToken: return "fcmToken" 37 | case .reportFeed: return "reportFeed" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Protocol/ContainCollectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContainCollectionView.swift 3 | // Eoljuga 4 | // 5 | // Created by youtak on 2022/11/21. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol ContainCollectionView { 11 | 12 | var collectionView: UICollectionView { get set } 13 | } 14 | 15 | extension ContainCollectionView { 16 | func collectionViewDelegate( 17 | viewController: UICollectionViewDelegate 18 | ) { 19 | collectionView.delegate = viewController 20 | } 21 | 22 | func collectionViewDelegate( 23 | viewController: UICollectionViewDelegate & UICollectionViewDataSource 24 | ) { 25 | collectionView.delegate = viewController 26 | collectionView.dataSource = viewController 27 | } 28 | 29 | func collectionViewScrollToTop() { 30 | collectionView.setContentOffset(.zero, animated: true) 31 | } 32 | 33 | func configureRefreshControl() { 34 | collectionView.refreshControl = UIRefreshControl() 35 | } 36 | 37 | func endCollectionViewRefreshing() { 38 | DispatchQueue.main.async { 39 | self.collectionView.refreshControl?.endRefreshing() 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource/ColorSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // R.swift 3 | // BCResource 4 | // 5 | // Created by SEUNGMIN OH on 2022/12/07. 6 | // 7 | 8 | import UIKit 9 | 10 | public extension UIColor { 11 | static let dynamicBlack = Assets.Color.black.color 12 | static let dynamicWhite = Assets.Color.white.color 13 | static let background = Assets.Color.background.color 14 | static let main = Assets.Color.main.color 15 | static let customRed = Assets.Color.red.color 16 | static let customOrange = Assets.Color.orange.color 17 | static let customYellow = Assets.Color.yellow.color 18 | static let customGreen = Assets.Color.green.color 19 | static let customTeal = Assets.Color.teal.color 20 | static let customBlue = Assets.Color.blue.color 21 | static let customIndigo = Assets.Color.indigo.color 22 | static let customPurple = Assets.Color.purple.color 23 | static let customPink = Assets.Color.pink.color 24 | static let customBrown = Assets.Color.brown.color 25 | static let brightYellow = Assets.Color.brightYellow.color 26 | static let brightGreen = Assets.Color.brightGreen.color 27 | static let brightOrange = Assets.Color.brightOrange.color 28 | } 29 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Extension/UI/View/UILabel+LineHeight.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UILabel+LineHeight.swift 3 | // Eoljuga 4 | // 5 | // Created by youtak on 2022/11/17. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UILabel { 11 | func setLineHeight160() { 12 | guard let labelText = self.text else { return } 13 | let fontSize = self.font.pointSize 14 | let lineHeight = fontSize * 1.6 15 | 16 | let style = NSMutableParagraphStyle() 17 | style.maximumLineHeight = lineHeight 18 | style.minimumLineHeight = lineHeight 19 | 20 | let baseLineOffset = (lineHeight - font.lineHeight) / 2.0 / 2.0 21 | 22 | let attributes: [NSAttributedString.Key: Any] = [ 23 | .paragraphStyle: style, 24 | .baselineOffset: baseLineOffset 25 | ] 26 | 27 | self.attributedText = NSAttributedString(string: labelText, attributes: attributes) 28 | } 29 | } 30 | 31 | // baslineOffset 참고 32 | //https://sujinnaljin.medium.com/swift-label%EC%9D%98-line-height-%EC%84%A4%EC%A0%95-%EB%B0%8F-%EA%B0%80%EC%9A%B4%EB%8D%B0-%EC%A0%95%EB%A0%AC-962f7c6e7512 33 | //http://blog.eppz.eu/uilabel-line-height-letter-spacing-and-more-uilabel-typography-extensions/ 34 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Presentation/Coordinator/Base/CoordinatorEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoordinatorEvent.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2022/12/07. 6 | // 7 | 8 | import Foundation 9 | 10 | enum AppCoordinatorEvent { 11 | case moveToAuthFlow 12 | case moveToTabBarFlow 13 | } 14 | 15 | enum AuthCoordinatorEvent { 16 | case moveToDomainScreen(userNickname: String) 17 | case moveToIDScreen 18 | case moveToBlogScreen 19 | case moveToTabBarScreen 20 | case showAlert(String) 21 | case moveToGithubLogIn 22 | } 23 | 24 | enum TabBarCoordinatorEvent { 25 | case moveToAuthFlow 26 | } 27 | 28 | enum HomeCoordinatorEvent { 29 | case moveToFeedDetail(feed: Feed) 30 | case moveToBlogSafari(url: URL) 31 | } 32 | 33 | enum ScrapPageCoordinatorEvent { 34 | case moveToFeedDetail(feed: Feed) 35 | } 36 | 37 | enum MyPageCoordinatorEvent { 38 | case moveToMyPageEditScreen 39 | case moveToOpenSourceScreen 40 | case moveToAuthFlow 41 | 42 | case moveMyPageEditScreenToBackScreen(toastMessage: String) 43 | case moveToGithubLogIn 44 | } 45 | 46 | enum FeedDetailCoordinatorEvent { 47 | case moveToBlogSafari(url: URL) 48 | case moveToPreviousScreen 49 | } 50 | -------------------------------------------------------------------------------- /modules/BCFirebase/BCFirebase/FireStorage/BCFireStorageService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FireStorageService.swift 3 | // burstcamp 4 | // 5 | // Created by neuli on 2022/11/29. 6 | // 7 | 8 | import Combine 9 | import UIKit 10 | 11 | import FirebaseStorage 12 | 13 | public final class BCFireStorageService { 14 | 15 | private let storagePath: Storage 16 | 17 | public init(storagePath: Storage) { 18 | self.storagePath = storagePath 19 | } 20 | 21 | public convenience init() { 22 | self.init(storagePath: Storage.storage()) 23 | } 24 | 25 | public func saveProfileImage(imageData: Data, to userUUID: String) async throws -> String { 26 | let ref = storagePath.reference(withPath: "images/profile/\(userUUID)") 27 | let metadata = StorageMetadata() 28 | metadata.contentType = "image/jpeg" 29 | 30 | _ = try await ref.putDataAsync(imageData, metadata: metadata) 31 | let imageURL = try await ref.downloadURL().absoluteString 32 | return imageURL 33 | } 34 | 35 | public func deleteProfileImage(userUUID: String) async throws { 36 | let ref = storagePath.reference(withPath: "images/profile/\(userUUID)") 37 | try await ref.delete() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Presentation/Common/View/LoadingVIew/LoadingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingView.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2022/12/13. 6 | // 7 | 8 | import UIKit 9 | 10 | import SnapKit 11 | import Then 12 | 13 | final class LoadingView: UIView { 14 | 15 | private let logoImage = UIImageView().then { 16 | $0.clipsToBounds = true 17 | $0.image = UIImage(named: "LaunchScreen") 18 | $0.contentMode = .scaleAspectFill 19 | } 20 | 21 | init() { 22 | super.init(frame: .zero) 23 | configureUI() 24 | } 25 | 26 | required init?(coder: NSCoder) { 27 | fatalError("init(coder:) has not been implemented") 28 | } 29 | 30 | private func configureUI() { 31 | configureView() 32 | configureImageView() 33 | } 34 | 35 | private func configureView() { 36 | backgroundColor = .background 37 | } 38 | 39 | private func configureImageView() { 40 | addSubview(logoImage) 41 | logoImage.snp.makeConstraints { make in 42 | make.centerX.equalToSuperview() 43 | make.centerY.equalTo(safeAreaLayoutGuide) 44 | make.width.height.equalTo(100) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Firebase-Functions/functions/util.js: -------------------------------------------------------------------------------- 1 | import MD5 from 'crypto-js/md5.js' 2 | 3 | /// https://stackoverflow.com/a/52171480/19782341 4 | String.prototype.hashCode = function() { 5 | return MD5(this).toString() 6 | } 7 | 8 | /** 9 | * @param {string} blogURL 블로그 주소 10 | * @returns {string} 블로그 RSS주소 11 | */ 12 | export function convertURL(blogURL) { 13 | if (blogURL.includes('tistory')) { 14 | return `${blogURL}/rss` 15 | } else if (blogURL.includes('velog')) { 16 | const nicknameCandidate = blogURL.match(/@[\w-]+/g) 17 | const nickname = nicknameCandidate[nicknameCandidate.length - 1] 18 | return `v2.velog.io/rss/${nickname}` 19 | } 20 | } 21 | 22 | /** 23 | * 제목에 백준, 프로그래머스 들어가 false 24 | * @param {String} feed.title 25 | * @returns {Bool} 26 | */ 27 | 28 | export function isSolvingAlgorithm(title) { 29 | let algorithmSite = ["백준", "프로그래머스"] 30 | for (let i = 0; i < algorithmSite.length; i++) { 31 | if (title.includes(algorithmSite[i])) { return true } 32 | } 33 | return false 34 | } 35 | 36 | /** 37 | * html 내용에 백준 링크가 있다면 false 38 | * @param {String} html 39 | * @returns {Bool} 40 | */ 41 | 42 | export function isContainBaekJoonLink(feedContent) { 43 | let baekJoonLink = "https://www.acmicpc.net/" 44 | return feedContent.includes(baekJoonLink) ? true : false 45 | } 46 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource/README.md: -------------------------------------------------------------------------------- 1 | # BCResource 2 | 3 | `Burstcamp`의 Resource를 담당하는 Module입니다. 4 | 5 | SwiftGen을 활용해 선언적인 코드를 작성할 수 있습니다. 6 | 7 | UIKit extension을 통해서 쉽게 Resource를 사용할 수 있습니다. 8 | 9 | ## How to use 10 | 11 | 1. SwiftGen을 설치합니다. 12 | 13 | ```bash 14 | brew install swiftgen 15 | ``` 16 | 17 | 1. `Resource` 디렉토리에 Asset을 추가합니다. 18 | * 추가하고자 하는 asset의 종류에 따라서 분류합니다. 19 | * 예를 들어, Color는 `Color` 디렉토리에, Image는 `Image` 디렉토리에 추가할 수 있습니다. 20 | * 추가적인 분류가 필요하다면, 새로운 asset 디렉토리를 추가합니다. 21 | * 디렉토리의 이름에 맞춰 `enum`이 생성됩니다. 22 | 23 | 24 | 1. 새로 추가된 파일이 `Assets`, `Font`가 아니라면 yml파일을 수정합니다. 25 | * 수정을 위해서는 [swiftgen.yml 템플릿](https://github.com/SwiftGen/SwiftGen/tree/stable/Documentation/templates)을 참고할 수 있습니다. 26 | 27 | 1. Build 28 | * Build Phase에 추가된 SwiftGen 스크립트를 통해 자동으로 코드가 생성됩니다. 29 | * 생성된 코드는 `Generated` 디렉토리에서 확인할 수 있습니다. 30 | 31 | 1. 앱에서 활용할 수 있도록 생성된 코드로 `extension`을 작성합니다. 32 | * 파일 이름은 `Set.swift`입니다. 33 | * 아래는 코드의 예시입니다. 34 | 35 | ```swift 36 | // ImageSet.swift 37 | 38 | public extension UIImage { 39 | static let burstcamper = Assets.Image.burstcamper100.image 40 | static let github = Assets.Image.github.image 41 | } 42 | ``` 43 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Image.xcassets/Github.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Github.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "filename" : "Github_dark.png", 16 | "idiom" : "universal", 17 | "scale" : "1x" 18 | }, 19 | { 20 | "filename" : "Github@2x.png", 21 | "idiom" : "universal", 22 | "scale" : "2x" 23 | }, 24 | { 25 | "appearances" : [ 26 | { 27 | "appearance" : "luminosity", 28 | "value" : "dark" 29 | } 30 | ], 31 | "filename" : "Github_dark@2x.png", 32 | "idiom" : "universal", 33 | "scale" : "2x" 34 | }, 35 | { 36 | "filename" : "Github@3x.png", 37 | "idiom" : "universal", 38 | "scale" : "3x" 39 | }, 40 | { 41 | "appearances" : [ 42 | { 43 | "appearance" : "luminosity", 44 | "value" : "dark" 45 | } 46 | ], 47 | "filename" : "Github_dark@3x.png", 48 | "idiom" : "universal", 49 | "scale" : "3x" 50 | } 51 | ], 52 | "info" : { 53 | "author" : "xcode", 54 | "version" : 1 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Presentation/Tab/ScrapPage/View/ScrapPageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScrapView.swift 3 | // Eoljuga 4 | // 5 | // Created by youtak on 2022/11/21. 6 | // 7 | 8 | import UIKit 9 | 10 | class ScrapPageView: UIView, ContainCollectionView { 11 | 12 | lazy var collectionView = UICollectionView( 13 | frame: .zero, 14 | collectionViewLayout: UICollectionViewFlowLayout() 15 | ).then { 16 | let layout = UICollectionViewFlowLayout() 17 | layout.scrollDirection = .vertical 18 | layout.minimumLineSpacing = Constant.zero.cgFloat 19 | layout.sectionInset = .zero 20 | $0.collectionViewLayout = layout 21 | $0.showsVerticalScrollIndicator = false 22 | $0.backgroundColor = .clear 23 | } 24 | 25 | override init(frame: CGRect) { 26 | super.init(frame: frame) 27 | configureUI() 28 | configureRefreshControl() 29 | } 30 | 31 | required init?(coder: NSCoder) { 32 | fatalError("init(coder:) has not been implemented") 33 | } 34 | 35 | private func configureUI() { 36 | configureScrapView() 37 | addSubview(collectionView) 38 | collectionView.snp.makeConstraints { 39 | $0.edges.equalToSuperview() 40 | } 41 | } 42 | 43 | private func configureScrapView() { 44 | backgroundColor = .background 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Extension/Type/TimeInterval+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimeInterval+.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2022/11/24. 6 | // 7 | 8 | import Foundation 9 | 10 | extension TimeInterval { 11 | 12 | // MARK: - Computed Type Properties 13 | 14 | static var secondsPerWeek: Double { return 7 * 24 * 60 * 60 } 15 | static var secondsPerDay: Double { return 24 * 60 * 60 } 16 | static var secondsPerHour: Double { return 60 * 60 } 17 | static var secondsPerMinute: Double { return 60 } 18 | 19 | static func before( 20 | seconds: Double = 0, 21 | minutes: Double = 0, 22 | hours: Double = 0, 23 | days: Double = 0 24 | ) -> TimeInterval { 25 | return -(days*secondsPerDay + hours*secondsPerHour + minutes*secondsPerMinute + seconds) 26 | } 27 | 28 | // MARK: - computed properties 29 | 30 | var toJustString: String { 31 | return "방금 전" 32 | } 33 | 34 | var toMinuteString: String { 35 | let minutes = Int(self/TimeInterval.secondsPerMinute) 36 | return "\(minutes)분 전" 37 | } 38 | 39 | var toHourString: String { 40 | let hours = Int(self/TimeInterval.secondsPerHour) 41 | return "\(hours)시간 전" 42 | } 43 | 44 | var toDayString: String { 45 | let days = Int(self/TimeInterval.secondsPerDay) 46 | return "\(days)일 전" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Resource/Assets.xcassets/LaunchScreen.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "LaunchScreen_Dark.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "filename" : "LaunchScreen_Dark 1.png", 16 | "idiom" : "universal", 17 | "scale" : "1x" 18 | }, 19 | { 20 | "filename" : "LaunchScreen_Dark@2x.png", 21 | "idiom" : "universal", 22 | "scale" : "2x" 23 | }, 24 | { 25 | "appearances" : [ 26 | { 27 | "appearance" : "luminosity", 28 | "value" : "dark" 29 | } 30 | ], 31 | "filename" : "LaunchScreen_Dark@2x 1.png", 32 | "idiom" : "universal", 33 | "scale" : "2x" 34 | }, 35 | { 36 | "filename" : "LaunchScreen_Dark@3x.png", 37 | "idiom" : "universal", 38 | "scale" : "3x" 39 | }, 40 | { 41 | "appearances" : [ 42 | { 43 | "appearance" : "luminosity", 44 | "value" : "dark" 45 | } 46 | ], 47 | "filename" : "LaunchScreen_Dark@3x 1.png", 48 | "idiom" : "universal", 49 | "scale" : "3x" 50 | } 51 | ], 52 | "info" : { 53 | "author" : "xcode", 54 | "version" : 1 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Image.xcassets/burstcamper100.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "burstcamper100.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "filename" : "burstcamper100_Dark.png", 16 | "idiom" : "universal", 17 | "scale" : "1x" 18 | }, 19 | { 20 | "filename" : "burstcamper100@2x.png", 21 | "idiom" : "universal", 22 | "scale" : "2x" 23 | }, 24 | { 25 | "appearances" : [ 26 | { 27 | "appearance" : "luminosity", 28 | "value" : "dark" 29 | } 30 | ], 31 | "filename" : "burstcamper100_Dark@2x.png", 32 | "idiom" : "universal", 33 | "scale" : "2x" 34 | }, 35 | { 36 | "filename" : "burstcamper100@3x.png", 37 | "idiom" : "universal", 38 | "scale" : "3x" 39 | }, 40 | { 41 | "appearances" : [ 42 | { 43 | "appearance" : "luminosity", 44 | "value" : "dark" 45 | } 46 | ], 47 | "filename" : "burstcamper100_Dark@3x.png", 48 | "idiom" : "universal", 49 | "scale" : "3x" 50 | } 51 | ], 52 | "info" : { 53 | "author" : "xcode", 54 | "version" : 1 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource/Resource/Image.xcassets/burstcamperStun100.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "burstcamperStun100.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "filename" : "burstcamperStunDark100.png", 16 | "idiom" : "universal", 17 | "scale" : "1x" 18 | }, 19 | { 20 | "filename" : "burstcamperStun100@2x.png", 21 | "idiom" : "universal", 22 | "scale" : "2x" 23 | }, 24 | { 25 | "appearances" : [ 26 | { 27 | "appearance" : "luminosity", 28 | "value" : "dark" 29 | } 30 | ], 31 | "filename" : "burstcamperStunDark100@2x.png", 32 | "idiom" : "universal", 33 | "scale" : "2x" 34 | }, 35 | { 36 | "filename" : "burstcamperStun100@3x.png", 37 | "idiom" : "universal", 38 | "scale" : "3x" 39 | }, 40 | { 41 | "appearances" : [ 42 | { 43 | "appearance" : "luminosity", 44 | "value" : "dark" 45 | } 46 | ], 47 | "filename" : "burstcamperStunDark100@3x.png", 48 | "idiom" : "universal", 49 | "scale" : "3x" 50 | } 51 | ], 52 | "info" : { 53 | "author" : "xcode", 54 | "version" : 1 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Presentation/Common/View/NormalFeedCell/Header/NormalFeedCellBadgeStackView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultFeedCellBadgeStackView.swift 3 | // Eoljuga 4 | // 5 | // Created by youtak on 2022/11/17. 6 | // 7 | 8 | import UIKit 9 | 10 | class NormalFeedCellBadgeStackView: UIStackView { 11 | 12 | private lazy var domainLabel = UILabel().then { 13 | $0.textColor = UIColor.customOrange 14 | $0.font = UIFont.bold10 15 | $0.text = "iOS" 16 | } 17 | 18 | private lazy var numberLabel = UILabel().then { 19 | $0.textColor = UIColor.customBlue 20 | $0.font = UIFont.bold12 21 | $0.text = "7기" 22 | } 23 | 24 | private lazy var camperIDLabel = UILabel().then { 25 | $0.textColor = UIColor.systemGray2 26 | $0.font = UIFont.bold12 27 | $0.text = "S057" 28 | } 29 | 30 | override init(frame: CGRect) { 31 | super.init(frame: frame) 32 | configureUI() 33 | } 34 | 35 | required init(coder: NSCoder) { 36 | fatalError("init(coder:) has not been implemented") 37 | } 38 | 39 | private func configureUI() { 40 | addArrangedSubViews([domainLabel, numberLabel, camperIDLabel]) 41 | configureStackView() 42 | } 43 | 44 | private func configureStackView() { 45 | axis = .horizontal 46 | distribution = .equalSpacing 47 | alignment = .fill 48 | spacing = Constant.space4.cgFloat 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Presentation/Tab/Detail/View/EmptyView/EmptyFeedView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmptyFeedView.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/28. 6 | // 7 | 8 | import UIKit 9 | 10 | final class EmptyFeedView: UIView { 11 | 12 | private let imageView = UIImageView().then { 13 | $0.image = UIImage.burstcamperStun 14 | } 15 | 16 | private let descriptionLabel = DefaultMultiLineLabel().then { 17 | $0.text = "컨텐츠 내용을 불러오는데 오류가 발생했어요\n블로그에서 내용을 확인해주세요!" 18 | $0.font = .bold14 19 | $0.textColor = .systemGray2 20 | $0.numberOfLines = 2 21 | } 22 | 23 | override init(frame: CGRect) { 24 | super.init(frame: frame) 25 | configureUI() 26 | configureConstraints() 27 | } 28 | 29 | required init?(coder: NSCoder) { 30 | fatalError("init(coder:) has not been implemented") 31 | } 32 | 33 | private func configureUI() { 34 | addSubViews([imageView, descriptionLabel]) 35 | } 36 | 37 | private func configureConstraints() { 38 | imageView.snp.makeConstraints { make in 39 | make.centerX.centerY.equalToSuperview() 40 | make.width.height.equalTo(Constant.Image.profileMedium) 41 | } 42 | 43 | descriptionLabel.snp.makeConstraints { make in 44 | make.centerX.equalToSuperview() 45 | make.top.equalTo(imageView.snp.bottom).offset(Constant.space8) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /modules/BCResource/BCResource/FontSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FontSet.swift 3 | // BCResource 4 | // 5 | // Created by SEUNGMIN OH on 2022/12/07. 6 | // 7 | 8 | import UIKit 9 | 10 | public extension UIFont { 11 | static let extraBold40 = Fonts.NanumSquareOTF.extraBold.font(size: 40) 12 | static let extraBold24 = Fonts.NanumSquareOTF.extraBold.font(size: 24) 13 | static let extraBold20 = Fonts.NanumSquareOTF.extraBold.font(size: 20) 14 | static let extraBold16 = Fonts.NanumSquareOTF.extraBold.font(size: 16) 15 | static let extraBold14 = Fonts.NanumSquareOTF.extraBold.font(size: 14) 16 | static let extraBold12 = Fonts.NanumSquareOTF.extraBold.font(size: 12) 17 | 18 | static let bold20 = Fonts.NanumSquareOTF.bold.font(size: 20) 19 | static let bold16 = Fonts.NanumSquareOTF.bold.font(size: 16) 20 | static let bold14 = Fonts.NanumSquareOTF.bold.font(size: 14) 21 | static let bold12 = Fonts.NanumSquareOTF.bold.font(size: 12) 22 | static let bold10 = Fonts.NanumSquareOTF.bold.font(size: 10) 23 | static let bold8 = Fonts.NanumSquareOTF.bold.font(size: 8) 24 | 25 | static let regular16 = Fonts.NanumSquareOTF.regular.font(size: 16) 26 | static let regular14 = Fonts.NanumSquareOTF.regular.font(size: 14) 27 | static let regular12 = Fonts.NanumSquareOTF.regular.font(size: 12) 28 | static let regular10 = Fonts.NanumSquareOTF.regular.font(size: 10) 29 | static let regular8 = Fonts.NanumSquareOTF.regular.font(size: 8) 30 | } 31 | -------------------------------------------------------------------------------- /Firebase-Functions/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | firebase-debug.log* 8 | firebase-debug.*.log* 9 | 10 | # Firebase cache 11 | .firebase/ 12 | 13 | # Firebase config 14 | 15 | # Uncomment this if you'd like others to create their own Firebase project. 16 | # For a team working on the same Firebase project(s), it is recommended to leave 17 | # it commented so all members can deploy to the same project(s) in .firebaserc. 18 | # .firebaserc 19 | 20 | # Runtime data 21 | pids 22 | *.pid 23 | *.seed 24 | *.pid.lock 25 | 26 | # Directory for instrumented libs generated by jscoverage/JSCover 27 | lib-cov 28 | 29 | # Coverage directory used by tools like istanbul 30 | coverage 31 | 32 | # nyc test coverage 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 36 | .grunt 37 | 38 | # Bower dependency directory (https://bower.io/) 39 | bower_components 40 | 41 | # node-waf configuration 42 | .lock-wscript 43 | 44 | # Compiled binary addons (http://nodejs.org/api/addons.html) 45 | build/Release 46 | 47 | # Dependency directories 48 | node_modules/ 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/App/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleURLTypes 6 | 7 | 8 | CFBundleTypeRole 9 | Editor 10 | CFBundleURLName 11 | com.codershigh.boostcamp.burstcamp 12 | CFBundleURLSchemes 13 | 14 | burstcamp 15 | 16 | 17 | 18 | FirebaseAppDelegateProxyEnabled 19 | NO 20 | UIAppFonts 21 | 22 | NanumSquareB.otf 23 | NanumSquareEB.otf 24 | NanumSquareR.otf 25 | 26 | UIApplicationSceneManifest 27 | 28 | UIApplicationSupportsMultipleScenes 29 | 30 | UISceneConfigurations 31 | 32 | UIWindowSceneSessionRoleApplication 33 | 34 | 35 | UISceneConfigurationName 36 | Default Configuration 37 | UISceneDelegateClassName 38 | $(PRODUCT_MODULE_NAME).SceneDelegate 39 | 40 | 41 | 42 | 43 | UIBackgroundModes 44 | 45 | fetch 46 | remote-notification 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Domain/Model/User/SignUpUser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignUpUser.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/15. 6 | // 7 | 8 | import Foundation 9 | 10 | struct SignUpUser { 11 | private var nickname: String? 12 | private var domain: Domain? 13 | private var camperID: String? 14 | private var blogURL: String = "" 15 | 16 | func getNickname() -> String? { 17 | return nickname 18 | } 19 | 20 | func getDomain() -> Domain? { 21 | return domain 22 | } 23 | 24 | func getCamperID() -> String? { 25 | return camperID 26 | } 27 | 28 | func getBlogURL() -> String { 29 | return blogURL 30 | } 31 | 32 | mutating func setNickname(_ nickname: String) { 33 | self.nickname = nickname 34 | } 35 | 36 | mutating func setDomain(_ domain: Domain) { 37 | self.domain = domain 38 | } 39 | 40 | mutating func setCamperID(_ camperID: String) { 41 | self.camperID = camperID 42 | } 43 | 44 | mutating func setBlogURL(_ blogURL: String) { 45 | self.blogURL = blogURL 46 | } 47 | 48 | mutating func initBlogURL() { 49 | self.blogURL = "" 50 | } 51 | 52 | mutating func removeBlogURLLastSlash() { 53 | var blogURL = blogURL 54 | if let lastIndex = blogURL.lastIndex(of: "/"), 55 | lastIndex == blogURL.index(before: blogURL.endIndex) { 56 | blogURL.removeLast() 57 | setBlogURL(blogURL) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Extension/UI/View/UICollectionView+emptyView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UICollectionView+emptyView.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2022/12/02. 6 | // 7 | 8 | import UIKit 9 | 10 | import SnapKit 11 | 12 | extension UICollectionView { 13 | 14 | func configureEmptyView() { 15 | let emptyView = UIView().then { 16 | $0.frame = CGRect( 17 | x: self.center.x, 18 | y: self.center.y, 19 | width: self.bounds.width, 20 | height: self.bounds.height 21 | ) 22 | } 23 | 24 | let imageView = UIImageView().then { 25 | $0.image = UIImage.burstcamper 26 | } 27 | 28 | let descriptionLabel = UILabel().then { 29 | $0.text = "아직 스크랩한 피드가 없어요" 30 | $0.font = .bold14 31 | $0.textColor = .systemGray2 32 | } 33 | 34 | emptyView.addSubViews([imageView, descriptionLabel]) 35 | 36 | imageView.snp.makeConstraints { make in 37 | make.centerX.centerY.equalToSuperview() 38 | make.width.height.equalTo(Constant.Image.profileMedium) 39 | } 40 | 41 | descriptionLabel.snp.makeConstraints { make in 42 | make.centerX.equalToSuperview() 43 | make.top.equalTo(imageView.snp.bottom).offset(Constant.space8) 44 | } 45 | 46 | self.backgroundView = emptyView 47 | } 48 | 49 | func resetEmptyView() { 50 | self.backgroundView = nil 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Domain/UseCase/Tab/Detail/DefaultFeedDetailUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultFeedDetailUseCase.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/14. 6 | // 7 | 8 | import Foundation 9 | 10 | final class DefaultFeedDetailUseCase: FeedDetailUseCase { 11 | 12 | private let feedRepository: FeedRepository 13 | 14 | init(feedRepository: FeedRepository) { 15 | self.feedRepository = feedRepository 16 | } 17 | 18 | func fetchFeed(by feedUUID: String) async throws -> Feed { 19 | return try await feedRepository.fetchFeed(by: feedUUID) 20 | } 21 | 22 | func scrapFeed(_ feed: Feed, userUUID: String) async throws -> Feed { 23 | return feed.isScraped 24 | ? try await feedRepository.unScrapFeed(feed, userUUID: userUUID) 25 | : try await feedRepository.scrapFeed(feed, userUUID: userUUID) 26 | } 27 | 28 | func blockFeed(_ feed: Feed) async throws { 29 | let user = UserManager.shared.user 30 | let wasScraped = user.scrapFeedUUIDs.contains(feed.feedUUID) 31 | 32 | try await feedRepository.blockFeed(feed, userUUID: user.userUUID, wasScraped: wasScraped) 33 | } 34 | 35 | func reportFeed(_ feed: Feed) async throws { 36 | let user = UserManager.shared.user 37 | let wasScraped = user.scrapFeedUUIDs.contains(feed.feedUUID) 38 | 39 | try await feedRepository.reportFeed(feed, userUUID: user.userUUID) 40 | try await feedRepository.blockFeed(feed, userUUID: user.userUUID, wasScraped: wasScraped) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Presentation/Tab/Detail/View/FeedInfoStackView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FeedInfoStackView.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2022/12/06. 6 | // 7 | 8 | import UIKit 9 | 10 | final class FeedInfoStackView: UIStackView { 11 | 12 | private let titleLabel = DefaultMultiLineLabel().then { 13 | $0.font = .extraBold16 14 | $0.textColor = .dynamicBlack 15 | $0.numberOfLines = 3 16 | } 17 | 18 | private let blogTitleLabel = UILabel().then { 19 | $0.font = .regular12 20 | $0.textColor = .systemGray2 21 | } 22 | 23 | private let pubDateLabel = UILabel().then { 24 | $0.font = .regular12 25 | $0.textColor = .dynamicBlack 26 | } 27 | 28 | override init(frame: CGRect) { 29 | super.init(frame: frame) 30 | configure() 31 | configureUI() 32 | } 33 | 34 | required init(coder: NSCoder) { 35 | fatalError("init(coder:) has not been implemented") 36 | } 37 | 38 | private func configure() { 39 | axis = .vertical 40 | spacing = Constant.space6.cgFloat 41 | alignment = .fill 42 | distribution = .equalSpacing 43 | } 44 | 45 | private func configureUI() { 46 | addArrangedSubViews([titleLabel, blogTitleLabel, pubDateLabel]) 47 | } 48 | } 49 | 50 | extension FeedInfoStackView { 51 | func updateView(feed: Feed) { 52 | titleLabel.text = feed.title 53 | blogTitleLabel.text = feed.writer.blogTitle 54 | pubDateLabel.text = feed.pubDate.monthDateFormatString 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Firebase-Functions/functions/service/withdrawalManager.js: -------------------------------------------------------------------------------- 1 | // import { logger } from 'firebase-functions'; 2 | import { initializeApp, getApps } from 'firebase-admin/app'; 3 | import { logger } from 'firebase-functions/v1'; 4 | import { deleteFCMToken, deleteUserScrapFeed, getUserFeedsUUIDs, updateUserScrapFeedUUIDs } from './firestoreManager.js' 5 | import { deleteFeedsAndUpdateRecommendFeed, getUserScrapFeedsUUIDs } from './firestoreManager.js' 6 | import { deleteUserUUIDAtScrapFeed, deleteUser } from './firestoreManager.js' 7 | 8 | if (!getApps().length) initializeApp() 9 | 10 | export async function deleteUserInfo(userUUID) { 11 | 12 | // 1. 유저가 적은 피드의 아이디들을 가져온다. 13 | const userFeedUUIDs = await getUserFeedsUUIDs(userUUID) 14 | 15 | if (userFeedUUIDs != null) { 16 | logger.log("사용자가 작성한 글이 있다.") 17 | // 2. 내가 쓴 글을 스크랩한 유저의 scrapFeedUUIDs를 업데이트한다. 18 | await updateUserScrapFeedUUIDs(userFeedUUIDs) 19 | 20 | // 3. 내가 쓴 글을 삭제하고 추천 피드를 업데이트한다. 21 | deleteFeedsAndUpdateRecommendFeed(userFeedUUIDs) 22 | } 23 | 24 | // 4. 내가 스크랩한 피드의 아이디들을 가져온다. 25 | const userScrapFeedUUIDs = await getUserScrapFeedsUUIDs(userUUID) 26 | 27 | if (userScrapFeedUUIDs != null ) { 28 | // 5. 내가 스크랩한 피드의 scrapUsers 에서 나의 userUUID를 지운다. 29 | await deleteUserUUIDAtScrapFeed(userUUID, userScrapFeedUUIDs) 30 | } 31 | 32 | // 6. User - ScrapFeed를 지운다. 33 | await deleteUserScrapFeed(userUUID) 34 | 35 | // 7. 유저의 FCM Token을 삭제한다. 36 | await deleteFCMToken(userUUID) 37 | 38 | // 8. 유저를 삭제한다. 39 | await deleteUser(userUUID) 40 | } 41 | 42 | -------------------------------------------------------------------------------- /modules/BCFirebase/BCFirebase/Messaging/BCFirebaseMessaging.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BCFirebaseMessaging.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/02/03. 6 | // 7 | 8 | import Foundation 9 | 10 | import Firebase 11 | 12 | public protocol BCFirebaseMessagingDelegate: AnyObject { 13 | func configureMessaging(token: String?) 14 | func refreshToken(token: String?) 15 | } 16 | 17 | public final class BCFirebaseMessaging: NSObject, MessagingDelegate { 18 | 19 | private let messaging: Messaging 20 | public weak var delegate: BCFirebaseMessagingDelegate? 21 | 22 | public init(messaging: Messaging = Messaging.messaging()) { 23 | self.messaging = messaging 24 | } 25 | 26 | public func saveApnsToken(apnsToken: Data) { 27 | messaging.apnsToken = apnsToken 28 | } 29 | 30 | public func configureMessaging() throws { 31 | messaging.delegate = self 32 | Task { [weak self] in 33 | do { 34 | let token = try await self?.messaging.token() 35 | self?.delegate?.configureMessaging(token: token) 36 | } catch { 37 | debugPrint("BCFirebaseMessaging - configureMessaging 에러") 38 | throw BCFirebaseMessagingError.configureMessaging 39 | } 40 | } 41 | } 42 | 43 | public func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { 44 | if let fcmToken = fcmToken { 45 | delegate?.refreshToken(token: fcmToken) 46 | } 47 | } 48 | } 49 | 50 | public enum BCFirebaseMessagingError: Error { 51 | case configureMessaging 52 | case refreshToken 53 | } 54 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/BCDateFormatter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateFormatter.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2022/11/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct BCDateFormatter { 11 | 12 | static var secondsPerYear: Double { return 365 * 24 * 60 * 60} 13 | static var secondsPerWeek: Double { return 7 * secondsPerDay } 14 | static var secondsPerDay: Double { return 24 * secondsPerHour } 15 | static var secondsPerHour: Double { return 60 * secondsPerMinute } 16 | static var secondsPerMinute: Double { return 60 } 17 | 18 | /// 시간 차이에 따라 String을 리턴해주는 함수 19 | /// - Parameters: 20 | /// - target:블로그 발행시간 21 | /// - standard: 기준 시간. default 값은 현재임 22 | /// - Returns: String 값 23 | /// @discussion 24 | /// - 방금 전 (59초까지) 25 | /// - 1분 전 ~ 59분 전 26 | /// - 1시간 전 ~ 23시간 전 27 | /// - 1일 전 ~ 6일 전 28 | /// - 1년 이전 : m월 d일 ex) 7월 18일 (월과 일 앞에 0붙이지 않음 07월, 05일 x) 29 | /// - 1년 이후 : yy.mm.dd ex) 21.10.25, 21.05.05 (월과 일 앞에 0붙임) 30 | static func relativeTimeString(for target: Date, relativeTo standard: Date = Date()) -> String { 31 | let interval = standard.timeIntervalSince(target) 32 | 33 | switch interval { 34 | case 0.. CGRect { 13 | var padding = super.rightViewRect(forBounds: bounds) 14 | padding.origin.x -= Constant.space16.cgFloat 15 | return padding 16 | } 17 | 18 | init( 19 | placeholder: String, 20 | clearButton: Bool = false 21 | ) { 22 | super.init(frame: .zero) 23 | self.placeholder = placeholder 24 | layer.borderWidth = 1 25 | layer.borderColor = UIColor.systemGray5.cgColor 26 | layer.cornerRadius = Constant.CornerRadius.radius8.cgFloat 27 | font = .regular16 28 | addLeftPadding() 29 | if clearButton { 30 | clearButtonMode = .always 31 | } else { 32 | addRightPadding() 33 | } 34 | } 35 | 36 | required init?(coder: NSCoder) { 37 | fatalError("init(coder:) has not been implemented") 38 | } 39 | 40 | private func addLeftPadding() { 41 | leftView = paddingView() 42 | leftViewMode = .always 43 | } 44 | 45 | private func addRightPadding() { 46 | rightView = paddingView() 47 | rightViewMode = .always 48 | } 49 | 50 | private func paddingView() -> UIView { 51 | return UIView(frame: CGRect( 52 | x: Constant.zero, 53 | y: Constant.zero, 54 | width: Constant.space16, 55 | height: Int(frame.height) 56 | )) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Extension/Type/Date+FormatString.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateFormatter.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2022/11/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Date { 11 | 12 | var monthDateFormatString: String { 13 | let formatter = DateFormatter() 14 | formatter.dateFormat = "M월 d일" 15 | 16 | return formatter.string(from: self) 17 | } 18 | 19 | var yearMonthDateFormatString: String { 20 | let formatter = DateFormatter() 21 | formatter.dateFormat = "yy.MM.dd" 22 | 23 | return formatter.string(from: self) 24 | } 25 | 26 | // swiftlint: disable orphaned_doc_comment 27 | /// 30일 지났는지 확인해주는 함수 28 | /// - Returns: Bool 값 29 | /// @discussion 30 | /// - 1월 1일 -> 1월 31일 true 31 | /// - 1월 2일 -> 1월 31일 false, 2월 1일 true 32 | 33 | // swiftlint: disable force_unwrapping 34 | func isPassed30Days() -> Bool { 35 | // 날짜에 맞추기 위해 뒤에 시간을 잘라줘야됨 2023-01-01-23:00:00 -> 2023-01-01 36 | // 시간이 남아있으면 1월 1일 오후 11시에 업데이트를 하면 1월 30일 오후 11시부터 업데이트가 가능함 37 | // formatter를 통해 시간을 짤라 날짜로만 계산함 38 | let formatter = DateFormatter() 39 | formatter.dateFormat = "yyyy-MM-dd" 40 | let stringDate = formatter.string(from: self) 41 | let targetDate = formatter.date(from: stringDate)! 42 | 43 | let after30Days = Calendar.current.date(byAdding: .day, value: 30, to: targetDate)! 44 | let now = Date() 45 | 46 | // after30Days < now 47 | return after30Days.compare(now) == .orderedAscending ? true : false 48 | } 49 | 50 | func after30Days() -> Date { 51 | return Calendar.current.date(byAdding: .day, value: +30, to: self)! 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Presentation/Common/View/NormalFeedCell/Header/NormalFeedCellHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultFeedCellHeader.swift 3 | // Eoljuga 4 | // 5 | // Created by youtak on 2022/11/17. 6 | // 7 | 8 | import UIKit 9 | 10 | import SnapKit 11 | 12 | class NormalFeedCellHeader: UIStackView { 13 | 14 | private lazy var profileImageView = UIImageView().then { 15 | $0.clipsToBounds = true 16 | $0.layer.cornerRadius = Constant.Image.profileSmall.cgFloat / 2 17 | $0.image = UIImage(systemName: "square.fill") 18 | $0.contentMode = .scaleAspectFill 19 | } 20 | 21 | private lazy var nameLabel = UILabel().then { 22 | $0.textColor = UIColor.systemGray 23 | $0.font = UIFont.bold12 24 | $0.text = "하늘이" 25 | } 26 | 27 | private lazy var badgeStackView = NormalFeedCellBadgeStackView() 28 | 29 | override init(frame: CGRect) { 30 | super.init(frame: frame) 31 | configureUI() 32 | } 33 | 34 | required init(coder: NSCoder) { 35 | fatalError("init(coder:) has not been implemented") 36 | } 37 | 38 | private func configureUI() { 39 | addArrangedSubViews([profileImageView, nameLabel, badgeStackView]) 40 | configureStackView() 41 | configureProfileImageView() 42 | } 43 | 44 | private func configureStackView() { 45 | axis = .horizontal 46 | distribution = .equalSpacing 47 | alignment = .center 48 | spacing = Constant.space8.cgFloat 49 | isLayoutMarginsRelativeArrangement = true 50 | } 51 | 52 | private func configureProfileImageView() { 53 | profileImageView.snp.makeConstraints { 54 | $0.width.height.equalTo(Constant.Image.profileSmall) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Presentation/Common/View/Badge/DefaultBadgeLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultBadgeLabel.swift 3 | // Eoljuga 4 | // 5 | // Created by neuli on 2022/11/18. 6 | // 7 | 8 | import UIKit 9 | 10 | final class DefaultBadgeLabel: UILabel { 11 | 12 | // MARK: - Properties 13 | 14 | private let padding = UIEdgeInsets( 15 | top: Constant.space2.cgFloat, 16 | left: Constant.space8.cgFloat, 17 | bottom: Constant.space2.cgFloat, 18 | right: Constant.space8.cgFloat 19 | ) 20 | 21 | override var intrinsicContentSize: CGSize { 22 | var contentSize = super.intrinsicContentSize 23 | if contentSize.height != 0 && contentSize.width != 0 { 24 | contentSize.height += padding.top + padding.bottom 25 | contentSize.width += padding.left + padding.right 26 | } 27 | return contentSize 28 | } 29 | 30 | // MARK: - Initializer 31 | 32 | init( 33 | textColor: UIColor, 34 | text: String = "" 35 | ) { 36 | super.init(frame: .zero) 37 | font = UIFont.bold10 38 | self.text = text 39 | self.textColor = textColor 40 | backgroundColor = .systemGray5 41 | layer.cornerRadius = Constant.CornerRadius.radius8.cgFloat 42 | clipsToBounds = true 43 | } 44 | 45 | required init?(coder: NSCoder) { 46 | fatalError("init(coder:) has not been implemented") 47 | } 48 | 49 | // MARK: - Methods 50 | 51 | override func drawText(in rect: CGRect) { 52 | super.drawText(in: rect.inset(by: padding)) 53 | } 54 | } 55 | 56 | extension DefaultBadgeLabel { 57 | func updateView(text: String) { 58 | DispatchQueue.main.async { 59 | self.text = text 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Domain/UseCase/Auth/Login/DefaultLoginUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultLoginUseCase.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/14. 6 | // 7 | 8 | import Foundation 9 | 10 | final class DefaultLoginUseCase: LoginUseCase { 11 | 12 | private let loginRepository: LoginRepository 13 | private let userRepository: UserRepository 14 | 15 | init(loginRepository: LoginRepository, userRepository: UserRepository) { 16 | self.loginRepository = loginRepository 17 | self.userRepository = userRepository 18 | } 19 | 20 | func checkIsExist(userUUID: String) async throws -> Bool { 21 | do { 22 | let user = try await userRepository.fetchUser(userUUID) 23 | UserManager.shared.setUser(user) 24 | KeyChainManager.save(user: user) 25 | return true 26 | } catch { 27 | if let error = error as? UserRepositoryError, error == .userNotExist { 28 | return false 29 | } else { 30 | throw LoginUseCaseError.fetchUser 31 | } 32 | } 33 | } 34 | 35 | func isLoggedIn() -> Bool { 36 | return loginRepository.isLoggedIn() && KeyChainManager.readUser() != nil 37 | } 38 | 39 | func loginWithGithub(code: String) async throws -> (userNickname: String, userUUID: String) { 40 | return try await loginRepository.loginWithGithub(code: code) 41 | } 42 | 43 | func loginWithApple(idTokenString: String, nonce: String) async throws -> String { 44 | return try await loginRepository.loginWithApple(idTokenString: idTokenString, nonce: nonce) 45 | } 46 | 47 | func createGuest(userUUID: String) async throws { 48 | let guestUser = try await userRepository.saveGuest(userUUID: userUUID) 49 | UserManager.shared.setUser(guestUser) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Presentation/Tab/Detail/View/EmptyView/LoadingFeedView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingFeedView.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/02/02. 6 | // 7 | 8 | import UIKit 9 | 10 | import SwiftyGif 11 | 12 | final class LoadingFeedView: UIView { 13 | 14 | private var imageView = UIImageView() 15 | 16 | private let descriptionLabel = DefaultMultiLineLabel().then { 17 | $0.text = "로딩 중이에요" 18 | $0.font = .bold14 19 | $0.textColor = .systemGray2 20 | $0.numberOfLines = 2 21 | } 22 | 23 | override init(frame: CGRect) { 24 | super.init(frame: frame) 25 | configureUI() 26 | configureConstraints() 27 | } 28 | 29 | required init?(coder: NSCoder) { 30 | fatalError("init(coder:) has not been implemented") 31 | } 32 | 33 | private func configureUI() { 34 | configureImageView() 35 | addSubViews([imageView, descriptionLabel]) 36 | } 37 | 38 | private func configureImageView() { 39 | do { 40 | let gifImage = try UIImage(gifName: "LoadingBurstcamper.gif") 41 | let imageView = UIImageView(gifImage: gifImage, loopCount: -1) 42 | self.imageView = imageView 43 | } catch { 44 | debugPrint("gif 설정 실패 \(error.localizedDescription)") 45 | imageView.image = UIImage.burstcamper 46 | } 47 | } 48 | 49 | private func configureConstraints() { 50 | imageView.snp.makeConstraints { make in 51 | make.centerX.centerY.equalToSuperview() 52 | make.width.height.equalTo(Constant.Image.profileMedium) 53 | } 54 | 55 | descriptionLabel.snp.makeConstraints { make in 56 | make.centerX.equalToSuperview() 57 | make.top.equalTo(imageView.snp.bottom).offset(Constant.space8) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Presentation/Tab/Home/ViewController/DataSource/HomeFeedListSkeletonDiffableDatasource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeFeedListSkeletonDiffableDatasource.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/26. 6 | // 7 | 8 | import UIKit 9 | 10 | import SkeletonView 11 | 12 | final class HomeFeedListSkeletonDiffableDatasource: 13 | UICollectionViewDiffableDataSource { 14 | override init( 15 | collectionView: UICollectionView, 16 | cellProvider: @escaping UICollectionViewDiffableDataSource.CellProvider 17 | ) { 18 | super.init(collectionView: collectionView, cellProvider: cellProvider) 19 | } 20 | } 21 | 22 | extension HomeFeedListSkeletonDiffableDatasource: SkeletonCollectionViewDataSource { 23 | func collectionSkeletonView( 24 | _ skeletonView: UICollectionView, 25 | cellIdentifierForItemAt indexPath: IndexPath 26 | ) -> SkeletonView.ReusableCellIdentifier { 27 | guard let feedCellType = FeedCellType(index: indexPath.section) else { fatalError("Reusable Identifier 에러") } 28 | switch feedCellType { 29 | case .recommend: 30 | return RecommendFeedCell.identifier 31 | case .normal: 32 | return NormalFeedCell.identifier 33 | } 34 | } 35 | 36 | func numSections(in collectionSkeletonView: UICollectionView) -> Int { 37 | return FeedCellType.count 38 | } 39 | 40 | // 초기 목업 데이터용 41 | func collectionSkeletonView(_ skeletonView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 42 | guard let feedCellType = FeedCellType(index: section) else { fatalError("Reusable Identifier 에러") } 43 | switch feedCellType { 44 | case .recommend: 45 | return 6 46 | case .normal: 47 | return 6 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Presentation/Auth/SignUp/CamperID/SignUpCamperIDViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignUpCamperIDViewModel.swift 3 | // burstcamp 4 | // 5 | // Created by 김기훈 on 2022/12/01. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | final class SignUpCamperIDViewModel { 12 | 13 | private let signUpUseCase: SignUpUseCase 14 | 15 | init(signUpUseCase: SignUpUseCase) { 16 | self.signUpUseCase = signUpUseCase 17 | self.camperDomain = signUpUseCase.getUserDomain() 18 | } 19 | 20 | let camperDomain: Domain 21 | var camperID: String = "" 22 | 23 | struct Input { 24 | let camperIDTextFieldDidEdit: AnyPublisher 25 | let nextButtonDidTap: AnyPublisher 26 | } 27 | 28 | struct Output { 29 | let validateCamperID: AnyPublisher 30 | let moveToBlogView: AnyPublisher 31 | let bindDomainText: Just 32 | let bindRepresentingDomainText: Just 33 | } 34 | 35 | func transform(input: Input) -> Output { 36 | let validateCamperID = input.camperIDTextFieldDidEdit 37 | .map { id in 38 | self.camperID = id 39 | let fullCamperID = "\(self.camperDomain.representing)" + id 40 | self.signUpUseCase.setUserCamperID(fullCamperID) 41 | return id.count == 3 && id.allSatisfy { $0.isNumber } ? true : false 42 | } 43 | .eraseToAnyPublisher() 44 | 45 | let moveToBlogView = input.nextButtonDidTap 46 | 47 | let domainText = Just(camperDomain.rawValue) 48 | 49 | let representingDomainText = Just(camperDomain.representing) 50 | 51 | return Output( 52 | validateCamperID: validateCamperID, 53 | moveToBlogView: moveToBlogView, 54 | bindDomainText: domainText, 55 | bindRepresentingDomainText: representingDomainText 56 | ) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Presentation/Coordinator/ScrapPage/ScrapPageCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScrapPageCoordinator.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2022/12/06. 6 | // 7 | 8 | import Combine 9 | import UIKit 10 | 11 | protocol ScrapPageCoordinatorProtocol: TabBarChildCoordinator { 12 | func moveToFeedDetail(feed: Feed, scrapPageViewController: ScrapPageViewController) 13 | } 14 | 15 | final class ScrapPageCoordinator: ScrapPageCoordinatorProtocol, ContainFeedDetailCoordinator { 16 | var childCoordinators: [Coordinator] = [] 17 | var navigationController: UINavigationController 18 | var cancelBag = Set() 19 | var dependencyFactory: DependencyFactoryProtocol 20 | 21 | init(navigationController: UINavigationController, dependencyFactory: DependencyFactoryProtocol) { 22 | self.navigationController = navigationController 23 | self.dependencyFactory = dependencyFactory 24 | } 25 | } 26 | 27 | extension ScrapPageCoordinator { 28 | func start(viewController: UIViewController) { 29 | guard let scrapPageViewController = viewController as? ScrapPageViewController else { 30 | return 31 | } 32 | 33 | scrapPageViewController.coordinatorPublisher 34 | .sink { [weak self] event in 35 | switch event { 36 | case .moveToFeedDetail(let feed): 37 | self?.moveToFeedDetail(feed: feed, scrapPageViewController: scrapPageViewController) 38 | } 39 | } 40 | .store(in: &cancelBag) 41 | } 42 | 43 | func moveToFeedDetail(feed: Feed, scrapPageViewController: ScrapPageViewController) { 44 | let feedDetailViewController = prepareFeedDetailViewController(feed: feed) 45 | sink(feedDetailViewController, parentViewController: scrapPageViewController) 46 | self.navigationController.pushViewController(feedDetailViewController, animated: true) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Domain/UseCase/Notification/DefaultNotificationUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultNotificationUseCase.swift 3 | // burstcamp 4 | // 5 | // Created by neuli on 2023/01/31. 6 | // 7 | 8 | import Foundation 9 | import UserNotifications 10 | 11 | final class DefaultNotificationUseCase: NotificationUseCase { 12 | 13 | // MARK: - Properties 14 | 15 | private let notificationRepository: NotificationRepository 16 | 17 | // MARK: - Initializer 18 | 19 | init (notificationRepository: NotificationRepository) { 20 | self.notificationRepository = notificationRepository 21 | } 22 | 23 | // MARK: - Methods 24 | 25 | func didReceiveNotification(response: UNNotificationResponse) { 26 | let userInfo = response.notification.request.content.userInfo 27 | guard let feedUUID = userInfo[NotificationKey.feedUUID] as? String else { return } 28 | notificationRepository.saveToUserDefaults(notificationFeedUUID: feedUUID) 29 | NotificationCenter.default.post(name: .Push, object: nil, userInfo: userInfo) 30 | } 31 | 32 | func saveIfDifferentFromTheStoredToken(fcmToken: String?) async throws { 33 | let userUUID = UserManager.shared.user.userUUID 34 | if let fcmToken = fcmToken, 35 | let savedFcmToken = notificationRepository.fcmTokenInUserDefaults(), 36 | fcmToken != savedFcmToken { 37 | notificationRepository.saveToUserDefaults(fcmToken: fcmToken) 38 | try await notificationRepository.saveFCMTokenToFirestore(fcmToken, to: userUUID) 39 | } 40 | } 41 | 42 | func refresh(fcmToken: String?) async throws { 43 | let userUUID = UserManager.shared.user.userUUID 44 | if let fcmToken = fcmToken { 45 | notificationRepository.saveToUserDefaults(fcmToken: fcmToken) 46 | if !userUUID.isEmpty { 47 | try await notificationRepository.saveFCMTokenToFirestore(fcmToken, to: userUUID) 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Extension/AsyncCompatible/Publisher+Async.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Publisher+Async.swift 3 | // burstcamp 4 | // 5 | // Created by SEUNGMIN OH on 2022/12/11. 6 | // 7 | 8 | import protocol Combine.Publisher 9 | import enum Combine.Publishers 10 | import class Combine.Future 11 | 12 | extension Publisher { 13 | func asyncMap( 14 | _ transform: @escaping (Output) async -> T 15 | ) -> Publishers.FlatMap< 16 | Future, 17 | Self 18 | > { 19 | flatMap { value in 20 | Future { promise in 21 | Task { 22 | let output = await transform(value) 23 | promise(.success(output)) 24 | } 25 | } 26 | } 27 | } 28 | 29 | func asyncMap( 30 | _ transform: @escaping (Output) async throws -> T 31 | ) -> Publishers.FlatMap< 32 | Future, 33 | Self 34 | > { 35 | flatMap { value in 36 | Future { promise in 37 | Task { 38 | do { 39 | let output = try await transform(value) 40 | promise(.success(output)) 41 | } catch { 42 | promise(.failure(error)) 43 | } 44 | } 45 | } 46 | } 47 | } 48 | 49 | func asyncMap( 50 | _ transform: @escaping (Output) async throws -> T 51 | ) -> Publishers.FlatMap< 52 | Future, 53 | Publishers.SetFailureType 54 | > { 55 | flatMap { value in 56 | Future { promise in 57 | Task { 58 | do { 59 | let output = try await transform(value) 60 | promise(.success(output)) 61 | } catch { 62 | promise(.failure(error)) 63 | } 64 | } 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /modules/BCFirebase/BCFirebase/Firestore/BCFirestoreUserListener.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BCUserListener.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/17. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | import FirebaseFirestore 12 | 13 | public final class BCFirestoreUserListener { 14 | 15 | private let firestoreService: FirestoreService 16 | private var userListener: ListenerRegistration? 17 | private var userListenerPublisher = PassthroughSubject() 18 | 19 | public init(firestoreService: FirestoreService) { 20 | self.firestoreService = firestoreService 21 | } 22 | 23 | public convenience init() { 24 | let firestoreService = FirestoreService() 25 | self.init(firestoreService: firestoreService) 26 | } 27 | 28 | public func userPublisher(userUUID: String) -> AnyPublisher { 29 | addListener(userUUID: userUUID) 30 | return userListenerPublisher.eraseToAnyPublisher() 31 | } 32 | 33 | private func addListener(userUUID: String) { 34 | let userPath = FirestoreCollection.user.path 35 | let documentReference = firestoreService.getDocumentReference(userPath, document: userUUID) 36 | 37 | self.userListener = documentReference.addSnapshotListener { [weak self] documentSnapshot, error in 38 | if let error = error { 39 | self?.userListenerPublisher.send(completion: .failure(error)) 40 | } 41 | guard let documentSnapshot = documentSnapshot, 42 | let data = documentSnapshot.data() 43 | else { 44 | self?.userListenerPublisher.send(completion: .failure(FirestoreServiceError.userListener)) 45 | return 46 | } 47 | let userAPIModel = UserAPIModel(data: data) 48 | self?.userListenerPublisher.send(userAPIModel) 49 | } 50 | } 51 | 52 | public func removeUserListener() { 53 | userListener?.remove() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Presentation/Common/View/Button/DefaultToggleButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultToggleButton.swift 3 | // burstcamp 4 | // 5 | // Created by SEUNGMIN OH on 2022/11/29. 6 | // 7 | 8 | import Combine 9 | import UIKit 10 | 11 | final class ToggleButton: UIButton { 12 | private let onImage: UIImage? 13 | private let offImage: UIImage? 14 | 15 | var isOn: Bool = false { 16 | didSet { 17 | configure() 18 | } 19 | } 20 | 21 | init( 22 | onImage: UIImage?, 23 | onColor: UIColor, 24 | offImage: UIImage?, 25 | offColor: UIColor 26 | ) { 27 | self.onImage = onImage?.withTintColor(onColor, renderingMode: .alwaysOriginal) 28 | self.offImage = offImage?.withTintColor(offColor, renderingMode: .alwaysOriginal) 29 | super.init(frame: .zero) 30 | } 31 | 32 | convenience init( 33 | image: UIImage?, 34 | onColor: UIColor, 35 | offColor: UIColor 36 | ) { 37 | self.init( 38 | onImage: image, 39 | onColor: onColor, 40 | offImage: image, 41 | offColor: offColor 42 | ) 43 | } 44 | 45 | convenience init( 46 | onImage: UIImage?, 47 | offImage: UIImage? 48 | ) { 49 | self.init( 50 | onImage: onImage, 51 | onColor: .systemBlue, 52 | offImage: offImage, 53 | offColor: .systemGray5 54 | ) 55 | } 56 | 57 | required init?(coder: NSCoder) { 58 | fatalError("init(coder:) has not been implemented") 59 | } 60 | 61 | private func configure() { 62 | let image = isOn ? onImage : offImage 63 | setImage(image, for: .normal) 64 | } 65 | } 66 | 67 | // MARK: Interface 68 | 69 | extension ToggleButton { 70 | var statePublisher: AnyPublisher { 71 | controlPublisher(for: .touchUpInside) 72 | .compactMap { $0 as? ToggleButton } 73 | .map { $0.isOn } 74 | .eraseToAnyPublisher() 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Service/Singleton/KeyChainManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyChainManager.swift 3 | // burstcamp 4 | // 5 | // Created by neuli on 2022/12/03. 6 | // 7 | 8 | import Foundation 9 | import Security 10 | 11 | struct KeyChainManager { 12 | 13 | static let userAccount = "userAccount" 14 | static let githubToken = "githubToken" 15 | 16 | static func save(user: User) { 17 | guard let data = try? JSONEncoder().encode(user) else { return } 18 | 19 | deleteUser() 20 | let query = saveUserQuery(data: data) 21 | SecItemAdd(query, nil) 22 | } 23 | 24 | static func readUser() -> User? { 25 | let query = readUserQuery() 26 | var item: CFTypeRef? 27 | 28 | if SecItemCopyMatching(query, &item) != errSecSuccess { return nil } 29 | 30 | guard let item = item as? [CFString: Any], 31 | let data = item[kSecAttrGeneric] as? Data, 32 | let user = try? JSONDecoder().decode(User.self, from: data) 33 | else { return nil } 34 | return user 35 | } 36 | 37 | static func deleteUser() { 38 | let query = deleteUserQuery() 39 | SecItemDelete(query) 40 | } 41 | 42 | private static func saveUserQuery(data: Data) -> CFDictionary { 43 | return [ 44 | kSecClass: kSecClassGenericPassword, 45 | kSecAttrAccount: userAccount, 46 | kSecAttrGeneric: data 47 | ] as CFDictionary 48 | } 49 | 50 | private static func readUserQuery() -> CFDictionary { 51 | return [ 52 | kSecClass: kSecClassGenericPassword, 53 | kSecAttrAccount: userAccount, 54 | kSecMatchLimit: kSecMatchLimitOne, 55 | kSecReturnAttributes: true, 56 | kSecReturnData: true 57 | ] as CFDictionary 58 | } 59 | 60 | private static func deleteUserQuery() -> CFDictionary { 61 | return [ 62 | kSecClass: kSecClassGenericPassword, 63 | kSecAttrAccount: userAccount 64 | ] as CFDictionary 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Service/Singleton/UserManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserManager.swift 3 | // burstcamp 4 | // 5 | // Created by neuli on 2022/11/30. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | import BCFirebase 12 | 13 | final class UserManager { 14 | 15 | static let shared = UserManager() 16 | 17 | private(set) var user = User(dictionary: [:]) 18 | private let bCFirestoreUserListener = BCFirestoreUserListener() 19 | private let bcFirebaseAuthService = BCFirebaseAuthService() 20 | let userUpdatePublisher = PassthroughSubject() 21 | 22 | private var cancelBag = Set() 23 | 24 | private init() {} 25 | 26 | private func userByKeyChain() { 27 | if let user = KeyChainManager.readUser() { 28 | self.user = user 29 | } 30 | } 31 | 32 | func appStart() { 33 | userByKeyChain() 34 | } 35 | 36 | func setUser(_ user: User) { 37 | self.user = user 38 | } 39 | 40 | func addUserListener() { 41 | if user.userUUID.isEmpty { fatalError("Listenr를 위한 UserUUID가 없음") } 42 | bCFirestoreUserListener.userPublisher(userUUID: user.userUUID) 43 | .sink { [weak self] completion in 44 | switch completion { 45 | case .failure(let error): self?.userUpdatePublisher.send(completion: .failure(error)) 46 | case .finished: return 47 | } 48 | } 49 | receiveValue: { [weak self] userAPIModel in 50 | let user = User(userAPIModel: userAPIModel) 51 | self?.user = user 52 | self?.userUpdatePublisher.send(user) 53 | } 54 | .store(in: &cancelBag) 55 | } 56 | 57 | func removeUserListener() { 58 | bCFirestoreUserListener.removeUserListener() 59 | } 60 | 61 | func deleteUserInfo() { 62 | user = User(dictionary: [:]) 63 | } 64 | 65 | func setUserUUID(_ userUUID: String) { 66 | user = User(dictionary: ["userUUID": userUUID]) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | env: 4 | PROJECT: 'burstcamp' 5 | 6 | on: 7 | pull_request: 8 | branches: [ main, develop ] 9 | 10 | jobs: 11 | build: 12 | runs-on: macos-12 13 | strategy: 14 | matrix: 15 | include: 16 | - xcode: "14.0" 17 | ios: "16.0" 18 | simulator: "iPhone 13 Pro" 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v3 23 | 24 | - name: Create secret file 25 | env: 26 | FIREBASE_SECRET: ${{ secrets.FIREBASE_SECRET }} 27 | API_SECRET: ${{ secrets.API_SECRET }} 28 | run: | 29 | echo $FIREBASE_SECRET | base64 -D -o ${{ env.PROJECT }}/GoogleService-Info.plist 30 | echo $API_SECRET | base64 -D -o ${{ env.PROJECT }}/Service-Info.plist 31 | ls -al ${{ env.PROJECT }} 32 | 33 | - name: Select Xcode ${{ matrix.xcode }} 34 | run: sudo xcode-select -switch /Applications/Xcode_${{ matrix.xcode}}.app && /usr/bin/xcodebuild -version 35 | 36 | - name: Cache SwiftPM 37 | uses: actions/cache@v3 38 | with: 39 | path: ~/Library/Developer/Xcode/DerivedData/${{ env.PROJECT }}*/SourcePackages/ 40 | key: ${{ runner.os }}-spm-${{ hashFiles('${{ env.PROJECT }}.xcworkspace/xcshareddata/swiftpm/Package.resolved') }} 41 | restore-keys: | 42 | ${{ runner.os }}-spm- 43 | 44 | - name: Cache DerivedData 45 | uses: actions/cache@v3 46 | with: 47 | path: ~/Library/Developer/Xcode/DerivedData 48 | key: ${{ runner.os }}-iOS_derived_data-xcode_${{ matrix.xcode }} 49 | restore-keys: | 50 | ${{ runner.os }}-iOS_derived_data- 51 | 52 | - name: Build iOS ${{ matrix.ios }} on ${{ matrix.simulator }} 53 | env: 54 | XCODEPROJ: "${{ env.PROJECT }}/${{ env.PROJECT }}.xcodeproj" 55 | run: > 56 | xcodebuild build 57 | -project ${{ env.XCODEPROJ }} 58 | -scheme burstcamp 59 | -destination 'platform=iOS Simulator,OS=${{ matrix.ios }},name=${{ matrix.simulator }}' 60 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Presentation/Common/View/Button/AuthButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthButton.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/30. 6 | // 7 | 8 | import UIKit 9 | 10 | enum AuthButtonKind { 11 | case apple 12 | case github 13 | } 14 | 15 | final class AuthButton: UIButton { 16 | 17 | init( 18 | kind: AuthButtonKind 19 | ) { 20 | super.init(frame: .zero) 21 | 22 | let text = getText(kind: kind) 23 | var string = AttributedString(text) 24 | string.font = UIFont.extraBold14 25 | string.foregroundColor = .dynamicWhite 26 | 27 | var configuration = UIButton.Configuration.filled() 28 | configuration.attributedTitle = string 29 | configuration.titleAlignment = .center 30 | 31 | configuration.image = getImage(kind: kind) 32 | configuration.imagePlacement = .leading 33 | configuration.imagePadding = 20 34 | 35 | configuration.baseBackgroundColor = .dynamicBlack 36 | configuration.cornerStyle = .medium 37 | 38 | configuration.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 20) 39 | 40 | self.configuration = configuration 41 | } 42 | 43 | required init?(coder: NSCoder) { 44 | fatalError("init(coder:) has not been implemented") 45 | } 46 | 47 | private func getText(kind: AuthButtonKind) -> String { 48 | switch kind { 49 | case .apple: return "Apple로 로그인" 50 | case .github: return "Github으로 로그인" 51 | } 52 | } 53 | 54 | private func getImage(kind: AuthButtonKind) -> UIImage? { 55 | switch kind { 56 | case .apple: 57 | guard #available(iOS 16, *) else { 58 | return UIImage(systemName: "applelogo")?.withTintColor(UIColor.dynamicWhite, renderingMode: .alwaysOriginal) 59 | } 60 | return UIImage(systemName: "apple.logo")?.withTintColor(UIColor.dynamicWhite, renderingMode: .alwaysOriginal) 61 | case .github: 62 | return .github 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Util/Error/NetworkError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkError.swift 3 | // FireStoreTest 4 | // 5 | // Created by neuli on 2022/11/20. 6 | // 7 | 8 | import Foundation 9 | 10 | enum NetworkError: Int, LocalizedError { 11 | case responseDecoingError 12 | case queryEncodingError 13 | case decodingError 14 | case encodingError 15 | case invalidURLError 16 | case invalidDataError 17 | case unknownError 18 | case invalidRequestError = 400 19 | case authenticationError = 401 20 | case forbiddenError = 403 21 | case notFoundError = 404 22 | case notAllowedHTTPMethodError = 405 23 | case timeoutError = 408 24 | case internalServerError = 500 25 | case notSupportedError = 501 26 | case badGatewayError = 502 27 | case invalidServiceError = 503 28 | } 29 | 30 | extension NetworkError { 31 | var errorDescription: String { 32 | switch self { 33 | case .responseDecoingError: return "RESPONSE_DECODING_ERROR" 34 | case .queryEncodingError: return "QUERY_ENCODING_ERROR" 35 | case .decodingError: return "DECODING_ERROR" 36 | case .encodingError: return "ENCODING_ERROR" 37 | case .invalidURLError: return " INVALID_URL_ERROR" 38 | case .invalidDataError: return "INVALID_DATA_ERROR" 39 | case .unknownError: return "UNKNOWN_ERROR" 40 | case .invalidRequestError: return "400:INVALID_URL_ERROR" 41 | case .authenticationError: return "401:AUTHENTICATION_FAILURE_ERROR" 42 | case .forbiddenError: return "403:AUTHENTICATION_FAILURE_ERROR" 43 | case .notFoundError: return "404:NOT_FOUND_ERROR" 44 | case .notAllowedHTTPMethodError: return "405:NOT_ALLOWED_HTTP_METHOD_ERROR" 45 | case .timeoutError: return "408:TIMEOUT_ERROR" 46 | case .internalServerError: return "500:INTERNAL_SERVER_ERROR" 47 | case .notSupportedError: return "501:NOT_SUPPORTED_ERROR" 48 | case .badGatewayError: return "502:BAD_GATEWAY_ERROR" 49 | case .invalidServiceError: return "503:INVALID_SERVICE_ERROR" 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Data/Repositories/NotificationRepository/DefaultNotificationRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultNotificationRepository.swift 3 | // burstcamp 4 | // 5 | // Created by neuli on 2023/01/31. 6 | // 7 | 8 | import Foundation 9 | 10 | import BCFirebase 11 | 12 | final class DefaultNotificationRepository: NotificationRepository { 13 | 14 | // MARK: - Properties 15 | 16 | private let userDefaultsService: UserDefaultsService 17 | private let bcFirestoreService: BCFirestoreServiceProtocol 18 | private let fcmTokenKey = UserDefaultsKey.fcmTokenKey 19 | private let notificationFeedUUIDKey = UserDefaultsKey.notificationFeedUUIDKey 20 | 21 | // MARK: Initializer 22 | 23 | init( 24 | userDefaultsService: UserDefaultsService, 25 | bcFirestoreService: BCFirestoreServiceProtocol 26 | ) { 27 | self.userDefaultsService = userDefaultsService 28 | self.bcFirestoreService = bcFirestoreService 29 | } 30 | 31 | // MARK: - Methods 32 | // MARK: FCMToken 33 | 34 | func saveToUserDefaults(fcmToken: String) { 35 | userDefaultsService.save(value: fcmToken, forKey: fcmTokenKey) 36 | } 37 | 38 | func fcmTokenInUserDefaults() -> String? { 39 | return userDefaultsService.stringValue(forKey: fcmTokenKey) 40 | } 41 | 42 | func saveFCMTokenToFirestore(_ fcmToken: String, to userUUID: String) async throws { 43 | do { 44 | return try await bcFirestoreService.saveFCMToken(fcmToken, to: userUUID) 45 | } catch { 46 | throw NotificationRepositoryError.failedToSaveFCMToken 47 | } 48 | } 49 | 50 | // MARK: NotificationFeedUUID 51 | 52 | func saveToUserDefaults(notificationFeedUUID: String) { 53 | userDefaultsService.save(value: notificationFeedUUID, forKey: notificationFeedUUIDKey) 54 | } 55 | 56 | func notificationFeedUUIDInUserDefaults() -> String? { 57 | return userDefaultsService.stringValue(forKey: notificationFeedUUIDKey) 58 | } 59 | 60 | func removeNotificationFeedUUID() { 61 | userDefaultsService.delete(forKey: notificationFeedUUIDKey) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Data/Local/FeedMockUpDatasource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FeedMockUpDatasource.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/31. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol FeedMockUpDataSource { 11 | func createMockUpRecommendFeedList(count: Int) -> [Feed] 12 | } 13 | 14 | final class DefaultFeedMockUpDataSource: FeedMockUpDataSource { 15 | 16 | func createMockUpRecommendFeedList(count: Int) -> [Feed] { 17 | if count >= 3 { 18 | return [createMockUpRecommendFeed1(), createMockUpRecommendFeed2(), createMockUpRecommendFeed3()] 19 | } else if count == 2 { 20 | return [createMockUpRecommendFeed1(), createMockUpRecommendFeed2()] 21 | } else if count == 1 { 22 | return [createMockUpRecommendFeed1()] 23 | } else { 24 | return [] 25 | } 26 | } 27 | 28 | private func createMockUpRecommendFeed1() -> Feed { 29 | return Feed( 30 | feedUUID: UUID().uuidString, 31 | writer: FeedWriter.getBurcam(domain: .web), 32 | title: "버스트 캠프에 오신걸 환영해요 🥳", 33 | pubDate: Date(), 34 | url: "", 35 | thumbnailURL: "", 36 | content: "", 37 | scrapCount: 0, 38 | isScraped: false 39 | ) 40 | } 41 | 42 | private func createMockUpRecommendFeed2() -> Feed { 43 | return Feed( 44 | feedUUID: UUID().uuidString, 45 | writer: FeedWriter.getBurcam(domain: .android), 46 | title: "캠퍼들의 블로그를 둘러보며 함께 성장해요 🔥", 47 | pubDate: Date(), 48 | url: "", 49 | thumbnailURL: "", 50 | content: "", 51 | scrapCount: 0, 52 | isScraped: false 53 | ) 54 | } 55 | 56 | private func createMockUpRecommendFeed3() -> Feed { 57 | return Feed( 58 | feedUUID: UUID().uuidString, 59 | writer: FeedWriter.getBurcam(domain: .iOS), 60 | title: "Burstcamp! Burstcamp! Burstcamp!", 61 | pubDate: Date(), 62 | url: "", 63 | thumbnailURL: "", 64 | content: "", 65 | scrapCount: 0, 66 | isScraped: false 67 | ) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Domain/UseCase/Tab/MyPage/MyPageEdit/DefaultMyPageEditUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultMyPageEditUseCase.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/14. 6 | // 7 | 8 | import Foundation 9 | 10 | final class DefaultMyPageEditUseCase: MyPageEditUseCase { 11 | 12 | private let imageRepository: ImageRepository 13 | private let userRepository: UserRepository 14 | 15 | init(imageRepository: ImageRepository, userRepository: UserRepository) { 16 | self.imageRepository = imageRepository 17 | self.userRepository = userRepository 18 | } 19 | 20 | func isValidNickname(_ nickname: String) async throws -> MyPageEditNicknameValidation { 21 | guard Validator.validate(nickname: nickname) else { 22 | return .regexError 23 | } 24 | 25 | guard try await !userRepository.isNicknameExist(nickname) else { 26 | return .duplicateError 27 | } 28 | 29 | return .success 30 | } 31 | 32 | func isValidBlogURL(_ blogURL: String) -> MyPageEditBlogValidation { 33 | return Validator.validateIsEmpty(blogLink: blogURL) ? .success : .regexError 34 | } 35 | 36 | func updateUser(user: User, imageData: Data?) async throws { 37 | let updateUser = user.setUpdateDate() 38 | let imageUpdateUser = try await updateFirestorageImage(imageData, to: updateUser) 39 | try await userRepository.updateUser(imageUpdateUser) 40 | UserManager.shared.setUser(imageUpdateUser) 41 | } 42 | 43 | // private func isUserChanged() -> Bool { 44 | // return editedUser != beforeUser 45 | // } 46 | // 47 | // private func isUserBlogURLChanged() -> Bool { 48 | // return editedUser.blogURL != beforeUser.blogURL 49 | // } 50 | 51 | private func updateFirestorageImage(_ imageData: Data?, to user: User) async throws -> User { 52 | // 새로운 이미지 업로드하면 기존 이미지는 덮어씌여짐. 굳이 삭제할 필요가 없음 53 | if let imageData = imageData { 54 | let newProfileImageURL = try await imageRepository.saveProfileImage( 55 | imageData: imageData, 56 | userUUID: user.userUUID 57 | ) 58 | return user.setProfileImageURL(newProfileImageURL) 59 | } 60 | return user 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Domain/Model/Feed/FeedWriter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FeedWriter.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2022/11/30. 6 | // 7 | 8 | import Foundation 9 | 10 | import BCFirebase 11 | 12 | struct FeedWriter: Hashable { 13 | let userUUID: String 14 | let nickname: String 15 | let camperID: String 16 | let ordinalNumber: Int 17 | let domain: Domain 18 | let profileImageURL: String 19 | let blogTitle: String 20 | } 21 | 22 | extension FeedWriter { 23 | init(data: [String: Any]) { 24 | self.userUUID = data["userUUID"] as? String ?? "" 25 | self.nickname = data["nickname"] as? String ?? "" 26 | self.camperID = data["camperID"] as? String ?? "" 27 | self.ordinalNumber = data["ordinalNumber"] as? Int ?? 0 28 | let domainString = data["domain"] as? String ?? "" 29 | self.domain = Domain(rawValue: domainString) ?? Domain.iOS 30 | self.profileImageURL = data["profileImageURL"] as? String ?? "" 31 | self.blogTitle = data["blogTitle"] as? String ?? "" 32 | } 33 | 34 | init(feedAPIModel: FeedAPIModel) { 35 | self.userUUID = feedAPIModel.writerUUID 36 | self.nickname = feedAPIModel.writerNickname 37 | self.camperID = feedAPIModel.writerCamperID 38 | self.ordinalNumber = feedAPIModel.writerOrdinalNumber 39 | let domainString = feedAPIModel.writerDomain 40 | self.domain = Domain(rawValue: domainString) ?? Domain.iOS 41 | self.profileImageURL = feedAPIModel.writerProfileImageURL 42 | self.blogTitle = feedAPIModel.writerBlogTitle 43 | } 44 | } 45 | 46 | extension FeedWriter { 47 | /// Mock Init 48 | init() { 49 | self.userUUID = "" 50 | self.nickname = "" 51 | self.camperID = "" 52 | self.ordinalNumber = 0 53 | self.domain = .iOS 54 | self.profileImageURL = "" 55 | self.blogTitle = "" 56 | } 57 | 58 | static func getBurcam(domain: Domain) -> FeedWriter { 59 | return FeedWriter( 60 | userUUID: UUID().uuidString, 61 | nickname: "버캠이", 62 | camperID: "", 63 | ordinalNumber: 7, 64 | domain: domain, 65 | profileImageURL: "", 66 | blogTitle: "" 67 | ) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Presentation/Common/View/Badge/DefaultBadgeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultBadgeView.swift 3 | // Eoljuga 4 | // 5 | // Created by neuli on 2022/11/19. 6 | // 7 | 8 | import UIKit 9 | 10 | final class DefaultBadgeView: UIView { 11 | 12 | // MARK: - Properties 13 | 14 | private lazy var domainLabel = DefaultBadgeLabel( 15 | textColor: Domain.iOS.color 16 | ) 17 | 18 | private lazy var numberLabel = DefaultBadgeLabel( 19 | textColor: .main 20 | ) 21 | 22 | private lazy var camperIDLabel = DefaultBadgeLabel( 23 | textColor: .systemGray2 24 | ) 25 | 26 | // MARK: - Initializer 27 | 28 | init() { 29 | super.init(frame: .zero) 30 | configureUI() 31 | } 32 | 33 | required init?(coder: NSCoder) { 34 | fatalError("init(coder:) has not been implemented") 35 | } 36 | 37 | // MARK: - Methods 38 | 39 | private func configureUI() { 40 | let badgeStackView = UIStackView( 41 | arrangedSubviews: [domainLabel, numberLabel, camperIDLabel] 42 | ).then { 43 | $0.axis = .horizontal 44 | $0.distribution = .equalSpacing 45 | $0.alignment = .fill 46 | $0.spacing = Constant.space4.cgFloat 47 | } 48 | addSubview(badgeStackView) 49 | badgeStackView.snp.makeConstraints { make in 50 | make.top.leading.bottom.equalToSuperview() 51 | } 52 | } 53 | } 54 | 55 | extension DefaultBadgeView { 56 | func updateView(user: User) { 57 | domainLabel.updateView(text: user.domain.rawValue) 58 | domainLabel.textColor = user.domain.color 59 | let number = "\(user.ordinalNumber)기" 60 | numberLabel.updateView(text: number) 61 | camperIDLabel.updateView(text: user.camperID) 62 | } 63 | 64 | func updateView(feedWriter: FeedWriter) { 65 | domainLabel.updateView(text: feedWriter.domain.rawValue) 66 | domainLabel.textColor = feedWriter.domain.color 67 | let number = "\(feedWriter.ordinalNumber)기" 68 | numberLabel.updateView(text: number) 69 | camperIDLabel.updateView(text: feedWriter.camperID) 70 | } 71 | 72 | func reset() { 73 | domainLabel.text = nil 74 | numberLabel.text = nil 75 | camperIDLabel.text = nil 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Presentation/Common/View/RecommendCell/RecommendFeedHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecommendFeedHeader.swift 3 | // Eoljuga 4 | // 5 | // Created by youtak on 2022/11/20. 6 | // 7 | 8 | import UIKit 9 | 10 | import SkeletonView 11 | 12 | final class RecommendFeedHeader: UICollectionReusableView { 13 | 14 | private let titleText = "이번 주\n캠퍼들의 PICK" 15 | 16 | private lazy var titleAttributeText = NSMutableAttributedString(string: titleText).then { 17 | let string = titleText as NSString 18 | let length = string.length 19 | $0.addAttribute( 20 | .font, value: UIFont.extraBold24, range: NSRange(location: 0, length: length) 21 | ) 22 | $0.addAttribute(.foregroundColor, value: UIColor.main, range: string.range(of: "이")) 23 | $0.addAttribute(.foregroundColor, value: UIColor.main, range: string.range(of: "P")) 24 | let paragraphStyle = NSMutableParagraphStyle() 25 | paragraphStyle.lineSpacing = 12 26 | $0.addAttribute( 27 | .paragraphStyle, 28 | value: paragraphStyle, 29 | range: NSRange(location: 0, length: length) 30 | ) 31 | } 32 | 33 | private lazy var titleLabel = DefaultMultiLineLabel().then { 34 | $0.numberOfLines = 2 35 | } 36 | 37 | override init(frame: CGRect) { 38 | super.init(frame: frame) 39 | configureUI() 40 | configureSkeleton() 41 | } 42 | 43 | required init?(coder: NSCoder) { 44 | fatalError("init(coder:) has not been implemented") 45 | } 46 | 47 | private func configureUI() { 48 | addSubview(titleLabel) 49 | configureTitleLabel() 50 | } 51 | 52 | private func configureTitleLabel() { 53 | titleLabel.snp.makeConstraints { 54 | $0.edges.equalToSuperview() 55 | } 56 | } 57 | 58 | private func configureSkeleton() { 59 | titleLabel.isSkeletonable = true 60 | titleLabel.skeletonTextNumberOfLines = 2 61 | titleLabel.linesCornerRadius = Constant.CornerRadius.radius4 62 | titleLabel.skeletonTextLineHeight = .fixed(28) 63 | } 64 | } 65 | 66 | extension RecommendFeedHeader { 67 | func updateTitleLabel() { 68 | DispatchQueue.main.async { 69 | self.titleLabel.attributedText = self.titleAttributeText 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Presentation/Common/View/RecommendCell/RecommendFeedUserView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecommendFeedUserView.swift 3 | // Eoljuga 4 | // 5 | // Created by youtak on 2022/11/20. 6 | // 7 | 8 | import UIKit 9 | 10 | final class RecommendFeedUserView: UIStackView { 11 | 12 | private lazy var profileImageView = UIImageView().then { 13 | $0.clipsToBounds = true 14 | $0.layer.cornerRadius = Constant.Image.profileSmall.cgFloat / 2 15 | $0.contentMode = .scaleAspectFill 16 | } 17 | 18 | private lazy var nicknameLabel = UILabel().then { 19 | $0.textColor = .black 20 | $0.font = .bold12 21 | } 22 | 23 | private lazy var blogTitleLabel = UILabel().then { 24 | $0.textColor = .black 25 | $0.font = .regular8 26 | } 27 | 28 | override init(frame: CGRect) { 29 | super.init(frame: frame) 30 | configureUI() 31 | } 32 | 33 | required init(coder: NSCoder) { 34 | fatalError("init(coder:) has not been implemented") 35 | } 36 | 37 | private func configureUI() { 38 | addArrangedSubViews([profileImageView, nicknameLabel, blogTitleLabel]) 39 | configureStackView() 40 | configureProfileImageView() 41 | } 42 | 43 | private func configureStackView() { 44 | axis = .horizontal 45 | distribution = .equalSpacing 46 | alignment = .center 47 | spacing = Constant.space8.cgFloat 48 | isLayoutMarginsRelativeArrangement = true 49 | } 50 | 51 | private func configureProfileImageView() { 52 | profileImageView.snp.makeConstraints { 53 | $0.width.height.equalTo(Constant.Image.profileSmall) 54 | } 55 | } 56 | 57 | private func updateProfileImage(profileImageURL: String) { 58 | if profileImageURL.isEmpty { 59 | profileImageView.image = .burstcamper 60 | } else { 61 | profileImageView.setImage(urlString: profileImageURL) 62 | } 63 | } 64 | } 65 | 66 | extension RecommendFeedUserView { 67 | func updateView(feedWriter: FeedWriter) { 68 | updateProfileImage(profileImageURL: feedWriter.profileImageURL) 69 | nicknameLabel.text = feedWriter.nickname 70 | blogTitleLabel.text = feedWriter.blogTitle 71 | } 72 | 73 | func resetUserImage() { 74 | profileImageView.image = nil 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /modules/Manager/RealmManager/RealmManager/WriteTransaction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WriteTransaction.swift 3 | // RealmManager 4 | // 5 | // Created by SEUNGMIN OH on 2022/12/13. 6 | // 7 | 8 | import struct RealmSwift.Realm 9 | import class RealmSwift.Object 10 | 11 | public final class WriteTransaction { 12 | 13 | private let realm: Realm 14 | 15 | internal init(realm: Realm) { 16 | self.realm = realm 17 | } 18 | 19 | public func add( 20 | _ value: T, 21 | update: Realm.UpdatePolicy = .modified 22 | ) { 23 | realm.add(value.realmModel(), update: update) 24 | } 25 | 26 | /// Object Model을 직접 받는 add 27 | public func add( 28 | _ value: T, 29 | update: Realm.UpdatePolicy = .modified 30 | ) { 31 | realm.add(value, update: update) 32 | } 33 | 34 | /// Object Model을 직접 받고 autoIncrement를 지원 35 | public func add( 36 | _ value: T, 37 | autoIncrement: Bool, 38 | defaultIndex: Int = 0, 39 | update: Realm.UpdatePolicy = .modified 40 | ) where T: Object & AutoIncrementable { 41 | if autoIncrement { 42 | let maxIndex = realm.objects(T.self) 43 | .map(\.autoIndex) 44 | .max() ?? defaultIndex 45 | 46 | value.autoIndex = maxIndex + 1 47 | } 48 | realm.add(value, update: update) 49 | } 50 | 51 | /// auto Increment를 지원 52 | public func add( 53 | _ value: T, 54 | autoIncrement: Bool, 55 | defaultIndex: Int = 0, 56 | update: Realm.UpdatePolicy = .modified 57 | ) where T.RealmModel: AutoIncrementable { 58 | add(value.realmModel(), autoIncrement: autoIncrement) 59 | } 60 | 61 | public func update( 62 | _ type: T.Type, 63 | values: [T.PropertyValue] 64 | ) { 65 | var dictionary: [String: Any] = [:] 66 | values.forEach { 67 | let pair = $0.propertyValuePair 68 | dictionary[pair.name] = pair.value 69 | } 70 | 71 | realm.create(T.RealmModel.self, value: dictionary, update: .modified) 72 | } 73 | 74 | /// Object Model을 직접 받고 autoIncrement를 지원 75 | public func delete( 76 | _ value: T 77 | ) where T: Object { 78 | realm.delete(value) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Service/Singleton/UserDefaultsManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultManager.swift 3 | // Eoljuga 4 | // 5 | // Created by neuli on 2022/11/21. 6 | // 7 | 8 | import Foundation 9 | 10 | // TODO: 삭제될 아이입니다. 11 | struct UserDefaultsManager { 12 | 13 | private static let appearanceKey = "AppearanceKey" 14 | private static let fcmTokenKey = "fcmTokenKey" 15 | private static let isForegroundKey = "isForegroundKey" 16 | private static let notificationFeedUUIDKey = "notificationFeedUUIDKey" 17 | 18 | private static var etagKeys: [String] = [] 19 | 20 | // dark mode 21 | static func saveAppearance(appearance: Appearance) { 22 | UserDefaults.standard.set(appearance.theme, forKey: appearanceKey) 23 | } 24 | 25 | static func currentAppearance() -> Appearance { 26 | guard let appearanceString = UserDefaults.standard.string(forKey: appearanceKey), 27 | let currentAppearance = Appearance(rawValue: appearanceString) 28 | else { return .light } 29 | return currentAppearance 30 | } 31 | 32 | // etag 33 | static func save(etag: String, urlString: String) { 34 | etagKeys.append(etag) 35 | UserDefaults.standard.set(etag, forKey: urlString) 36 | } 37 | 38 | static func etag(urlString: String) -> String? { 39 | return UserDefaults.standard.string(forKey: urlString) 40 | } 41 | 42 | static func removeAllEtags() { 43 | etagKeys.forEach { 44 | UserDefaults.standard.removeObject(forKey: $0) 45 | } 46 | } 47 | 48 | // TODO: 이미지 메모리 캐시 etag 정보 앱 종료시 모두 삭제 ㅇㅅㅇ.. 49 | 50 | // FCMToken 51 | static func save(fcmToken: String) { 52 | UserDefaults.standard.set(fcmToken, forKey: fcmToken) 53 | } 54 | 55 | static func fcmToken() -> String? { 56 | return UserDefaults.standard.string(forKey: fcmTokenKey) 57 | } 58 | 59 | // notificationFeedUUID 60 | static func save(notificationFeedUUID: String) { 61 | UserDefaults.standard.set(notificationFeedUUID, forKey: notificationFeedUUIDKey) 62 | } 63 | 64 | static func notificationFeedUUID() -> String? { 65 | return UserDefaults.standard.string(forKey: notificationFeedUUIDKey) 66 | } 67 | 68 | static func removeNotificationFeedUUID() { 69 | UserDefaults.standard.removeObject(forKey: notificationFeedUUIDKey) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Presentation/Coordinator/Home/HomeCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeCoordinator.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2022/12/06. 6 | // 7 | 8 | import Combine 9 | import UIKit 10 | 11 | protocol HomeCoordinatorProtocol: TabBarChildCoordinator { 12 | func moveToFeedDetail(feed: Feed, homeViewController: HomeViewController) 13 | func moveToFeedDetail(feedUUID: String) 14 | func moveToBlogSafari(url: URL) 15 | } 16 | 17 | final class HomeCoordinator: HomeCoordinatorProtocol, ContainFeedDetailCoordinator { 18 | var childCoordinators: [Coordinator] = [] 19 | var navigationController: UINavigationController 20 | var cancelBag = Set() 21 | var dependencyFactory: DependencyFactoryProtocol 22 | 23 | init(navigationController: UINavigationController, dependencyFactory: DependencyFactoryProtocol) { 24 | self.navigationController = navigationController 25 | self.dependencyFactory = dependencyFactory 26 | } 27 | } 28 | 29 | extension HomeCoordinator { 30 | func start(viewController: UIViewController) { 31 | guard let homeViewController = viewController as? HomeViewController else { 32 | return 33 | } 34 | 35 | homeViewController.coordinatorPublisher 36 | .sink { [weak self] event in 37 | switch event { 38 | case .moveToFeedDetail(let feed): 39 | self?.moveToFeedDetail(feed: feed, homeViewController: homeViewController) 40 | case .moveToBlogSafari(let url): 41 | self?.moveToBlogSafari(url: url) 42 | } 43 | } 44 | .store(in: &cancelBag) 45 | } 46 | 47 | func moveToFeedDetail(feed: Feed, homeViewController: HomeViewController) { 48 | let feedDetailViewController = prepareFeedDetailViewController(feed: feed) 49 | sink(feedDetailViewController, parentViewController: homeViewController) 50 | self.navigationController.pushViewController(feedDetailViewController, animated: true) 51 | } 52 | 53 | func moveToFeedDetail(feedUUID: String) { 54 | let feedDetailViewController = prepareFeedDetailViewController(feedUUID: feedUUID) 55 | sinkFeedViewController(feedDetailViewController) 56 | self.navigationController.pushViewController(feedDetailViewController, animated: true) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Presentation/Auth/LogIn/ViewModel/LogInViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogInViewModel.swift 3 | // Eoljuga 4 | // 5 | // Created by 김기훈 on 2022/11/18. 6 | // 7 | 8 | import AuthenticationServices 9 | import Combine 10 | import Foundation 11 | 12 | final class LogInViewModel { 13 | 14 | private let loginUseCase: LoginUseCase 15 | 16 | private var logInPublisher = PassthroughSubject() 17 | 18 | init(loginUseCase: LoginUseCase) { 19 | self.loginUseCase = loginUseCase 20 | } 21 | 22 | struct Input { 23 | let githubLogInButtonDidTap: AnyPublisher 24 | } 25 | 26 | struct Output { 27 | let openGithubLogInView: AnyPublisher 28 | let moveToOtherView: AnyPublisher 29 | } 30 | 31 | func transform(input: Input) -> Output { 32 | let openLogInView = input.githubLogInButtonDidTap 33 | .throttle(for: 1, scheduler: DispatchQueue.main, latest: false) 34 | .eraseToAnyPublisher() 35 | 36 | let moveToOtherView = logInPublisher 37 | .throttle(for: 1, scheduler: DispatchQueue.main, latest: false) 38 | .eraseToAnyPublisher() 39 | 40 | return Output( 41 | openGithubLogInView: openLogInView, 42 | moveToOtherView: moveToOtherView 43 | ) 44 | } 45 | 46 | func loginWithGithub(code: String) async throws { 47 | let (userNickname, userUUID) = try await loginUseCase.loginWithGithub(code: code) 48 | UserManager.shared.setUserUUID(userUUID) 49 | 50 | let isUserExist = try await loginUseCase.checkIsExist(userUUID: userUUID) 51 | if isUserExist { 52 | logInPublisher.send(.moveToTabBarScreen) 53 | } else { 54 | logInPublisher.send(.moveToDomainScreen(userNickname: userNickname)) 55 | } 56 | } 57 | 58 | func loginWithApple(idTokenString: String, nonce: String) async throws { 59 | let userUUID = try await loginUseCase.loginWithApple(idTokenString: idTokenString, nonce: nonce) 60 | UserManager.shared.setUserUUID(userUUID) 61 | 62 | let isUserExist = try await loginUseCase.checkIsExist(userUUID: userUUID) 63 | if !isUserExist { 64 | try await loginUseCase.createGuest(userUUID: userUUID) 65 | } 66 | logInPublisher.send(.moveToTabBarScreen) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /burstcamp/burstcamp/Domain/UseCase/Auth/SignUp/DefaultSignUpUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultSignUpUseCase.swift 3 | // burstcamp 4 | // 5 | // Created by youtak on 2023/01/14. 6 | // 7 | 8 | import Foundation 9 | 10 | final class DefaultSignUpUseCase: SignUpUseCase { 11 | 12 | private var signUpUser: SignUpUser 13 | private let userRepository: UserRepository 14 | private let blogRepository: BlogRepository 15 | 16 | init(userRepository: UserRepository, blogRepository: BlogRepository) { 17 | self.signUpUser = SignUpUser() 18 | self.userRepository = userRepository 19 | self.blogRepository = blogRepository 20 | } 21 | 22 | func setUserNickname(_ nickname: String) { 23 | signUpUser.setNickname(nickname) 24 | } 25 | 26 | func setUserDomain(_ domain: Domain) { 27 | signUpUser.setDomain(domain) 28 | } 29 | 30 | func setUserCamperID(_ camperID: String) { 31 | signUpUser.setCamperID(camperID) 32 | } 33 | 34 | func setUserBlogURL(_ blogURL: String) { 35 | signUpUser.setBlogURL(blogURL) 36 | } 37 | 38 | func getUserDomain() -> Domain { 39 | if let domain = signUpUser.getDomain() { 40 | return domain 41 | } 42 | fatalError("캠퍼 ID 선택하는데 도메인이 없음") 43 | } 44 | 45 | func getUserBlogURL() -> String { 46 | return signUpUser.getBlogURL() 47 | } 48 | 49 | func getBlogTitle(blogURL: String) async throws -> String { 50 | return try await blogRepository.checkBlogTitle(link: blogURL) 51 | } 52 | 53 | func getUser(userUUID: String, blogTitle: String = "") throws -> User { 54 | if blogTitle.isEmpty { signUpUser.initBlogURL() } 55 | let signUpUser = signUpUser 56 | if let user = User(userUUID: userUUID, signUpUser: signUpUser, blogTitle: blogTitle) { 57 | return user 58 | } else { 59 | throw SignUpUseCaseError.createUser 60 | } 61 | } 62 | 63 | func signUp(_ user: User) async throws { 64 | try await userRepository.saveUser(user) 65 | KeyChainManager.save(user: user) 66 | UserManager.shared.setUser(user) 67 | } 68 | 69 | func saveFCMToken(_ token: String, to userUUID: String) async throws { 70 | try await userRepository.saveFCMToken(token, to: userUUID) 71 | } 72 | 73 | func isValidateBlogURL(_ blogURL: String) -> Bool { 74 | return blogRepository.isValidateLink(blogURL) 75 | } 76 | } 77 | --------------------------------------------------------------------------------