├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yaml │ └── feature_request.yaml ├── pull_request_template.md └── workflows │ ├── BackEnd_CD_DEV.yml │ ├── BackEnd_CD_PROD.yml │ ├── BackEnd_CI.yml │ └── iOS_CI.yml ├── .gitignore ├── BackEnd ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── Dockerfile ├── nest-cli.json ├── package-lock.json ├── package.json ├── public │ └── index.html ├── src │ ├── admin │ │ ├── admin.controller.ts │ │ ├── admin.module.ts │ │ ├── admin.service.ts │ │ ├── dto │ │ │ └── login.dto.ts │ │ ├── entities │ │ │ └── admin.entity.ts │ │ └── exceptions │ │ │ └── admin.exception.ts │ ├── app.controller.spec.ts │ ├── app.controller.ts │ ├── app.module.ts │ ├── auth │ │ ├── auth-apple.service.ts │ │ ├── auth.controller.ts │ │ ├── auth.module.ts │ │ ├── auth.service.ts │ │ ├── decorator │ │ │ └── apple-token.decorator.ts │ │ ├── dto │ │ │ ├── auth-response.dto.ts │ │ │ ├── getUserByUserIdAndProvider.dto.ts │ │ │ ├── signin.dto.ts │ │ │ ├── signinRedirectRes.dto.ts │ │ │ └── signup.dto.ts │ │ ├── exceptions │ │ │ └── auth.exception.ts │ │ └── guard │ │ │ └── bearerToken.guard.ts │ ├── common │ │ ├── Interceptors │ │ │ └── responseTransform. Interceptor.ts │ │ ├── common.module.ts │ │ ├── common.service.spec.ts │ │ ├── common.service.ts │ │ ├── const │ │ │ └── orm-operation.const.ts │ │ ├── dto │ │ │ ├── SuccessRes.dto.ts │ │ │ ├── base-paginate-res.dto.ts │ │ │ └── base-pagination.dto.ts │ │ ├── exceptionFilters │ │ │ └── httpException.filter.ts │ │ ├── mocks │ │ │ └── mocks.ts │ │ ├── redis.module.ts │ │ └── type │ │ │ ├── base-model.type.ts │ │ │ └── query-options.type.ts │ ├── config │ │ ├── jwksApple.config.ts │ │ ├── redis.config.ts │ │ ├── swagger.config.ts │ │ └── typeorm.config.ts │ ├── images │ │ ├── constant │ │ │ └── images.constant.ts │ │ ├── dto │ │ │ ├── images.response.spec.ts │ │ │ └── images.response.ts │ │ ├── exceptions │ │ │ └── images.exception.ts │ │ ├── images.controller.ts │ │ ├── images.module.ts │ │ ├── images.service.spec.ts │ │ ├── images.service.ts │ │ ├── intercepters │ │ │ └── wetri-files.interceptor.ts │ │ ├── interface │ │ │ └── images.interface.ts │ │ └── pipe │ │ │ ├── validate-files.pip.spec.ts │ │ │ └── validate-files.pip.ts │ ├── live-workouts │ │ ├── events │ │ │ ├── dto │ │ │ │ └── checkMatching.dto.ts │ │ │ ├── entities │ │ │ │ └── event.entity.ts │ │ │ ├── events.gateway.ts │ │ │ ├── events.module.ts │ │ │ ├── events.service.ts │ │ │ ├── extensionWebSocket.service.ts │ │ │ ├── extensions │ │ │ │ ├── extensionWebSocket.ts │ │ │ │ └── extensionWebSocketServer.ts │ │ │ └── types │ │ │ │ └── custom-websocket.type.ts │ │ └── matches │ │ │ ├── constant │ │ │ └── matches.constant.ts │ │ │ ├── dto │ │ │ ├── create-match.dto.spec.ts │ │ │ ├── create-match.dto.ts │ │ │ ├── random-match.dto.spec.ts │ │ │ └── random-match.dto.ts │ │ │ ├── matches.controller.ts │ │ │ ├── matches.module.ts │ │ │ ├── matches.service.spec.ts │ │ │ └── matches.service.ts │ ├── main.ts │ ├── middlewares │ │ └── logger.middleware.ts │ ├── posts │ │ ├── dto │ │ │ ├── create-post.dto.ts │ │ │ ├── delete-post-response.dto.ts │ │ │ ├── get-create-update-post-response.dto.ts │ │ │ ├── get-posts-response.dto.ts │ │ │ ├── paginate-post.dto.ts │ │ │ └── update-post.dto.ts │ │ ├── entities │ │ │ └── posts.entity.ts │ │ ├── exceptions │ │ │ └── posts.exception.ts │ │ ├── mocks │ │ │ └── mocks.ts │ │ ├── posts.controller.spec.ts │ │ ├── posts.controller.ts │ │ ├── posts.module.ts │ │ ├── posts.service.spec.ts │ │ ├── posts.service.ts │ │ └── queryOptions │ │ │ └── get-create-update.queryOptions.ts │ ├── profiles │ │ ├── decorator │ │ │ └── profile.decorator.ts │ │ ├── dto │ │ │ ├── get-nickname-availability.dto.ts │ │ │ ├── get-profile-posts-response.dto.ts │ │ │ ├── get-profile-response.dto.ts │ │ │ ├── paginate-profile-post.dto.ts │ │ │ └── update-profile.dto.ts │ │ ├── entities │ │ │ └── profiles.entity.ts │ │ ├── exception │ │ │ └── profile.exception.ts │ │ ├── profiles.controller.ts │ │ ├── profiles.module.ts │ │ ├── profiles.service.ts │ │ └── queryOptions │ │ │ └── get-profilePosts-queryOptions.ts │ ├── records │ │ ├── dto │ │ │ ├── create-exerciseLog.dto.ts │ │ │ └── record-response.dto.ts │ │ ├── entities │ │ │ └── records.entity.ts │ │ ├── exceptions │ │ │ └── records.exception.ts │ │ ├── records.controller.ts │ │ ├── records.module.ts │ │ └── records.service.ts │ ├── users │ │ ├── entities │ │ │ └── users.entity.ts │ │ ├── exceptions │ │ │ └── users.exception.ts │ │ ├── users.controller.ts │ │ ├── users.module.ts │ │ └── users.service.ts │ └── workouts │ │ ├── dto │ │ ├── workout-response.dto.spec.ts │ │ └── workout-response.dto.ts │ │ ├── entities │ │ ├── workout.entity.spec.ts │ │ └── workout.entity.ts │ │ ├── exceptions │ │ └── workouts.exception.ts │ │ ├── workouts.controller.spec.ts │ │ ├── workouts.controller.ts │ │ ├── workouts.module.ts │ │ ├── workouts.service.spec.ts │ │ └── workouts.service.ts ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json ├── tsconfig.build.json └── tsconfig.json ├── README.md └── iOS ├── .gitignore ├── .swiftformat ├── .swiftlint.yml ├── Makefile ├── Plugins ├── DependencyPlugin │ ├── Plugin.swift │ └── ProjectDescriptionHelpers │ │ ├── Dependency+Target.swift │ │ └── Path+relativeTo.swift └── EnvironmentPlugin │ ├── Plugin.swift │ └── ProjectDescriptionHelpers │ └── ProjectEnvironment.swift ├── Projects ├── App │ └── WeTri │ │ ├── Project.swift │ │ ├── Resources │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ └── WeTri-Logo-Background.png │ │ │ └── Contents.json │ │ └── LaunchScreen.storyboard │ │ ├── Sources │ │ ├── Application │ │ │ ├── AppDelegate.swift │ │ │ └── SceneDelegate.swift │ │ ├── CommonScene │ │ │ └── Coordinator │ │ │ │ ├── AppCoordinator.swift │ │ │ │ └── Protocol │ │ │ │ └── AppCoordinating.swift │ │ └── TabBarScene │ │ │ └── Coordinator │ │ │ ├── Protocol │ │ │ └── TabBarCoordinating.swift │ │ │ └── TabBarCoordinator.swift │ │ └── WeTri.entitlements ├── Core │ ├── Cacher │ │ ├── Project.swift │ │ ├── Sources │ │ │ ├── CacheManager.swift │ │ │ ├── Cacher.swift │ │ │ └── MemoryCacheManager.swift │ │ └── Tests │ │ │ └── CacherTest.swift │ ├── Coordinator │ │ ├── Project.swift │ │ └── Sources │ │ │ ├── CoordinatorFlow.swift │ │ │ ├── Delegate │ │ │ └── CoordinatorFinishDelegate.swift │ │ │ └── Protocol │ │ │ └── Coordinating.swift │ ├── Keychain │ │ ├── Project.swift │ │ ├── Sources │ │ │ ├── Keychain.swift │ │ │ └── Protocol │ │ │ │ └── KeyChaining.swift │ │ └── Tests │ │ │ ├── KeychainTests.swift │ │ │ └── Mock │ │ │ └── MockKeychain.swift │ ├── Network │ │ ├── Project.swift │ │ ├── Sources │ │ │ ├── EmptyModel.swift │ │ │ ├── Foundation │ │ │ │ ├── TNEndPoint.swift │ │ │ │ ├── TNHeader.swift │ │ │ │ ├── TNHeaders.swift │ │ │ │ └── TNMethod.swift │ │ │ ├── GWResponse.swift │ │ │ ├── Mock │ │ │ │ ├── MockURLSession.swift │ │ │ │ └── MockWebSocketSession.swift │ │ │ ├── Multipart │ │ │ │ ├── MultipartFormData.swift │ │ │ │ ├── MultipartItem.swift │ │ │ │ └── TNFormDataEndPoint.swift │ │ │ ├── Protocol │ │ │ │ ├── URLSessionProtocol.swift │ │ │ │ ├── URLSessionWebSocketProtocol.swift │ │ │ │ └── WebSocketTaskProtocol.swift │ │ │ ├── Provider │ │ │ │ ├── TNProvidable.swift │ │ │ │ └── TNSocketProvider.swift │ │ │ ├── TNError.swift │ │ │ └── TNRequestInterceptor.swift │ │ └── Tests │ │ │ ├── EndPointTests.swift │ │ │ ├── InterceptorTest.swift │ │ │ ├── MockEndPoint.swift │ │ │ ├── SessionProtocolTests.swift │ │ │ └── SessionWebSocketProtocolTests.swift │ └── Persistence │ │ └── Project.swift ├── Domain │ ├── Entity │ │ └── Project.swift │ ├── Repositories │ │ └── Project.swift │ └── Usecase │ │ └── Project.swift ├── Features │ ├── Home │ │ ├── Project.swift │ │ ├── Sources │ │ │ ├── Data │ │ │ │ ├── FeedRepository.swift │ │ │ │ └── HomeRepository.swift │ │ │ ├── Domain │ │ │ │ ├── Entity │ │ │ │ │ └── FeedElement.swift │ │ │ │ ├── HomeUseCase.swift │ │ │ │ └── RepositoryInterface │ │ │ │ │ └── FeedRepositoryRepresentable.swift │ │ │ └── Presntaion │ │ │ │ ├── Coordinator │ │ │ │ └── HomeCoordinator.swift │ │ │ │ └── HomeScene │ │ │ │ ├── VIew │ │ │ │ ├── FeedImageCell.swift │ │ │ │ └── FeedItemCollectionViewCell.swift │ │ │ │ ├── ViewController │ │ │ │ ├── HomeViewController+CompositionlLayout.swift │ │ │ │ └── HomeViewController.swift │ │ │ │ └── ViewModel │ │ │ │ └── HomeViewModel.swift │ │ └── Tests │ │ │ ├── HomeFeatureTests.swift │ │ │ └── Keep.swift │ ├── Login │ │ ├── Project.swift │ │ ├── Resources │ │ │ ├── Assets.xcassets │ │ │ │ ├── AccentColor.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── AppIcon.appiconset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── Persistency │ │ │ │ ├── InitialUser.json │ │ │ │ └── Token.json │ │ │ ├── cycleing.mp4 │ │ │ └── running.mp4 │ │ ├── Sources │ │ │ ├── Data │ │ │ │ ├── DTO │ │ │ │ │ ├── AuthorizationInfoRequestDTO.swift │ │ │ │ │ └── Response.swift │ │ │ │ └── Repositories │ │ │ │ │ ├── AuthorizationRepository.swift │ │ │ │ │ └── KeychainRepository.swift │ │ │ ├── Domain │ │ │ │ ├── Entity │ │ │ │ │ ├── AuthorizationInfo.swift │ │ │ │ │ └── InitialUser.swift │ │ │ │ ├── Interfaces │ │ │ │ │ └── Repositories │ │ │ │ │ │ ├── AuthorizationRepositoryRepresentable.swift │ │ │ │ │ │ └── KeychainRepositoryRepresentable.swift │ │ │ │ └── UseCases │ │ │ │ │ ├── AuthorizeUseCase.swift │ │ │ │ │ └── Protocol │ │ │ │ │ └── AuthorizeUseCaseRepresentable.swift │ │ │ └── Presentation │ │ │ │ └── LoginScene │ │ │ │ ├── Coordinator │ │ │ │ ├── Delegate │ │ │ │ │ └── LoginDidFinishedDelegate.swift │ │ │ │ ├── LoginFeatureCoordinator.swift │ │ │ │ └── Protocol │ │ │ │ │ ├── LoginCoordinating.swift │ │ │ │ │ └── LoginFeatureCoordinating.swift │ │ │ │ ├── LoginViewController.swift │ │ │ │ └── ViewModel │ │ │ │ └── LoginViewModel.swift │ │ └── Tests │ │ │ └── LoginFeatureTests.swift │ ├── Onboarding │ │ ├── Project.swift │ │ ├── Resources │ │ │ ├── HealthOnboardingImage.png │ │ │ ├── HealthOnboardingPropertyText.json │ │ │ ├── MapOnboardingImage.png │ │ │ └── MapOnboardingPropertyText.json │ │ ├── Sources │ │ │ ├── Data │ │ │ │ ├── DTO │ │ │ │ │ └── OnboardingScenePropertyResponse.swift │ │ │ │ └── Repository │ │ │ │ │ └── OnboardingPropertyLoadRepository.swift │ │ │ ├── Domain │ │ │ │ └── UseCasee │ │ │ │ │ ├── Interface │ │ │ │ │ └── OnboardingPropertyLoadRepositoryRepresentable.swift │ │ │ │ │ └── OnboardingPropertyLoadUseCase.swift │ │ │ └── Presentaion │ │ │ │ ├── Common │ │ │ │ └── OnboardingCoordinator.swift │ │ │ │ ├── ViewModel │ │ │ │ └── OnboardingViewModel.swift │ │ │ │ └── ViweController │ │ │ │ └── OnboardingViewController.swift │ │ └── Tests │ │ │ └── OnboardingFeatureTests.swift │ ├── Profile │ │ ├── Project.swift │ │ ├── Resources │ │ │ └── Persistency │ │ │ │ ├── GetPosts.json │ │ │ │ └── GetProfile.json │ │ ├── Sources │ │ │ ├── Data │ │ │ │ ├── DTO │ │ │ │ │ ├── PostsRequestDTO.swift │ │ │ │ │ ├── PostsResponseDTO.swift │ │ │ │ │ └── ProfileDTO.swift │ │ │ │ └── Repositories │ │ │ │ │ ├── KeychainRepository.swift │ │ │ │ │ ├── ProfileRepository.swift │ │ │ │ │ └── ProfileSettingsRepository.swift │ │ │ ├── Domain │ │ │ │ ├── Entities │ │ │ │ │ └── Profile.swift │ │ │ │ ├── Interfaces │ │ │ │ │ ├── KeychainRepositoryRepresentable.swift │ │ │ │ │ ├── ProfileRepositoryRepresentable.swift │ │ │ │ │ └── ProfileSettingsRepositoryRepresentable.swift │ │ │ │ └── UseCases │ │ │ │ │ ├── LogoutUseCase.swift │ │ │ │ │ ├── ProfileSettingsUseCase.swift │ │ │ │ │ ├── ProfileUseCase.swift │ │ │ │ │ └── Protocol │ │ │ │ │ ├── LogoutUseCaseRepresentable.swift │ │ │ │ │ ├── ProfileSettingsUseCaseRepresentable.swift │ │ │ │ │ └── ProfileUseCaseRepresentable.swift │ │ │ └── Presentation │ │ │ │ ├── Coordinator │ │ │ │ ├── ProfileCoordinator.swift │ │ │ │ └── Protocol │ │ │ │ │ └── ProfileCoordinating.swift │ │ │ │ ├── ProfileScene │ │ │ │ ├── PostsEmptyStateView.swift │ │ │ │ ├── ProfileHeaderView.swift │ │ │ │ ├── ProfileLayouts │ │ │ │ │ ├── CompositionalLayoutSugarMethods.swift │ │ │ │ │ └── SectionItem.swift │ │ │ │ ├── ProfilePostCell.swift │ │ │ │ ├── ProfileViewController.swift │ │ │ │ └── ProfileViewModel.swift │ │ │ │ ├── ProfileSettingsScene │ │ │ │ ├── ProfileSettingsHeaderView.swift │ │ │ │ ├── ProfileSettingsViewController.swift │ │ │ │ └── ProfileSettingsViewModel.swift │ │ │ │ └── SettingsScene │ │ │ │ ├── SettingsViewController.swift │ │ │ │ └── SettingsViewModel.swift │ │ └── Tests │ │ │ └── ProfileFeatureTests.swift │ ├── Record │ │ ├── Project.swift │ │ ├── Resources │ │ │ └── Persistency │ │ │ │ ├── MatchesCancel.json │ │ │ │ ├── MatchesRandom.json │ │ │ │ ├── MatchesStart.json │ │ │ │ ├── PeerTypes.json │ │ │ │ ├── Records.json │ │ │ │ ├── WorkoutSession.json │ │ │ │ ├── WorkoutSummary.json │ │ │ │ └── WorkoutTypes.json │ │ ├── Sources │ │ │ ├── Data │ │ │ │ ├── DTO │ │ │ │ │ ├── DateRequestDTO.swift │ │ │ │ │ ├── IsMatchedRandomPeersRequest.swift │ │ │ │ │ ├── IsMatchedRandomPeersResponse.swift │ │ │ │ │ ├── MatchCancelRequest.swift │ │ │ │ │ ├── MatchStartRequest.swift │ │ │ │ │ ├── PeerMatchDTO.swift │ │ │ │ │ ├── PeerTypeDTO.swift │ │ │ │ │ ├── RecordResponseDTO.swift │ │ │ │ │ ├── WorkoutSummaryDTO.swift │ │ │ │ │ └── WorkoutTypeDTO.swift │ │ │ │ ├── Error │ │ │ │ │ └── DataLayerError.swift │ │ │ │ ├── MockRepositories │ │ │ │ │ └── Records.json │ │ │ │ └── Repositories │ │ │ │ │ ├── HealthRepository.swift │ │ │ │ │ ├── MapImageUploadRepository.swift │ │ │ │ │ ├── WorkoutEnvironmentSetupNetworkRepository.swift │ │ │ │ │ ├── WorkoutPeerRandomMatchingRepository.swift │ │ │ │ │ ├── WorkoutRecordRepository.swift │ │ │ │ │ ├── WorkoutRecordsRepository.swift │ │ │ │ │ ├── WorkoutSocketRepository.swift │ │ │ │ │ └── WorkoutSummaryRepository.swift │ │ │ ├── Domain │ │ │ │ ├── Entities │ │ │ │ │ ├── DateInfo.swift │ │ │ │ │ ├── KalmanFilterCensored.swift │ │ │ │ │ ├── KalmanFilterUpdateRequireElement.swift │ │ │ │ │ ├── MapRegion.swift │ │ │ │ │ ├── Matrix.swift │ │ │ │ │ ├── Peer.swift │ │ │ │ │ ├── PeerType.swift │ │ │ │ │ ├── Record.swift │ │ │ │ │ ├── SessionPeerType.swift │ │ │ │ │ ├── WorkoutDataForm.swift │ │ │ │ │ ├── WorkoutHealthForm.swift │ │ │ │ │ ├── WorkoutMode.swift │ │ │ │ │ ├── WorkoutRealTimeModel.swift │ │ │ │ │ ├── WorkoutSessionElement.swift │ │ │ │ │ ├── WorkoutSetting.swift │ │ │ │ │ ├── WorkoutSummaryModel.swift │ │ │ │ │ └── WorkoutType.swift │ │ │ │ ├── Error │ │ │ │ │ └── DomainError.swift │ │ │ │ ├── Interfaces │ │ │ │ │ └── Repositories │ │ │ │ │ │ ├── HealthRepositoryRepresentable.swift │ │ │ │ │ │ ├── MapImageUploadRepositoryRepresentable.swift │ │ │ │ │ │ ├── WorkoutEnvironmentSetupNetworkRepositoryRepresentable.swift │ │ │ │ │ │ ├── WorkoutRecordRepositoryRepresentable.swift │ │ │ │ │ │ ├── WorkoutRecordsRepositoryRepresentable.swift │ │ │ │ │ │ ├── WorkoutSocketRepositoryRepresentable.swift │ │ │ │ │ │ └── WorkoutSummaryRepositoryRepresentable.swift │ │ │ │ └── UseCases │ │ │ │ │ ├── CountDownBeforeWorkoutStartTimerUseCase.swift │ │ │ │ │ ├── DateProvideUseCase.swift │ │ │ │ │ ├── KalmanFilter.swift │ │ │ │ │ ├── KalmanUseCase.swift │ │ │ │ │ ├── LocationPathUseCase.swift │ │ │ │ │ ├── MapImageUploadUseCase.swift │ │ │ │ │ ├── OneSecondsTimerUseCase.swift │ │ │ │ │ ├── Protocol │ │ │ │ │ ├── DateProvideUseCaseRepresentable.swift │ │ │ │ │ ├── LocationPathUseCaseRepresentable.swift │ │ │ │ │ ├── MapImageUploadUseCaseRepresentable.swift │ │ │ │ │ ├── RecordUpdateUseCaseRepresentable.swift │ │ │ │ │ └── WorkoutPeerRandomMatchingRepositoryRepresentable.swift │ │ │ │ │ ├── RecordUpdateUseCase.swift │ │ │ │ │ ├── TimerUseCase.swift │ │ │ │ │ ├── UserInformationUseCase.swift │ │ │ │ │ ├── WorkoutEnvironmentSetupUseCase.swift │ │ │ │ │ ├── WorkoutPeerRandomMatchingUseCase.swift │ │ │ │ │ ├── WorkoutRecordUseCase.swift │ │ │ │ │ ├── WorkoutSessionUseCase.swift │ │ │ │ │ └── WorkoutSummaryUseCase.swift │ │ │ ├── Infrastructure │ │ │ │ └── WorkoutRecordEndPoint.swift │ │ │ └── Presentation │ │ │ │ ├── Common │ │ │ │ └── Coordinator │ │ │ │ │ ├── Delegate │ │ │ │ │ └── WorkoutSettingCoordinatorFinishDelegate.swift │ │ │ │ │ ├── Protocol │ │ │ │ │ ├── RecordFeatureCoordinating.swift │ │ │ │ │ ├── WorkoutCoordinating.swift │ │ │ │ │ ├── WorkoutEnvironmentSetUpCoordinating.swift │ │ │ │ │ └── WorkoutSessionCoordinating.swift │ │ │ │ │ ├── RecordFeatureCoordinator.swift │ │ │ │ │ ├── WorkoutEnvironmentSetUpCoordinator.swift │ │ │ │ │ └── WorkoutSessionCoordinator.swift │ │ │ │ ├── CountDownBeforeWorkoutScene │ │ │ │ ├── ViewController │ │ │ │ │ └── CountDownBeforeWorkoutViewController.swift │ │ │ │ └── ViewModel │ │ │ │ │ └── CountDownBeforeWorkoutViewModel.swift │ │ │ │ ├── RecordScene │ │ │ │ ├── CollectionViewCell │ │ │ │ │ ├── CalendarCollectionViewCell.swift │ │ │ │ │ └── WorkoutInformationCollectionViewCell.swift │ │ │ │ ├── View │ │ │ │ │ └── NoRecordsView.swift │ │ │ │ ├── ViewController │ │ │ │ │ ├── RecordCalendarViewController.swift │ │ │ │ │ ├── RecordContainerViewController.swift │ │ │ │ │ └── RecordListViewController.swift │ │ │ │ └── ViewModel │ │ │ │ │ ├── RecordCalendarViewModel.swift │ │ │ │ │ └── RecordListViewModel.swift │ │ │ │ ├── WorkoutEnvironmentScene │ │ │ │ ├── View │ │ │ │ │ ├── WorkoutPeerTypeSelectCell.swift │ │ │ │ │ └── WorkoutSelectTypeCell.swift │ │ │ │ ├── ViewController │ │ │ │ │ ├── WorkoutEnvironmentSetupViewController.swift │ │ │ │ │ ├── WorkoutPeerSelectViewController.swift │ │ │ │ │ └── WorkoutSelectViewController.swift │ │ │ │ └── ViewModel │ │ │ │ │ └── WorkoutEnvironmentSetupViewModel.swift │ │ │ │ ├── WorkoutPeerMatchingScene │ │ │ │ ├── ViewController │ │ │ │ │ └── WorkoutPeerRandomMatchingViewController.swift │ │ │ │ └── ViewModel │ │ │ │ │ └── WorkoutPeerRandomMatchingViewModel.swift │ │ │ │ ├── WorkoutSessionGroupScene │ │ │ │ ├── RouteMapScene │ │ │ │ │ ├── WorkoutRouteMapViewController.swift │ │ │ │ │ └── WorkoutRouteMapViewModel.swift │ │ │ │ ├── SessionScene │ │ │ │ │ ├── SessionParticipantCell.swift │ │ │ │ │ ├── WorkoutSessionViewController.swift │ │ │ │ │ └── WorkoutSessionViewModel.swift │ │ │ │ ├── WorkoutSessionContainerViewController.swift │ │ │ │ └── WorkoutSessionContainerViewModel.swift │ │ │ │ └── WorkoutSummaryScene │ │ │ │ ├── WorkoutSummaryCardView.swift │ │ │ │ ├── WorkoutSummaryViewController.swift │ │ │ │ └── WorkoutSummaryViewModel.swift │ │ └── Tests │ │ │ └── RecordFeatureTests.swift │ ├── SignUp │ │ ├── Project.swift │ │ ├── Resources │ │ │ └── Token.json │ │ ├── Sources │ │ │ ├── Data │ │ │ │ ├── DTO │ │ │ │ │ └── NickNameDuplicateRequestDTO.swift │ │ │ │ └── Repository │ │ │ │ │ ├── ImageFormRepository.swift │ │ │ │ │ ├── KeyChainRepository.swift │ │ │ │ │ └── SignUpRepository.swift │ │ │ ├── Domain │ │ │ │ ├── Entities │ │ │ │ │ ├── Gender.swift │ │ │ │ │ ├── ImageForm.swift │ │ │ │ │ ├── NewUserInformation.swift │ │ │ │ │ └── SignUpUser.swift │ │ │ │ ├── Interfaces │ │ │ │ │ ├── ImageFormRepositoryRepresentable.swift │ │ │ │ │ ├── KeyChainRepositoryRepresentable.swift │ │ │ │ │ └── SignUpRepositoryRepresentable.swift │ │ │ │ └── UseCase │ │ │ │ │ ├── DateFormatUseCase.swift │ │ │ │ │ ├── ImageTransmitUseCase.swift │ │ │ │ │ ├── NickNameCheckUseCase.swift │ │ │ │ │ ├── SignUpUseCase.swift │ │ │ │ │ └── SignUpUserDefaultsManagerUseCase.swift │ │ │ └── Presentation │ │ │ │ ├── Common │ │ │ │ ├── Coordinator │ │ │ │ │ ├── Protocol │ │ │ │ │ │ ├── SignUpFeatureCoordinating.swift │ │ │ │ │ │ └── SingUpCoordinating.swift │ │ │ │ │ └── SignUpFeatureCoordinator.swift │ │ │ │ └── Extension │ │ │ │ │ └── UIButton+.swift │ │ │ │ ├── SignUpContainerScene │ │ │ │ └── SignUpContainerViewController.swift │ │ │ │ ├── SignUpGenderBirthScene │ │ │ │ ├── SignUpGenderBirthViewController.swift │ │ │ │ ├── View │ │ │ │ │ └── DatePickerBoxView.swift │ │ │ │ └── ViewModel │ │ │ │ │ └── SignUpGenderBirthViewModel.swift │ │ │ │ └── SignUpProfileScene │ │ │ │ ├── SignUpProfileViewController.swift │ │ │ │ ├── View │ │ │ │ ├── CheckerView.swift │ │ │ │ ├── ImageCheckerView.swift │ │ │ │ ├── NickNameBoxView.swift │ │ │ │ ├── NickNameCheckerView.swift │ │ │ │ └── NickNameDuplicatingCheckerView.swift │ │ │ │ └── ViewModel │ │ │ │ └── SignUpProfileViewModel.swift │ │ └── Tests │ │ │ └── SignupTests.swift │ ├── Splash │ │ ├── Project.swift │ │ ├── Sources │ │ │ ├── DTO │ │ │ │ ├── ReissueAccessTokenDTO.swift │ │ │ │ └── ReissueRefreshTokenDTO.swift │ │ │ ├── Data │ │ │ │ └── Repositories │ │ │ │ │ ├── PersistencyRepository.swift │ │ │ │ │ └── SplashTokenRepository.swift │ │ │ ├── Domain │ │ │ │ ├── Interfaces │ │ │ │ │ └── SplashTokenRepositoryRepresentable.swift │ │ │ │ └── UseCases │ │ │ │ │ ├── Protocol │ │ │ │ │ └── SplashUseCaseRepresentable.swift │ │ │ │ │ └── SplashUseCase.swift │ │ │ └── SplashScene │ │ │ │ ├── Coordinator │ │ │ │ ├── Protocol │ │ │ │ │ └── SplashCoordinating.swift │ │ │ │ └── SplashCoordinator.swift │ │ │ │ ├── SplashViewController.swift │ │ │ │ └── ViewModel │ │ │ │ ├── Protocol │ │ │ │ └── SplashViewModelRepresentable.swift │ │ │ │ └── SplashViewModel.swift │ │ └── Tests │ │ │ └── SplashFeatureTests.swift │ └── WriteBoard │ │ ├── Project.swift │ │ ├── Sources │ │ ├── Dombain │ │ │ └── Entities │ │ │ │ └── Record.swift │ │ └── Presentation │ │ │ ├── Common │ │ │ └── Coordinator │ │ │ │ └── WriteBoardCoordinator.swift │ │ │ ├── ContainerViewController │ │ │ ├── ContainerViewController.swift │ │ │ └── ContainerViewModel.swift │ │ │ ├── WirteBoardScene │ │ │ ├── View │ │ │ │ ├── AttachPictureCollectionViewCell.swift │ │ │ │ └── WorkoutHistoryDescriptionView.swift │ │ │ ├── ViewController │ │ │ │ ├── AttachPictureViewController.swift │ │ │ │ └── WriteBoardViewController.swift │ │ │ └── ViewModel │ │ │ │ └── WriteBoardViewModel.swift │ │ │ └── WorkoutHistorySelectScene │ │ │ ├── View │ │ │ ├── WorkoutHistoryCell.swift │ │ │ └── WorkoutHistorySelectViewController.swift │ │ │ └── ViewModel │ │ │ └── WorkoutHistorySelectViewModel.swift │ │ └── Tests │ │ └── test.swift └── Shared │ ├── Auth │ ├── Project.swift │ └── Sources │ │ ├── AuthProvider.swift │ │ └── Token.swift │ ├── CombineCocoa │ ├── Project.swift │ ├── Sources │ │ ├── EventSubscription.swift │ │ ├── GestureSubscription.swift │ │ ├── UIControl+Publisher.swift │ │ └── UIView+Publisher.swift │ └── Tests │ │ └── UIControl+PublisherTests.swift │ ├── CombineExtension │ ├── Project.swift │ ├── Sources │ │ ├── Publisher+bind.swift │ │ ├── Publisher+withLatestFrom.swift │ │ └── withUnretained.swift │ └── Tests │ │ ├── Publisher+WithLatestFromTests.swift │ │ ├── Publisher+bindErrorTests.swift │ │ └── Publisher+bindTests.swift │ ├── CommonNetworkingKeyManager │ ├── Project.swift │ └── Sources │ │ ├── RefreshTokenAdaptor.swift │ │ ├── TNKeychainInterceptor.swift │ │ └── Tokens.swift │ ├── DesignSystem │ ├── Project.swift │ ├── Resources │ │ ├── Colors.xcassets │ │ │ ├── Contents.json │ │ │ ├── Error.colorset │ │ │ │ └── Contents.json │ │ │ ├── Gray-01.colorset │ │ │ │ └── Contents.json │ │ │ ├── Gray-02.colorset │ │ │ │ └── Contents.json │ │ │ ├── Gray-03.colorset │ │ │ │ └── Contents.json │ │ │ ├── Main-01.colorset │ │ │ │ └── Contents.json │ │ │ ├── Main-02.colorset │ │ │ │ └── Contents.json │ │ │ ├── Main-03.colorset │ │ │ │ └── Contents.json │ │ │ ├── PrimaryBackground.colorset │ │ │ │ └── Contents.json │ │ │ ├── PrimaryText.colorset │ │ │ │ └── Contents.json │ │ │ ├── SecondaryBackground.colorset │ │ │ │ └── Contents.json │ │ │ ├── Success.colorset │ │ │ │ └── Contents.json │ │ │ └── Warning.colorset │ │ │ │ └── Contents.json │ │ └── Images.xcassets │ │ │ ├── Contents.json │ │ │ ├── Logo.imageset │ │ │ ├── Contents.json │ │ │ └── WeTri-Logo.png │ │ │ ├── LogoForDarkMode.imageset │ │ │ ├── Contents.json │ │ │ └── LogoForDarkModeWith75.png │ │ │ ├── MapEmptyState.imageset │ │ │ ├── Contents.json │ │ │ └── MapEmptyState.svg │ │ │ ├── NoResults.imageset │ │ │ ├── Contents.json │ │ │ └── No Results.svg │ │ │ └── Pencil.imageset │ │ │ ├── Contents.json │ │ │ └── Pencil.svg │ └── Sources │ │ ├── ConstraintsGuideLine.swift │ │ ├── DesignSystemColor.swift │ │ ├── GWPageConrol.swift │ │ ├── GWProfileButton.swift │ │ ├── GWRoundShadowView.swift │ │ ├── GWShadow.swift │ │ ├── UIButtonConfiguration+Font.swift │ │ ├── UIButtonConfiguration+Main.swift │ │ ├── UIButtonConfiguration+MainCircular.swift │ │ ├── UIFont+preferredFont.swift │ │ └── UIImage+assets.swift │ ├── ImageDownsampling │ ├── Project.swift │ └── Sources │ │ ├── Data+downsampling.swift │ │ ├── ImageDownSamplingError.swift │ │ ├── Scale.swift │ │ └── UIImage+downsampling.swift │ ├── Log │ ├── Project.swift │ └── Sources │ │ └── Logger.swift │ └── UserInformationManager │ ├── Project.swift │ ├── Resources │ └── DefaultsProfileImage.png │ └── Sources │ ├── UserInformationFetcher.swift │ └── UserInformationManager.swift ├── Scripts └── create_module.sh ├── Tuist ├── Config.swift ├── ProjectDescriptionHelpers │ ├── Project+Templates.swift │ ├── Scripts+Templates.swift │ └── Target+Templates.swift └── Templates │ ├── Demo │ ├── AppDelegate.stencil │ ├── Demo.swift │ ├── LaunchScreen.stencil │ └── SceneDelegate.stencil │ └── Feature │ ├── Feature.swift │ ├── TempFeatureTests.stencil │ ├── TempViewController.stencil │ └── TempViewModel.stencil ├── Workspace.swift └── graph.png /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Screenshots 📸 2 | 3 | |Name| 4 | |:-:| 5 | |(image here)| 6 | 7 |

8 | 9 | ## 고민, 과정, 근거 💬 10 | 11 |

12 | 13 | ## References 📋 14 | 15 | 16 |

17 | 18 | --- 19 | 20 | - Closed: #(issue-here) 21 | -------------------------------------------------------------------------------- /.github/workflows/BackEnd_CI.yml: -------------------------------------------------------------------------------- 1 | name: BackEnd-CI 2 | 3 | on: 4 | push: 5 | branches: 'feature/BE/*' 6 | pull_request: 7 | branches: 'develop' 8 | types: [opened, synchronize, reopened, labeled] 9 | 10 | jobs: 11 | test: 12 | if: contains(github.event.pull_request.labels.*.name, '💻백엔드') 13 | runs-on: self-hosted 14 | steps: 15 | - name: 코드 체크아웃 16 | uses: actions/checkout@v3 17 | 18 | - name: node 세팅 19 | uses: actions/setup-node@v2 20 | with: 21 | node-version: '18' 22 | 23 | - name: 의존성 설치 24 | run: npm install 25 | working-directory: ./BackEnd 26 | 27 | - name: 테스트 진행 28 | run: npm test 29 | working-directory: ./BackEnd 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/macos 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | 14 | # Thumbnails 15 | ._* 16 | 17 | # Files that might appear in the root of a volume 18 | .DocumentRevisions-V100 19 | .fseventsd 20 | .Spotlight-V100 21 | .TemporaryItems 22 | .Trashes 23 | .VolumeIcon.icns 24 | .com.apple.timemachine.donotpresent 25 | 26 | # Directories potentially created on remote AFP share 27 | .AppleDB 28 | .AppleDesktop 29 | Network Trash Folder 30 | Temporary Items 31 | .apdisk 32 | 33 | ### macOS Patch ### 34 | # iCloud generated files 35 | *.icloud 36 | 37 | ### BackEnd Common ### 38 | .idea 39 | .vscode -------------------------------------------------------------------------------- /BackEnd/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /BackEnd/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json 36 | 37 | # Env 38 | *.env -------------------------------------------------------------------------------- /BackEnd/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /BackEnd/Dockerfile: -------------------------------------------------------------------------------- 1 | # 빌드 단계 2 | FROM node:18 AS builder 3 | WORKDIR /app 4 | COPY BackEnd/package*.json ./ 5 | RUN npm install 6 | COPY BackEnd/ . 7 | COPY BackEnd/public /app/public 8 | RUN npm run build 9 | 10 | # 실행 단계 11 | FROM node:18 12 | WORKDIR /app 13 | COPY --from=builder /app/dist ./dist 14 | COPY --from=builder /app/public ./public 15 | COPY --from=builder /app/package*.json ./ 16 | RUN npm install --only=production 17 | RUN npm install pm2 -g 18 | EXPOSE 3000 19 | CMD ["pm2-runtime", "start", "dist/main.js"] 20 | 21 | -------------------------------------------------------------------------------- /BackEnd/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /BackEnd/src/admin/admin.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Post } from '@nestjs/common'; 2 | import { AdminService } from './admin.service'; 3 | import { LoginDto } from './dto/login.dto'; 4 | import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; 5 | import { SignupResDto } from '../auth/dto/auth-response.dto'; 6 | 7 | @ApiTags('Admin') 8 | @Controller('api/v1/admin') 9 | export class AdminController { 10 | constructor(private readonly adminService: AdminService) {} 11 | 12 | @ApiOperation({ summary: 'Admin 로그인' }) 13 | @ApiBody({ description: 'ID && PW', type: LoginDto }) 14 | @ApiResponse({ 15 | status: 200, 16 | description: '로그인 성공', 17 | type: SignupResDto, 18 | }) 19 | @Post('login') 20 | login(@Body() body: LoginDto) { 21 | return this.adminService.authenticateWithAdminIdAndPassword(body); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /BackEnd/src/admin/admin.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AdminService } from './admin.service'; 3 | import { AdminController } from './admin.controller'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { Admin } from './entities/admin.entity'; 6 | import { AuthModule } from '../auth/auth.module'; 7 | 8 | @Module({ 9 | imports: [TypeOrmModule.forFeature([Admin]), AuthModule], 10 | controllers: [AdminController], 11 | providers: [AdminService], 12 | }) 13 | export class AdminModule {} 14 | -------------------------------------------------------------------------------- /BackEnd/src/admin/dto/login.dto.ts: -------------------------------------------------------------------------------- 1 | import { PickType } from '@nestjs/swagger'; 2 | import { Admin } from '../entities/admin.entity'; 3 | 4 | export class LoginDto extends PickType(Admin, ['adminId', 'adminPw']) {} 5 | -------------------------------------------------------------------------------- /BackEnd/src/admin/entities/admin.entity.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsString } from 'class-validator'; 3 | import { Profile } from '../../profiles/entities/profiles.entity'; 4 | import { 5 | Column, 6 | CreateDateColumn, 7 | DeleteDateColumn, 8 | Entity, 9 | JoinColumn, 10 | OneToOne, 11 | PrimaryGeneratedColumn, 12 | UpdateDateColumn, 13 | } from 'typeorm'; 14 | 15 | @Entity() 16 | export class Admin { 17 | @PrimaryGeneratedColumn() 18 | id: number; 19 | 20 | @ApiProperty({ 21 | example: 'Uqweszvc4fds1342...', 22 | description: 'AdminId 필드입니다.', 23 | }) 24 | @IsString() 25 | @Column() 26 | adminId: string; 27 | 28 | @ApiProperty({ 29 | example: '2314wrUdsfa2ads...', 30 | description: 'AdminPw 필드입니다.', 31 | }) 32 | @IsString() 33 | @Column() 34 | adminPw: string; 35 | 36 | @CreateDateColumn() 37 | createdAt: Date; 38 | 39 | @UpdateDateColumn() 40 | updatedAt: Date; 41 | 42 | @DeleteDateColumn() 43 | deletedAt: Date; 44 | 45 | @OneToOne(() => Profile, (profile) => profile.admin, { 46 | eager: true, 47 | }) 48 | @JoinColumn() 49 | profile: Profile; 50 | } 51 | -------------------------------------------------------------------------------- /BackEnd/src/admin/exceptions/admin.exception.ts: -------------------------------------------------------------------------------- 1 | import { HttpException } from '@nestjs/common'; 2 | 3 | export class NotFoundAdminIdException extends HttpException { 4 | constructor() { 5 | const response = { 6 | statusCode: 2000, 7 | message: 'not found account error', 8 | }; 9 | const httpCode = 404; 10 | super(response, httpCode); 11 | } 12 | } 13 | 14 | export class IncorrectPasswordException extends HttpException { 15 | constructor() { 16 | const response = { 17 | statusCode: 2005, 18 | message: 'password is incorrect', 19 | }; 20 | const httpCode = 400; 21 | super(response, httpCode); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /BackEnd/src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { Response } from 'express'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | let mockResponse: Response; 8 | 9 | beforeEach(async () => { 10 | const app: TestingModule = await Test.createTestingModule({ 11 | controllers: [AppController], 12 | }).compile(); 13 | 14 | appController = app.get(AppController); 15 | mockResponse = { 16 | sendFile: jest.fn(), 17 | } as unknown as Response; 18 | }); 19 | 20 | describe('indexHTML', () => { 21 | it('should call sendFile', () => { 22 | appController.indexHTML(mockResponse); 23 | expect(mockResponse.sendFile).toHaveBeenCalled(); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /BackEnd/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Res } from '@nestjs/common'; 2 | import { Response } from 'express'; 3 | import { join } from 'path'; 4 | import { ApiTags } from '@nestjs/swagger'; 5 | 6 | @ApiTags('메인 페이지') 7 | @Controller() 8 | export class AppController { 9 | @Get() 10 | indexHTML(@Res() response: Response): void { 11 | response.sendFile(join(__dirname, '..', 'public', 'index.html')); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /BackEnd/src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AuthService } from './auth.service'; 3 | import { AuthController } from './auth.controller'; 4 | import { JwtModule } from '@nestjs/jwt'; 5 | import { UsersModule } from '../users/users.module'; 6 | import { ProfilesModule } from '../profiles/profiles.module'; 7 | import { AuthAppleService } from './auth-apple.service'; 8 | 9 | @Module({ 10 | imports: [JwtModule.register({}), UsersModule, ProfilesModule], 11 | exports: [AuthService, AuthAppleService], 12 | controllers: [AuthController], 13 | providers: [AuthService, AuthAppleService], 14 | }) 15 | export class AuthModule {} 16 | -------------------------------------------------------------------------------- /BackEnd/src/auth/decorator/apple-token.decorator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExecutionContext, 3 | InternalServerErrorException, 4 | createParamDecorator, 5 | } from '@nestjs/common'; 6 | 7 | export const IdentityToken = createParamDecorator( 8 | (data, context: ExecutionContext) => { 9 | const req = context.switchToHttp().getRequest(); 10 | 11 | const token = req.body.identityToken; 12 | 13 | if (!token) { 14 | throw new InternalServerErrorException('token이 없습니다.'); 15 | } 16 | 17 | return token; 18 | }, 19 | ); 20 | -------------------------------------------------------------------------------- /BackEnd/src/auth/dto/auth-response.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty, PickType } from '@nestjs/swagger'; 2 | import { SuccessResDto } from '../../common/dto/SuccessRes.dto'; 3 | 4 | class Token { 5 | @ApiProperty({ 6 | example: 'ewaf1313RWDFA...', 7 | description: 'Access Token 입니다.', 8 | }) 9 | accessToken: string; 10 | 11 | @ApiProperty({ 12 | example: 'ewaf1313RWDFdddA...', 13 | description: 'Refresh Token 입니다.', 14 | }) 15 | refreshToken: string; 16 | } 17 | 18 | class AccessToken extends PickType(Token, ['accessToken']) {} 19 | 20 | class RefreshToken extends PickType(Token, ['refreshToken']) {} 21 | 22 | export class SignupResDto extends SuccessResDto { 23 | @ApiProperty({ type: () => Token }) 24 | data: Token; 25 | } 26 | 27 | export class CreateAccessTokenResDto extends SuccessResDto { 28 | @ApiProperty({ type: () => AccessToken }) 29 | data: AccessToken; 30 | } 31 | 32 | export class CreateRefreshTokenResDto extends SuccessResDto { 33 | @ApiProperty({ type: () => RefreshToken }) 34 | data: RefreshToken; 35 | } 36 | -------------------------------------------------------------------------------- /BackEnd/src/auth/dto/getUserByUserIdAndProvider.dto.ts: -------------------------------------------------------------------------------- 1 | import { PickType } from '@nestjs/swagger'; 2 | import { User } from '../../users/entities/users.entity'; 3 | 4 | export class GetuserByUserIdAndProViderDto extends PickType(User, [ 5 | 'userId', 6 | 'provider', 7 | ]) {} 8 | -------------------------------------------------------------------------------- /BackEnd/src/auth/dto/signin.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsString } from 'class-validator'; 3 | 4 | export class SignInDto { 5 | @ApiProperty({ 6 | example: '23432USDFAS2134AD...', 7 | description: 'Identity token', 8 | }) 9 | @IsString() 10 | identityToken: string; 11 | 12 | @ApiProperty({ 13 | example: '23432USDFAS2134AD...', 14 | description: 'Authorization Code', 15 | }) 16 | @IsString() 17 | authorizationCode: string; 18 | } 19 | -------------------------------------------------------------------------------- /BackEnd/src/auth/dto/signinRedirectRes.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsString } from 'class-validator'; 3 | import { SuccessResDto } from '../../common/dto/SuccessRes.dto'; 4 | 5 | class SigninFirstRes { 6 | @ApiProperty({ 7 | example: true, 8 | description: '첫 로그인이면 true', 9 | }) 10 | @IsString() 11 | isFirstLogined: boolean; 12 | 13 | @ApiProperty({ 14 | example: '1233498sdafksdjhfk...', 15 | description: 16 | 'mappedUserID -> 애플리케이션에서 고유한 apple id를 매핑한 uuid', 17 | }) 18 | @IsString() 19 | mappedUserID: string; 20 | 21 | @ApiProperty({ 22 | example: 'apple', 23 | description: '로그인 플랫폼', 24 | }) 25 | @IsString() 26 | provider: string; 27 | } 28 | 29 | export class SigninFirstResDto extends SuccessResDto { 30 | @ApiProperty({ type: () => SigninFirstRes }) 31 | data: SigninFirstRes; 32 | } 33 | -------------------------------------------------------------------------------- /BackEnd/src/auth/dto/signup.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty, IntersectionType, PickType } from '@nestjs/swagger'; 2 | import { Profile } from '../../profiles/entities/profiles.entity'; 3 | import { User } from '../../users/entities/users.entity'; 4 | import { IsString } from 'class-validator'; 5 | 6 | class UserDto extends PickType(User, ['provider']) {} 7 | class ProfileDto extends PickType(Profile, [ 8 | 'nickname', 9 | 'gender', 10 | 'birthdate', 11 | 'profileImage', 12 | ]) {} 13 | 14 | export class SignupDto extends IntersectionType(UserDto, ProfileDto) { 15 | @ApiProperty({ 16 | example: 'WQRWR-214-SADF', 17 | description: 'userId를 매핑한 uuid입니다.', 18 | }) 19 | @IsString() 20 | mappedUserID: string; 21 | } 22 | -------------------------------------------------------------------------------- /BackEnd/src/common/Interceptors/responseTransform. Interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | NestInterceptor, 4 | ExecutionContext, 5 | CallHandler, 6 | } from '@nestjs/common'; 7 | import { Observable } from 'rxjs'; 8 | import { map } from 'rxjs/operators'; 9 | 10 | @Injectable() 11 | export class ResponseTransformInterceptor 12 | implements NestInterceptor 13 | { 14 | intercept(context: ExecutionContext, next: CallHandler): Observable { 15 | return next.handle().pipe( 16 | map((data) => { 17 | return { 18 | code: null, 19 | errorMessage: null, 20 | data, 21 | }; 22 | }), 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /BackEnd/src/common/common.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { CommonService } from './common.service'; 3 | 4 | @Module({ 5 | controllers: [], 6 | exports: [CommonService], 7 | providers: [CommonService], 8 | }) 9 | export class CommonModule {} 10 | -------------------------------------------------------------------------------- /BackEnd/src/common/const/orm-operation.const.ts: -------------------------------------------------------------------------------- 1 | import { LessThan, MoreThan } from 'typeorm'; 2 | 3 | export const ORM_OPERATION = { 4 | less_then: LessThan, 5 | more_then: MoreThan, 6 | }; 7 | -------------------------------------------------------------------------------- /BackEnd/src/common/dto/SuccessRes.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class SuccessResDto { 4 | @ApiProperty({ 5 | example: null, 6 | description: '에러 코드 성공시 null', 7 | nullable: true, 8 | }) 9 | code: number | null; 10 | 11 | @ApiProperty({ 12 | example: null, 13 | description: '에러 코드 성공시 null', 14 | nullable: true, 15 | }) 16 | errorMessage: string | null; 17 | } 18 | -------------------------------------------------------------------------------- /BackEnd/src/common/dto/base-paginate-res.dto.ts: -------------------------------------------------------------------------------- 1 | export class PaginateResponseDto { 2 | items: any[]; 3 | metaData: { 4 | lastItemId: number | null; 5 | isLastCursor: boolean; 6 | count: number; 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /BackEnd/src/common/dto/base-pagination.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsIn, IsNumber, IsOptional } from 'class-validator'; 3 | 4 | export class BasePaginationDto { 5 | @ApiProperty({ 6 | example: 5, 7 | description: 'id값보다 낮은 아이템을 가져옵니다.', 8 | required: false, 9 | }) 10 | @IsOptional() 11 | @IsNumber() 12 | where__id__less_then?: number; 13 | 14 | @ApiProperty({ 15 | example: 5, 16 | description: 'id값보다 높은 아이템을 가져옵니다.', 17 | required: false, 18 | }) 19 | @IsOptional() 20 | @IsNumber() 21 | where__id__more_then?: number; 22 | 23 | @ApiProperty({ 24 | example: 'ASC | DESC', 25 | description: 'createdAt을 기준으로 정렬합니다.', 26 | required: false, 27 | }) 28 | @IsOptional() 29 | @IsIn(['ASC', 'DESC']) 30 | order__createdAt: 'ASC' | 'DESC' = 'DESC'; 31 | 32 | @ApiProperty({ 33 | example: 5, 34 | description: '가져올 아이템 수를 의미합니다.', 35 | required: false, 36 | }) 37 | @IsOptional() 38 | @IsNumber() 39 | take: number = 15; 40 | } 41 | -------------------------------------------------------------------------------- /BackEnd/src/common/exceptionFilters/httpException.filter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArgumentsHost, 3 | Catch, 4 | ExceptionFilter, 5 | HttpException, 6 | HttpStatus, 7 | } from '@nestjs/common'; 8 | 9 | @Catch(HttpException) 10 | export class HttpExceptionFilter implements ExceptionFilter { 11 | catch(exception: any, host: ArgumentsHost) { 12 | const ctx = host.switchToHttp(); 13 | const response = ctx.getResponse(); 14 | const status = exception.getStatus(); 15 | const exceptionResponse = exception.getResponse(); 16 | 17 | let errorMessage = exceptionResponse['message']; 18 | let statusCode = exceptionResponse['statusCode']; 19 | if (typeof errorMessage !== 'string') { 20 | errorMessage = String(errorMessage); 21 | } 22 | let data = null; 23 | if(status === HttpStatus.OK) { 24 | data = exceptionResponse['data']; 25 | } 26 | response.status(status).json({ 27 | code: statusCode, 28 | errorMessage, 29 | data, 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /BackEnd/src/common/redis.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, Global } from '@nestjs/common'; 2 | import Redis from 'ioredis'; 3 | import { redisConfig } from '../config/redis.config'; 4 | import { CommonModule } from './common.module'; 5 | 6 | @Global() 7 | @Module({ 8 | providers: [ 9 | { 10 | provide: 'DATA_REDIS', 11 | useFactory: () => { 12 | return new Redis(redisConfig); 13 | }, 14 | }, 15 | { 16 | provide: 'SUBSCRIBE_REDIS', 17 | useFactory: () => { 18 | return new Redis(redisConfig); 19 | }, 20 | }, 21 | ], 22 | exports: ['DATA_REDIS', 'SUBSCRIBE_REDIS'], 23 | imports: [CommonModule], 24 | }) 25 | export class RedisModule {} 26 | -------------------------------------------------------------------------------- /BackEnd/src/common/type/base-model.type.ts: -------------------------------------------------------------------------------- 1 | export interface BaseModel { 2 | id: number; 3 | } 4 | -------------------------------------------------------------------------------- /BackEnd/src/common/type/query-options.type.ts: -------------------------------------------------------------------------------- 1 | export interface JoinType { 2 | joinColumn: string; 3 | joinAlias: string; 4 | } 5 | 6 | export interface QueryOptions { 7 | mainAlias: string; 8 | joins?: JoinType[]; 9 | selects?: string[]; 10 | } 11 | -------------------------------------------------------------------------------- /BackEnd/src/config/jwksApple.config.ts: -------------------------------------------------------------------------------- 1 | import * as jwksClient from 'jwks-rsa'; 2 | 3 | export const jwksApple: jwksClient.JwksClient = jwksClient({ 4 | jwksUri: 'https://appleid.apple.com/auth/keys', 5 | }); 6 | -------------------------------------------------------------------------------- /BackEnd/src/config/redis.config.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | import * as process from 'process'; 3 | 4 | dotenv.config(); 5 | 6 | export const redisConfig: object = { 7 | host: process.env.REDIS_HOST, 8 | port: parseInt(process.env.REDIS_PORT), 9 | enableReadyCheck: true, 10 | enableOfflineQueue: true, 11 | }; 12 | -------------------------------------------------------------------------------- /BackEnd/src/config/swagger.config.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 3 | 4 | export const SwaggerSetting = (app: INestApplication) => { 5 | const config = new DocumentBuilder() 6 | .setTitle('Wetri') 7 | .setDescription('API') 8 | .setVersion('1.0') 9 | .addTag('wonholim') 10 | .build(); 11 | 12 | const document = SwaggerModule.createDocument(app, config); 13 | SwaggerModule.setup('api', app, document); 14 | }; 15 | -------------------------------------------------------------------------------- /BackEnd/src/config/typeorm.config.ts: -------------------------------------------------------------------------------- 1 | import { TypeOrmModuleOptions } from '@nestjs/typeorm'; 2 | import * as dotenv from 'dotenv'; 3 | 4 | dotenv.config(); 5 | 6 | export const typeOrmConfig: TypeOrmModuleOptions = { 7 | type: 'mysql', 8 | host: process.env.DB_HOST, 9 | port: parseInt(process.env.DB_PORT), 10 | username: process.env.DB_USERNAME, 11 | password: process.env.DB_PASSWORD, 12 | database: process.env.DB_NAME, 13 | entities: [__dirname + '/../**/*.entity.{js,ts}'], 14 | synchronize: false, 15 | logging: true, 16 | charset: 'utf8mb4', 17 | extra: { 18 | connectionLimit: 10, 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /BackEnd/src/images/constant/images.constant.ts: -------------------------------------------------------------------------------- 1 | export const MAX_IMAGE_SIZE = 1024 * 1024 * 5; 2 | export const ADULT_RATIO = 0.3; 3 | export const PORN_RATIO = 0.2; 4 | -------------------------------------------------------------------------------- /BackEnd/src/images/images.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ImagesController } from './images.controller'; 3 | import { ImagesService } from './images.service'; 4 | import { AuthModule } from '../auth/auth.module'; 5 | import { ProfilesModule } from '../profiles/profiles.module'; 6 | 7 | @Module({ 8 | imports: [AuthModule, ProfilesModule], 9 | controllers: [ImagesController], 10 | providers: [ImagesService], 11 | }) 12 | export class ImagesModule {} 13 | -------------------------------------------------------------------------------- /BackEnd/src/images/intercepters/wetri-files.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | ExecutionContext, 4 | CallHandler, 5 | Logger, 6 | } from '@nestjs/common'; 7 | import { FilesInterceptor } from '@nestjs/platform-express'; 8 | import { Observable, throwError } from 'rxjs'; 9 | import { catchError } from 'rxjs/operators'; 10 | import { InvalidFileCountOrFieldNameException } from '../exceptions/images.exception'; 11 | 12 | @Injectable() 13 | export class WetriFilesInterceptor extends FilesInterceptor('images', 5) { 14 | intercept(context: ExecutionContext, next: CallHandler): Observable { 15 | return next.handle().pipe( 16 | catchError((err) => { 17 | Logger.warn(err); 18 | throw new InvalidFileCountOrFieldNameException(); 19 | }), 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /BackEnd/src/images/interface/images.interface.ts: -------------------------------------------------------------------------------- 1 | export interface FileUploadOptions { 2 | maxSize: number; 3 | fileType: string[]; 4 | } 5 | -------------------------------------------------------------------------------- /BackEnd/src/images/pipe/validate-files.pip.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, BadRequestException, PipeTransform } from '@nestjs/common'; 2 | import { FileUploadOptions } from '../interface/images.interface'; 3 | import { 4 | FileSizeTooLargeException, 5 | InvalidFileTypeException, 6 | } from '../exceptions/images.exception'; 7 | 8 | @Injectable() 9 | export class ValidateFilesPipe implements PipeTransform { 10 | constructor(private options: FileUploadOptions) {} 11 | 12 | transform(files: Express.Multer.File[]): Express.Multer.File[] { 13 | files.forEach((file) => { 14 | this.validateFileSize(file); 15 | this.validateFileType(file); 16 | }); 17 | 18 | return files; 19 | } 20 | 21 | private validateFileSize(file: Express.Multer.File): void { 22 | const maxSize = this.options.maxSize; 23 | if (file.size > maxSize) { 24 | throw new FileSizeTooLargeException(); 25 | } 26 | } 27 | 28 | private validateFileType(file: Express.Multer.File): void { 29 | const allowedTypes = this.options.fileType; 30 | if (!allowedTypes.includes(file.mimetype)) { 31 | throw new InvalidFileTypeException(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /BackEnd/src/live-workouts/events/dto/checkMatching.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString } from 'class-validator'; 2 | 3 | export class CheckMatchingDto { 4 | @IsString() 5 | matchingKey: string; 6 | 7 | @IsString() 8 | roomId: string; 9 | } 10 | -------------------------------------------------------------------------------- /BackEnd/src/live-workouts/events/entities/event.entity.ts: -------------------------------------------------------------------------------- 1 | export class Event {} 2 | -------------------------------------------------------------------------------- /BackEnd/src/live-workouts/events/events.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { EventsService } from './events.service'; 3 | import { EventsGateway } from './events.gateway'; 4 | import { ExtensionWebSocketService } from './extensionWebSocket.service'; 5 | import { JwtModule } from '@nestjs/jwt'; 6 | import { UsersModule } from '../../users/users.module'; 7 | import { ProfilesModule } from '../../profiles/profiles.module'; 8 | import { AuthController } from '../../auth/auth.controller'; 9 | import { AuthService } from '../../auth/auth.service'; 10 | import { AuthAppleService } from '../../auth/auth-apple.service'; 11 | 12 | @Module({ 13 | imports: [JwtModule.register({}), UsersModule, ProfilesModule], 14 | controllers: [AuthController], 15 | providers: [ 16 | EventsGateway, 17 | EventsService, 18 | ExtensionWebSocketService, 19 | AuthService, 20 | AuthAppleService, 21 | ], 22 | }) 23 | export class EventsModule {} 24 | -------------------------------------------------------------------------------- /BackEnd/src/live-workouts/events/events.service.ts: -------------------------------------------------------------------------------- 1 | import { Redis } from 'ioredis'; 2 | import { Inject, Injectable } from '@nestjs/common'; 3 | import { CheckMatchingDto } from './dto/checkMatching.dto'; 4 | 5 | @Injectable() 6 | export class EventsService { 7 | constructor(@Inject('DATA_REDIS') private readonly redisData: Redis) {} 8 | 9 | async checkMatching(matchInfo: CheckMatchingDto) { 10 | const resultRoomId = await this.redisData.get(matchInfo.matchingKey); 11 | if (resultRoomId !== matchInfo.roomId) { 12 | return false; 13 | } 14 | return true; 15 | } 16 | 17 | checkMsgRoomId(data: any) { 18 | if (!data.roomId) { 19 | return false; 20 | } 21 | return true; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /BackEnd/src/live-workouts/events/extensionWebSocket.service.ts: -------------------------------------------------------------------------------- 1 | import { Redis } from 'ioredis'; 2 | import { Inject, Injectable } from '@nestjs/common'; 3 | 4 | import { WetriServer, WetriWebSocket } from './types/custom-websocket.type'; 5 | import { ExtensionWebSocket } from './extensions/extensionWebSocket'; 6 | import { ExtensionWebSocketServer } from './extensions/extensionWebSocketServer'; 7 | 8 | @Injectable() 9 | export class ExtensionWebSocketService { 10 | constructor( 11 | @Inject('DATA_REDIS') private readonly redisData: Redis, 12 | @Inject('SUBSCRIBE_REDIS') private readonly redisSubscribe: Redis, 13 | ) {} 14 | 15 | webSocketServer(server: WetriServer) { 16 | new ExtensionWebSocketServer(server, this.redisData, this.redisSubscribe); 17 | } 18 | 19 | webSocket(client: WetriWebSocket, server: WetriServer) { 20 | new ExtensionWebSocket(client, server); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /BackEnd/src/live-workouts/matches/constant/matches.constant.ts: -------------------------------------------------------------------------------- 1 | export const MAX_USERS = 5; 2 | export const MIN_USERS = 2; 3 | export const WAITING_60_TIME = 60; 4 | export const WAITING_40_TIME = 40; 5 | export const USER_WAITED_60_MIN_USERS = 2; 6 | export const USER_WAITED_40_MIN_USERS = 3; 7 | export const USER_WAITED_20_MIN_USERS = 4; 8 | export const ALONE_USER = 1; 9 | export const WAITING_20_TIME = 20; 10 | export const MATCHING_DELAY = 15; 11 | export const UTC_REMOVE_TIME = 600; 12 | export const MATCHES_API_TIME_OUT = 20; 13 | -------------------------------------------------------------------------------- /BackEnd/src/live-workouts/matches/dto/create-match.dto.spec.ts: -------------------------------------------------------------------------------- 1 | import { plainToInstance } from 'class-transformer'; 2 | import { validate } from 'class-validator'; 3 | import { CreateMatchDto } from './create-match.dto'; 4 | 5 | describe('CreateMatchDto', () => { 6 | it('workoutId가 숫자일 때, 에러는 발생하지 않는다.', async () => { 7 | const dto = plainToInstance(CreateMatchDto, { workoutId: 1 }); 8 | const errors = await validate(dto); 9 | expect(errors.length).toBe(0); 10 | }); 11 | 12 | it('workoutId가 문자로 들어왔을 때, 에러가 발생한다.', async () => { 13 | const dto = plainToInstance(CreateMatchDto, { workoutId: '닌자' }); 14 | const errors = await validate(dto); 15 | expect(errors.length).toBeGreaterThan(0); 16 | }); 17 | 18 | it('만약 dto가 비어 있다면, 에러가 발생한다.', async () => { 19 | const dto = plainToInstance(CreateMatchDto, {}); 20 | const errors = await validate(dto); 21 | expect(errors.length).toBeGreaterThan(0); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /BackEnd/src/live-workouts/matches/dto/create-match.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsNumber } from 'class-validator'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | export class CreateMatchDto { 4 | @IsNumber() 5 | @IsNotEmpty() 6 | @ApiProperty({ example: 1 }) 7 | workoutId: number; 8 | } 9 | -------------------------------------------------------------------------------- /BackEnd/src/live-workouts/matches/matches.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MatchesService } from './matches.service'; 3 | import { MatchesController } from './matches.controller'; 4 | import { JwtModule } from '@nestjs/jwt'; 5 | import { UsersModule } from '../../users/users.module'; 6 | import { ProfilesModule } from '../../profiles/profiles.module'; 7 | import { AuthModule } from '../../auth/auth.module'; 8 | 9 | @Module({ 10 | imports: [JwtModule.register({}), UsersModule, ProfilesModule, AuthModule], 11 | controllers: [MatchesController], 12 | providers: [MatchesService], 13 | }) 14 | export class MatchesModule {} 15 | -------------------------------------------------------------------------------- /BackEnd/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import { SwaggerSetting } from './config/swagger.config'; 4 | import { ValidationPipe } from '@nestjs/common'; 5 | import { ResponseTransformInterceptor } from './common/Interceptors/responseTransform. Interceptor'; 6 | import { HttpExceptionFilter } from './common/exceptionFilters/httpException.filter'; 7 | import { WsAdapter } from '@nestjs/platform-ws'; 8 | import * as express from 'express'; 9 | 10 | async function bootstrap() { 11 | const app = await NestFactory.create(AppModule); 12 | app.useGlobalPipes( 13 | new ValidationPipe({ 14 | transform: true, 15 | transformOptions: { 16 | enableImplicitConversion: true, 17 | }, 18 | }), 19 | ); 20 | app.useGlobalFilters(new HttpExceptionFilter()); 21 | app.useGlobalInterceptors(new ResponseTransformInterceptor()); 22 | app.useWebSocketAdapter(new WsAdapter(app)); 23 | app.use('/static', express.static('public')); 24 | SwaggerSetting(app); 25 | await app.listen(3000); 26 | } 27 | bootstrap(); 28 | -------------------------------------------------------------------------------- /BackEnd/src/middlewares/logger.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger, NestMiddleware } from '@nestjs/common'; 2 | import { Request, Response, NextFunction } from 'express'; 3 | 4 | @Injectable() 5 | export class LoggerMiddleware implements NestMiddleware { 6 | private readonly logger = new Logger(LoggerMiddleware.name); 7 | 8 | use(request: Request, response: Response, next: NextFunction) { 9 | const { ip, method, originalUrl } = request; 10 | const userAgent = request.get('user-agent'); 11 | 12 | request.on('end', () => { 13 | const { statusCode } = response; 14 | this.logger.log( 15 | `${method} ${originalUrl} ${statusCode} ${ip} ${userAgent}`, 16 | ); 17 | }); 18 | 19 | next(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /BackEnd/src/posts/dto/create-post.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty, PickType } from '@nestjs/swagger'; 2 | import { Post } from '../entities/posts.entity'; 3 | import { IsNumber } from 'class-validator'; 4 | 5 | export class CreatePostDto extends PickType(Post, ['content', 'imagesUrl']) { 6 | @ApiProperty({ 7 | example: 1, 8 | description: 'recordId를 뜻합니다.', 9 | }) 10 | @IsNumber() 11 | recordId: number; 12 | } 13 | -------------------------------------------------------------------------------- /BackEnd/src/posts/dto/delete-post-response.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { SuccessResDto } from '../../common/dto/SuccessRes.dto'; 3 | 4 | export class DeletePostResponseDto extends SuccessResDto {} 5 | -------------------------------------------------------------------------------- /BackEnd/src/posts/dto/get-create-update-post-response.dto.ts: -------------------------------------------------------------------------------- 1 | import { SuccessResDto } from '../../common/dto/SuccessRes.dto'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | import { PostDto } from './get-posts-response.dto'; 4 | 5 | export class GetPostResponseDto extends SuccessResDto { 6 | @ApiProperty({ type: () => PostDto }) 7 | data: PostDto; 8 | } 9 | -------------------------------------------------------------------------------- /BackEnd/src/posts/dto/paginate-post.dto.ts: -------------------------------------------------------------------------------- 1 | import { BasePaginationDto } from '../../common/dto/base-pagination.dto'; 2 | 3 | export class PaginatePostDto extends BasePaginationDto {} 4 | -------------------------------------------------------------------------------- /BackEnd/src/posts/dto/update-post.dto.ts: -------------------------------------------------------------------------------- 1 | import { PickType } from '@nestjs/swagger'; 2 | import { Post } from '../entities/posts.entity'; 3 | 4 | export class UpdatePostDto extends PickType(Post, ['content', 'imagesUrl']) {} 5 | -------------------------------------------------------------------------------- /BackEnd/src/posts/exceptions/posts.exception.ts: -------------------------------------------------------------------------------- 1 | import { HttpException } from '@nestjs/common'; 2 | 3 | export class ExistPostException extends HttpException { 4 | constructor() { 5 | const response = { 6 | statusCode: 8130, 7 | message: 'post already exists', 8 | }; 9 | const httpCode = 400; 10 | super(response, httpCode); 11 | } 12 | } 13 | 14 | export class NotFoundPostException extends HttpException { 15 | constructor() { 16 | const response = { 17 | statusCode: 8000, 18 | message: 'not found post error', 19 | }; 20 | const httpCode = 404; 21 | super(response, httpCode); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /BackEnd/src/posts/posts.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PostsService } from './posts.service'; 3 | import { PostsController } from './posts.controller'; 4 | import { AuthModule } from '../auth/auth.module'; 5 | import { ProfilesModule } from '../profiles/profiles.module'; 6 | import { TypeOrmModule } from '@nestjs/typeorm'; 7 | import { Post } from './entities/posts.entity'; 8 | import { RecordsModule } from '../records/records.module'; 9 | import { CommonModule } from '../common/common.module'; 10 | 11 | @Module({ 12 | imports: [ 13 | TypeOrmModule.forFeature([Post]), 14 | AuthModule, 15 | ProfilesModule, 16 | RecordsModule, 17 | CommonModule, 18 | ], 19 | controllers: [PostsController], 20 | providers: [PostsService], 21 | }) 22 | export class PostsModule {} 23 | -------------------------------------------------------------------------------- /BackEnd/src/posts/queryOptions/get-create-update.queryOptions.ts: -------------------------------------------------------------------------------- 1 | import { QueryOptions } from '../../common/type/query-options.type'; 2 | 3 | export const getCreateUpdateQueryOptions: QueryOptions = { 4 | mainAlias: 'post', 5 | joins: [ 6 | { 7 | joinColumn: 'post.record', 8 | joinAlias: 'record', 9 | }, 10 | { 11 | joinColumn: 'post.profile', 12 | joinAlias: 'profile', 13 | }, 14 | ], 15 | selects: [ 16 | 'post', 17 | 'record.id', 18 | 'record.workoutTime', 19 | 'record.distance', 20 | 'record.calorie', 21 | 'record.avgHeartRate', 22 | 'record.minHeartRate', 23 | 'record.maxHeartRate', 24 | 'profile.nickname', 25 | 'profile.profileImage', 26 | ], 27 | }; 28 | -------------------------------------------------------------------------------- /BackEnd/src/profiles/decorator/profile.decorator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExecutionContext, 3 | InternalServerErrorException, 4 | createParamDecorator, 5 | } from '@nestjs/common'; 6 | 7 | export const ProfileDeco = createParamDecorator( 8 | (data, context: ExecutionContext) => { 9 | const req = context.switchToHttp().getRequest(); 10 | 11 | const profile = req.profile; 12 | 13 | if (!profile) { 14 | throw new InternalServerErrorException( 15 | 'AccessTokenGuard를 사용하지 않았습니다.', 16 | ); 17 | } 18 | return profile; 19 | }, 20 | ); 21 | -------------------------------------------------------------------------------- /BackEnd/src/profiles/dto/get-nickname-availability.dto.ts: -------------------------------------------------------------------------------- 1 | import { PickType } from "@nestjs/swagger"; 2 | import { Profile } from "../entities/profiles.entity"; 3 | 4 | export class GetNicknameAvailAbailityDto extends PickType(Profile, ['nickname']) {} -------------------------------------------------------------------------------- /BackEnd/src/profiles/dto/get-profile-posts-response.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty, PickType } from '@nestjs/swagger'; 2 | import { SuccessResDto } from '../../common/dto/SuccessRes.dto'; 3 | import { MetaDataDto } from '../../posts/dto/get-posts-response.dto'; 4 | import { Post } from '../../posts/entities/posts.entity'; 5 | 6 | export class ProfilePostDto extends PickType(Post, ['id', 'imagesUrl']) {} 7 | 8 | class ProfilePostsPaginateResDto { 9 | @ApiProperty({ type: () => [ProfilePostDto] }) 10 | item: ProfilePostDto[]; 11 | 12 | @ApiProperty({ type: () => MetaDataDto }) 13 | metaData: MetaDataDto; 14 | } 15 | 16 | export class GetProfilePostsResponseDto extends SuccessResDto { 17 | @ApiProperty({ type: () => ProfilePostsPaginateResDto }) 18 | data: ProfilePostsPaginateResDto; 19 | } 20 | -------------------------------------------------------------------------------- /BackEnd/src/profiles/dto/get-profile-response.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty, PickType } from '@nestjs/swagger'; 2 | import { Profile } from '../entities/profiles.entity'; 3 | import { SuccessResDto } from '../../common/dto/SuccessRes.dto'; 4 | 5 | class GetProfileDto extends PickType(Profile, [ 6 | 'nickname', 7 | 'gender', 8 | 'birthdate', 9 | 'publicId', 10 | 'profileImage', 11 | ]) {} 12 | 13 | export class GetProfileResponseDto extends SuccessResDto { 14 | @ApiProperty({ type: () => GetProfileDto }) 15 | data: GetProfileDto; 16 | } 17 | -------------------------------------------------------------------------------- /BackEnd/src/profiles/dto/paginate-profile-post.dto.ts: -------------------------------------------------------------------------------- 1 | import { BasePaginationDto } from '../../common/dto/base-pagination.dto'; 2 | 3 | export class PaginateProfilePostDto extends BasePaginationDto {} 4 | -------------------------------------------------------------------------------- /BackEnd/src/profiles/dto/update-profile.dto.ts: -------------------------------------------------------------------------------- 1 | import { PickType } from '@nestjs/swagger'; 2 | import { Profile } from '../entities/profiles.entity'; 3 | 4 | export class UpdateProfileDto extends PickType(Profile, [ 5 | 'nickname', 6 | 'profileImage', 7 | ]) {} 8 | -------------------------------------------------------------------------------- /BackEnd/src/profiles/exception/profile.exception.ts: -------------------------------------------------------------------------------- 1 | import { HttpException } from '@nestjs/common'; 2 | 3 | export class PublicIdMismatchException extends HttpException { 4 | constructor() { 5 | const response = { 6 | statusCode: 2030, 7 | message: 'misMatch', 8 | }; 9 | const httpCode = 400; 10 | super(response, httpCode); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /BackEnd/src/profiles/profiles.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ProfilesService } from './profiles.service'; 3 | import { ProfilesController } from './profiles.controller'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { Profile } from './entities/profiles.entity'; 6 | import { Post } from '../posts/entities/posts.entity'; 7 | import { AuthService } from '../auth/auth.service'; 8 | import { JwtModule } from '@nestjs/jwt'; 9 | import { UsersService } from '../users/users.service'; 10 | import { AuthAppleService } from '../auth/auth-apple.service'; 11 | import { User } from '../users/entities/users.entity'; 12 | import { CommonModule } from '../common/common.module'; 13 | @Module({ 14 | imports: [ 15 | TypeOrmModule.forFeature([Profile, Post, User]), 16 | JwtModule.register({}), 17 | CommonModule, 18 | ], 19 | exports: [ProfilesService], 20 | controllers: [ProfilesController], 21 | providers: [ProfilesService, AuthService, UsersService, AuthAppleService], 22 | }) 23 | export class ProfilesModule {} 24 | -------------------------------------------------------------------------------- /BackEnd/src/profiles/queryOptions/get-profilePosts-queryOptions.ts: -------------------------------------------------------------------------------- 1 | import { QueryOptions } from '../../common/type/query-options.type'; 2 | 3 | export const getProfilePostsQueryOptions: QueryOptions = { 4 | mainAlias: 'profile', 5 | }; 6 | -------------------------------------------------------------------------------- /BackEnd/src/records/dto/create-exerciseLog.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty, PickType } from '@nestjs/swagger'; 2 | import { Record } from '../entities/records.entity'; 3 | import { IsNumber } from 'class-validator'; 4 | 5 | export class CreateExerciseLogDto extends PickType(Record, [ 6 | 'workoutTime', 7 | 'calorie', 8 | 'distance', 9 | 'avgHeartRate', 10 | 'maxHeartRate', 11 | 'minHeartRate', 12 | 'mapCapture', 13 | 'gps' 14 | ]) { 15 | @ApiProperty({ 16 | example: 1, 17 | description: '운동 종류 ID입니다.', 18 | }) 19 | @IsNumber() 20 | workoutId: number; 21 | } 22 | -------------------------------------------------------------------------------- /BackEnd/src/records/dto/record-response.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty, PickType } from '@nestjs/swagger'; 2 | import { SuccessResDto } from '../../common/dto/SuccessRes.dto'; 3 | import { Record } from '../entities/records.entity'; 4 | import { Workout } from 'src/workouts/entities/workout.entity'; 5 | class WorkoutDto extends PickType(Workout, ['name']) {} 6 | class GetRecord extends PickType(Record, [ 7 | 'profile', 8 | 'workoutTime', 9 | 'distance', 10 | 'calorie', 11 | 'avgHeartRate', 12 | 'minHeartRate', 13 | 'maxHeartRate', 14 | 'createdAt', 15 | 'mapCapture', 16 | 'gps' 17 | ]) { 18 | @ApiProperty( {type: () => WorkoutDto} ) 19 | workout: WorkoutDto; 20 | } 21 | class GetRecordWithId extends PickType(Record, ['id']) {} 22 | export class CreateRecordResDto extends SuccessResDto { 23 | @ApiProperty({ type: () => GetRecordWithId }) 24 | data: Pick; 25 | } 26 | export class GetUsersRecordsResDto extends SuccessResDto { 27 | @ApiProperty({ type: () => [GetRecord] }) 28 | data: GetRecord[]; 29 | } 30 | export class GetRecordResDto extends SuccessResDto { 31 | @ApiProperty({ type: () => GetRecord }) 32 | data: GetRecord; 33 | } -------------------------------------------------------------------------------- /BackEnd/src/records/exceptions/records.exception.ts: -------------------------------------------------------------------------------- 1 | import { HttpException } from '@nestjs/common'; 2 | 3 | export class NotFoundRecordException extends HttpException { 4 | constructor() { 5 | const response = { 6 | statusCode: 10400, 7 | message: 'no records.', 8 | }; 9 | const httpCode = 404; 10 | super(response, httpCode); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /BackEnd/src/records/records.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { RecordsService } from './records.service'; 3 | import { RecordsController } from './records.controller'; 4 | import { AuthModule } from '../auth/auth.module'; 5 | import { ProfilesModule } from '../profiles/profiles.module'; 6 | import { TypeOrmModule } from '@nestjs/typeorm'; 7 | import { Record } from './entities/records.entity'; 8 | import { WorkoutsModule } from '../workouts/workouts.module'; 9 | 10 | @Module({ 11 | imports: [ 12 | TypeOrmModule.forFeature([Record]), 13 | AuthModule, 14 | ProfilesModule, 15 | WorkoutsModule, 16 | ], 17 | exports: [RecordsService], 18 | controllers: [RecordsController], 19 | providers: [RecordsService], 20 | }) 21 | export class RecordsModule {} 22 | -------------------------------------------------------------------------------- /BackEnd/src/users/entities/users.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, 3 | PrimaryGeneratedColumn, 4 | Column, 5 | OneToOne, 6 | JoinColumn, 7 | } from 'typeorm'; 8 | import { Profile } from '../../profiles/entities/profiles.entity'; 9 | import { IsString } from 'class-validator'; 10 | import { ApiProperty } from '@nestjs/swagger'; 11 | @Entity() 12 | export class User { 13 | @PrimaryGeneratedColumn() 14 | id: number; 15 | 16 | @ApiProperty({ 17 | example: 'testid132', 18 | description: 'user의 id를 뜻합니다. (apple이면 apple id)', 19 | }) 20 | @Column() 21 | @IsString({ 22 | message: 'userId는 string 타입을 입력해야합니다.', 23 | }) 24 | userId: string; 25 | 26 | @ApiProperty({ example: 'apple', description: 'userId의 플랫폼입니다.' }) 27 | @Column() 28 | @IsString({ 29 | message: 'provider는 string 타입을 입력해야합니다.', 30 | }) 31 | provider: string; 32 | 33 | @OneToOne(() => Profile, (profile) => profile.user, { 34 | eager: true, 35 | cascade: true, 36 | }) 37 | @JoinColumn() 38 | profile: Profile; 39 | } 40 | -------------------------------------------------------------------------------- /BackEnd/src/users/exceptions/users.exception.ts: -------------------------------------------------------------------------------- 1 | import { HttpException } from '@nestjs/common'; 2 | 3 | export class NotFoundUserException extends HttpException { 4 | constructor() { 5 | const response = { 6 | statusCode: 2000, 7 | message: 'not found account error', 8 | }; 9 | const httpCode = 404; 10 | super(response, httpCode); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /BackEnd/src/users/users.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Delete, Get, UseGuards } from '@nestjs/common'; 2 | import { UsersService } from './users.service'; 3 | import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; 4 | import { AccessTokenGuard } from '../auth/guard/bearerToken.guard'; 5 | import { ProfileDeco } from '../profiles/decorator/profile.decorator'; 6 | import { Profile } from '../profiles/entities/profiles.entity'; 7 | import { SuccessResDto } from '../common/dto/SuccessRes.dto'; 8 | 9 | @ApiTags('유저 API') 10 | @Controller('api/v1/users') 11 | export class UsersController { 12 | constructor(private readonly usersService: UsersService) {} 13 | 14 | @UseGuards(AccessTokenGuard) 15 | @ApiOperation({ summary: '유저를 삭제함 권한 필요' }) 16 | @ApiResponse({ type: SuccessResDto }) 17 | @Delete('me') 18 | async deleteUser(@ProfileDeco() profile: Profile) { 19 | await this.usersService.deleteUser(profile.user, profile.publicId); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /BackEnd/src/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, forwardRef } from '@nestjs/common'; 2 | import { UsersService } from './users.service'; 3 | import { UsersController } from './users.controller'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { User } from './entities/users.entity'; 6 | import { AuthModule } from '../auth/auth.module'; 7 | import { ProfilesModule } from '../profiles/profiles.module'; 8 | import { PostsModule } from '../posts/posts.module'; 9 | 10 | @Module({ 11 | imports: [ 12 | TypeOrmModule.forFeature([User]), 13 | forwardRef(() => AuthModule), 14 | ProfilesModule, 15 | ], 16 | exports: [UsersService], //exports 해야 다른 모듈에서 사용 가능 17 | controllers: [UsersController], 18 | providers: [UsersService], 19 | }) 20 | export class UsersModule {} 21 | -------------------------------------------------------------------------------- /BackEnd/src/workouts/dto/workout-response.dto.spec.ts: -------------------------------------------------------------------------------- 1 | import { WorkoutResDto } from './workout-response.dto'; 2 | import { Workout } from '../entities/workout.entity'; 3 | 4 | describe('WorkoutResDto', () => { 5 | it('WorkoutResDto는 배열을 리턴하며 내부에는 id, name, icon이 존재한다.', () => { 6 | const workout1 = new Workout(); 7 | workout1.id = 1; 8 | workout1.name = '달리기'; 9 | workout1.icon = 'running.svg'; 10 | 11 | const workout2 = new Workout(); 12 | workout2.id = 2; 13 | workout2.name = '수영'; 14 | workout2.icon = 'swimming.svg'; 15 | 16 | const dto = new WorkoutResDto(); 17 | dto.data = [workout1, workout2]; 18 | 19 | expect(dto.data).toBeInstanceOf(Array); 20 | expect(dto.data[0]).toBeInstanceOf(Workout); 21 | expect(dto.data[0].name).toBe('달리기'); 22 | expect(dto.data[1].name).toBe('수영'); 23 | expect(dto.data[1].icon).toBe('swimming.svg'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /BackEnd/src/workouts/dto/workout-response.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty, PickType } from '@nestjs/swagger'; 2 | import { SuccessResDto } from '../../common/dto/SuccessRes.dto'; 3 | import { Workout } from '../entities/workout.entity'; 4 | 5 | export class WorkoutResDto extends SuccessResDto { 6 | @ApiProperty({ type: () => [Workout] }) 7 | data: Workout[]; 8 | } 9 | -------------------------------------------------------------------------------- /BackEnd/src/workouts/entities/workout.entity.spec.ts: -------------------------------------------------------------------------------- 1 | import { Workout } from './workout.entity'; 2 | import { validate } from 'class-validator'; 3 | 4 | describe('Workout Entity', () => { 5 | it('Workout에서 id, name, icon이 엔티티에 정의한대로 올바르면, 에러가 발생하지 않는다.', async () => { 6 | const workout = new Workout(); 7 | workout.id = 1; 8 | workout.name = '달리기'; 9 | workout.icon = 'running'; 10 | 11 | const errors = await validate(workout); 12 | expect(errors).toHaveLength(0); 13 | }); 14 | 15 | it('Workout에서 name이 공백이면, 에러가 발생한다.', async () => { 16 | const workout = new Workout(); 17 | workout.id = 1; 18 | workout.name = ''; 19 | workout.icon = 'running'; 20 | 21 | const errors = await validate(workout); 22 | expect(errors).toHaveLength(1); 23 | }); 24 | 25 | it('Workout에서 icon이 공백이면, 에러가 발생한다.', async () => { 26 | const workout = new Workout(); 27 | workout.id = 1; 28 | workout.name = '달리기'; 29 | workout.icon = ''; 30 | 31 | const errors = await validate(workout); 32 | expect(errors).toHaveLength(1); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /BackEnd/src/workouts/entities/workout.entity.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; 3 | import { Record } from '../../records/entities/records.entity'; 4 | import { IsNotEmpty, IsString } from 'class-validator'; 5 | 6 | @Entity() 7 | export class Workout { 8 | @ApiProperty({ 9 | example: 1, 10 | description: '운동 종류 ID', 11 | }) 12 | @PrimaryGeneratedColumn() 13 | id: number; 14 | 15 | @ApiProperty({ 16 | example: '달리기', 17 | description: '운동 종류 이름', 18 | }) 19 | @Column() 20 | @IsString() 21 | @IsNotEmpty() 22 | name: string; 23 | 24 | @ApiProperty({ 25 | example: '운동 종류의 아이콘 경로 문자열', 26 | description: 'figure.outdoor.workout', 27 | }) 28 | @Column() 29 | @IsString() 30 | @IsNotEmpty() 31 | icon: string; 32 | 33 | @OneToMany(() => Record, (record) => record.workout) 34 | records: Record[]; 35 | } 36 | -------------------------------------------------------------------------------- /BackEnd/src/workouts/exceptions/workouts.exception.ts: -------------------------------------------------------------------------------- 1 | import { HttpException } from '@nestjs/common'; 2 | 3 | export class NotFoundAllWorkoutsException extends HttpException { 4 | constructor() { 5 | const response = { 6 | statusCode: 3000, 7 | message: 'not found All Workouts error.', 8 | }; 9 | const httpCode = 404; 10 | super(response, httpCode); 11 | } 12 | } 13 | export class NotFoundWorkoutException extends HttpException { 14 | constructor() { 15 | const response = { 16 | statusCode: 3100, 17 | message: 'not found Workout error.', 18 | }; 19 | const httpCode = 404; 20 | super(response, httpCode); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /BackEnd/src/workouts/workouts.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, UseGuards } from '@nestjs/common'; 2 | import { WorkoutsService } from './workouts.service'; 3 | import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; 4 | import { WorkoutResDto } from './dto/workout-response.dto'; 5 | import { AccessTokenGuard } from '../auth/guard/bearerToken.guard'; 6 | import { Workout } from './entities/workout.entity'; 7 | 8 | @ApiTags('운동 종류 API') 9 | @Controller('api/v1/workouts') 10 | @UseGuards(AccessTokenGuard) 11 | export class WorkoutsController { 12 | constructor(private readonly workoutsService: WorkoutsService) {} 13 | @Get() 14 | @ApiOperation({ summary: '모든 운동 종류 조회' }) 15 | @ApiResponse({ type: WorkoutResDto }) 16 | getAllWorkout(): Promise { 17 | return this.workoutsService.findAllWorkouts(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /BackEnd/src/workouts/workouts.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { WorkoutsService } from './workouts.service'; 3 | import { WorkoutsController } from './workouts.controller'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { Workout } from './entities/workout.entity'; 6 | import { JwtModule } from '@nestjs/jwt'; 7 | import { UsersModule } from '../users/users.module'; 8 | import { ProfilesModule } from '../profiles/profiles.module'; 9 | import { AuthModule } from '../auth/auth.module'; 10 | 11 | @Module({ 12 | imports: [ 13 | TypeOrmModule.forFeature([Workout]), 14 | JwtModule.register({}), 15 | UsersModule, 16 | ProfilesModule, 17 | AuthModule, 18 | ], 19 | exports: [WorkoutsService], 20 | controllers: [WorkoutsController], 21 | providers: [WorkoutsService], 22 | }) 23 | export class WorkoutsModule {} 24 | -------------------------------------------------------------------------------- /BackEnd/src/workouts/workouts.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository } from 'typeorm'; 4 | import { Workout } from './entities/workout.entity'; 5 | import { 6 | NotFoundAllWorkoutsException, 7 | NotFoundWorkoutException, 8 | } from './exceptions/workouts.exception'; 9 | 10 | @Injectable() 11 | export class WorkoutsService { 12 | constructor( 13 | @InjectRepository(Workout) 14 | private readonly workoutModelRepository: Repository, 15 | ) {} 16 | async findAllWorkouts(): Promise { 17 | const workouts = await this.workoutModelRepository.find(); 18 | if (workouts.length === 0) { 19 | throw new NotFoundAllWorkoutsException(); 20 | } 21 | return workouts; 22 | } 23 | 24 | async findByIdWorkout(id: number): Promise { 25 | const workout = await this.workoutModelRepository.findOne({ 26 | where: { 27 | id, 28 | }, 29 | }); 30 | if (!workout) { 31 | throw new NotFoundWorkoutException(); 32 | } 33 | return workout; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /BackEnd/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /BackEnd/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /BackEnd/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /BackEnd/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2021", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/.swiftformat: -------------------------------------------------------------------------------- 1 | --swiftversion 5.9 2 | 3 | # format options 4 | 5 | --indent 2 6 | --selfrequired 7 | --importgrouping alpha 8 | --enable acronyms 9 | --acronyms "URL, ID, UUID" 10 | --enable blankLineAfterImports 11 | --enable blankLinesAroundMark 12 | --enable blockComments 13 | --enable docComments 14 | --enable isEmpty 15 | --enable markTypes 16 | --enable sortSwitchCases 17 | --enable wrapEnumCases 18 | --enable wrapSwitchCases 19 | --guardelse "next-line" 20 | 21 | --disable wrapMultilineStatementBraces 22 | --disable andOperator 23 | --disable redundantReturn 24 | -------------------------------------------------------------------------------- /iOS/Makefile: -------------------------------------------------------------------------------- 1 | generate: 2 | tuist fetch 3 | TUIST_ROOT_DIR=${PWD} TUIST_SCHEME=DEBUG tuist generate 4 | 5 | release: 6 | tuist fetch 7 | TUIST_ROOT_DIR=${PWD} TUIST_SCHEME=RELEASE tuist generate 8 | 9 | feature: 10 | chmod +x Scripts/create_module.sh 11 | @./Scripts/create_module.sh Feature 12 | tuist edit 13 | 14 | demo: 15 | chmod +x Scripts/create_module.sh 16 | @./Scripts/create_module.sh Demo 17 | tuist edit 18 | 19 | ci: 20 | tuist clean 21 | tuist fetch 22 | TUIST_ROOT_DIR=${PWD} TUIST_CI=TRUE tuist test 23 | 24 | test: 25 | tuist clean 26 | tuist fetch 27 | TUIST_ROOT_DIR=${PWD} tuist test 28 | 29 | build: 30 | TUIST_ROOT_DIR=${PWD} tuist build 31 | 32 | clean: 33 | rm -rf **/*.xcodeproj 34 | rm -rf *.xcworkspace 35 | 36 | reset: 37 | tuist clean 38 | rm -rf **/*.xcodeproj 39 | rm -rf *.xcworkspace 40 | -------------------------------------------------------------------------------- /iOS/Plugins/DependencyPlugin/Plugin.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | 3 | let dependencyPlugin = Plugin(name: "DependencyPlugin") 4 | -------------------------------------------------------------------------------- /iOS/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Path+relativeTo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Path+relativeTo.swift 3 | // DependencyPlugin 4 | // 5 | // Created by 홍승현 on 11/19/23. 6 | // 7 | 8 | import ProjectDescription 9 | 10 | public extension Path { 11 | static func relativeToFeature(_ path: String) -> Path { 12 | .relativeToRoot("Projects/Feature/\(path)") 13 | } 14 | 15 | static func relativeToApp(_ path: String) -> Path { 16 | .relativeToRoot("Projects/App/\(path)") 17 | } 18 | 19 | static func relativeToCore(_ path: String) -> Path { 20 | .relativeToRoot("Projects/Core/\(path)") 21 | } 22 | 23 | static func relativeToShared(_ path: String) -> Path { 24 | .relativeToRoot("Projects/Shared/\(path)") 25 | } 26 | 27 | static func relativeToXCConfig(_ path: String = "Shared") -> Path { 28 | .relativeToRoot("XCConfig/\(path).xcconfig") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /iOS/Plugins/EnvironmentPlugin/Plugin.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | 3 | let environmentPlugin = Plugin(name: "EnvironmentPlugin") 4 | -------------------------------------------------------------------------------- /iOS/Plugins/EnvironmentPlugin/ProjectDescriptionHelpers/ProjectEnvironment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProjectEnvironment.swift 3 | // DependencyPlugin 4 | // 5 | // Created by 홍승현 on 11/20/23. 6 | // 7 | 8 | import ProjectDescription 9 | 10 | public struct ProjectEnvironment { 11 | public let appName: String 12 | public let targetName: String 13 | public let prefixBundleID: String 14 | public let deploymentTarget: DeploymentTarget 15 | public let baseSetting: SettingsDictionary 16 | 17 | private init(appName: String, targetName: String, prefixBundleID: String, deploymentTarget: DeploymentTarget, baseSetting: SettingsDictionary) { 18 | self.appName = appName 19 | self.targetName = targetName 20 | self.prefixBundleID = prefixBundleID 21 | self.deploymentTarget = deploymentTarget 22 | self.baseSetting = baseSetting 23 | } 24 | 25 | public static var `default`: ProjectEnvironment { 26 | ProjectEnvironment( 27 | appName: "WeTri", 28 | targetName: "WeTri", 29 | prefixBundleID: "kr.codesquad.boostcamp8", 30 | deploymentTarget: .iOS(targetVersion: "16.0", devices: .iphone), 31 | baseSetting: [:] 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /iOS/Projects/App/WeTri/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "WeTri-Logo-Background.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /iOS/Projects/App/WeTri/Resources/Assets.xcassets/AppIcon.appiconset/WeTri-Logo-Background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm2023/iOS08-WeTri/83d94b1e6f269048d1d7a40a06e8488def29834c/iOS/Projects/App/WeTri/Resources/Assets.xcassets/AppIcon.appiconset/WeTri-Logo-Background.png -------------------------------------------------------------------------------- /iOS/Projects/App/WeTri/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /iOS/Projects/App/WeTri/Sources/Application/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @main 4 | final class AppDelegate: UIResponder, UIApplicationDelegate { 5 | func application( 6 | _: UIApplication, 7 | didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil 8 | ) -> Bool { 9 | return true 10 | } 11 | 12 | func application( 13 | _: UIApplication, 14 | configurationForConnecting connectingSceneSession: UISceneSession, 15 | options _: UIScene.ConnectionOptions 16 | ) 17 | -> UISceneConfiguration { 18 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /iOS/Projects/App/WeTri/Sources/Application/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // WeTri 4 | // 5 | // Created by 홍승현 on 11/10/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | import DesignSystem 12 | 13 | final class SceneDelegate: UIResponder, UIWindowSceneDelegate { 14 | var window: UIWindow? 15 | private var coordinating: AppCoordinating? 16 | 17 | func scene(_ scene: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) { 18 | guard let windowScene = scene as? UIWindowScene else { return } 19 | window = UIWindow(windowScene: windowScene) 20 | let navigationController = UINavigationController() 21 | window?.rootViewController = navigationController 22 | let coordinator = AppCoordinator(navigationController: navigationController) 23 | coordinating = coordinator 24 | coordinator.start() 25 | window?.makeKeyAndVisible() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /iOS/Projects/App/WeTri/Sources/CommonScene/Coordinator/Protocol/AppCoordinating.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppCoordinating.swift 3 | // WeTri 4 | // 5 | // Created by 안종표 on 2023/11/15. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Coordinator 10 | import Foundation 11 | 12 | protocol AppCoordinating: Coordinating { 13 | func showLoginFlow() 14 | func showTabBarFlow() 15 | func showOnboardingFlow() 16 | } 17 | -------------------------------------------------------------------------------- /iOS/Projects/App/WeTri/Sources/TabBarScene/Coordinator/Protocol/TabBarCoordinating.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabBarCoordinating.swift 3 | // WeTri 4 | // 5 | // Created by 안종표 on 2023/11/15. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Coordinator 10 | import UIKit 11 | 12 | protocol TabBarCoordinating: Coordinating { 13 | var tabBarController: UITabBarController { get } 14 | } 15 | -------------------------------------------------------------------------------- /iOS/Projects/App/WeTri/WeTri.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.applesignin 6 | 7 | Default 8 | 9 | com.apple.developer.healthkit 10 | 11 | com.apple.developer.healthkit.access 12 | 13 | com.apple.developer.healthkit.background-delivery 14 | 15 | keychain-access-groups 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /iOS/Projects/Core/Cacher/Project.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | import ProjectDescriptionHelpers 3 | 4 | let project = Project.makeModule( 5 | name: "Cacher", 6 | targets: .custom( 7 | name: "Cacher", 8 | product: .framework, 9 | testingOptions: [.unitTest], 10 | dependencies: [.log] 11 | ) 12 | ) 13 | -------------------------------------------------------------------------------- /iOS/Projects/Core/Cacher/Sources/CacheManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CacheManager.swift 3 | // ProjectDescriptionHelpers 4 | // 5 | // Created by 안종표 on 12/3/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public final class CacheManager { 11 | public static let shared = CacheManager() 12 | 13 | private let cacher = Cacher(fileManager: FileManager.default) 14 | 15 | private init() {} 16 | 17 | public func fetch(cacheKey: String) throws -> Data? { 18 | try cacher.fetch(cacheKey: cacheKey) 19 | } 20 | 21 | public func set(cacheKey: String, data: Data) throws { 22 | try cacher.set(data: data, cacheKey: cacheKey) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /iOS/Projects/Core/Cacher/Sources/MemoryCacheManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MemoryCacheManager.swift 3 | // Cacher 4 | // 5 | // Created by MaraMincho on 12/6/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// 인메모리 캐시를 활용하여 정보를 저장하고 fetch합니다. 12 | public final class MemoryCacheManager { 13 | public static let shared = MemoryCacheManager() 14 | 15 | private let cacher = Cacher(fileManager: FileManager.default) 16 | 17 | private init() {} 18 | 19 | /// 인메모리 캐시를 활용하여 정보를 fetch합니다. 20 | public func fetch(cacheKey key: String) -> Data? { 21 | return cacher.fetchMemoryData(cacheKey: key) 22 | } 23 | 24 | /// 인메모리 캐시를 활용하여 정보를 저장합니다. 25 | public func set(cacheKey key: String, data: Data) { 26 | cacher.setMemory(data: data, cacheKey: key) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /iOS/Projects/Core/Cacher/Tests/CacherTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CacherTest.swift 3 | // ProjectDescriptionHelpers 4 | // 5 | // Created by 안종표 on 12/3/23. 6 | // 7 | 8 | import XCTest 9 | 10 | final class CacherTest: XCTestCase {} 11 | -------------------------------------------------------------------------------- /iOS/Projects/Core/Coordinator/Project.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project.swift 3 | // ProjectDescriptionHelpers 4 | // 5 | // Created by 안종표 on 2023/11/16. 6 | // 7 | 8 | import ProjectDescription 9 | import ProjectDescriptionHelpers 10 | 11 | let project = Project.makeModule( 12 | name: "Coordinator", 13 | targets: .custom(name: "Coordinator", product: .framework) 14 | ) 15 | -------------------------------------------------------------------------------- /iOS/Projects/Core/Coordinator/Sources/CoordinatorFlow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoordinatorFlow.swift 3 | // Coordinator 4 | // 5 | // Created by 안종표 on 2023/11/16. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - CoordinatorFlow 12 | 13 | public enum CoordinatorFlow { 14 | case splash 15 | case login 16 | case signup 17 | case tabBar 18 | case workoutSetting 19 | case workout 20 | case onboarding 21 | case profile 22 | case home 23 | case writeBoard 24 | } 25 | -------------------------------------------------------------------------------- /iOS/Projects/Core/Coordinator/Sources/Delegate/CoordinatorFinishDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoordinatorFinishDelegate.swift 3 | // WeTri 4 | // 5 | // Created by 안종표 on 2023/11/15. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol CoordinatorFinishDelegate: AnyObject { 12 | func flowDidFinished(childCoordinator: Coordinating) 13 | } 14 | -------------------------------------------------------------------------------- /iOS/Projects/Core/Coordinator/Sources/Protocol/Coordinating.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Coordinating.swift 3 | // WeTri 4 | // 5 | // Created by 안종표 on 2023/11/15. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | // MARK: - Coordinating 12 | 13 | public protocol Coordinating: AnyObject { 14 | var navigationController: UINavigationController { get set } 15 | var childCoordinators: [Coordinating] { get set } 16 | var finishDelegate: CoordinatorFinishDelegate? { get set } 17 | var flow: CoordinatorFlow { get } 18 | 19 | func start() 20 | func finish() 21 | } 22 | 23 | public extension Coordinating { 24 | func finish() { 25 | childCoordinators.removeAll() 26 | finishDelegate?.flowDidFinished(childCoordinator: self) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /iOS/Projects/Core/Keychain/Project.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project.swift 3 | // ProjectDescriptionHelpers 4 | // 5 | // Created by 안종표 on 11/28/23. 6 | // 7 | 8 | import ProjectDescription 9 | import ProjectDescriptionHelpers 10 | 11 | let project = Project.makeModule( 12 | name: "Keychain", 13 | targets: .custom(name: "Keychain", product: .framework, testingOptions: [.unitTest]) 14 | ) 15 | -------------------------------------------------------------------------------- /iOS/Projects/Core/Keychain/Sources/Protocol/KeyChaining.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyChaining.swift 3 | // Keychain 4 | // 5 | // Created by 안종표 on 11/28/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol Keychaining { 12 | /// 키체인에 키-data로 데이터를 저장합니다. 13 | @discardableResult 14 | func save(key: String, data: Data) -> OSStatus 15 | 16 | /// 키체인에서 키를 통해 data 값을 얻어옵니다. 17 | func load(key: String) -> Data? 18 | 19 | /// 키체인에서 해당하는 키를 삭제합니다. 20 | @discardableResult 21 | func delete(key: String) -> OSStatus 22 | } 23 | -------------------------------------------------------------------------------- /iOS/Projects/Core/Keychain/Tests/Mock/MockKeychain.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockKeychain.swift 3 | // Keychain 4 | // 5 | // Created by 안종표 on 11/28/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Keychain 11 | 12 | final class MockKeychain: Keychaining { 13 | var keyChain: [String: Data] = [:] 14 | 15 | @discardableResult 16 | func save(key: String, data: Data) -> OSStatus { 17 | keyChain[key] = data 18 | return noErr 19 | } 20 | 21 | func load(key: String) -> Data? { 22 | guard let data = keyChain[key] else { 23 | return nil 24 | } 25 | return data 26 | } 27 | 28 | @discardableResult 29 | func delete(key: String) -> OSStatus { 30 | guard keyChain[key] != nil else { 31 | return errSecItemNotFound 32 | } 33 | keyChain.removeValue(forKey: key) 34 | return noErr 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /iOS/Projects/Core/Network/Project.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | import ProjectDescriptionHelpers 3 | 4 | let project = Project.makeModule( 5 | name: "Trinet", 6 | targets: .custom(name: "Trinet", product: .framework, testingOptions: [.unitTest]) 7 | ) 8 | -------------------------------------------------------------------------------- /iOS/Projects/Core/Network/Sources/EmptyModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmptyModel.swift 3 | // Trinet 4 | // 5 | // Created by 홍승현 on 12/8/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// 빈 데이터를 전달받을 때 사용합니다. 12 | public struct EmptyModel: Codable {} 13 | -------------------------------------------------------------------------------- /iOS/Projects/Core/Network/Sources/Foundation/TNHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TNHeader.swift 3 | // Trinet 4 | // 5 | // Created by MaraMincho on 11/14/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - TNHeader 12 | 13 | /// HTTP 헤더를 나타냅니다. 14 | public struct TNHeader: Hashable { 15 | let key: String 16 | let value: String 17 | 18 | public init(key: String, value: String) { 19 | self.key = key 20 | self.value = value 21 | } 22 | } 23 | 24 | public extension TNHeader { 25 | static func accept(_ value: String) -> Self { 26 | TNHeader(key: "Accept", value: value) 27 | } 28 | 29 | static func contentType(_ value: String) -> Self { 30 | TNHeader(key: "Content-Type", value: value) 31 | } 32 | 33 | static func authorization(bearer token: String) -> Self { 34 | TNHeader(key: "Authorization", value: "Bearer \(token)") 35 | } 36 | } 37 | 38 | // MARK: CustomStringConvertible 39 | 40 | extension TNHeader: CustomStringConvertible { 41 | public var description: String { 42 | return "\(key): \(value)" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /iOS/Projects/Core/Network/Sources/Foundation/TNMethod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TNMethod.swift 3 | // Trinet 4 | // 5 | // Created by MaraMincho on 11/14/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// HTTP Method를 나타냅니다. 12 | public enum TNMethod: String { 13 | /// The `GET` Method. 14 | case get = "GET" 15 | 16 | /// The `POST` Method. 17 | case post = "POST" 18 | 19 | /// The `DELETE` Method. 20 | case delete = "DELETE" 21 | 22 | /// The `PUT` Method. 23 | case put = "PUT" 24 | } 25 | -------------------------------------------------------------------------------- /iOS/Projects/Core/Network/Sources/GWResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GWResponse.swift 3 | // Trinet 4 | // 5 | // Created by 홍승현 on 12/8/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct GWResponse: Decodable where T: Decodable { 12 | public let code: Int? 13 | public let errorMessage: String? 14 | public let data: T? 15 | } 16 | -------------------------------------------------------------------------------- /iOS/Projects/Core/Network/Sources/Multipart/MultipartItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultipartItem.swift 3 | // Trinet 4 | // 5 | // Created by 홍승현 on 12/8/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - MultipartItem 12 | 13 | public struct MultipartItem { 14 | let data: Data 15 | let mimeType: MimeType 16 | let fileExtension: FileExtension 17 | 18 | public init(data: Data, mimeType: MimeType, fileExtension: FileExtension = .png) { 19 | self.data = data 20 | self.mimeType = mimeType 21 | self.fileExtension = fileExtension 22 | } 23 | 24 | public enum MimeType: String { 25 | case imagePNG = "image/png" 26 | } 27 | } 28 | 29 | // MARK: - FileExtension 30 | 31 | public enum FileExtension: String { 32 | case png = ".png" 33 | case jpg = ".jpg" 34 | case jpeg = ".jpeg" 35 | } 36 | -------------------------------------------------------------------------------- /iOS/Projects/Core/Network/Sources/Multipart/TNFormDataEndPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TNFormDataEndPoint.swift 3 | // Trinet 4 | // 5 | // Created by 안종표 on 12/7/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - TNFormDataEndPoint 12 | 13 | public protocol TNFormDataEndPoint { 14 | var baseURL: String { get } 15 | var path: String { get } 16 | var method: TNMethod { get } 17 | var body: Data? { get } 18 | var headers: TNHeaders { get } 19 | } 20 | 21 | public extension TNFormDataEndPoint { 22 | var baseURL: String { 23 | // request를 생성할 때 빈 문자열이면 invalidURL Error로 자연스레 들어갑니다. 24 | return Bundle.main.infoDictionary?["BaseURL"] as? String ?? "" 25 | } 26 | } 27 | 28 | public extension TNFormDataEndPoint { 29 | func request() throws -> URLRequest { 30 | guard let targetURL = URL(string: baseURL)?.appending(path: path) 31 | else { 32 | throw TNError.invalidURL 33 | } 34 | var request = URLRequest(url: targetURL) 35 | request.httpMethod = method.rawValue 36 | request.allHTTPHeaderFields = headers.dictionary 37 | request.httpBody = body 38 | return request 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /iOS/Projects/Core/Network/Sources/Protocol/URLSessionProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLSessionProtocol.swift 3 | // Trinet 4 | // 5 | // Created by MaraMincho on 11/15/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - URLSessionProtocol 12 | 13 | public protocol URLSessionProtocol { 14 | func data(for request: URLRequest, delegate: URLSessionTaskDelegate?) async throws -> (Data, URLResponse) 15 | 16 | func upload(for request: URLRequest, from: Data) async throws -> (Data, URLResponse) 17 | 18 | func dataTask( 19 | with request: URLRequest, 20 | completionHandler: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void 21 | ) -> URLSessionDataTask 22 | } 23 | 24 | // MARK: - URLSession + URLSessionProtocol 25 | 26 | extension URLSession: URLSessionProtocol {} 27 | -------------------------------------------------------------------------------- /iOS/Projects/Core/Network/Sources/Protocol/URLSessionWebSocketProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLSessionWebSocketProtocol.swift 3 | // Trinet 4 | // 5 | // Created by 홍승현 on 11/29/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - URLSessionWebSocketProtocol 12 | 13 | public protocol URLSessionWebSocketProtocol { 14 | func webSocketTask(with request: URLRequest) -> WebSocketTaskProtocol 15 | } 16 | 17 | // MARK: - URLSession + URLSessionWebSocketProtocol 18 | 19 | extension URLSession: URLSessionWebSocketProtocol { 20 | public func webSocketTask(with request: URLRequest) -> WebSocketTaskProtocol { 21 | let socketTask: URLSessionWebSocketTask = webSocketTask(with: request) 22 | return socketTask 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /iOS/Projects/Core/Network/Sources/Protocol/WebSocketTaskProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebSocketTaskProtocol.swift 3 | // Trinet 4 | // 5 | // Created by 홍승현 on 11/29/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - WebSocketTaskProtocol 12 | 13 | public protocol WebSocketTaskProtocol { 14 | func send(_ message: URLSessionWebSocketTask.Message) async throws 15 | 16 | func receive() async throws -> URLSessionWebSocketTask.Message 17 | 18 | func resume() 19 | } 20 | 21 | // MARK: - URLSessionWebSocketTask + WebSocketTaskProtocol 22 | 23 | extension URLSessionWebSocketTask: WebSocketTaskProtocol {} 24 | -------------------------------------------------------------------------------- /iOS/Projects/Core/Network/Sources/TNError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TNError.swift 3 | // Trinet 4 | // 5 | // Created by MaraMincho on 11/14/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum TNError: LocalizedError { 12 | case invalidURL 13 | case unknownError 14 | case redirectError 15 | case clientError 16 | case serverError 17 | case httpResponseDownCastingError 18 | case cantMakeURLSessionWithAdaptor 19 | 20 | public var errorDescription: String? { 21 | switch self { 22 | case .invalidURL: 23 | return "URL이 잘못되었습니다." 24 | case .clientError: 25 | return "Client Response Error가 발생하였습니다." 26 | case .redirectError: 27 | return "Client는 요청 완료를 위해 리다이렉션과 같은 작업을 수행해야 합니다." 28 | case .serverError: 29 | return "Server Error가 발생하였습니다." 30 | case .unknownError: 31 | return "UnknownError가 발생하였습니다." 32 | case .httpResponseDownCastingError: 33 | return "HTTPResponse를 다운캐스팅 할 수 없습니다. 요청하는 URL주소를 다시 확인하거나, Mock Response를 확인해 주세요" 34 | case .cantMakeURLSessionWithAdaptor: 35 | return "adaptor함수를 잘못 만들어서 에러가 발생했습니다. adaptor함수를 다시 확인해주세요" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /iOS/Projects/Core/Network/Sources/TNRequestInterceptor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TNRequestInterceptor.swift 3 | // Trinet 4 | // 5 | // Created by MaraMincho on 11/30/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - TNRequestAdaptor 12 | 13 | /// 네트워크 통신 직전 urlRequest를 얻는다. 14 | /// 얻어진 urlRequest를 통해 적절한 작업을 통해 URLRequest를 리턴한다. 15 | /// eg) AccessToken을 추가한다거나, contentType header를 추가한다. 16 | public protocol TNRequestAdaptor { 17 | func adapt(_ request: URLRequest, session: URLSessionProtocol) -> URLRequest 18 | } 19 | 20 | // MARK: - TNRequestRetrier 21 | 22 | /// 네트워크 통신 하자마자 얻은 데이터를 인풋으로 얻는다. 23 | /// 얻은 인풋을 적절한 작업을 통해 재 전송하거나, 다른 작업을 취한다. 24 | /// TriNet의 기본 Data요청인 async await을 통한 데이터 return타입을 따른다 25 | /// eg) 현재 매개변수인 response를 적절하게 파싱하여 statusCode를 얻고, 이에 따라 Redirect를 보낸다 26 | public protocol TNRequestRetrier { 27 | func retry( 28 | _ request: URLRequest, 29 | session: URLSessionProtocol, 30 | data: Data, 31 | response: URLResponse, 32 | delegate: URLSessionDelegate? 33 | ) async throws -> (Data, URLResponse) 34 | } 35 | 36 | public typealias TNRequestInterceptor = TNRequestAdaptor & TNRequestRetrier 37 | -------------------------------------------------------------------------------- /iOS/Projects/Core/Network/Tests/MockEndPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockEndPoint.swift 3 | // TrinetTests 4 | // 5 | // Created by 홍승현 on 11/27/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Trinet 11 | 12 | struct MockEndPoint: TNEndPoint { 13 | var baseURL: String = "base" 14 | var path: String = "path" 15 | var method: TNMethod = .get 16 | var query: Encodable? = nil 17 | var body: Encodable? = nil 18 | var headers: TNHeaders = [] 19 | } 20 | -------------------------------------------------------------------------------- /iOS/Projects/Core/Persistence/Project.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | import ProjectDescriptionHelpers 3 | -------------------------------------------------------------------------------- /iOS/Projects/Domain/Entity/Project.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | import ProjectDescriptionHelpers 3 | -------------------------------------------------------------------------------- /iOS/Projects/Domain/Repositories/Project.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | import ProjectDescriptionHelpers 3 | -------------------------------------------------------------------------------- /iOS/Projects/Domain/Usecase/Project.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | import ProjectDescriptionHelpers 3 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Home/Project.swift: -------------------------------------------------------------------------------- 1 | import DependencyPlugin 2 | import ProjectDescription 3 | import ProjectDescriptionHelpers 4 | 5 | let project = Project.makeModule( 6 | name: "HomeFeature", 7 | targets: .feature( 8 | .home, 9 | testingOptions: [.unitTest], 10 | dependencies: [ 11 | .designSystem, 12 | .log, 13 | .combineCocoa, 14 | .trinet, 15 | .combineExtension, 16 | .coordinator, 17 | .commonNetworkingKeyManager, 18 | .downSampling, 19 | ], 20 | testDependencies: [] 21 | ) 22 | ) 23 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Home/Sources/Data/HomeRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeRepository.swift 3 | // HomeFeature 4 | // 5 | // Created by MaraMincho on 12/7/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Home/Sources/Domain/Entity/FeedElement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FeedElement.swift 3 | // HomeFeature 4 | // 5 | // Created by MaraMincho on 12/7/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct FeedElement: Hashable, Codable { 12 | /// 개시물의 아이디 입니다. 13 | let ID: Int 14 | 15 | /// 게시물을 올린 유저의 고유 식별자 입니다. 16 | let publicID: String 17 | 18 | /// 게시물을 올린 유저의 닉네임 입니다. 19 | let nickName: String 20 | 21 | /// 언제 게시물을 발행했는지에 대한 정보 입니다. 22 | let publishDate: Date 23 | 24 | /// 게시물 올린 유저의 프로피 이미지 입니다. 25 | let profileImage: URL? 26 | 27 | /// 어떤 운동을 했는지에 관한 값 입니다. 28 | let sportText: String 29 | 30 | /// 게시물의 내용 입니다. 31 | let content: String 32 | 33 | /// 게시물 관련 이미지에 관한 값 입니다. 34 | let postImages: [URL?] 35 | 36 | /// 좋아요 갯수 입니다. 37 | let like: Int 38 | } 39 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Home/Sources/Domain/RepositoryInterface/FeedRepositoryRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FeedRepositoryRepresentable.swift 3 | // HomeFeature 4 | // 5 | // Created by MaraMincho on 1/2/24. 6 | // Copyright © 2024 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | // MARK: - FeedRepositoryRepresentable 13 | 14 | public protocol FeedRepositoryRepresentable { 15 | func fetchFeed(at page: Int) -> AnyPublisher<[FeedElement], Never> 16 | func refreshFeed() -> AnyPublisher<[FeedElement], Never> 17 | } 18 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Home/Sources/Presntaion/HomeScene/ViewController/HomeViewController+CompositionlLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeViewController+CompositionlLayout.swift 3 | // HomeFeature 4 | // 5 | // Created by MaraMincho on 1/3/24. 6 | // Copyright © 2024 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension HomeViewController { 12 | static func makeFeedCollectionViewLayout() -> UICollectionViewCompositionalLayout { 13 | let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1)) 14 | let item = NSCollectionLayoutItem(layoutSize: itemSize) 15 | item.contentInsets = .init(top: 9, leading: 0, bottom: 9, trailing: 0) 16 | 17 | let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(455)) 18 | let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item]) 19 | 20 | let section = NSCollectionLayoutSection(group: group) 21 | 22 | return UICollectionViewCompositionalLayout(section: section) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Home/Tests/HomeFeatureTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class HomeFeatureTests: XCTestCase { 4 | func testAlwaysPassed() { 5 | XCTAssertTrue(true) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Home/Tests/Keep.swift: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm2023/iOS08-WeTri/83d94b1e6f269048d1d7a40a06e8488def29834c/iOS/Projects/Features/Home/Tests/Keep.swift -------------------------------------------------------------------------------- /iOS/Projects/Features/Login/Project.swift: -------------------------------------------------------------------------------- 1 | import DependencyPlugin 2 | import ProjectDescription 3 | import ProjectDescriptionHelpers 4 | 5 | let project = Project.makeModule( 6 | name: "LoginFeature", 7 | targets: .feature( 8 | .login, 9 | testingOptions: [.unitTest], 10 | dependencies: [.trinet, .keychain, .combineCocoa, .log, .auth, .userInformationManager], 11 | testDependencies: [], 12 | resources: "Resources/**" 13 | ) 14 | ) 15 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Login/Resources/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 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Login/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Login/Resources/Persistency/InitialUser.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": 201, 3 | "errorMessage": null, 4 | "data": { 5 | "isFirstLogined": true, 6 | "mappedUserID": "1233498sdafksdjhfk...", 7 | "provider": "apple" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Login/Resources/Persistency/Token.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": 200, 3 | "errorMessage": null, 4 | "data": { 5 | "accessToken": "ewaf1313RWDFA...", 6 | "refreshToken": "ewaf1313RWDFdddA..." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Login/Resources/cycleing.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm2023/iOS08-WeTri/83d94b1e6f269048d1d7a40a06e8488def29834c/iOS/Projects/Features/Login/Resources/cycleing.mp4 -------------------------------------------------------------------------------- /iOS/Projects/Features/Login/Resources/running.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm2023/iOS08-WeTri/83d94b1e6f269048d1d7a40a06e8488def29834c/iOS/Projects/Features/Login/Resources/running.mp4 -------------------------------------------------------------------------------- /iOS/Projects/Features/Login/Sources/Data/DTO/AuthorizationInfoRequestDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthorizationInfoRequestDTO.swift 3 | // LoginFeature 4 | // 5 | // Created by 안종표 on 11/29/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// 애플로그인을 통해 받아온 데이터 entity 12 | struct AuthorizationInfoRequestDTO: Codable { 13 | /// identityToken 14 | let identityToken: String 15 | 16 | /// authorizationCode 17 | let authorizationCode: String 18 | } 19 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Login/Sources/Data/DTO/Response.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Response.swift 3 | // LoginFeature 4 | // 5 | // Created by 안종표 on 12/7/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - Response 12 | 13 | struct Response { 14 | let code: Int? 15 | let errorMessage: String? 16 | } 17 | 18 | // MARK: Codable 19 | 20 | extension Response: Codable {} 21 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Login/Sources/Domain/Entity/AuthorizationInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthorizationInfo.swift 3 | // LoginFeature 4 | // 5 | // Created by 안종표 on 11/28/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - AuthorizationInfo 12 | 13 | /// 애플로그인을 통해 받아온 데이터 entity 14 | public struct AuthorizationInfo { 15 | /// identityToken 16 | let identityToken: Data 17 | 18 | /// authorizationCode 19 | let authorizationCode: Data 20 | } 21 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Login/Sources/Domain/Entity/InitialUser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InitialUser.swift 3 | // LoginFeature 4 | // 5 | // Created by 안종표 on 12/7/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Auth 10 | import Foundation 11 | 12 | // MARK: - InitialUser 13 | 14 | /// 처음 로그인 하는 유저의 Response를 담을 Entity 15 | public struct InitialUser { 16 | /// 처음 로그인 하는지 아닌지 17 | public let isFirstLogined: Bool? 18 | 19 | /// 20 | public let mappedUserID: String 21 | 22 | /// OAuth 로그인 종류 23 | public let provider: AuthProvider 24 | 25 | public init( 26 | isFirstLogined: Bool, 27 | mappedUserID: String, 28 | provider: AuthProvider 29 | ) { 30 | self.isFirstLogined = isFirstLogined 31 | self.mappedUserID = mappedUserID 32 | self.provider = provider 33 | } 34 | } 35 | 36 | // MARK: Codable 37 | 38 | extension InitialUser: Codable {} 39 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Login/Sources/Domain/Interfaces/Repositories/AuthorizationRepositoryRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthorizationRepositoryRepresentable.swift 3 | // LoginFeature 4 | // 5 | // Created by 안종표 on 11/28/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Auth 10 | import Combine 11 | import Foundation 12 | 13 | protocol AuthorizationRepositoryRepresentable { 14 | typealias LoginResponse = (token: Token?, initialUser: InitialUser?) 15 | 16 | func fetch(authorizationInfo: AuthorizationInfo) -> AnyPublisher 17 | } 18 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Login/Sources/Domain/Interfaces/Repositories/KeychainRepositoryRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeychainRepositoryRepresentable.swift 3 | // LoginFeature 4 | // 5 | // Created by 안종표 on 11/29/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | protocol KeychainRepositoryRepresentable { 13 | /// 키체인에 키-data로 데이터를 저장합니다. 14 | func save(key: String, value: String) 15 | 16 | /// 키체인에서 키를 통해 data 값을 얻어옵니다. 17 | func load(key: String) -> AnyPublisher 18 | 19 | /// 키체인에서 해당하는 키를 삭제합니다. 20 | func delete(key: String) -> AnyPublisher 21 | } 22 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Login/Sources/Domain/UseCases/Protocol/AuthorizeUseCaseRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthorizeUseCaseRepresentable.swift 3 | // LoginFeature 4 | // 5 | // Created by 안종표 on 11/28/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Auth 10 | import Combine 11 | import Foundation 12 | 13 | protocol AuthorizeUseCaseRepresentable { 14 | typealias LoginResponse = (token: Token?, initialUser: InitialUser?) 15 | 16 | func authorize(authorizationInfo: AuthorizationInfo) -> AnyPublisher 17 | func accessTokenSave(_ token: String) 18 | func refreshTokenSave(_ token: String) 19 | } 20 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Login/Sources/Presentation/LoginScene/Coordinator/Delegate/LoginDidFinishedDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginDidFinishedDelegate.swift 3 | // LoginFeature 4 | // 5 | // Created by 안종표 on 12/7/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Auth 10 | import Foundation 11 | 12 | public protocol LoginDidFinishedDelegate: AnyObject { 13 | func loginCoordinatorDidFinished(initialUser: InitialUser?, token: Token?) 14 | } 15 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Login/Sources/Presentation/LoginScene/Coordinator/Protocol/LoginCoordinating.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginCoordinating.swift 3 | // LoginFeature 4 | // 5 | // Created by 안종표 on 12/7/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Auth 10 | import Coordinator 11 | import Foundation 12 | 13 | protocol LoginCoordinating: Coordinating { 14 | func finish(initialUser: InitialUser?, token: Token?) 15 | } 16 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Login/Sources/Presentation/LoginScene/Coordinator/Protocol/LoginFeatureCoordinating.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginFeatureCoordinating.swift 3 | // LoginFeature 4 | // 5 | // Created by 안종표 on 12/7/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Auth 10 | import Coordinator 11 | import Foundation 12 | 13 | protocol LoginFeatureCoordinating: Coordinating { 14 | func showLoginFlow() 15 | func finishLogin(initialUser: InitialUser?, token: Token?) 16 | } 17 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Login/Tests/LoginFeatureTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class LoginFeatureTests: XCTestCase { 4 | func testAlwaysPassed() { 5 | XCTAssertTrue(true) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Onboarding/Project.swift: -------------------------------------------------------------------------------- 1 | import DependencyPlugin 2 | import ProjectDescription 3 | import ProjectDescriptionHelpers 4 | 5 | let project = Project.makeModule( 6 | name: "OnboardingFeature", 7 | targets: .feature( 8 | .onboarding, 9 | testingOptions: [.unitTest], 10 | dependencies: [.designSystem, .combineCocoa, .coordinator, .log, .trinet], 11 | testDependencies: [.designSystem, .combineCocoa, .log], 12 | resources: "Resources/**" 13 | ) 14 | ) 15 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Onboarding/Resources/HealthOnboardingImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm2023/iOS08-WeTri/83d94b1e6f269048d1d7a40a06e8488def29834c/iOS/Projects/Features/Onboarding/Resources/HealthOnboardingImage.png -------------------------------------------------------------------------------- /iOS/Projects/Features/Onboarding/Resources/HealthOnboardingPropertyText.json: -------------------------------------------------------------------------------- 1 | { 2 | "code" : 0, 3 | "errorMessage": null, 4 | "data" : 5 | { 6 | "titleText" : "위트리는 건강 정보를 활용하는 앱이예요", 7 | "descriptionText" : "건강정보 허용을 통해 원활하게 앱을 사용해 보세요", 8 | "id" : 2 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Onboarding/Resources/MapOnboardingImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm2023/iOS08-WeTri/83d94b1e6f269048d1d7a40a06e8488def29834c/iOS/Projects/Features/Onboarding/Resources/MapOnboardingImage.png -------------------------------------------------------------------------------- /iOS/Projects/Features/Onboarding/Resources/MapOnboardingPropertyText.json: -------------------------------------------------------------------------------- 1 | { 2 | "code" : 0, 3 | "errorMessage": null, 4 | "data" : 5 | { 6 | "titleText" : "위트리는 지도를 활용하는 앱이예요", 7 | "descriptionText" : "위치권한을 통해서 원활하게 앱을 사용해 보세요", 8 | "id" : 1 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Onboarding/Sources/Data/DTO/OnboardingScenePropertyResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnboardingScenePropertyResponse.swift 3 | // OnboardingFeature 4 | // 5 | // Created by MaraMincho on 11/29/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct OnboardingScenePropertyResponse: Decodable { 12 | let id: Int 13 | let titleText: String 14 | let descriptionText: String 15 | var imageData: Data? 16 | init(id: Int, titleText: String, descriptionText: String, imageData: Data?) { 17 | self.id = id 18 | self.titleText = titleText 19 | self.descriptionText = descriptionText 20 | self.imageData = imageData 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Onboarding/Sources/Domain/UseCasee/Interface/OnboardingPropertyLoadRepositoryRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnboardingPropertyLoadRepositoryRepresentable.swift 3 | // OnboardingFeature 4 | // 5 | // Created by MaraMincho on 11/29/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - OnboardingImageRepositoryRepresentable 12 | 13 | public protocol OnboardingPropertyLoadRepositoryRepresentable { 14 | func mapOnboardingProperty() -> OnboardingScenePropertyResponse? 15 | func healthOnboardingProperty() -> OnboardingScenePropertyResponse? 16 | } 17 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Onboarding/Tests/OnboardingFeatureTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class OnboardingFeatureTests: XCTestCase { 4 | func testAlwaysPassed() { 5 | XCTAssertTrue(true) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Profile/Project.swift: -------------------------------------------------------------------------------- 1 | import DependencyPlugin 2 | import ProjectDescription 3 | import ProjectDescriptionHelpers 4 | 5 | let project = Project.makeModule( 6 | name: "ProfileFeature", 7 | targets: .feature( 8 | .profile, 9 | testingOptions: [.unitTest], 10 | dependencies: [ 11 | .designSystem, 12 | .trinet, 13 | .combineExtension, 14 | .combineCocoa, 15 | .log, 16 | .coordinator, 17 | .commonNetworkingKeyManager, 18 | .keychain, 19 | .userInformationManager, 20 | .feature(.writeBoard), 21 | ], 22 | testDependencies: [], 23 | resources: "Resources/**" 24 | ) 25 | ) 26 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Profile/Resources/Persistency/GetProfile.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": null, 3 | "errorMessage": null, 4 | "data": { 5 | "nickname": "홍승현", 6 | "gender": "남자", 7 | "birthdate": "2021-01-01", 8 | "publicId": "adsd2daw-ad2dawd-q1323123", 9 | "profileImage": "https://gblafytgdduy20857289.cdn.ntruss.com/30ab314b-a59a-44c8-b9c5-44d94b4542f0.png" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Profile/Sources/Data/DTO/PostsRequestDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostsRequestDTO.swift 3 | // ProfileFeature 4 | // 5 | // Created by 홍승현 on 12/4/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - PostsRequestDTO 12 | 13 | public struct PostsRequestDTO: Encodable { 14 | /// id값보다 낮은 아이템을 가져올 때 설정합니다. 15 | /// 16 | /// `idMoreThan`과 동시에 사용하면 안 됩니다. 17 | let idLessThan: Int? 18 | 19 | /// id값보다 높은 아이템을 가져올 때 설정합니다. 20 | /// 21 | /// `idMoreThan`과 동시에 사용해선 안 됩니다. 22 | let idMoreThan: Int? 23 | 24 | /// 정렬 기준입니다. 25 | let orderCreatedAt: Order? 26 | 27 | /// 가져올 아이템 수를 의미합니다. 28 | let limit: Int? 29 | 30 | enum CodingKeys: String, CodingKey { 31 | case idLessThan = "where__id__less_then" 32 | case idMoreThan = "where__id__more_then" 33 | case orderCreatedAt = "order__createdAt" 34 | case limit = "take" 35 | } 36 | } 37 | 38 | // MARK: - Order 39 | 40 | enum Order: String, Codable { 41 | /// 오름차순 42 | case ascending = "ASC" 43 | 44 | /// 내림차순 45 | case descending = "DESC" 46 | } 47 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Profile/Sources/Data/DTO/ProfileDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileDTO.swift 3 | // ProfileFeature 4 | // 5 | // Created by 홍승현 on 12/4/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - ProfileDTO 12 | 13 | /// 프로필 정보를 갖습니다. 14 | public struct ProfileDTO: Decodable { 15 | /// 닉네임 16 | let nickname: String 17 | 18 | /// 성별 19 | /// 20 | /// `남자`, `여자`로 반환됩니다. 21 | let gender: String 22 | 23 | /// 생일 24 | let birthdate: Date 25 | 26 | /// 프로필 이미지 27 | let profileImage: URL 28 | } 29 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Profile/Sources/Domain/Entities/Profile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Profile.swift 3 | // ProfileFeature 4 | // 5 | // Created by 홍승현 on 12/4/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Profile { 12 | /// 프로필 이미지 데이터 13 | let profileData: Data 14 | 15 | /// 프로필 닉네임 16 | let nickname: String 17 | 18 | /// 사용자의 생년월일 19 | let birth: String? 20 | 21 | init(profileData: Data, nickname: String, birth: String? = nil) { 22 | self.profileData = profileData 23 | self.nickname = nickname 24 | self.birth = birth 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Profile/Sources/Domain/Interfaces/KeychainRepositoryRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeychainRepositoryRepresentable.swift 3 | // ProfileFeature 4 | // 5 | // Created by 홍승현 on 12/11/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | public protocol KeychainRepositoryRepresentable { 13 | /// 키체인에 키-data로 데이터를 저장합니다. 14 | func save(key: String, value: String) 15 | 16 | /// 키체인에서 키를 통해 data 값을 얻어옵니다. 17 | func load(key: String) -> AnyPublisher 18 | 19 | /// 키체인에서 해당하는 키를 삭제합니다. 20 | func delete(key: String) -> AnyPublisher 21 | } 22 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Profile/Sources/Domain/Interfaces/ProfileRepositoryRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileRepositoryRepresentable.swift 3 | // ProfileFeature 4 | // 5 | // Created by 홍승현 on 12/4/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | public protocol ProfileRepositoryRepresentable { 13 | func fetchProfiles() -> AnyPublisher 14 | func fetchPosts(nextID: Int?) -> AnyPublisher 15 | } 16 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Profile/Sources/Domain/Interfaces/ProfileSettingsRepositoryRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileSettingsRepositoryRepresentable.swift 3 | // ProfileFeature 4 | // 5 | // Created by 홍승현 on 12/11/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol ProfileSettingsRepositoryRepresentable { 12 | func userInformation() throws -> Profile 13 | } 14 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Profile/Sources/Domain/UseCases/LogoutUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogoutUseCase.swift 3 | // ProfileFeature 4 | // 5 | // Created by 홍승현 on 12/11/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import CommonNetworkingKeyManager 11 | import Foundation 12 | import Log 13 | 14 | // MARK: - AuthorizeUseCase 15 | 16 | final class LogoutUseCase: LogoutUseCaseRepresentable { 17 | private let keychainRepository: KeychainRepositoryRepresentable 18 | 19 | init( 20 | keychainRepository: KeychainRepositoryRepresentable 21 | ) { 22 | self.keychainRepository = keychainRepository 23 | } 24 | 25 | func logout() -> AnyPublisher { 26 | keychainRepository.delete(key: Tokens.accessToken) 27 | .combineLatest(keychainRepository.delete(key: Tokens.refreshToken)) 28 | .map { _ in true } // 삭제 성공 시 true 29 | .catch { _ in Just(false) } // 실패 시 false 30 | .eraseToAnyPublisher() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Profile/Sources/Domain/UseCases/ProfileSettingsUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileSettingsUseCase.swift 3 | // ProfileFeature 4 | // 5 | // Created by 홍승현 on 12/11/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct ProfileSettingsUseCase: ProfileSettingsUseCaseRepresentable { 12 | private let repository: ProfileSettingsRepositoryRepresentable 13 | 14 | init(repository: ProfileSettingsRepositoryRepresentable) { 15 | self.repository = repository 16 | } 17 | 18 | func userInformation() throws -> Profile { 19 | return try repository.userInformation() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Profile/Sources/Domain/UseCases/Protocol/LogoutUseCaseRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogoutUseCaseRepresentable.swift 3 | // ProfileFeature 4 | // 5 | // Created by 홍승현 on 12/11/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Combine 10 | 11 | protocol LogoutUseCaseRepresentable { 12 | func logout() -> AnyPublisher 13 | } 14 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Profile/Sources/Domain/UseCases/Protocol/ProfileSettingsUseCaseRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileSettingsUseCaseRepresentable.swift 3 | // ProfileFeature 4 | // 5 | // Created by 홍승현 on 12/11/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol ProfileSettingsUseCaseRepresentable { 12 | func userInformation() throws -> Profile 13 | } 14 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Profile/Sources/Domain/UseCases/Protocol/ProfileUseCaseRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileUseCaseRepresentable.swift 3 | // ProfileFeature 4 | // 5 | // Created by 홍승현 on 12/4/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | public protocol ProfileUseCaseRepresentable { 13 | func fetchProfile() -> AnyPublisher 14 | func fetchPosts(lastItem: ProfileItem?, refresh: Bool) -> AnyPublisher<[Post], Error> 15 | } 16 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Profile/Sources/Presentation/Coordinator/Protocol/ProfileCoordinating.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileCoordinating.swift 3 | // ProfileFeature 4 | // 5 | // Created by 홍승현 on 12/4/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Coordinator 10 | import UIKit 11 | 12 | public protocol ProfileCoordinating: Coordinating { 13 | /// 설정 창으로 이동합니다. 14 | func pushToSettings() 15 | 16 | /// 로그인 창으로 이동합니다. 17 | func moveToLogin() 18 | 19 | /// 프로필 설정 화면으로 넘어갑니다. 20 | func moveToProfileSettings() 21 | 22 | /// 글쓰기 화면으로 넘어갑니다. 23 | func presentWriteBoard() 24 | } 25 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Profile/Sources/Presentation/ProfileScene/ProfileLayouts/SectionItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SectionItem.swift 3 | // ProfileFeature 4 | // 5 | // Created by 홍승현 on 12/5/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - ProfileSection 12 | 13 | public enum ProfileSection: Int { 14 | case header 15 | case main 16 | case emptyState 17 | } 18 | 19 | public typealias ProfileItem = Post 20 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Profile/Tests/ProfileFeatureTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class ProfileFeatureTests: XCTestCase { 4 | func testAlwaysPassed() { 5 | XCTAssertTrue(true) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Project.swift: -------------------------------------------------------------------------------- 1 | import DependencyPlugin 2 | import ProjectDescription 3 | import ProjectDescriptionHelpers 4 | 5 | let project = Project.makeModule( 6 | name: "RecordFeature", 7 | targets: .feature( 8 | .record, 9 | testingOptions: [.unitTest], 10 | dependencies: [.trinet, .designSystem, .combineCocoa, .combineExtension, .coordinator, .log, .commonNetworkingKeyManager, .cacher, .userInformationManager], 11 | testDependencies: [.trinet, .designSystem, .combineCocoa, .log, .cacher], 12 | resources: "Resources/**" 13 | ) 14 | ) 15 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Resources/Persistency/MatchesCancel.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": null, 3 | "errorMessage": null, 4 | "data": null 5 | } 6 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Resources/Persistency/MatchesRandom.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": null, 3 | "errorMessage": null, 4 | "data": { 5 | "matched": true, 6 | "liveWorkoutStartTime": "2023-12-05 16:33:00", 7 | "roomId": "uuid", 8 | "publicId": "someStringss", 9 | "peers": [ 10 | { 11 | "nickname": "나는 1번 타자", 12 | "publicId": "someString", 13 | "profileImage": "https://upload.wikimedia.org/wikipedia/commons/thumb/7/74/Cat-eating-prey.jpg/220px-Cat-eating-prey.jpg", 14 | "etc": "그 외 나머지 모든 컬럼" 15 | }, 16 | { 17 | "nickname": "나는 2번타자", 18 | "publicId": "someStringw", 19 | "profileImage": "https://www.telegraph.co.uk/content/dam/pets/2017/01/06/1-JS117202740-yana-two-face-cat-news_trans_NvBQzQNjv4BqJNqHJA5DVIMqgv_1zKR2kxRY9bnFVTp4QZlQjJfe6H0.jpg?imwidth=450", 20 | "etc": "그 외 나머지 모든 컬럼" 21 | } 22 | ] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Resources/Persistency/MatchesStart.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": null, 3 | "errorMessage": null, 4 | "data": null 5 | } 6 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Resources/Persistency/PeerTypes.json: -------------------------------------------------------------------------------- 1 | { 2 | "code" : 0, 3 | "errorMessage": null, 4 | "data" : [ 5 | { 6 | "description" : "혼자 진행하기", 7 | "icon" : "person.fill", 8 | "title" : "혼자 하기", 9 | "typeCode" : 1 10 | }, 11 | { 12 | "description" : "랜덤한 사용자와 같이 진행하세요", 13 | "icon" : "person.3.fill", 14 | "title" : "같이 하기", 15 | "typeCode" : 2 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Resources/Persistency/WorkoutSession.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": null, 3 | "errorMessage": null, 4 | "data": { 5 | "recordId": 1 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Resources/Persistency/WorkoutSummary.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": null, 3 | "errorMessage": null, 4 | "data": { 5 | "id": 8, 6 | "workout": "cycling", 7 | "workoutTime": 3600, 8 | "distance": 15, 9 | "calorie": 120, 10 | "avgBpm": 120, 11 | "minBpm": 78, 12 | "maxBpm": 154, 13 | "createdAt": "2023-11-22T02:24:11.942Z", 14 | "isPosted": false, 15 | "locations": [ 16 | { 17 | "latitude": 37.316769, 18 | "longitude": 126.830915 19 | }, 20 | { 21 | "latitude": 37.31621310776837, 22 | "longitude": 126.84403674469276 23 | }, 24 | { 25 | "latitude": 37.3116009783644, 26 | "longitude": 126.84472990193373 27 | } 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Resources/Persistency/WorkoutTypes.json: -------------------------------------------------------------------------------- 1 | { 2 | "code" : 0, 3 | "errorMessage": null, 4 | "data" : [ 5 | { 6 | "icon": "figure.outdoor.cycle", 7 | "description": "사이클", 8 | "typeCode" : 3 9 | }, 10 | { 11 | "icon": "figure.pool.swim", 12 | "description": "수영", 13 | "typeCode" : 2 14 | }, 15 | { 16 | "icon": "figure.run", 17 | "description": "달리기", 18 | "typeCode" : 1 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Data/DTO/DateRequestDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateRequestDTO.swift 3 | // RecordFeature 4 | // 5 | // Created by 안종표 on 11/30/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - DateRequestDTO 12 | 13 | /// 기록을 가져오기 위해 Date를 year, month, day로 변환해서 서버로 request 요청할 때 사용하는 DTO 14 | struct DateRequestDTO { 15 | /// 연도 16 | let year: Int 17 | 18 | /// 월 19 | let month: Int 20 | 21 | /// 일 22 | let day: Int 23 | } 24 | 25 | // MARK: Codable 26 | 27 | extension DateRequestDTO: Codable {} 28 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Data/DTO/IsMatchedRandomPeersRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IsMatchedRandomPeersRequest.swift 3 | // RecordFeature 4 | // 5 | // Created by MaraMincho on 12/5/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// randomMatching API의 requst에 활용합니다. 12 | /// 13 | /// 어떤 운동과, 얼마나 randomMatching을 기다렸는지 알려주는 객체 입니다. 14 | struct IsMatchedRandomPeersRequest: Encodable { 15 | /// 어떤 운동을 랜덤 매칭 하는지 알려줍니다. 16 | /// 17 | /// 1번 : 달리기 18 | /// 2번 : 사이클 19 | /// 3번 : 수영 20 | let workoutID: Int 21 | 22 | /// 몇초를 대기방에서 기다렸는지 알려줍니다. 23 | let waitingTime: Int 24 | 25 | enum CodingKeys: String, CodingKey { 26 | case workoutID = "workoutId" 27 | case waitingTime 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Data/DTO/IsMatchedRandomPeersResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IsMatchedRandomPeersResponse.swift 3 | // RecordFeature 4 | // 5 | // Created by MaraMincho on 12/5/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - IsMatchedRandomPeersResponse 12 | 13 | struct IsMatchedRandomPeersResponse: Decodable { 14 | let matched: Bool 15 | let liveWorkoutStartTime: String? 16 | let myPublicID: String? 17 | let roomID: String? 18 | let peers: [IsMatchedRandomPeersForPeerResponse]? 19 | 20 | enum CodingKeys: String, CodingKey { 21 | case matched 22 | case liveWorkoutStartTime 23 | case roomID = "roomId" 24 | case myPublicID = "myPublicId" 25 | case peers 26 | } 27 | } 28 | 29 | // MARK: - IsMatchedRandomPeersForPeerResponse 30 | 31 | struct IsMatchedRandomPeersForPeerResponse: Decodable { 32 | let nickname: String 33 | let publicID: String 34 | let profileImage: String 35 | let etc: String? 36 | 37 | enum CodingKeys: String, CodingKey { 38 | case nickname 39 | case publicID = "publicId" 40 | case profileImage 41 | case etc 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Data/DTO/MatchCancelRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MatchCancelRequest.swift 3 | // RecordFeature 4 | // 5 | // Created by MaraMincho on 12/10/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct MatchCancelRequest: Encodable { 12 | let workoutID: Int 13 | 14 | enum CodingKeys: String, CodingKey { 15 | case workoutID = "workoutId" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Data/DTO/MatchStartRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MatchStartRequest.swift 3 | // RecordFeature 4 | // 5 | // Created by MaraMincho on 12/5/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// 매칭이 시작 될 떄 사용하는 Request Body입니다. 12 | struct MatchStartRequest: Encodable { 13 | /// workoutId는 운동 번호를 의미합니다. 14 | let workoutID: Int 15 | 16 | enum CodingKeys: String, CodingKey { 17 | case workoutID = "workoutId" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Data/DTO/PeerMatchDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PeerMatchDTO.swift 3 | // RecordFeature 4 | // 5 | // Created by MaraMincho on 11/26/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - PeerMatchResponseDTO 12 | 13 | struct PeerMatchResponseDTO: Codable { 14 | let roomURLString: String 15 | let peersDTO: [PeerDTO] 16 | 17 | enum CodingKeys: String, CodingKey { 18 | case roomURLString = "url" 19 | case peersDTO = "peers" 20 | } 21 | } 22 | 23 | // MARK: - PeerDTO 24 | 25 | struct PeerDTO: Codable { 26 | let profileImageURL: String 27 | let name: String 28 | 29 | enum CodingKeys: String, CodingKey { 30 | case profileImageURL = "profileImage" 31 | case name = "nickname" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Data/DTO/PeerTypeDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PeerTypeDTO.swift 3 | // RecordFeature 4 | // 5 | // Created by MaraMincho on 11/21/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct PeerTypeDTO: Decodable { 12 | let icon: String 13 | let title: String 14 | let description: String 15 | let typeCode: Int 16 | } 17 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Data/DTO/WorkoutTypeDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WorkoutTypeDTO.swift 3 | // RecordFeature 4 | // 5 | // Created by MaraMincho on 11/21/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct WorkoutTypeDTO: Decodable { 12 | let icon: String 13 | let description: String 14 | let typeCode: Int 15 | } 16 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Data/Error/DataLayerError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataLayerError.swift 3 | // RecordFeature 4 | // 5 | // Created by MaraMincho on 11/21/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum DataLayerError: LocalizedError { 12 | case noData 13 | case repositoryDidDeinit 14 | case curreptedData 15 | } 16 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Domain/Entities/DateInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateInfo.swift 3 | // RecordFeature 4 | // 5 | // Created by 안종표 on 2023/11/21. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - DateInfo 12 | 13 | struct DateInfo { 14 | let year: String 15 | let month: String 16 | let date: String 17 | let dayOfWeek: String? 18 | } 19 | 20 | // MARK: Equatable 21 | 22 | extension DateInfo: Equatable { 23 | static func == (lhs: DateInfo, rhs: DateInfo) -> Bool { 24 | return (lhs.year == rhs.year) && (lhs.month == rhs.month) && (lhs.date == rhs.date) 25 | } 26 | } 27 | 28 | // MARK: CustomStringConvertible 29 | 30 | extension DateInfo: CustomStringConvertible { 31 | var description: String { 32 | return "\(year) \(month) \(date) \(dayOfWeek)" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Domain/Entities/KalmanFilterCensored.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KalmanFilterCensored.swift 3 | // RecordFeature 4 | // 5 | // Created by MaraMincho on 12/4/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - KalmanFilterCensored 12 | 13 | public struct KalmanFilterCensored { 14 | let longitude: Double 15 | let latitude: Double 16 | } 17 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Domain/Entities/KalmanFilterUpdateRequireElement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KalmanFilterUpdateRequireElement.swift 3 | // RecordFeature 4 | // 5 | // Created by MaraMincho on 12/4/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import CoreLocation 10 | import Foundation 11 | 12 | // MARK: - KalmanFilterUpdateRequireElement 13 | 14 | struct KalmanFilterUpdateRequireElement { 15 | let prevCLLocation: CLLocation 16 | let currentCLLocation: CLLocation 17 | } 18 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Domain/Entities/MapRegion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapRegion.swift 3 | // RecordFeature 4 | // 5 | // Created by 홍승현 on 12/7/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// 측정된 위도와 경도를 통해서, 최댓값 및 최솟값을 나타냅니다. 12 | public struct MapRegion { 13 | /// 측정된 거리에의 위도 중에서, 가장 큰 작은 값 입니다. 14 | var minLatitude: Double 15 | 16 | /// 측정된 거릐의 위도 중에서, 가장 작은 큰 값 입니다. 17 | var maxLatitude: Double 18 | 19 | /// 측정된 거리의 경도 중에서, 가장 작은 값 입니다. 20 | var minLongitude: Double 21 | 22 | /// 측정된 거리의 경도 중에서, 가장 작은 큰 값 입니다. 23 | var maxLongitude: Double 24 | } 25 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Domain/Entities/Peer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Peer.swift 3 | // RecordFeature 4 | // 5 | // Created by MaraMincho on 12/5/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Peer { 12 | let nickname: String 13 | let imageURL: String 14 | } 15 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Domain/Entities/Record.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Record.swift 3 | // RecordFeature 4 | // 5 | // Created by 안종표 on 2023/11/21. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - Record 12 | 13 | /// 기록 목록을 표시하기위해 사용하는 모델입니다. 14 | struct Record { 15 | /// 현재 운동의 목록을 나타냅니다. 16 | let mode: String 17 | 18 | /// 운동 시작 시간 19 | let startTime: String 20 | 21 | /// 운동 끝 시간 22 | let endTime: String 23 | 24 | /// 총 운동한 거리를 "미터"단위로 표시해줍니다. 25 | let distance: Int 26 | } 27 | 28 | // MARK: Codable 29 | 30 | extension Record: Codable {} 31 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Domain/Entities/SessionPeerType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionPeerType.swift 3 | // RecordFeature 4 | // 5 | // Created by 홍승현 on 11/30/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// 운동 세션 중 사용자의 UI 정보를 업데이트 하기 위해 사용합니다. 12 | struct SessionPeerType: Identifiable { 13 | /// 사용자 닉네임 14 | let nickname: String 15 | 16 | /// 사용자의 Identifier 17 | let id: String 18 | 19 | /// 사용자의 프로필 이미지 주소 20 | let profileImageURL: URL? 21 | } 22 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Domain/Entities/WorkoutHealthForm.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WorkoutHealthForm.swift 3 | // RecordFeature 4 | // 5 | // Created by 홍승현 on 11/30/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// 자신이 기록한 건강 데이터를 저장할 때 사용합니다. 12 | public struct WorkoutHealthForm { 13 | /// 총 운동한 거리 14 | let distance: Double? 15 | 16 | /// 소모한 칼로리 17 | let calorie: Double? 18 | 19 | /// 평균 심박수 20 | let averageHeartRate: Double? 21 | 22 | /// 운동 중에 기록한 최소 심박수 23 | let minimumHeartRate: Double? 24 | 25 | /// 운동 중에 기록한 최대 심박수 26 | let maximumHeartRate: Double? 27 | } 28 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Domain/Entities/WorkoutMode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WorkoutMode.swift 3 | // RecordFeature 4 | // 5 | // Created by MaraMincho on 12/9/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum WorkoutMode { 12 | case solo 13 | case random 14 | } 15 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Domain/Entities/WorkoutSetting.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WorkoutSetting.swift 3 | // RecordFeature 4 | // 5 | // Created by 안종표 on 2023/11/20. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - WorkoutSetting 12 | 13 | /// 어떤 운동을 할지와, 어떻게 운동을 할지 알려주는 객체 입니다. 14 | struct WorkoutSetting { 15 | let workoutType: WorkoutType 16 | 17 | let workoutPeerType: PeerType 18 | 19 | let isWorkoutAlone: Bool 20 | 21 | init(workoutType: WorkoutType, workoutPeerType: PeerType, isWorkoutAlone: Bool = true) { 22 | self.workoutType = workoutType 23 | self.workoutPeerType = workoutPeerType 24 | self.isWorkoutAlone = isWorkoutAlone 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Domain/Error/DomainError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DomainError.swift 3 | // RecordFeature 4 | // 5 | // Created by MaraMincho on 11/21/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum DomainError: LocalizedError { 12 | case didDeinitUseCase 13 | } 14 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Domain/Interfaces/Repositories/HealthRepositoryRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HealthRepositoryRepresentable.swift 3 | // RecordFeature 4 | // 5 | // Created by 홍승현 on 11/28/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | protocol HealthRepositoryRepresentable { 13 | /// 심박수를 가져옵니다. 같은 시작시간으로 요청해도 알아서 중복된 데이터를 제외하고 가져와줍니다. 14 | /// - Parameter startDate: 데이터를 가져올 시작 시간대 15 | /// - Returns: 심박수 데이터(bpm) 16 | func getHeartRateSample(startDate: Date) -> AnyPublisher<[Double], Error> 17 | 18 | /// 달리거나 걸었을 때의 거리를 가져옵니다. 같은 시작시간으로 요청해도 알아서 중복된 데이터를 제외하고 가져와줍니다. 19 | /// - Parameter startDate: 데이터를 가져올 시작 시간대 20 | /// - Returns: 지나온 거리(m) 21 | func getDistanceWalkingRunningSample(startDate: Date) -> AnyPublisher<[Double], Error> 22 | 23 | /// 소모한 칼로리를 가져옵니다.. 같은 시작시간으로 요청해도 알아서 중복된 데이터를 제외하고 가져와줍니다. 24 | /// - Parameter startDate: 데이터를 가져올 시작 시간대 25 | /// - Returns: 칼로리(kcal) 26 | func getCaloriesSample(startDate: Date) -> AnyPublisher<[Double], Error> 27 | } 28 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Domain/Interfaces/Repositories/MapImageUploadRepositoryRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapImageUploadRepositoryRepresentable.swift 3 | // RecordFeature 4 | // 5 | // Created by 홍승현 on 12/7/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | protocol MapImageUploadRepositoryRepresentable { 13 | func upload(with imageData: Data) -> AnyPublisher 14 | } 15 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Domain/Interfaces/Repositories/WorkoutEnvironmentSetupNetworkRepositoryRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WorkoutEnvironmentSetupNetworkRepositoryRepresentable.swift 3 | // RecordFeature 4 | // 5 | // Created by MaraMincho on 11/22/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | // MARK: - WorkoutEnvironmentSetupNetworkRepositoryRepresentable 13 | 14 | protocol WorkoutEnvironmentSetupNetworkRepositoryRepresentable { 15 | func workoutTypes() -> AnyPublisher<[WorkoutTypeDTO], Error> 16 | func peerType() -> AnyPublisher<[PeerTypeDTO], Error> 17 | } 18 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Domain/Interfaces/Repositories/WorkoutRecordRepositoryRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WorkoutRecordRepositoryRepresentable.swift 3 | // RecordFeature 4 | // 5 | // Created by 홍승현 on 11/25/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | protocol WorkoutRecordRepositoryRepresentable { 13 | func record(dataForm: WorkoutDataForm) -> AnyPublisher 14 | } 15 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Domain/Interfaces/Repositories/WorkoutRecordsRepositoryRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WorkoutRecordsRepositoryRepresentable.swift 3 | // RecordFeature 4 | // 5 | // Created by 안종표 on 2023/11/21. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | protocol WorkoutRecordsRepositoryRepresentable { 13 | /// 서버로부터 [Record]를 가져오는 로직 14 | func fetchRecordsList(date: Date, isToday: Bool) -> AnyPublisher<[Record], Error> 15 | 16 | /// 메모리와 디스크로부터 [Record]를 가져오는 로직 17 | func fetchCachedRecords(date: Date) -> AnyPublisher<[Record], Error> 18 | } 19 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Domain/Interfaces/Repositories/WorkoutSocketRepositoryRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WorkoutSocketRepositoryRepresentable.swift 3 | // RecordFeature 4 | // 5 | // Created by 홍승현 on 11/30/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | protocol WorkoutSocketRepositoryRepresentable { 13 | /// 참여자의 실시간 운동 정보를 가져옵니다. 14 | func fetchParticipantsRealTime() -> AnyPublisher 15 | 16 | /// 나의 운동 정보를 전달합니다. 17 | func sendMyWorkout(with model: WorkoutRealTimeModel) -> AnyPublisher 18 | } 19 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Domain/Interfaces/Repositories/WorkoutSummaryRepositoryRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WorkoutSummaryRepositoryRepresentable.swift 3 | // RecordFeature 4 | // 5 | // Created by 홍승현 on 11/22/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | // MARK: - WorkoutSummaryRepositoryRepresentable 13 | 14 | protocol WorkoutSummaryRepositoryRepresentable { 15 | /// 운동 요약 데이터를 가져옵니다. 16 | /// - Parameter id: 운동 데이터의 고유 Identifier 값 17 | func fetchWorkoutSummary(with id: Int) -> AnyPublisher 18 | } 19 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Domain/UseCases/KalmanUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KalmanUseCase.swift 3 | // RecordFeature 4 | // 5 | // Created by MaraMincho on 12/4/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import CoreLocation 10 | import Foundation 11 | 12 | // MARK: - KalmanUseCaseRepresentable 13 | 14 | protocol KalmanUseCaseRepresentable { 15 | func updateFilter(_ element: KalmanFilterUpdateRequireElement) -> KalmanFilterCensored? 16 | } 17 | 18 | // MARK: - KalmanUseCase 19 | 20 | final class KalmanUseCase { 21 | var filter: KalmanFilter? 22 | 23 | init() {} 24 | } 25 | 26 | // MARK: KalmanUseCaseRepresentable 27 | 28 | extension KalmanUseCase: KalmanUseCaseRepresentable { 29 | func updateFilter(_ element: KalmanFilterUpdateRequireElement) -> KalmanFilterCensored? { 30 | if filter == nil { 31 | let currentLocation = element.currentCLLocation 32 | filter = .init(initLocation: currentLocation) 33 | return nil 34 | } 35 | filter?.update(currentLocation: element.currentCLLocation) 36 | 37 | return filter?.latestCensoredPosition 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Domain/UseCases/LocationPathUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocationPathUseCase.swift 3 | // RecordFeature 4 | // 5 | // Created by 홍승현 on 12/7/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct LocationPathUseCase: LocationPathUseCaseRepresentable { 12 | func processPath(locations: [LocationModel]) -> MapRegion { 13 | return MapRegion( 14 | minLatitude: locations.map(\.latitude).min() ?? 0, 15 | maxLatitude: locations.map(\.latitude).max() ?? 0, 16 | minLongitude: locations.map(\.longitude).min() ?? 0, 17 | maxLongitude: locations.map(\.longitude).max() ?? 0 18 | ) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Domain/UseCases/MapImageUploadUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapImageUploadUseCase.swift 3 | // RecordFeature 4 | // 5 | // Created by 홍승현 on 12/7/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | struct MapImageUploadUseCase: MapImageUploadUseCaseRepresentable { 13 | private let repository: MapImageUploadRepositoryRepresentable 14 | 15 | init(repository: MapImageUploadRepositoryRepresentable) { 16 | self.repository = repository 17 | } 18 | 19 | func uploadImage(included data: Data?) -> AnyPublisher { 20 | guard let data 21 | else { 22 | return Just(nil).eraseToAnyPublisher() 23 | } 24 | return repository.upload(with: data) 25 | .map { $0 } 26 | .catch { _ in Just(nil) } 27 | .eraseToAnyPublisher() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Domain/UseCases/OneSecondsTimerUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OneSecondsTimerUseCase.swift 3 | // RecordFeature 4 | // 5 | // Created by MaraMincho on 11/28/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | import Log 12 | 13 | // MARK: - OneSecondsTimerUseCaseRepresentable 14 | 15 | protocol OneSecondsTimerUseCaseRepresentable: TimerUseCaseRepresentable { 16 | func oneSecondsTimerPublisher() -> AnyPublisher 17 | } 18 | 19 | // MARK: - OneSecondsTimerUseCase 20 | 21 | final class OneSecondsTimerUseCase: TimerUseCase { 22 | override init(initDate: Date, timerPeriod: Double = 1) { 23 | super.init(initDate: initDate, timerPeriod: timerPeriod) 24 | startTimer() 25 | } 26 | } 27 | 28 | // MARK: OneSecondsTimerUseCaseRepresentable 29 | 30 | extension OneSecondsTimerUseCase: OneSecondsTimerUseCaseRepresentable { 31 | func oneSecondsTimerPublisher() -> AnyPublisher { 32 | return intervalCurrentAndInitEverySecondsPublisher() 33 | .map { abs($0) } 34 | .eraseToAnyPublisher() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Domain/UseCases/Protocol/DateProvideUseCaseRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateProvideUseCaseRepresentable.swift 3 | // RecordFeature 4 | // 5 | // Created by 안종표 on 2023/11/22. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol DateProvideUseCaseRepresentable { 12 | func todayIndex(sectionCount: Int) -> IndexPath 13 | func transform(date: Date) -> DateInfo 14 | func transform(dateInfo: DateInfo) -> Date? 15 | func fetchAllDatesThisMonth() -> [DateInfo] 16 | func selectedDateInfo(index: Int) -> DateInfo? 17 | func isToday(date: Date) -> Bool 18 | } 19 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Domain/UseCases/Protocol/LocationPathUseCaseRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocationPathUseCaseRepresentable.swift 3 | // RecordFeature 4 | // 5 | // Created by 홍승현 on 12/7/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol LocationPathUseCaseRepresentable { 12 | func processPath(locations: [LocationModel]) -> MapRegion 13 | } 14 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Domain/UseCases/Protocol/MapImageUploadUseCaseRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapImageUploadUseCaseRepresentable.swift 3 | // RecordFeature 4 | // 5 | // Created by 홍승현 on 12/7/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | protocol MapImageUploadUseCaseRepresentable { 13 | func uploadImage(included data: Data?) -> AnyPublisher 14 | } 15 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Domain/UseCases/Protocol/RecordUpdateUseCaseRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecordUpdateUseCaseRepresentable.swift 3 | // RecordFeature 4 | // 5 | // Created by 안종표 on 2023/11/21. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | // MARK: - RecordUpdateUseCaseRepresentable 13 | 14 | protocol RecordUpdateUseCaseRepresentable { 15 | /// API로부터 [Record] 데이터를 불러옵니다. 16 | func execute(date: Date, isToday: Bool) -> AnyPublisher<[Record], Error> 17 | 18 | /// 메모리, 디스크로부터 [Record] 데이터를 불러옵니다. 19 | func executeCached(date: Date) -> AnyPublisher<[Record], Error> 20 | } 21 | 22 | // MARK: - RecordUpdateUseCaseError 23 | 24 | enum RecordUpdateUseCaseError: Error { 25 | case noRecord 26 | } 27 | 28 | // MARK: LocalizedError 29 | 30 | extension RecordUpdateUseCaseError: LocalizedError { 31 | var errorDescription: String? { 32 | switch self { 33 | case .noRecord: 34 | "기록이 없습니다." 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Domain/UseCases/Protocol/WorkoutPeerRandomMatchingRepositoryRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WorkoutPeerRandomMatchingRepositoryRepresentable.swift 3 | // RecordFeature 4 | // 5 | // Created by MaraMincho on 11/26/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | // MARK: - WorkoutPeerRandomMatchingRepositoryRepresentable 13 | 14 | protocol WorkoutPeerRandomMatchingRepositoryRepresentable { 15 | func matchStart(workoutTypeCode: Int) -> AnyPublisher, Never> 16 | func matchCancel(workoutTypeCode: Int) 17 | func isMatchedRandomPeer(isMatchedRandomPeersRequest: IsMatchedRandomPeersRequest) -> AnyPublisher, Never> 18 | } 19 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Domain/UseCases/WorkoutRecordUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WorkoutRecordUseCase.swift 3 | // RecordFeature 4 | // 5 | // Created by 홍승현 on 11/25/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | // MARK: - WorkoutRecordUseCaseRepresentable 13 | 14 | protocol WorkoutRecordUseCaseRepresentable { 15 | func record(dataForm: WorkoutDataForm) -> AnyPublisher 16 | } 17 | 18 | // MARK: - WorkoutRecordUseCase 19 | 20 | struct WorkoutRecordUseCase { 21 | private let repository: WorkoutRecordRepositoryRepresentable 22 | 23 | init(repository: WorkoutRecordRepositoryRepresentable) { 24 | self.repository = repository 25 | } 26 | } 27 | 28 | // MARK: WorkoutRecordUseCaseRepresentable 29 | 30 | extension WorkoutRecordUseCase: WorkoutRecordUseCaseRepresentable { 31 | func record(dataForm: WorkoutDataForm) -> AnyPublisher { 32 | repository.record(dataForm: dataForm) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Domain/UseCases/WorkoutSummaryUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WorkoutSummaryUseCase.swift 3 | // RecordFeature 4 | // 5 | // Created by 홍승현 on 11/22/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Combine 10 | 11 | // MARK: - WorkoutSummaryUseCaseRepresentable 12 | 13 | protocol WorkoutSummaryUseCaseRepresentable { 14 | func workoutSummaryInformation() -> AnyPublisher 15 | } 16 | 17 | // MARK: - WorkoutSummaryUseCase 18 | 19 | struct WorkoutSummaryUseCase { 20 | private let repository: WorkoutSummaryRepositoryRepresentable 21 | private let workoutRecordID: Int 22 | 23 | init(repository: WorkoutSummaryRepositoryRepresentable, workoutRecordID: Int) { 24 | self.repository = repository 25 | self.workoutRecordID = workoutRecordID 26 | } 27 | } 28 | 29 | // MARK: WorkoutSummaryUseCaseRepresentable 30 | 31 | extension WorkoutSummaryUseCase: WorkoutSummaryUseCaseRepresentable { 32 | func workoutSummaryInformation() -> AnyPublisher { 33 | repository.fetchWorkoutSummary(with: workoutRecordID) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Infrastructure/WorkoutRecordEndPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WorkoutRecordEndPoint.swift 3 | // RecordFeature 4 | // 5 | // Created by 안종표 on 2023/11/21. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Trinet 11 | 12 | struct WorkoutRecordTestEndPoint: TNEndPoint { 13 | var baseURL: String = "??" 14 | var path: String = "??" 15 | var method: TNMethod = .post 16 | var query: Encodable? 17 | var body: Encodable? 18 | var headers: TNHeaders = .init(headers: [TNHeader(key: "dd", value: "ff")]) 19 | } 20 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Presentation/Common/Coordinator/Delegate/WorkoutSettingCoordinatorFinishDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WorkoutSettingCoordinatorFinishDelegate.swift 3 | // RecordFeature 4 | // 5 | // Created by 안종표 on 2023/11/20. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol WorkoutSettingCoordinatorFinishDelegate: AnyObject { 12 | func workoutSettingCoordinatorDidFinished(workoutSessionComponents: WorkoutSessionComponents) 13 | } 14 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Presentation/Common/Coordinator/Protocol/RecordFeatureCoordinating.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecordFeatureCoordinating.swift 3 | // RecordFeature 4 | // 5 | // Created by 안종표 on 2023/11/20. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Coordinator 10 | import Foundation 11 | 12 | protocol RecordFeatureCoordinating: Coordinating { 13 | func showSettingFlow() 14 | func showWorkoutFlow(_ workoutSessionComponents: WorkoutSessionComponents) 15 | } 16 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Presentation/Common/Coordinator/Protocol/WorkoutCoordinating.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WorkoutCoordinating.swift 3 | // RecordFeature 4 | // 5 | // Created by 안종표 on 2023/11/20. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Coordinator 10 | import Foundation 11 | 12 | protocol WorkoutCoordinating: Coordinating { 13 | func pushWorkoutSummaryViewController() 14 | func pushWorkoutMapViewController() 15 | func pushWorkoutResultViewController() 16 | } 17 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Presentation/Common/Coordinator/Protocol/WorkoutEnvironmentSetUpCoordinating.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WorkoutEnvironmentSetUpCoordinating.swift 3 | // RecordFeature 4 | // 5 | // Created by 안종표 on 2023/11/20. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Coordinator 10 | import Foundation 11 | 12 | protocol WorkoutEnvironmentSetUpCoordinating: Coordinating { 13 | func pushWorkoutSelectViewController() 14 | func pushWorkoutEnvironmentSetupViewController() 15 | func pushPeerRandomMatchingViewController(workoutSetting: WorkoutSetting) 16 | func finish(workoutSessionComponents: WorkoutSessionComponents) 17 | func popPeerRandomMatchingViewController() 18 | } 19 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Sources/Presentation/Common/Coordinator/Protocol/WorkoutSessionCoordinating.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WorkoutSessionCoordinating.swift 3 | // RecordFeature 4 | // 5 | // Created by 홍승현 on 11/26/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Coordinator 10 | import Foundation 11 | 12 | protocol WorkoutSessionCoordinating: Coordinating { 13 | /// 운동 요약 화면으로 이동합니다. 14 | /// - Parameter recordID: 요약 화면을 보여주기 위한 기록 Identifier 15 | func pushWorkoutSummaryViewController(recordID: Int) 16 | 17 | /// 운동 화면으로 이동합니다. 18 | func pushWorkoutSession() 19 | 20 | /// 운동전 카운트 다운 화면으로 이동합니다. 21 | func pushCountDownBeforeWorkout() 22 | 23 | /// 메인 기록 화면으로 이동합니다. 24 | func setToMainRecord() 25 | } 26 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Record/Tests/RecordFeatureTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class RecordFeatureTests: XCTestCase { 4 | func testAlwaysPassed() { 5 | XCTAssertTrue(true) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /iOS/Projects/Features/SignUp/Project.swift: -------------------------------------------------------------------------------- 1 | import DependencyPlugin 2 | import ProjectDescription 3 | import ProjectDescriptionHelpers 4 | 5 | let project = Project.makeModule( 6 | name: "SignUpFeature", 7 | targets: .feature( 8 | .signUp, 9 | testingOptions: [.unitTest], 10 | dependencies: [.trinet, .keychain, .combineCocoa, .coordinator, .log, .designSystem, .commonNetworkingKeyManager, .auth, .downSampling, .userInformationManager], 11 | testDependencies: [], 12 | resources: "Resources/**" 13 | ) 14 | ) 15 | -------------------------------------------------------------------------------- /iOS/Projects/Features/SignUp/Resources/Token.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": null, 3 | "errorMessage": null, 4 | "data": { 5 | "accessToken": "ewaf1313RWDFA...", 6 | "refreshToken": "ewaf1313RWDFdddA..." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /iOS/Projects/Features/SignUp/Sources/Data/DTO/NickNameDuplicateRequestDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NickNameDuplicateRequestDTO.swift 3 | // SignUpFeature 4 | // 5 | // Created by 안종표 on 12/11/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct NickNameDuplicateRequestDTO: Codable { 12 | let nickname: String 13 | } 14 | -------------------------------------------------------------------------------- /iOS/Projects/Features/SignUp/Sources/Domain/Entities/Gender.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Gender.swift 3 | // SignUpFeature 4 | // 5 | // Created by 안종표 on 12/5/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum Gender: String { 12 | case male 13 | case female 14 | } 15 | -------------------------------------------------------------------------------- /iOS/Projects/Features/SignUp/Sources/Domain/Entities/ImageForm.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageForm.swift 3 | // SignUpFeature 4 | // 5 | // Created by 안종표 on 12/6/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - ImageForm 12 | 13 | public struct ImageForm { 14 | let imageName: String? 15 | let imageURL: URL? 16 | } 17 | 18 | // MARK: Codable 19 | 20 | extension ImageForm: Codable { 21 | enum CodingKeys: String, CodingKey { 22 | case imageName 23 | case imageURL = "imageUrl" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /iOS/Projects/Features/SignUp/Sources/Domain/Entities/NewUserInformation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewUserInformation.swift 3 | // SignUpFeature 4 | // 5 | // Created by 안종표 on 12/7/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Auth 10 | import Foundation 11 | 12 | // MARK: - NewUserInformation 13 | 14 | /// 처음 로그인 하는 유저의 Response를 담을 Entity 15 | public struct NewUserInformation { 16 | /// 애플 토큰에 있는 유저정보인데 보안때문에 UUID로 매핑한 ID 17 | public let mappedUserID: String 18 | 19 | /// OAuth 로그인 종류 20 | public let provider: AuthProvider 21 | 22 | public init(mappedUserID: String, provider: AuthProvider) { 23 | self.mappedUserID = mappedUserID 24 | self.provider = provider 25 | } 26 | } 27 | 28 | // MARK: Codable 29 | 30 | extension NewUserInformation: Codable {} 31 | -------------------------------------------------------------------------------- /iOS/Projects/Features/SignUp/Sources/Domain/Entities/SignUpUser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignUpUser.swift 3 | // SignUpFeature 4 | // 5 | // Created by 안종표 on 12/7/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - SignUpUser 12 | 13 | public struct SignUpUser { 14 | let provider: String 15 | let nickname: String 16 | let gender: String 17 | let birthDate: String 18 | let profileImage: URL? 19 | let mappedUserID: String 20 | } 21 | 22 | // MARK: Codable 23 | 24 | extension SignUpUser: Codable { 25 | enum CodingKeys: String, CodingKey { 26 | case provider 27 | case nickname 28 | case gender 29 | case birthDate = "birthdate" 30 | case profileImage 31 | case mappedUserID 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /iOS/Projects/Features/SignUp/Sources/Domain/Interfaces/ImageFormRepositoryRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageFormRepositoryRepresentable.swift 3 | // SignUpFeature 4 | // 5 | // Created by 안종표 on 12/6/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | public protocol ImageFormRepositoryRepresentable { 13 | func send(imageData: Data) -> AnyPublisher<[ImageForm], Error> 14 | } 15 | -------------------------------------------------------------------------------- /iOS/Projects/Features/SignUp/Sources/Domain/Interfaces/KeyChainRepositoryRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyChainRepositoryRepresentable.swift 3 | // SignUpFeature 4 | // 5 | // Created by 안종표 on 12/7/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | public protocol KeychainRepositoryRepresentable { 13 | /// 키체인에 키-data로 데이터를 저장합니다. 14 | func save(key: String, value: String) 15 | 16 | /// 키체인에서 키를 통해 data 값을 얻어옵니다. 17 | func load(key: String) -> AnyPublisher 18 | 19 | /// 키체인에서 해당하는 키를 삭제합니다. 20 | func delete(key: String) -> AnyPublisher 21 | } 22 | -------------------------------------------------------------------------------- /iOS/Projects/Features/SignUp/Sources/Domain/Interfaces/SignUpRepositoryRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignUpRepositoryRepresentable.swift 3 | // SignUpFeature 4 | // 5 | // Created by 안종표 on 12/7/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Auth 10 | import Combine 11 | import Foundation 12 | 13 | public protocol SignUpRepositoryRepresentable { 14 | func signUp(signUpUser: SignUpUser) -> AnyPublisher 15 | func duplicateTest(nickName: String) -> AnyPublisher 16 | } 17 | -------------------------------------------------------------------------------- /iOS/Projects/Features/SignUp/Sources/Domain/UseCase/DateFormatUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateFormatUseCase.swift 3 | // SignUpFeature 4 | // 5 | // Created by 안종표 on 12/5/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - DateFormatUseCaseRepresentable 12 | 13 | public protocol DateFormatUseCaseRepresentable { 14 | func formatyyyyMMdd(date: Date) -> String 15 | } 16 | 17 | // MARK: - DateFormatUseCase 18 | 19 | public final class DateFormatUseCase: DateFormatUseCaseRepresentable { 20 | public init() {} 21 | 22 | public func formatyyyyMMdd(date: Date) -> String { 23 | let dateFormatter = DateFormatter() 24 | dateFormatter.dateFormat = "yyyy-MM-dd" 25 | return dateFormatter.string(from: date) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /iOS/Projects/Features/SignUp/Sources/Domain/UseCase/ImageTransmitUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageTransmitUseCase.swift 3 | // SignUpFeature 4 | // 5 | // Created by 안종표 on 12/6/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | import Log 12 | 13 | // MARK: - ImageTransmitUseCaseRepresentable 14 | 15 | public protocol ImageTransmitUseCaseRepresentable { 16 | func transmit(imageData: Data) -> AnyPublisher<[ImageForm], Error> 17 | } 18 | 19 | // MARK: - ImageTransmitUseCase 20 | 21 | public final class ImageTransmitUseCase: ImageTransmitUseCaseRepresentable { 22 | private let imageFormRepository: ImageFormRepositoryRepresentable 23 | 24 | public init(imageFormRepository: ImageFormRepositoryRepresentable) { 25 | self.imageFormRepository = imageFormRepository 26 | } 27 | 28 | public func transmit(imageData: Data) -> AnyPublisher<[ImageForm], Error> { 29 | return imageFormRepository.send(imageData: imageData) 30 | .eraseToAnyPublisher() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /iOS/Projects/Features/SignUp/Sources/Domain/UseCase/NickNameCheckUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NickNameCheckUseCase.swift 3 | // SignUpFeature 4 | // 5 | // Created by 안종표 on 12/6/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - NickNameCheckUseCaseRepresentable 12 | 13 | public protocol NickNameCheckUseCaseRepresentable { 14 | func check(nickName: String) -> Bool 15 | } 16 | 17 | // MARK: - NickNameCheckUseCase 18 | 19 | public struct NickNameCheckUseCase: NickNameCheckUseCaseRepresentable { 20 | public init() {} 21 | 22 | public func check(nickName: String) -> Bool { 23 | let regex = "^[^\\W_]{2,20}$" 24 | let check = NSPredicate(format: "SELF MATCHES %@", regex) 25 | return check.evaluate(with: nickName) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /iOS/Projects/Features/SignUp/Sources/Presentation/Common/Coordinator/Protocol/SignUpFeatureCoordinating.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignUpFeatureCoordinating.swift 3 | // SignUpFeature 4 | // 5 | // Created by 안종표 on 12/6/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Coordinator 10 | import Foundation 11 | 12 | public protocol SignUpFeatureCoordinating: Coordinating { 13 | func pushSingUpContainerViewController() 14 | } 15 | -------------------------------------------------------------------------------- /iOS/Projects/Features/SignUp/Sources/Presentation/Common/Coordinator/Protocol/SingUpCoordinating.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SingUpCoordinating.swift 3 | // SignUpFeature 4 | // 5 | // Created by 안종표 on 12/7/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Coordinator 10 | import Foundation 11 | 12 | public protocol SignUpCoordinating: Coordinating { 13 | func pushSingUpContainerViewController() 14 | } 15 | -------------------------------------------------------------------------------- /iOS/Projects/Features/SignUp/Sources/Presentation/Common/Extension/UIButton+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIButton+.swift 3 | // SignUpFeature 4 | // 5 | // Created by 안종표 on 12/4/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import DesignSystem 10 | import UIKit 11 | 12 | extension UIButton { 13 | func updateConfiguration(title: String) { 14 | if isSelected { 15 | configuration?.font = .preferredFont(forTextStyle: .headline, weight: .bold) 16 | configuration?.titleAlignment = .center 17 | configuration = .mainEnabled(title: title) 18 | } else { 19 | configuration?.font = .preferredFont(forTextStyle: .headline, weight: .bold) 20 | configuration?.titleAlignment = .center 21 | configuration = .mainDeSelected(title: title) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /iOS/Projects/Features/SignUp/Sources/Presentation/SignUpProfileScene/View/ImageCheckerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageCheckerView.swift 3 | // SignUpFeature 4 | // 5 | // Created by 안종표 on 12/11/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final class ImageCheckerView: CheckerView { 12 | override init(frame: CGRect) { 13 | super.init(frame: frame) 14 | } 15 | 16 | override func configureEnabled() { 17 | super.configureEnabled() 18 | label.text = "이미지를 추가하셨습니다." 19 | } 20 | 21 | override func configureDisabled() { 22 | super.configureDisabled() 23 | label.text = "이미지를 추가해주세요." 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /iOS/Projects/Features/SignUp/Sources/Presentation/SignUpProfileScene/View/NickNameCheckerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NickNameCheckerView.swift 3 | // SignUpFeature 4 | // 5 | // Created by 안종표 on 12/11/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final class NickNameCheckerView: CheckerView { 12 | override init(frame: CGRect) { 13 | super.init(frame: frame) 14 | label.text = "글자수는 2~20자, 특수문자는 사용할 수 없어요." 15 | } 16 | 17 | override func configureEnabled() { 18 | super.configureEnabled() 19 | label.text = "사용가능한 닉네임이에요." 20 | } 21 | 22 | override func configureDisabled() { 23 | super.configureDisabled() 24 | label.text = "글자수는 2~20자, 특수문자는 사용할 수 없어요." 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /iOS/Projects/Features/SignUp/Sources/Presentation/SignUpProfileScene/View/NickNameDuplicatingCheckerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NickNameDuplicatingCheckerView.swift 3 | // SignUpFeature 4 | // 5 | // Created by 안종표 on 12/11/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final class NickNameDuplicatingCheckerView: CheckerView { 12 | override init(frame: CGRect) { 13 | super.init(frame: frame) 14 | } 15 | 16 | override func configureEnabled() { 17 | super.configureEnabled() 18 | label.text = "닉네임이 중복되지 않았습니다." 19 | } 20 | 21 | override func configureDisabled() { 22 | super.configureDisabled() 23 | label.text = "닉네임이 중복되었습니다." 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /iOS/Projects/Features/SignUp/Tests/SignupTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignupTests.swift 3 | // ProjectDescriptionHelpers 4 | // 5 | // Created by 안종표 on 12/4/23. 6 | // 7 | 8 | import XCTest 9 | 10 | final class SignupTests: XCTestCase { 11 | override func setUp() {} 12 | } 13 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Splash/Project.swift: -------------------------------------------------------------------------------- 1 | import DependencyPlugin 2 | import ProjectDescription 3 | import ProjectDescriptionHelpers 4 | 5 | let project = Project.makeModule( 6 | name: "SplashFeature", 7 | targets: .feature( 8 | .splash, 9 | testingOptions: [.unitTest], 10 | dependencies: [.designSystem, .log, .commonNetworkingKeyManager, .trinet, .keychain, .userInformationManager], 11 | testDependencies: [] 12 | ) 13 | ) 14 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Splash/Sources/DTO/ReissueAccessTokenDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReissueAccessTokenDTO.swift 3 | // SplashFeature 4 | // 5 | // Created by 홍승현 on 12/3/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct ReissueAccessTokenDTO: Decodable { 12 | let accessToken: String 13 | } 14 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Splash/Sources/DTO/ReissueRefreshTokenDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReissueRefreshTokenDTO.swift 3 | // SplashFeature 4 | // 5 | // Created by 홍승현 on 12/3/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - ReissueRefreshTokenDTO 12 | 13 | public struct ReissueRefreshTokenDTO: Decodable { 14 | let refreshToken: String 15 | } 16 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Splash/Sources/Data/Repositories/PersistencyRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PersistencyRepository.swift 3 | // SplashFeature 4 | // 5 | // Created by MaraMincho on 12/11/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import CommonNetworkingKeyManager 11 | import Foundation 12 | import Keychain 13 | import Trinet 14 | import UserInformationManager 15 | 16 | // MARK: - PersistencyRepository 17 | 18 | struct PersistencyRepository: PersistencyRepositoryRepresentable { 19 | func saveAccessToken(accessToken: Data) { 20 | Keychain.shared.delete(key: Tokens.accessToken) 21 | Keychain.shared.save(key: Tokens.accessToken, data: accessToken) 22 | } 23 | 24 | func saveRefreshToken(refreshToken: Data) { 25 | Keychain.shared.delete(key: Tokens.refreshToken) 26 | Keychain.shared.save(key: Tokens.refreshToken, data: refreshToken) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Splash/Sources/Domain/Interfaces/SplashTokenRepositoryRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplashTokenRepositoryRepresentable.swift 3 | // SplashFeature 4 | // 5 | // Created by 홍승현 on 12/3/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | // MARK: - SplashTokenRepositoryRepresentable 13 | 14 | public protocol SplashTokenRepositoryRepresentable { 15 | func reissueAccessToken() -> AnyPublisher 16 | func reissueRefreshToken() -> AnyPublisher 17 | } 18 | 19 | // MARK: - PersistencyRepositoryRepresentable 20 | 21 | public protocol PersistencyRepositoryRepresentable { 22 | func saveAccessToken(accessToken: Data) 23 | func saveRefreshToken(refreshToken: Data) 24 | } 25 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Splash/Sources/Domain/UseCases/Protocol/SplashUseCaseRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplashUseCaseRepresentable.swift 3 | // SplashFeature 4 | // 5 | // Created by 홍승현 on 12/3/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | // MARK: - SplashUseCaseRepresentable 13 | 14 | public protocol SplashUseCaseRepresentable { 15 | func reissueToken() -> AnyPublisher 16 | } 17 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Splash/Sources/SplashScene/Coordinator/Protocol/SplashCoordinating.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplashCoordinating.swift 3 | // WeTri 4 | // 5 | // Created by 홍승현 on 12/3/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Coordinator 10 | import Foundation 11 | 12 | protocol SplashCoordinating: Coordinating { 13 | func showLoginOrMainFlow(when hasTokenExpired: Bool) 14 | } 15 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Splash/Sources/SplashScene/ViewModel/Protocol/SplashViewModelRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplashViewModelRepresentable.swift 3 | // SplashFeature 4 | // 5 | // Created by 홍승현 on 12/3/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | // MARK: - SplashViewModelInput 13 | 14 | public struct SplashViewModelInput { 15 | let viewDidLoadPublisher: AnyPublisher 16 | } 17 | 18 | public typealias SplashViewModelOutput = AnyPublisher 19 | 20 | // MARK: - SplashState 21 | 22 | public enum SplashState { 23 | case idle 24 | } 25 | 26 | // MARK: - SplashViewModelRepresentable 27 | 28 | public protocol SplashViewModelRepresentable { 29 | func transform(input: SplashViewModelInput) -> SplashViewModelOutput 30 | } 31 | -------------------------------------------------------------------------------- /iOS/Projects/Features/Splash/Tests/SplashFeatureTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class SplashFeatureTests: XCTestCase { 4 | func testAlwaysPassed() { 5 | XCTAssertTrue(true) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /iOS/Projects/Features/WriteBoard/Project.swift: -------------------------------------------------------------------------------- 1 | import DependencyPlugin 2 | import ProjectDescription 3 | import ProjectDescriptionHelpers 4 | 5 | let project = Project.makeModule( 6 | name: "WriteBoardFeature", 7 | targets: .feature( 8 | .writeBoard, 9 | testingOptions: [.unitTest], 10 | dependencies: [.designSystem, .log, .combineCocoa, .trinet, .combineExtension, .coordinator, .commonNetworkingKeyManager], 11 | testDependencies: [] 12 | ) 13 | ) 14 | -------------------------------------------------------------------------------- /iOS/Projects/Features/WriteBoard/Tests/test.swift: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm2023/iOS08-WeTri/83d94b1e6f269048d1d7a40a06e8488def29834c/iOS/Projects/Features/WriteBoard/Tests/test.swift -------------------------------------------------------------------------------- /iOS/Projects/Shared/Auth/Project.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project.swift 3 | // ProjectDescriptionHelpers 4 | // 5 | // Created by 안종표 on 12/9/23. 6 | // 7 | 8 | import ProjectDescription 9 | import ProjectDescriptionHelpers 10 | 11 | let project = Project.makeModule( 12 | name: "Auth", 13 | targets: .custom( 14 | name: "Auth", 15 | product: .framework, 16 | testingOptions: [] 17 | ) 18 | ) 19 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/Auth/Sources/AuthProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthProvider.swift 3 | // Auth 4 | // 5 | // Created by 안종표 on 12/9/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - AuthProvider 12 | 13 | public enum AuthProvider: String, Codable { 14 | case apple 15 | } 16 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/Auth/Sources/Token.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Token.swift 3 | // LoginFeature 4 | // 5 | // Created by 안종표 on 11/29/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - Token 12 | 13 | /// 백엔드로부터 받아온 JWT를 담을 데이터 entity 14 | public struct Token { 15 | /// accessToken 16 | public let accessToken: String? 17 | 18 | /// refreshToken 19 | public let refreshToken: String? 20 | 21 | public init(accesToken: String? = nil, refreshToken: String? = nil) { 22 | accessToken = accesToken 23 | self.refreshToken = refreshToken 24 | } 25 | } 26 | 27 | // MARK: Codable 28 | 29 | extension Token: Codable {} 30 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/CombineCocoa/Project.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | import ProjectDescriptionHelpers 3 | 4 | let project = Project.makeModule( 5 | name: "CombineCocoa", 6 | targets: .custom( 7 | name: "CombineCocoa", 8 | product: .framework, 9 | testingOptions: [.unitTest] 10 | ) 11 | ) 12 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/CombineCocoa/Sources/EventSubscription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventSubscription.swift 3 | // TNCocoaCombine 4 | // 5 | // Created by MaraMincho on 11/15/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import UIKit 11 | 12 | final class EventSubscription: 13 | Subscription where EventSubscriber.Input == UIControl, EventSubscriber.Failure == Never { 14 | func request(_: Subscribers.Demand) {} 15 | 16 | func cancel() { 17 | subscriber = nil 18 | control.removeAction(action, for: event) 19 | } 20 | 21 | private let control: UIControl 22 | private let event: UIControl.Event 23 | private var subscriber: EventSubscriber? 24 | private let action: UIAction 25 | 26 | init(control: UIControl, event: UIControl.Event, subscriber: EventSubscriber) { 27 | self.control = control 28 | self.event = event 29 | self.subscriber = subscriber 30 | action = .init { _ in 31 | _ = subscriber.receive(control) 32 | } 33 | 34 | control.addAction(action, for: event) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/CombineCocoa/Sources/GestureSubscription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GestureSubscription.swift 3 | // CombineCocoa 4 | // 5 | // Created by MaraMincho on 1/11/24. 6 | // Copyright © 2024 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import UIKit 11 | 12 | final class GestureSubscription: Subscription where T.Input == UIGestureRecognizer, T.Failure == Never { 13 | var subscriber: T? 14 | let gesture: UIGestureRecognizer 15 | var targetView: UIView? 16 | 17 | @objc func action() { 18 | _ = subscriber?.receive(gesture) 19 | } 20 | 21 | init(subscriber: T, gesture: UIGestureRecognizer, targetView: UIView) { 22 | self.subscriber = subscriber 23 | self.gesture = gesture 24 | self.targetView = targetView 25 | 26 | gesture.addTarget(self, action: #selector(action)) 27 | targetView.addGestureRecognizer(gesture) 28 | } 29 | 30 | func request(_: Subscribers.Demand) {} 31 | 32 | func cancel() { 33 | gesture.removeTarget(self, action: #selector(action)) 34 | targetView?.removeGestureRecognizer(gesture) 35 | targetView = nil 36 | subscriber = nil 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/CombineCocoa/Sources/UIControl+Publisher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIControl+Publisher.swift 3 | // TNCocoaCombine 4 | // 5 | // Created by MaraMincho on 11/15/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import UIKit 11 | 12 | public extension UIControl { 13 | func publisher(_ event: UIControl.Event) -> EventPublisher { 14 | return EventPublisher(control: self, event: event) 15 | } 16 | 17 | struct EventPublisher: Publisher { 18 | public typealias Output = UIControl 19 | public typealias Failure = Never 20 | 21 | let control: UIControl 22 | let event: UIControl.Event 23 | 24 | public func receive(subscriber: S) where S: Subscriber, Never == S.Failure, UIControl == S.Input { 25 | let subscription = EventSubscription(control: control, event: event, subscriber: subscriber) 26 | subscriber.receive(subscription: subscription) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/CombineExtension/Project.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | import ProjectDescriptionHelpers 3 | 4 | let project = Project.makeModule( 5 | name: "CombineExtension", 6 | targets: .custom( 7 | name: "CombineExtension", 8 | product: .framework, 9 | testingOptions: [.unitTest] 10 | ) 11 | ) 12 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/CombineExtension/Sources/Publisher+bind.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Publisher+bind.swift 3 | // CombineCocoa 4 | // 5 | // Created by 홍승현 on 11/23/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Combine 10 | 11 | public extension Publisher where Failure == Never { 12 | func bind(to subject: S) -> AnyCancellable where S: Subject, S.Output == Output, S.Failure == Failure { 13 | return sink { value in 14 | subject.send(value) 15 | } 16 | } 17 | } 18 | 19 | public extension Publisher { 20 | func bind(to subject: S) -> AnyCancellable where S: Subject, S.Output == Output, S.Failure == Failure { 21 | return sink { completion in 22 | subject.send(completion: completion) 23 | } receiveValue: { value in 24 | subject.send(value) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/CombineExtension/Sources/withUnretained.swift: -------------------------------------------------------------------------------- 1 | // 2 | // withUnretained.swift 3 | // CombineCocoa 4 | // 5 | // Created by 홍승현 on 12/1/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | public extension Publisher { 13 | func withUnretained(_ owner: Owner) -> Publishers.TryMap { 14 | tryMap { [weak owner] output in 15 | guard let owner else { throw UnretainedError.failedRetaining } 16 | return (owner, output) 17 | } 18 | } 19 | } 20 | 21 | // MARK: - UnretainedError 22 | 23 | private enum UnretainedError: Error { 24 | case failedRetaining 25 | } 26 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/CommonNetworkingKeyManager/Project.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | import ProjectDescriptionHelpers 3 | 4 | let project = Project.makeModule( 5 | name: "CommonNetworkingKeyManager", 6 | targets: .custom( 7 | name: "CommonNetworkingKeyManager", 8 | product: .framework, 9 | dependencies: [.log, .keychain, .trinet] 10 | ) 11 | ) 12 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/CommonNetworkingKeyManager/Sources/Tokens.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tokens.swift 3 | // CommonNetworkingKeyManager 4 | // 5 | // Created by MaraMincho on 11/30/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// AccessToken과 Refresh토큰에 관한 KeyChain에 Key의 관한 값을 다음과 같이 쓰면 됩니다. 12 | /// 13 | public enum Tokens { 14 | public static let accessToken = "AccessToken" 15 | public static let refreshToken = "RefreshToken" 16 | } 17 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/DesignSystem/Project.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | import ProjectDescriptionHelpers 3 | 4 | let project = Project.makeModule( 5 | name: "DesignSystem", 6 | targets: .custom(name: "DesignSystem", product: .framework, resources: "Resources/**") 7 | ) 8 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/DesignSystem/Resources/Colors.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/DesignSystem/Resources/Colors.xcassets/Error.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x2B", 9 | "green" : "0x5B", 10 | "red" : "0xF7" 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" : "0x79", 27 | "green" : "0x66", 28 | "red" : "0xCF" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/DesignSystem/Resources/Colors.xcassets/Gray-01.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xF4", 9 | "green" : "0xF3", 10 | "red" : "0xF2" 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" : "0x66", 27 | "green" : "0x66", 28 | "red" : "0x66" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/DesignSystem/Resources/Colors.xcassets/Gray-02.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xC4", 9 | "green" : "0xC4", 10 | "red" : "0xC4" 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" : "0x4D", 27 | "green" : "0x4D", 28 | "red" : "0x4D" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/DesignSystem/Resources/Colors.xcassets/Gray-03.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x8C", 9 | "green" : "0x8C", 10 | "red" : "0x8C" 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" : "0x33", 27 | "green" : "0x33", 28 | "red" : "0x33" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/DesignSystem/Resources/Colors.xcassets/Main-01.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFE", 9 | "green" : "0xF6", 10 | "red" : "0xF6" 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" : "0xCB", 27 | "green" : "0x86", 28 | "red" : "0x79" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/DesignSystem/Resources/Colors.xcassets/Main-02.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFA", 9 | "green" : "0xE1", 10 | "red" : "0xE1" 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" : "0xC0", 27 | "green" : "0x6B", 28 | "red" : "0x5C" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/DesignSystem/Resources/Colors.xcassets/Main-03.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xF9", 9 | "green" : "0x5F", 10 | "red" : "0x5F" 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" : "0xB5", 27 | "green" : "0x51", 28 | "red" : "0x3F" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/DesignSystem/Resources/Colors.xcassets/PrimaryBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xF9", 9 | "green" : "0xF9", 10 | "red" : "0xF9" 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" : "0x12", 27 | "green" : "0x12", 28 | "red" : "0x12" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/DesignSystem/Resources/Colors.xcassets/PrimaryText.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x36", 9 | "green" : "0x36", 10 | "red" : "0x36" 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" : "0xE0", 27 | "green" : "0xE0", 28 | "red" : "0xE0" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/DesignSystem/Resources/Colors.xcassets/SecondaryBackground.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" : "0x1E", 28 | "red" : "0x1E" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/DesignSystem/Resources/Colors.xcassets/Success.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x7D", 9 | "green" : "0xC4", 10 | "red" : "0x1B" 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" : "0xC6", 27 | "green" : "0xDA", 28 | "red" : "0x03" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/DesignSystem/Resources/Colors.xcassets/Warning.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x00", 9 | "green" : "0xCE", 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" : "0x4D", 27 | "green" : "0xB7", 28 | "red" : "0xFF" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/DesignSystem/Resources/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/DesignSystem/Resources/Images.xcassets/Logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "WeTri-Logo.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/DesignSystem/Resources/Images.xcassets/Logo.imageset/WeTri-Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm2023/iOS08-WeTri/83d94b1e6f269048d1d7a40a06e8488def29834c/iOS/Projects/Shared/DesignSystem/Resources/Images.xcassets/Logo.imageset/WeTri-Logo.png -------------------------------------------------------------------------------- /iOS/Projects/Shared/DesignSystem/Resources/Images.xcassets/LogoForDarkMode.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "LogoForDarkModeWith75.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/DesignSystem/Resources/Images.xcassets/LogoForDarkMode.imageset/LogoForDarkModeWith75.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm2023/iOS08-WeTri/83d94b1e6f269048d1d7a40a06e8488def29834c/iOS/Projects/Shared/DesignSystem/Resources/Images.xcassets/LogoForDarkMode.imageset/LogoForDarkModeWith75.png -------------------------------------------------------------------------------- /iOS/Projects/Shared/DesignSystem/Resources/Images.xcassets/MapEmptyState.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "MapEmptyState.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/DesignSystem/Resources/Images.xcassets/NoResults.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "No Results.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/DesignSystem/Resources/Images.xcassets/Pencil.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Pencil.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/DesignSystem/Resources/Images.xcassets/Pencil.imageset/Pencil.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/DesignSystem/Sources/ConstraintsGuideLine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConstraintsGuideLine.swift 3 | // DesignSystem 4 | // 5 | // Created by MaraMincho on 11/18/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum ConstraintsGuideLine { 12 | public static let value: CGFloat = 23 13 | public static let secondaryValue: CGFloat = 23 14 | } 15 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/DesignSystem/Sources/DesignSystemColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DesignSystemColor.swift 3 | // ProjectDescriptionHelpers 4 | // 5 | // Created by 안종표 on 2023/11/13. 6 | // 7 | 8 | import UIKit 9 | 10 | // MARK: - DesignSystemColor 11 | 12 | public enum DesignSystemColor { 13 | public static let error: UIColor = .error 14 | public static let gray01: UIColor = .gray01 15 | public static let gray02: UIColor = .gray02 16 | public static let gray03: UIColor = .gray03 17 | public static let main01: UIColor = .main01 18 | public static let main02: UIColor = .main02 19 | public static let main03: UIColor = .main03 20 | public static let primaryBackground: UIColor = .primaryBackground 21 | public static let primaryText: UIColor = .primaryText 22 | public static let secondaryBackground: UIColor = .secondaryBackground 23 | public static let success: UIColor = .success 24 | public static let warning: UIColor = .warning 25 | } 26 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/DesignSystem/Sources/GWShadow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GWShadow.swift 3 | // DesignSystem 4 | // 5 | // Created by MaraMincho on 11/16/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | import CoreGraphics 9 | 10 | // MARK: - GWShadow 11 | 12 | public struct GWShadow { 13 | let shadowColor: CGColor 14 | let shadowOffset: CGSize 15 | let shadowOpacity: Float 16 | let shadowRadius: CGFloat 17 | } 18 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/DesignSystem/Sources/UIButtonConfiguration+Font.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIButtonConfiguration+Font.swift 3 | // DesignSystem 4 | // 5 | // Created by 홍승현 on 11/14/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public extension UIButton.Configuration { 12 | var font: UIFont? { 13 | get { 14 | attributedTitle?.font 15 | } 16 | set { 17 | attributedTitle?.font = newValue 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/DesignSystem/Sources/UIImage+assets.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+assets.swift 3 | // DesignSystem 4 | // 5 | // Created by 홍승현 on 12/3/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public extension UIImage { 12 | static let logoImage: UIImage = .logo 13 | static let logoImageDark: UIImage = .logoForDarkMode 14 | static let mapEmptyStateImage: UIImage = .mapEmptyState 15 | static let pencilImage: UIImage = .pencil 16 | static let noResultsImage: UIImage = .noResults 17 | } 18 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/ImageDownsampling/Project.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project.swift 3 | // ProjectDescriptionHelpers 4 | // 5 | // Created by 안종표 on 12/10/23. 6 | // 7 | 8 | import ProjectDescription 9 | import ProjectDescriptionHelpers 10 | 11 | let project = Project.makeModule( 12 | name: "ImageDownsampling", 13 | targets: .custom( 14 | name: "ImageDownsampling", 15 | product: .framework, 16 | testingOptions: [] 17 | ) 18 | ) 19 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/ImageDownsampling/Sources/ImageDownSamplingError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageDownSamplingError.swift 3 | // ImageDownsampling 4 | // 5 | // Created by 안종표 on 12/10/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - ImageDownsamplingError 12 | 13 | public enum ImageDownsamplingError: Error { 14 | case failImageSource 15 | case failThumbnailImage 16 | } 17 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/ImageDownsampling/Sources/Scale.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Scale.swift 3 | // ImageDownsampling 4 | // 5 | // Created by 안종표 on 12/10/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum Scale: CGFloat { 12 | case x2 = 2.0 13 | case x3 = 3.0 14 | } 15 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/Log/Project.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | import ProjectDescriptionHelpers 3 | 4 | let project = Project.makeModule( 5 | name: "Log", 6 | targets: .custom(name: "Log", product: .framework) 7 | ) 8 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/Log/Sources/Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logger.swift 3 | // DesignSystem 4 | // 5 | // Created by 홍승현 on 11/23/23. 6 | // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. 7 | // 8 | 9 | import OSLog 10 | 11 | // MARK: - Log 12 | 13 | /// 로그 14 | public enum Log { 15 | /// Logger를 생성합니다. 16 | /// - Parameter category: Log를 구분하는 Category 17 | public static func make(with category: LogCategory = .default) -> Logger { 18 | return Logger(subsystem: .bundleIdentifier, category: category.rawValue) 19 | } 20 | } 21 | 22 | // MARK: - LogCategory 23 | 24 | /// 로그 카테고리 25 | public enum LogCategory: String { 26 | /// 기본값으로 들어갑니다. 27 | case `default` 28 | 29 | /// UI 로그를 작성할 때 사용합니다. 30 | case userInterface 31 | 32 | /// 네트워크 로그를 작성할 때 사용합니다. 33 | case network 34 | 35 | /// HealthKit 로그를 담당합니다. 36 | case healthKit 37 | 38 | /// Socket 로그를 담당합니다. 39 | case socket 40 | 41 | /// 운동 요약 화면의 로그를 담당합니다. 42 | case workoutSummary 43 | } 44 | 45 | private extension String { 46 | static let bundleIdentifier: String = Bundle.main.bundleIdentifier ?? "None" 47 | } 48 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/UserInformationManager/Project.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | import ProjectDescriptionHelpers 3 | 4 | let project = Project.makeModule( 5 | name: "UserInformationManager", 6 | targets: .custom( 7 | name: "UserInformationManager", 8 | product: .framework, 9 | dependencies: [.cacher], 10 | resources: "Resources/**" 11 | ) 12 | ) 13 | -------------------------------------------------------------------------------- /iOS/Projects/Shared/UserInformationManager/Resources/DefaultsProfileImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm2023/iOS08-WeTri/83d94b1e6f269048d1d7a40a06e8488def29834c/iOS/Projects/Shared/UserInformationManager/Resources/DefaultsProfileImage.png -------------------------------------------------------------------------------- /iOS/Tuist/Config.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | 3 | let config = Config( 4 | plugins: [ 5 | .local(path: .relativeToRoot("Plugins/DependencyPlugin")), 6 | .local(path: .relativeToRoot("Plugins/EnvironmentPlugin")), 7 | ] 8 | ) 9 | -------------------------------------------------------------------------------- /iOS/Tuist/Templates/Demo/AppDelegate.stencil: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @main 4 | final class AppDelegate: UIResponder, UIApplicationDelegate { 5 | func application( 6 | _: UIApplication, 7 | didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil 8 | ) 9 | -> Bool { 10 | return true 11 | } 12 | 13 | func application( 14 | _: UIApplication, 15 | configurationForConnecting connectingSceneSession: UISceneSession, 16 | options _: UIScene.ConnectionOptions 17 | ) 18 | -> UISceneConfiguration { 19 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOS/Tuist/Templates/Demo/Demo.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | 3 | private let nameAttribute = Template.Attribute.required("name") 4 | 5 | private let template = Template( 6 | description: "A template for a new module's demo target", 7 | attributes: [ 8 | nameAttribute, 9 | ], 10 | items: [ 11 | .file( 12 | path: "Projects/App/\(nameAttribute)/Resources/LaunchScreen.storyboard", 13 | templatePath: "LaunchScreen.stencil" 14 | ), 15 | .file( 16 | path: "Projects/App/\(nameAttribute)/Sources/Application/AppDelegate.swift", 17 | templatePath: "AppDelegate.stencil" 18 | ), 19 | .file( 20 | path: "Projects/App/\(nameAttribute)/Sources/Application/SceneDelegate.swift", 21 | templatePath: "SceneDelegate.stencil" 22 | ), 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /iOS/Tuist/Templates/Demo/SceneDelegate.stencil: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class SceneDelegate: UIResponder, UIWindowSceneDelegate { 4 | var window: UIWindow? 5 | 6 | func scene(_ scene: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) { 7 | guard let windowScene = scene as? UIWindowScene else { return } 8 | let navigationController = UINavigationController() 9 | window = UIWindow(windowScene: windowScene) 10 | window?.rootViewController = navigationController 11 | window?.makeKeyAndVisible() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /iOS/Tuist/Templates/Feature/Feature.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Feature.swift 3 | // ProjectDescriptionHelpers 4 | // 5 | // Created by 홍승현 on 11/19/23. 6 | // 7 | 8 | import ProjectDescription 9 | 10 | private let nameAttribute = Template.Attribute.required("name") 11 | 12 | private let template = Template( 13 | description: "A template for a new module's demo target", 14 | attributes: [ 15 | nameAttribute, 16 | ], 17 | items: [ 18 | .file( 19 | path: "Projects/Features/\(nameAttribute)/Sources/TempScene/TempViewController.swift", 20 | templatePath: "TempViewController.stencil" 21 | ), 22 | .file( 23 | path: "Projects/Features/\(nameAttribute)/Sources/TempScene/TempViewViewModel.swift", 24 | templatePath: "TempViewModel.stencil" 25 | ), 26 | .file( 27 | path: "Projects/Features/\(nameAttribute)/Tests/TempFeatureTests.swift", 28 | templatePath: "TempFeatureTests.stencil" 29 | ), 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /iOS/Tuist/Templates/Feature/TempFeatureTests.stencil: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class RecordFeatureTests: XCTestCase { 4 | func testAlwaysPassed() { 5 | XCTAssertTrue(true) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /iOS/Tuist/Templates/Feature/TempViewController.stencil: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | 4 | public final class TempViewController: UIViewController { 5 | 6 | // MARK: Properties 7 | // 여기에 UI를 제외한 나머지 프로퍼티를 설정하세요. 8 | 9 | private let viewModel: TempViewModelRepresentable 10 | 11 | // MARK: UI Components 12 | // 여기에 View 프로퍼티를 설정하세요. 13 | 14 | // MARK: Initializations 15 | 16 | public init(viewModel: TempViewModelRepresentable) { 17 | self.viewModel = viewModel 18 | super.init(nibName: nil, bundle: nil) 19 | } 20 | 21 | @available(*, unavailable) 22 | required init?(coder _: NSCoder) { 23 | fatalError("init(coder:) has not been implemented") 24 | } 25 | 26 | // MARK: Life Cycles 27 | 28 | override public func viewDidLoad() { 29 | super.viewDidLoad() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /iOS/Tuist/Templates/Feature/TempViewModel.stencil: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | public struct TempViewModelInput {} 4 | 5 | public typealias TempViewModelOutput = AnyPublisher 6 | 7 | public enum TempState { 8 | case idle 9 | } 10 | 11 | public protocol TempViewModelRepresentable { 12 | func transform(input: TempViewModelInput) -> TempViewModelOutput 13 | } 14 | 15 | public final class TempViewModel { 16 | // MARK: Properties 17 | 18 | private var subscriptions: Set = [] 19 | 20 | // MARK: Initializations 21 | 22 | public init() {} 23 | } 24 | 25 | extension TempViewModel: TempViewModelRepresentable { 26 | public func transform(input: TempViewModelInput) -> TempViewModelOutput { 27 | for subscription in subscriptions { 28 | subscription.cancel() 29 | } 30 | subscriptions.removeAll() 31 | 32 | let initialState: TempViewModelOutput = Just(.idle).eraseToAnyPublisher() 33 | 34 | return initialState 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /iOS/Workspace.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | import ProjectDescriptionHelpers 3 | 4 | let workspace = Workspace( 5 | name: "WeTri", 6 | projects: [ 7 | "Projects/App/WeTri", 8 | ] 9 | ) 10 | -------------------------------------------------------------------------------- /iOS/graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm2023/iOS08-WeTri/83d94b1e6f269048d1d7a40a06e8488def29834c/iOS/graph.png --------------------------------------------------------------------------------