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