├── 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 |
--------------------------------------------------------------------------------