├── .gitconfig ├── .githooks └── pre-push ├── .github ├── ISSUE_TEMPLATE │ ├── 기본-템플릿.md │ └── 버그-리포트-템플릿.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── CI.yml ├── .gitignore ├── .mise.toml ├── .swiftformat ├── Package.resolved ├── Package.swift ├── Plugin ├── ConfigurationPlugin │ ├── Plugin.swift │ └── ProjectDescriptionHelpers │ │ └── Configuration+Extension.swift ├── DependencyPlugin │ ├── Plugin.swift │ └── ProjectDescriptionHelpers │ │ ├── Dependency+ModularTarget.swift │ │ ├── Dependency+SPM.swift │ │ ├── ModulePaths.swift │ │ └── PathExtension.swift ├── EnvironmentPlugin │ ├── Plugin.swift │ └── ProjectDescriptionHelpers │ │ └── ProjectEnvironment.swift └── TemplatePlugin │ ├── Plugin.swift │ ├── ProjectDescriptionHelpers │ └── Extensions │ │ ├── ResourceFileElements+Extension.swift │ │ └── SourceFilesList+Extension.swift │ └── Templates │ ├── Demo │ ├── Demo.swift │ ├── DemoResources.stencil │ └── DemoSources.stencil │ ├── Interface │ ├── Interface.stencil │ └── Interface.swift │ ├── Sources │ ├── Sources.stencil │ └── Sources.swift │ ├── Testing │ ├── Testing.stencil │ └── Testing.swift │ └── Tests │ ├── Tests.stencil │ └── Tests.swift ├── Projects ├── App │ ├── BroadcastExtension.entitlements │ ├── BroadcastUploadExtension │ │ ├── Info.plist │ │ └── Sources │ │ │ └── SampleHandler.swift │ ├── Project.swift │ ├── Resources │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── AppIcon.png │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ └── LaunchScreen.storyboard │ ├── Shook.entitlements │ ├── Sources │ │ ├── AppDelegate.swift │ │ ├── DIContainer.swift │ │ ├── MockFetchChannelListUsecaseImpl.swift │ │ ├── SceneDelegate.swift │ │ └── Splash │ │ │ ├── EmptyViewModel.swift │ │ │ ├── SplashGradientView.swift │ │ │ └── SplashViewController.swift │ └── Tests │ │ └── IOS08ShookTests.swift ├── Domains │ ├── BaseDomain │ │ ├── Interface │ │ │ └── Interface.swift │ │ ├── Project.swift │ │ ├── Sources │ │ │ ├── BaseRepository.swift │ │ │ └── Utils │ │ │ │ ├── Secrets.swift │ │ │ │ └── ServiceUrlType.swift │ │ ├── Testing │ │ │ └── Testing.swift │ │ └── Tests │ │ │ └── BaseDomainTest.swift │ ├── BroadcastDomain │ │ ├── Interface │ │ │ ├── Entity │ │ │ │ └── BroadcastInfoEntity.swift │ │ │ ├── Repository │ │ │ │ └── BroadcastRepository.swift │ │ │ └── Usecase │ │ │ │ ├── DeleteBroadcastUsecase.swift │ │ │ │ ├── FetchAllBroadcastUsecase.swift │ │ │ │ └── MakeBroadcastUsecase.swift │ │ ├── Project.swift │ │ ├── Sources │ │ │ ├── DTO │ │ │ │ ├── BaseDTO.swift │ │ │ │ └── BroadcastDTO.swift │ │ │ ├── Endpoint │ │ │ │ └── BroadcastEndpoint.swift │ │ │ ├── Repository │ │ │ │ └── BroadcastRepositoryImpl.swift │ │ │ └── Usecase │ │ │ │ ├── DeleteBroadcastUsecaseImpl.swift │ │ │ │ ├── FetchAllBroadcastUsecaseImpl.swift │ │ │ │ └── MakeBroadcastUsecaseImpl.swift │ │ ├── Testing │ │ │ └── Testing.swift │ │ └── Tests │ │ │ └── BroadcastDomainTest.swift │ ├── ChattingDomain │ │ ├── Interface │ │ │ ├── Repository │ │ │ │ └── ChatRepository.swift │ │ │ └── UseCase │ │ │ │ ├── DeleteChatRoomUseCase.swift │ │ │ │ └── MakeChatRoomUseCase.swift │ │ ├── Sources │ │ │ ├── DTO │ │ │ │ ├── Request │ │ │ │ │ └── MakeRoomRequestDTO.swift │ │ │ │ └── Response │ │ │ │ │ └── BaseChatDTO.swift │ │ │ ├── Endpoint │ │ │ │ └── ChatEndpoint.swift │ │ │ ├── Repository │ │ │ │ └── ChatRepositoryImpl.swift │ │ │ └── Usecase │ │ │ │ ├── DeleteChatRoomUseCaseImpl.swift │ │ │ │ └── MakeChatRoomUseCaseImpl.swift │ │ ├── Testing │ │ │ └── Testing.swift │ │ └── Tests │ │ │ └── ChattingDomainTest.swift │ └── LiveStationDomain │ │ ├── Interface │ │ ├── Entity │ │ │ ├── BroadcastEntity.swift │ │ │ ├── ChannelEntity.swift │ │ │ ├── ChannelInfoEntity.swift │ │ │ └── VideoEntity.swift │ │ ├── Repository │ │ │ └── LiveStationRepository.swift │ │ └── UseCase │ │ │ ├── CreateChannelUsecase.swift │ │ │ ├── DeleteChannelUsecase.swift │ │ │ ├── FetchChannelInfoUsecase.swift │ │ │ ├── FetchChannelListUsecase.swift │ │ │ └── FetchVideoListUsecase.swift │ │ ├── Project.swift │ │ ├── Sources │ │ ├── DTO │ │ │ └── Response │ │ │ │ ├── BroadcastResponseDTO.swift │ │ │ │ ├── ChannelInfoResponseDTO.swift │ │ │ │ ├── ChannelListResponseDTO.swift │ │ │ │ ├── ChannelResponseDTO.swift │ │ │ │ ├── ThumbnailResponseDTO.swift │ │ │ │ └── VideoListResponseDTO.swift │ │ ├── Endpoint │ │ │ └── LiveStationEndpoint.swift │ │ ├── Repository │ │ │ └── LiveStationRepositoryImpl.swift │ │ └── UseCase │ │ │ ├── CreateChannelUsecaseImpl.swift │ │ │ ├── DeleteChannelUsecaseImpl.swift │ │ │ ├── FetchChannelInfoUsecaseImpl.swift │ │ │ ├── FetchChannelListUsecaseImpl.swift │ │ │ └── FetchVideoListUsecaseImpl.swift │ │ └── Testing │ │ └── Testing.swift ├── Features │ ├── AuthFeature │ │ ├── Demo │ │ │ ├── Resources │ │ │ │ └── LaunchScreen.storyboard │ │ │ └── Sources │ │ │ │ ├── AppDelegate.swift │ │ │ │ └── MockCreateChannelUsecaseImpl.swift │ │ ├── Interface │ │ │ └── Interface.swift │ │ ├── Project.swift │ │ ├── Sources │ │ │ ├── SignUpGradientView.swift │ │ │ ├── SignUpViewController.swift │ │ │ └── SignUpViewModel.swift │ │ └── Tests │ │ │ └── AuthFeatureTest.swift │ ├── BaseFeature │ │ ├── Demo │ │ │ ├── Resources │ │ │ │ └── LaunchScreen.storyboard │ │ │ └── Sources │ │ │ │ └── AppDelegate.swift │ │ ├── Interface │ │ │ ├── ViewLifeCycle.swift │ │ │ └── ViewModel.swift │ │ ├── Project.swift │ │ ├── Sources │ │ │ ├── BaseCollectionViewCell.swift │ │ │ ├── BaseNavigationController.swift │ │ │ ├── BaseTableViewCell.swift │ │ │ ├── BaseView.swift │ │ │ └── BaseViewController.swift │ │ ├── Testing │ │ │ └── Testing.swift │ │ └── Tests │ │ │ └── BaseFeatureTest.swift │ ├── LiveStreamFeature │ │ ├── Demo │ │ │ ├── Resources │ │ │ │ └── LaunchScreen.storyboard │ │ │ └── Sources │ │ │ │ └── AppDelegate.swift │ │ ├── Interface │ │ │ └── LiveStreamViewControllerFactory.swift │ │ ├── Project.swift │ │ ├── Sources │ │ │ ├── Chating │ │ │ │ ├── Models │ │ │ │ │ └── ChatInfo.swift │ │ │ │ └── Views │ │ │ │ │ ├── ChatEmptyView.swift │ │ │ │ │ ├── ChatInputField.swift │ │ │ │ │ ├── ChattingCell.swift │ │ │ │ │ ├── ChattingListView.swift │ │ │ │ │ └── SystemAlarmCell.swift │ │ │ ├── Factory │ │ │ │ └── LiveStreamViewControllerFactoryImpl.swift │ │ │ └── Player │ │ │ │ ├── ViewControllers │ │ │ │ └── LiveStreamViewController.swift │ │ │ │ ├── ViewModels │ │ │ │ └── LiveStreamViewModel.swift │ │ │ │ └── Views │ │ │ │ ├── LiveStreamInfoView.swift │ │ │ │ ├── PlayerControlView.swift │ │ │ │ ├── ShookPlayerView.swift │ │ │ │ └── TimeControlView.swift │ │ └── Tests │ │ │ └── LiveStreamFeatureTest.swift │ └── MainFeature │ │ ├── BroadcastUploadExtension │ │ └── SampleHandler.swift │ │ ├── Demo │ │ ├── Resources │ │ │ └── LaunchScreen.storyboard │ │ └── Sources │ │ │ ├── AppDelegate.swift │ │ │ ├── MockCreateChannelUsecaseImpl.swift │ │ │ ├── MockDeleteBroadcastUsecaseImpl.swift │ │ │ ├── MockDeleteChannelUsecaseImpl.swift │ │ │ ├── MockFetchAllBroadcastUsecaseImpl.swift │ │ │ ├── MockFetchChannelInfoUsecaseImpl.swift │ │ │ ├── MockFetchChannelListUsecaseImpl.swift │ │ │ ├── MockLiveStreamViewControllerFactory.swift │ │ │ ├── MockLiveStreamingViewController.swift │ │ │ ├── MockMakeBroadcastUsecaseImpl.swift │ │ │ └── MockShookPlayerView.swift │ │ ├── Interface │ │ ├── BroadcastViewControllerFactory.swift │ │ └── SettingViewControllerFactory.swift │ │ ├── Project.swift │ │ ├── Sources │ │ ├── Factory │ │ │ ├── BroadcastViewControllerFactoryImpl.swift │ │ │ └── SettingViewControllerFactoryImpl.swift │ │ ├── Models │ │ │ └── Channel.swift │ │ ├── Utilities │ │ │ ├── CollectionViewCellTransitioning.swift │ │ │ └── NotificationName.swift │ │ ├── ViewControllers │ │ │ ├── BroadcastCollectionViewController.swift │ │ │ ├── BroadcastViewController.swift │ │ │ └── SettingUIViewController.swift │ │ ├── ViewModels │ │ │ ├── BroadcastCollectionViewModel.swift │ │ │ └── SettingViewModel.swift │ │ └── Views │ │ │ ├── BroadcastCollectionLoadView.swift │ │ │ ├── BroadcastCollectionViewCell │ │ │ ├── EmptyBroadcastCollectionViewCell.swift │ │ │ ├── LargeBroadcastCollectionViewCell.swift │ │ │ └── SmallBroadcastCollectionViewCell.swift │ │ │ ├── BroadcastThumbnailView.swift │ │ │ ├── PaddingLabel.swift │ │ │ └── SettingTableViewCell.swift │ │ └── Tests │ │ └── MainFeatureTest.swift ├── Modules │ ├── ChatSoketModule │ │ ├── Demo │ │ │ ├── Resources │ │ │ │ └── LaunchScreen.storyboard │ │ │ └── Sources │ │ │ │ └── AppDelegate.swift │ │ ├── Interface │ │ │ └── Interface.swift │ │ ├── Project.swift │ │ ├── Sources │ │ │ ├── Message │ │ │ │ ├── ChatMessage.swift │ │ │ │ └── MessageType.swift │ │ │ ├── SoketTestViewController.swift │ │ │ └── WebSocket.swift │ │ ├── Testing │ │ │ └── Testing.swift │ │ └── Tests │ │ │ └── ChatSoketModuleTest.swift │ ├── EasyLayout │ │ ├── Demo │ │ │ ├── Resources │ │ │ │ └── LaunchScreen.storyboard │ │ │ └── Sources │ │ │ │ ├── AppDelegate.swift │ │ │ │ └── ViewController.swift │ │ ├── Project.swift │ │ └── Sources │ │ │ ├── Anchor.swift │ │ │ ├── EasyConstraint.swift │ │ │ ├── EasyLayout.swift │ │ │ └── Protocol │ │ │ └── Anchorable.swift │ ├── FastNetwork │ │ ├── Demo │ │ │ ├── Resources │ │ │ │ └── LaunchScreen.storyboard │ │ │ └── Sources │ │ │ │ └── AppDelegate.swift │ │ ├── Interface │ │ │ └── Interface.swift │ │ ├── Project.swift │ │ ├── Sources │ │ │ ├── Client │ │ │ │ ├── NetworkClient.swift │ │ │ │ └── Requestable.swift │ │ │ ├── Endpoint.swift │ │ │ ├── Error │ │ │ │ ├── HTTPError.swift │ │ │ │ └── NetworkError.swift │ │ │ ├── Extensions │ │ │ │ └── URL+Extension.swift │ │ │ ├── Interceptor │ │ │ │ ├── DefaultLoggingInterceptor.swift │ │ │ │ └── Interceptor.swift │ │ │ ├── Request │ │ │ │ ├── Components │ │ │ │ │ ├── HTTPMethod.swift │ │ │ │ │ └── RequestTask.swift │ │ │ │ └── Encoding │ │ │ │ │ ├── ParamterJSONEncoder.swift │ │ │ │ │ ├── RequestParameterEncodable.swift │ │ │ │ │ └── URLQueryEncoder.swift │ │ │ └── Response │ │ │ │ └── Response.swift │ │ ├── Testing │ │ │ ├── MockData.swift │ │ │ ├── MockResponse.swift │ │ │ └── MockURLProtocol.swift │ │ └── Tests │ │ │ ├── MockEndpoint.swift │ │ │ ├── NetworkClientTest.swift │ │ │ └── NetworkEncoderTests.swift │ └── ThirdPartyLibModule │ │ ├── Interface │ │ └── Interface.swift │ │ ├── Project.swift │ │ ├── Sources │ │ └── Sources.swift │ │ └── Tests │ │ └── ThirdPartyLibModuleTest.swift └── UserInterfaces │ └── DesignSystem │ ├── Interface │ └── Interface.swift │ ├── Project.swift │ ├── Resources │ ├── Color.xcassets │ │ ├── Contents.json │ │ ├── DarkGray.colorset │ │ │ └── Contents.json │ │ ├── ErrorRed.colorset │ │ │ └── Contents.json │ │ ├── Gray.colorset │ │ │ └── Contents.json │ │ ├── MainBlack.colorset │ │ │ └── Contents.json │ │ ├── MainBlue.colorset │ │ │ └── Contents.json │ │ ├── MainGreen.colorset │ │ │ └── Contents.json │ │ ├── PointYellow.colorset │ │ │ └── Contents.json │ │ └── White.colorset │ │ │ └── Contents.json │ ├── Image.xcassets │ │ ├── Contents.json │ │ ├── appIcon_small.imageset │ │ │ ├── Contents.json │ │ │ ├── appIcon_small@2x.png │ │ │ └── appIcon_small@3x.png │ │ ├── chat_48.imageset │ │ │ ├── Contents.json │ │ │ ├── chat@2x.png │ │ │ └── chat@3x.png │ │ ├── chevronDown_24.imageset │ │ │ ├── Contents.json │ │ │ ├── chevronDownCircle-1.png │ │ │ └── chevronDownCircle.png │ │ ├── heart_24.imageset │ │ │ ├── Contents.json │ │ │ ├── Property 1=Emoji@2x.png │ │ │ └── heart.png │ │ ├── main_logo.imageset │ │ │ ├── Contents.json │ │ │ ├── main_logo@2x.png │ │ │ └── main_logo@3x.png │ │ ├── pause_48.imageset │ │ │ ├── Contents.json │ │ │ ├── stop_48@2x.png │ │ │ └── stop_48@3x.png │ │ ├── play_48.imageset │ │ │ ├── Contents.json │ │ │ ├── play_48@2x.png │ │ │ └── play_48@3x.png │ │ ├── rewind_48.imageset │ │ │ ├── Contents.json │ │ │ ├── rewind_48@2x.png │ │ │ └── rewind_48@3x.png │ │ ├── send_24.imageset │ │ │ ├── Contents.json │ │ │ ├── Property 1=Send.png │ │ │ └── send.png │ │ ├── tv_48.imageset │ │ │ ├── Contents.json │ │ │ ├── tv_48@2x.png │ │ │ └── tv_48@3x.png │ │ ├── xmark_24.imageset │ │ │ ├── Contents.json │ │ │ ├── xmarkCircle.png │ │ │ └── xmarkCircle@2x.png │ │ ├── zoomIn_24.imageset │ │ │ ├── Contents.json │ │ │ ├── Property 1=zoomIn@2x.png │ │ │ └── zoomIn.png │ │ └── zoomOut_24.imageset │ │ │ ├── 48@2x.png │ │ │ ├── 48@3x.png │ │ │ └── Contents.json │ ├── Lottie │ │ ├── confetti.json │ │ ├── loading.json │ │ ├── shook.json │ │ └── splash.json │ └── PretendardVariable.ttf │ └── Sources │ ├── SHFontSystem.swift │ ├── SHLoadingView.swift │ └── SHRefreshControl.swift ├── README.md ├── Scripts ├── .swiftlint.yml ├── Setup.sh ├── SwiftLintRunScript.sh ├── generateModule.swift └── generatePlugin.swift ├── Tuist ├── Config.swift └── ProjectDescriptionHelpers │ ├── Environment │ └── GenerationEnvironment.swift │ ├── Extensions │ ├── InfoPlist+Extension.swift │ ├── Project+Extension.swift │ ├── Scheme+Extension.swift │ ├── SettingsDictionary+Extension.swift │ ├── Target+Extension.swift │ └── TargetScript+Extension.swift │ ├── Protocol │ └── Configurable.swift │ └── TargetSpec │ └── TargetSpec.swift ├── Workspace.swift └── makefile /.gitconfig: -------------------------------------------------------------------------------- 1 | [core] 2 | hooksPath = .githooks -------------------------------------------------------------------------------- /.githooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 명령어 경로 저장 4 | FORMAT=$(which swiftformat) 5 | 6 | # 설치 확인 7 | if [[ -e "${FORMAT}" ]]; then 8 | echo "🚀 SwiftFormat 시작..." 9 | echo "🔍 SwiftFormat 적용 경로: ./Projects" 10 | else 11 | echo "SwiftFormat이 존재하지 않습니다. 설치해주세요 ! 'brew install swiftformat'" 12 | exit 1 13 | fi 14 | 15 | # 포맷팅 결과 , 16 | RESULT=$($FORMAT ./Projects --config .swiftformat) 17 | 18 | if [ "$RESULT" == '' ]; then 19 | git add . 20 | git commit -m "style swiftformat 적용" 21 | printf "\n 🎉 SwiftFormat 적용을 완료했습니다 !! \n" 22 | else 23 | echo "" 24 | printf "❌ SwiftFormat Failed 아래 내용을 확인해주세요 \n" 25 | while read -r line; do 26 | FILEPATH=$(echo $line | cut -d : -f 1) 27 | L=$(echo $line | cut -d : -f 2) 28 | C=$(echo $line | cut -d : -f 3) 29 | TYPE=$(echo $line | cut -d : -f 4 | cut -c 2-) 30 | MESSAGE=$(echo $line | cut -d : -f 5 | cut -c 2-) 31 | DESCRIPTION=$(echo $line | cut -d : -f 6 | cut -c 2-) 32 | if [ $TYPE == 'warning' ]; then # 33 | printf "\n 🚧 $TYPE\n" 34 | printf " $FILEPATH:$L:$C\n" 35 | printf " 📌 $MESSAGE: - $DESCRIPTION\n" 36 | exit 0 # 정상 종료 37 | elif [ $TYPE == 'error' ]; then 38 | printf "\n 🚨 $TYPE\n" 39 | fi 40 | printf " ✅ $FILEPATH:$L:$C\n" 41 | printf " 📌 $MESSAGE: - $DESCRIPTION\n" 42 | done <<< "$RESULT" 43 | 44 | printf "\n 🚑 커밋실패!! SwiftFormat 실행이 실패하였습니다 🥺 \n" 45 | exit 1 # 비정상 종료 46 | fi -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/기본-템플릿.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 기본 템플릿 3 | about: Describe this issue template's purpose here. 4 | title: "{ 제목 }" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## 📝 요약 11 | 12 | 13 | 14 | ## ✨ 설명 15 | 16 | 17 | 18 | ## ✔️ 할일 19 | 20 | - [ ] 할일 1 21 | - [ ] 할일 2 22 | - [ ] 할일 3 23 | 24 | ## 🗒️ 추가 정보 (Optional) 25 | 26 | 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/버그-리포트-템플릿.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 버그 리포트 템플릿 3 | about: Describe this issue template's purpose here. 4 | title: "{ 제목 }" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## 📋 버그가 재현되는 단계 11 | 12 | 13 | 14 | ## ⚙️ 재현에 필요한 조건 및 빈도 15 | 16 | 17 | 18 | ## 👀 기대하는 정상적인 동작 19 | 20 | 21 | 22 | ## 🐛 실제로 나타난 현상 23 | 24 | 25 | 26 | ## 🗒️ 추가 정보 (Optional) 27 | 28 | 29 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## 💡 요약 및 이슈 2 | 7 | 8 | - Resolves: #{이슈번호} 9 | 10 | ## 📃 작업내용 11 | 12 | 16 | 17 | ## 🙋‍♂️ 리뷰노트 18 | 19 | 26 | 27 | ## ✅ PR 체크리스트 28 | 29 | 32 | 33 | - [ ] 이 작업으로 인해 변경이 필요한 문서가 변경되었나요? (e.g. `XCConfig`, `노션`, `README`) 34 | - [ ] 이 작업을 하고나서 공유해야할 팀원들에게 공유되었나요? (e.g. `"API 개발 완료됐어요"`, `"XCConfig 값 추가되었어요"`) 35 | - [ ] 작업한 코드가 정상적으로 동작하나요? 36 | - [ ] Merge 대상 브랜치가 올바른가요? 37 | - [ ] PR과 관련 없는 작업이 있지는 않나요? 38 | 39 | ## 🎸 기타 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### macOS ### 2 | # General 3 | .DS_Store 4 | .AppleDouble 5 | .LSOverride 6 | 7 | # Icon must end with two 8 | Icon 9 | 10 | # Thumbnails 11 | ._* 12 | 13 | # Files that might appear in the root of a volume 14 | .DocumentRevisions-V100 15 | .fseventsd 16 | .Spotlight-V100 17 | .TemporaryItems 18 | .Trashes 19 | .VolumeIcon.icns 20 | .com.apple.timemachine.donotpresent 21 | 22 | # Directories potentially created on remote AFP share 23 | .AppleDB 24 | .AppleDesktop 25 | Network Trash Folder 26 | Temporary Items 27 | .apdisk 28 | 29 | ### Xcode ### 30 | # Xcode 31 | # 32 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 33 | 34 | ## User settings 35 | xcuserdata/ 36 | 37 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 38 | *.xcscmblueprint 39 | *.xccheckout 40 | 41 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 42 | build/ 43 | DerivedData/ 44 | *.moved-aside 45 | *.pbxuser 46 | !default.pbxuser 47 | *.mode1v3 48 | !default.mode1v3 49 | *.mode2v3 50 | !default.mode2v3 51 | *.perspectivev3 52 | !default.perspectivev3 53 | 54 | ### Xcode Patch ### 55 | *.xcodeproj/* 56 | !*.xcodeproj/project.pbxproj 57 | !*.xcodeproj/xcshareddata/ 58 | !*.xcworkspace/contents.xcworkspacedata 59 | /*.gcno 60 | 61 | ### Projects ### 62 | *.xcodeproj 63 | *.xcworkspace 64 | 65 | ### Tuist derived files ### 66 | graph.png 67 | graph.dot 68 | Derived/ 69 | 70 | ### Tuist managed dependencies ### 71 | .build 72 | 73 | ### Secret 74 | Secrets.xcconfig -------------------------------------------------------------------------------- /.mise.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | tuist = "4.12.1" 3 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | # format options 2 | --swiftversion 6.0 3 | --indent 4 4 | 5 | # file options 6 | --exclude /Tuist 7 | 8 | # rules 9 | --disable trailingCommas 10 | --enable blockComments 11 | --enable markTypes 12 | --enable noExplicitOwnership 13 | --enable sortSwitchCases 14 | --enable wrapEnumCases -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "haishinkit.swift", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/shogo4405/HaishinKit.swift.git", 7 | "state" : { 8 | "revision" : "87046accc6c5aa705b2db8b6ca142bf7c2621760", 9 | "version" : "2.0.0" 10 | } 11 | }, 12 | { 13 | "identity" : "logboard", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/shogo4405/Logboard.git", 16 | "state" : { 17 | "revision" : "272976e1f3e8873e60ffe4b08fe50df48a93751b", 18 | "version" : "2.5.0" 19 | } 20 | }, 21 | { 22 | "identity" : "lottie-ios", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/airbnb/lottie-ios.git", 25 | "state" : { 26 | "revision" : "fe4c6fe3a0aa66cdeb51d549623c82ca9704b9a5", 27 | "version" : "4.5.0" 28 | } 29 | } 30 | ], 31 | "version" : 2 32 | } 33 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | import PackageDescription 3 | 4 | #if TUIST 5 | import ProjectDescription 6 | 7 | let packageSettings = PackageSettings( 8 | // Customize the product types for specific package product 9 | // Default is .staticFramework 10 | // productTypes: ["Alamofire": .framework,] 11 | productTypes: [:], 12 | baseSettings: .settings(configurations: [ 13 | .debug(name: .debug), 14 | .release(name: .release) 15 | ]) 16 | ) 17 | #endif 18 | 19 | let package = Package( 20 | name: "ShookPackage", 21 | dependencies: [ 22 | // Add your own dependencies here: 23 | // .package(url: "https://github.com/Alamofire/Alamofire", from: "5.0.0"), 24 | // You can read more about dependencies here: https://docs.tuist.io/documentation/tuist/dependencies 25 | .package(url: "https://github.com/shogo4405/HaishinKit.swift.git", from: "2.0.0"), 26 | .package(url: "https://github.com/airbnb/lottie-ios.git", from: "4.5.0") 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /Plugin/ConfigurationPlugin/Plugin.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | let configurationPlugin = Plugin(name: "ConfigurationPlugin") -------------------------------------------------------------------------------- /Plugin/ConfigurationPlugin/ProjectDescriptionHelpers/Configuration+Extension.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | 3 | public extension Array where Element == Configuration { 4 | static let `default`: [Configuration] = [ 5 | .debug(name: .debug, xcconfig: .relativeToRoot("Projects/App/XCConfig/Secrets.xcconfig")), 6 | .release(name: .release, xcconfig: .relativeToRoot("Projects/App/XCConfig/Secrets.xcconfig")) 7 | ] 8 | } 9 | 10 | public extension ProjectDescription.ConfigurationName { 11 | // define custom configuration 12 | // static let qa = ConfigurationName.configuration("QA") 13 | } 14 | -------------------------------------------------------------------------------- /Plugin/DependencyPlugin/Plugin.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | let dependencyPlugin = Plugin(name: "DependencyPlugin") -------------------------------------------------------------------------------- /Plugin/DependencyPlugin/ProjectDescriptionHelpers/Dependency+ModularTarget.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ProjectDescription 3 | 4 | public extension TargetDependency { 5 | static func feature( 6 | target: ModulePaths.Feature, 7 | type: MoudlarTargetType = .sources 8 | ) -> TargetDependency { 9 | .project( 10 | target: target.targetName(type: type), 11 | path: .relativeToFeature(target.rawValue) 12 | ) 13 | } 14 | 15 | static func module( 16 | target: ModulePaths.Module, 17 | type: MoudlarTargetType = .sources 18 | ) -> TargetDependency { 19 | .project( 20 | target: target.targetName(type: type), 21 | path: .relativeToModule(target.rawValue) 22 | ) 23 | } 24 | 25 | static func domain( 26 | target: ModulePaths.Domain, 27 | type: MoudlarTargetType = .sources 28 | ) -> TargetDependency { 29 | .project( 30 | target: target.targetName(type: type), 31 | path: .relativeToDomain(target.rawValue) 32 | ) 33 | } 34 | 35 | static func userInterface( 36 | target: ModulePaths.UserInterface, 37 | type: MoudlarTargetType = .sources 38 | ) -> TargetDependency { 39 | .project( 40 | target: target.targetName(type: type), 41 | path: .relativeToUserInterfaces(target.rawValue) 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Plugin/DependencyPlugin/ProjectDescriptionHelpers/Dependency+SPM.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | 3 | public extension TargetDependency { 4 | struct SPM {} 5 | } 6 | 7 | public extension TargetDependency.SPM { 8 | // MARK: external 9 | // ex) static let Moya = TargetDependency.external(name: "Moya") 10 | 11 | static let HaishinKit = TargetDependency.external(name: "HaishinKit") 12 | static let Lottie = TargetDependency.external(name: "Lottie") 13 | } 14 | -------------------------------------------------------------------------------- /Plugin/DependencyPlugin/ProjectDescriptionHelpers/ModulePaths.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum ModulePaths { 4 | case feature(Feature) 5 | case module(Module) 6 | case domain(Domain) 7 | case userInterface(UserInterface) 8 | } 9 | 10 | extension ModulePaths: ModularPathConvertable { 11 | public func targetName(type: MoudlarTargetType) -> String { 12 | switch self { 13 | case let .feature(module as any ModularPathConvertable), 14 | let .module(module as any ModularPathConvertable), 15 | let .domain(module as any ModularPathConvertable), 16 | let .userInterface(module as any ModularPathConvertable): 17 | return module.targetName(type: type) 18 | } 19 | } 20 | } 21 | 22 | public extension ModulePaths { 23 | enum Feature: String, ModularPathConvertable { 24 | case AuthFeature 25 | case LiveStreamFeature 26 | case MainFeature 27 | case BaseFeature 28 | } 29 | } 30 | 31 | public extension ModulePaths { 32 | enum Module: String, ModularPathConvertable { 33 | case ChatSoketModule 34 | case FastNetwork 35 | case EasyLayout 36 | case ThirdPartyLibModule 37 | } 38 | } 39 | 40 | public extension ModulePaths { 41 | enum Domain: String, ModularPathConvertable { 42 | case BroadcastDomain 43 | case LiveStationDomain 44 | case BaseDomain 45 | } 46 | } 47 | 48 | 49 | public extension ModulePaths { 50 | enum UserInterface: String, ModularPathConvertable { 51 | case DesignSystem 52 | 53 | } 54 | } 55 | 56 | public enum MoudlarTargetType: String { 57 | case interface = "Interface" 58 | case sources = "" 59 | case testing = "Testing" 60 | case unitTest = "Tests" 61 | case demo = "Demo" 62 | } 63 | 64 | public protocol ModularPathConvertable { 65 | func targetName(type: MoudlarTargetType) -> String 66 | } 67 | 68 | public extension ModularPathConvertable where Self: RawRepresentable { 69 | func targetName(type: MoudlarTargetType) -> String { 70 | "\(self.rawValue)\(type.rawValue)" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Plugin/DependencyPlugin/ProjectDescriptionHelpers/PathExtension.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | 3 | public extension ProjectDescription.Path { 4 | static func relativeToFeature(_ path: String) -> Self { 5 | return .relativeToRoot("Projects/Features/\(path)") 6 | } 7 | 8 | static func relativeToModule(_ path: String) -> Self { 9 | return .relativeToRoot("Projects/Modules/\(path)") 10 | } 11 | 12 | static func relativeToDomain(_ path: String) -> Self { 13 | return .relativeToRoot("Projects/Domains/\(path)") 14 | } 15 | 16 | static func relativeToUserInterfaces(_ path: String) -> Self { 17 | return .relativeToRoot("Projects/UserInterfaces/\(path)") 18 | } 19 | 20 | static var scripts: Self { 21 | return .relativeToRoot("Scripts") 22 | } 23 | 24 | static var app: Self { 25 | return .relativeToRoot("Projects/App") 26 | } 27 | 28 | static func + (lhs: Self, rhs: String) -> Self { 29 | return Path.relativeToRoot(lhs.pathString + rhs) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Plugin/EnvironmentPlugin/Plugin.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | let environmentPlugin = Plugin(name: "EnvironmentPlugin") -------------------------------------------------------------------------------- /Plugin/EnvironmentPlugin/ProjectDescriptionHelpers/ProjectEnvironment.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ProjectDescription 3 | 4 | public struct ProjectEnvironment { 5 | public let name: String 6 | public let organizationName: String 7 | public let bundleID: String 8 | public let deploymentTargets: DeploymentTargets 9 | public let destinations : Destinations 10 | // public let baseSetting: SettingsDictionary 11 | 12 | 13 | } 14 | 15 | public let env = ProjectEnvironment( 16 | name: "Shook", 17 | organizationName: "kr.codesquad.boostcamp9", 18 | bundleID: "kr.codesquad.boostcamp9.Shook", 19 | deploymentTargets: .iOS("16.0"), 20 | destinations: [.iPhone] 21 | // baseSetting: SettingsDictionary() 22 | // .marketingVersion("3.2.0") 23 | // .currentProjectVersion("0") 24 | // .debugInformationFormat(DebugInformationFormat.dwarfWithDsym) 25 | // .otherLinkerFlags(["-ObjC"]) 26 | // .bitcodeEnabled(false) 27 | ) 28 | -------------------------------------------------------------------------------- /Plugin/TemplatePlugin/Plugin.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | let templatePlugin = Plugin(name: "TemplatePlugin") -------------------------------------------------------------------------------- /Plugin/TemplatePlugin/ProjectDescriptionHelpers/Extensions/ResourceFileElements+Extension.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | 3 | public extension ResourceFileElements { 4 | static let resources: ResourceFileElements = "Resources/**" 5 | } 6 | -------------------------------------------------------------------------------- /Plugin/TemplatePlugin/ProjectDescriptionHelpers/Extensions/SourceFilesList+Extension.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | 3 | public extension SourceFilesList { 4 | static let demoSources: SourceFilesList = "Demo/Sources/**" 5 | static let interface: SourceFilesList = "Interface/**" 6 | static let sources: SourceFilesList = "Sources/**" 7 | static let testing: SourceFilesList = "Testing/**" 8 | static let unitTests: SourceFilesList = "Tests/**" 9 | static let uiTests: SourceFilesList = "UITests/**" 10 | } 11 | -------------------------------------------------------------------------------- /Plugin/TemplatePlugin/Templates/Demo/Demo.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | 3 | private let layerAttribute = Template.Attribute.required("layer") 4 | private let nameAttribute = Template.Attribute.required("name") 5 | 6 | private let template = Template( 7 | description: "A template for a new module's demo target", 8 | attributes: [ 9 | layerAttribute, 10 | nameAttribute 11 | ], 12 | items: [ 13 | .file( 14 | path: "Projects/\(layerAttribute)/\(nameAttribute)/Demo/Sources/AppDelegate.swift", 15 | templatePath: "DemoSources.stencil" 16 | ), 17 | .file( 18 | path: "Projects/\(layerAttribute)/\(nameAttribute)/Demo/Resources/LaunchScreen.storyboard", 19 | templatePath: "DemoResources.stencil" 20 | ) 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /Plugin/TemplatePlugin/Templates/Demo/DemoResources.stencil: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Plugin/TemplatePlugin/Templates/Demo/DemoSources.stencil: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @main 4 | final class AppDelegate: UIResponder, UIApplicationDelegate { 5 | var window: UIWindow? 6 | 7 | func application( 8 | _ application: UIApplication, 9 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil 10 | ) -> Bool { 11 | window = UIWindow(frame: UIScreen.main.bounds) 12 | let viewController = UIViewController() 13 | viewController.view.backgroundColor = .yellow 14 | window?.rootViewController = viewController 15 | window?.makeKeyAndVisible() 16 | 17 | return true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Plugin/TemplatePlugin/Templates/Interface/Interface.stencil: -------------------------------------------------------------------------------- 1 | // Empty 2 | -------------------------------------------------------------------------------- /Plugin/TemplatePlugin/Templates/Interface/Interface.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | 3 | private let layerAttribute = Template.Attribute.required("layer") 4 | private let nameAttribute = Template.Attribute.required("name") 5 | 6 | private let template = Template( 7 | description: "A template for a new module's interface target", 8 | attributes: [ 9 | layerAttribute, 10 | nameAttribute 11 | ], 12 | items: [ 13 | .file( 14 | path: "Projects/\(layerAttribute)/\(nameAttribute)/Interface/Interface.swift", 15 | templatePath: "Interface.stencil" 16 | ) 17 | ] 18 | ) 19 | -------------------------------------------------------------------------------- /Plugin/TemplatePlugin/Templates/Sources/Sources.stencil: -------------------------------------------------------------------------------- 1 | // Empty 2 | -------------------------------------------------------------------------------- /Plugin/TemplatePlugin/Templates/Sources/Sources.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | 3 | private let layerAttribute = Template.Attribute.required("layer") 4 | private let nameAttribute = Template.Attribute.required("name") 5 | 6 | private let template = Template( 7 | description: "A template for a new module's sources target", 8 | attributes: [ 9 | layerAttribute, 10 | nameAttribute 11 | ], 12 | items: [ 13 | .file( 14 | path: "Projects/\(layerAttribute)/\(nameAttribute)/Sources/Sources.swift", 15 | templatePath: "Sources.stencil" 16 | ) 17 | ] 18 | ) 19 | -------------------------------------------------------------------------------- /Plugin/TemplatePlugin/Templates/Testing/Testing.stencil: -------------------------------------------------------------------------------- 1 | // Empty 2 | -------------------------------------------------------------------------------- /Plugin/TemplatePlugin/Templates/Testing/Testing.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | 3 | private let layerAttribute = Template.Attribute.required("layer") 4 | private let nameAttribute = Template.Attribute.required("name") 5 | 6 | private let template = Template( 7 | description: "A template for a new module's testing target", 8 | attributes: [ 9 | layerAttribute, 10 | nameAttribute 11 | ], 12 | items: [ 13 | .file( 14 | path: "Projects/\(layerAttribute)/\(nameAttribute)/Testing/Testing.swift", 15 | templatePath: "Testing.stencil" 16 | ) 17 | ] 18 | ) 19 | -------------------------------------------------------------------------------- /Plugin/TemplatePlugin/Templates/Tests/Tests.stencil: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class {{ name }}Tests: XCTestCase { 4 | override func setUpWithError() throws {} 5 | 6 | override func tearDownWithError() throws {} 7 | 8 | func testExample() { 9 | XCTAssertEqual(1, 1) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Plugin/TemplatePlugin/Templates/Tests/Tests.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | 3 | private let layerAttribute = Template.Attribute.required("layer") 4 | private let nameAttribute = Template.Attribute.required("name") 5 | 6 | private let template = Template( 7 | description: "A template for a new module's unit test target", 8 | attributes: [ 9 | layerAttribute, 10 | nameAttribute 11 | ], 12 | items: [ 13 | .file( 14 | path: "Projects/\(layerAttribute)/\(nameAttribute)/Tests/\(nameAttribute)Test.swift", 15 | templatePath: "Tests.stencil" 16 | ), 17 | ] 18 | ) 19 | -------------------------------------------------------------------------------- /Projects/App/BroadcastExtension.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.kr.codesquad.boostcamp9.Shook 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Projects/App/BroadcastUploadExtension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionPointIdentifier 8 | com.apple.broadcast-services-upload 9 | NSExtensionPrincipalClass 10 | $(PRODUCT_MODULE_NAME).SampleHandler 11 | RPBroadcastProcessMode 12 | RPBroadcastProcessModeSampleBuffer 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Projects/App/BroadcastUploadExtension/Sources/SampleHandler.swift: -------------------------------------------------------------------------------- 1 | import ReplayKit 2 | 3 | import HaishinKit 4 | 5 | final class SampleHandler: RPBroadcastSampleHandler { 6 | // MARK: - App group 7 | 8 | private let sharedDefaults = UserDefaults(suiteName: "group.kr.codesquad.boostcamp9.Shook")! 9 | private let isStreamingKey = "IS_STREAMING" 10 | 11 | // MARK: - HaishinKit 12 | 13 | private let mixer = MediaMixer() 14 | private let connection = RTMPConnection() 15 | private lazy var stream = RTMPStream(connection: connection) 16 | private let rotator = VideoRotator() 17 | 18 | // MARK: - RTMP Service URL and Streaming key 19 | 20 | private let rtmp = "RTMP_SEVICE_URL" 21 | private let streamKey = "STREAMING_KEY" 22 | 23 | override func broadcastStarted(withSetupInfo _: [String: NSObject]?) { 24 | Task { 25 | var videoSettings = VideoCodecSettings() 26 | videoSettings.videoSize = CGSize(width: 1280, height: 720) 27 | videoSettings.scalingMode = .letterbox 28 | 29 | await stream.setVideoSettings(videoSettings) 30 | await mixer.addOutput(stream) 31 | 32 | guard let rtmpURL = sharedDefaults.string(forKey: rtmp), 33 | let streamKey = sharedDefaults.string(forKey: streamKey) else { return } 34 | 35 | _ = try await connection.connect(rtmpURL) 36 | _ = try await stream.publish(streamKey) 37 | } 38 | 39 | sharedDefaults.set(true, forKey: isStreamingKey) 40 | } 41 | 42 | override func broadcastFinished() { 43 | Task { 44 | _ = try await stream.close() 45 | try await connection.close() 46 | } 47 | 48 | sharedDefaults.set(false, forKey: isStreamingKey) 49 | } 50 | 51 | override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) { 52 | Task { 53 | switch sampleBufferType { 54 | case .video: 55 | if case let .success(rotatedBuffer) = rotator?.rotate(buffer: sampleBuffer) { 56 | await mixer.append(rotatedBuffer) 57 | } else { 58 | await mixer.append(sampleBuffer) 59 | } 60 | 61 | case .audioApp: 62 | await mixer.append(sampleBuffer, track: 0) 63 | 64 | case .audioMic: 65 | await mixer.append(sampleBuffer, track: 1) 66 | 67 | default: break 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Projects/App/Project.swift: -------------------------------------------------------------------------------- 1 | import ConfigurationPlugin 2 | import ProjectDescription 3 | import ProjectDescriptionHelpers 4 | import TemplatePlugin 5 | 6 | let project = Project.project 7 | -------------------------------------------------------------------------------- /Projects/App/Resources/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x8D", 9 | "green" : "0xC9", 10 | "red" : "0x34" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS08-Shook/667278070168ce27a9a03922cc176aca93952eb6/Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png -------------------------------------------------------------------------------- /Projects/App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Projects/App/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Projects/App/Resources/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Projects/App/Shook.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.kr.codesquad.boostcamp9.Shook 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Projects/App/Sources/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @main 4 | final class AppDelegate: UIResponder, UIApplicationDelegate { 5 | func application( 6 | _: UIApplication, 7 | didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? 8 | ) -> Bool { 9 | true 10 | } 11 | 12 | func application( 13 | _: UIApplication, 14 | configurationForConnecting connectingSceneSession: UISceneSession, 15 | options _: UIScene.ConnectionOptions 16 | ) -> UISceneConfiguration { 17 | UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 18 | } 19 | 20 | func application( 21 | _: UIApplication, 22 | didDiscardSceneSessions _: Set 23 | ) {} 24 | } 25 | -------------------------------------------------------------------------------- /Projects/App/Sources/DIContainer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class DIContainer { 4 | static let shared = DIContainer() 5 | 6 | private var dependencies: [String: Any] = [:] 7 | 8 | private init() {} 9 | 10 | func register(_ type: T.Type, dependency: T) { 11 | let key = String(describing: type) 12 | dependencies[key] = dependency 13 | } 14 | 15 | func resolve(_ type: T.Type) -> T { 16 | let key = String(describing: type) 17 | guard let dependency = dependencies[key] as? T else { 18 | fatalError("No registered factory for \(key)") 19 | } 20 | return dependency 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Projects/App/Sources/MockFetchChannelListUsecaseImpl.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import UIKit 3 | 4 | import LiveStationDomainInterface 5 | 6 | // MARK: - MockFetchChannelListUsecaseImpl 7 | 8 | struct MockFetchChannelListUsecaseImpl: FetchChannelListUsecase { 9 | func execute() -> AnyPublisher<[ChannelEntity], any Error> { 10 | let fetcher = MockChannelListFetcher() 11 | 12 | return Future<[ChannelEntity], Error> { promise in 13 | Task { 14 | let channels = await fetcher.fetch() 15 | promise(.success(channels)) 16 | } 17 | } 18 | .eraseToAnyPublisher() 19 | } 20 | } 21 | 22 | // MARK: - MockChannelListFetcher 23 | 24 | final class MockChannelListFetcher { 25 | enum Image { 26 | case ratio16x9 27 | case ratio4x3 28 | 29 | func fetch() async -> UIImage? { 30 | let size: (width: Int, height: Int) = switch self { 31 | case .ratio16x9: (1920, 1080) 32 | case .ratio4x3: (1440, 1080) 33 | } 34 | return await fetchImage(width: size.width, height: size.height) 35 | } 36 | 37 | private func fetchImage(width: Int, height: Int) async -> UIImage? { 38 | guard let url = URL(string: "https://picsum.photos/\(width)/\(height)") else { return nil } 39 | 40 | let data = await (try? URLSession.shared.data(from: url).0) ?? Data() 41 | return UIImage(data: data) 42 | } 43 | } 44 | 45 | func fetch() async -> [ChannelEntity] { 46 | let random = Int.random(in: 3 ... 7) 47 | var channels: [ChannelEntity] = [] 48 | 49 | for _ in 0 ..< random { 50 | let nameLength = Int.random(in: 4 ... 50) 51 | let name = String((0 ..< nameLength).map { _ in 52 | "가나다라마바사아자차카타파하".randomElement()! 53 | }) 54 | 55 | channels.append(ChannelEntity(id: UUID().uuidString, name: name)) 56 | } 57 | 58 | return channels 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Projects/App/Sources/Splash/EmptyViewModel.swift: -------------------------------------------------------------------------------- 1 | import BaseFeatureInterface 2 | 3 | public class EmptyViewModel: ViewModel { 4 | public struct Input {} 5 | public struct Output {} 6 | 7 | public func transform(input _: Input) -> Output { 8 | Output() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Projects/App/Sources/Splash/SplashGradientView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class SplashGradientView: UIView { 4 | private let gradientLayer = CAGradientLayer() 5 | 6 | private let colors: [[CGColor]] = [ 7 | [CGColor(red: 31 / 255, green: 52 / 255, blue: 55 / 255, alpha: 1), 8 | CGColor(red: 0 / 255, green: 0 / 255, blue: 0 / 255, alpha: 1)], 9 | 10 | [CGColor(red: 31 / 255, green: 52 / 255, blue: 55 / 255, alpha: 1), 11 | CGColor(red: 22 / 255, green: 23 / 255, blue: 31 / 255, alpha: 1)], 12 | 13 | [CGColor(red: 22 / 255, green: 23 / 255, blue: 31 / 255, alpha: 1), 14 | CGColor(red: 31 / 255, green: 52 / 255, blue: 55 / 255, alpha: 1)], 15 | 16 | [CGColor(red: 0 / 255, green: 0 / 255, blue: 0 / 255, alpha: 1), 17 | CGColor(red: 31 / 255, green: 52 / 255, blue: 55 / 255, alpha: 1)] 18 | ] 19 | 20 | override init(frame: CGRect) { 21 | super.init(frame: frame) 22 | setupGradientLayer() 23 | startDynamicColorAnimation() 24 | } 25 | 26 | required init?(coder: NSCoder) { 27 | super.init(coder: coder) 28 | setupGradientLayer() 29 | startDynamicColorAnimation() 30 | } 31 | 32 | override func layoutSubviews() { 33 | super.layoutSubviews() 34 | gradientLayer.frame = bounds 35 | } 36 | 37 | private func setupGradientLayer() { 38 | gradientLayer.colors = colors.first 39 | gradientLayer.startPoint = CGPoint(x: 0, y: 1) 40 | gradientLayer.endPoint = CGPoint(x: 1, y: 0) 41 | layer.insertSublayer(gradientLayer, at: 0) 42 | } 43 | 44 | private func startDynamicColorAnimation() { 45 | let colorAnimation = CAKeyframeAnimation(keyPath: "colors") 46 | colorAnimation.values = colors 47 | colorAnimation.keyTimes = [0.0, 0.33, 0.66, 1.0] 48 | colorAnimation.duration = 4.0 49 | colorAnimation.autoreverses = true 50 | colorAnimation.repeatCount = .infinity 51 | gradientLayer.add(colorAnimation, forKey: "dynamicColorChange") 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Projects/App/Tests/IOS08ShookTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | 4 | final class IOS08ShookTests: XCTestCase { 5 | func test_twoPlusTwo_isFour() { 6 | XCTAssertEqual(2 + 2, 4) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Projects/Domains/BaseDomain/Interface/Interface.swift: -------------------------------------------------------------------------------- 1 | // Empty 2 | -------------------------------------------------------------------------------- /Projects/Domains/BaseDomain/Project.swift: -------------------------------------------------------------------------------- 1 | import DependencyPlugin 2 | import ProjectDescription 3 | import ProjectDescriptionHelpers 4 | 5 | let project = Project.module( 6 | name: ModulePaths.Domain.BaseDomain.rawValue, 7 | targets: [ 8 | .interface(module: .domain(.BaseDomain)), 9 | .implements(module: .domain(.BaseDomain), dependencies: [ 10 | .domain(target: .BaseDomain, type: .interface), 11 | .module(target: .FastNetwork) 12 | ]), 13 | .testing(module: .domain(.BaseDomain), dependencies: [ 14 | .domain(target: .BaseDomain, type: .interface) 15 | ]), 16 | .tests(module: .domain(.BaseDomain), dependencies: [ 17 | .domain(target: .BaseDomain), 18 | .domain(target: .BaseDomain, type: .testing) 19 | ]) 20 | ] 21 | ) 22 | -------------------------------------------------------------------------------- /Projects/Domains/BaseDomain/Sources/BaseRepository.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | import FastNetwork 5 | 6 | // MARK: - BaseRepository 7 | 8 | open class BaseRepository { 9 | private let decoder: JSONDecoder = .init() 10 | private let client: NetworkClient 11 | 12 | public init() { 13 | var interceptors: [any Interceptor] = [] 14 | #if DEBUG 15 | interceptors.append(DefaultLoggingInterceptor()) 16 | #endif 17 | client = NetworkClient(interceptors: interceptors) 18 | } 19 | 20 | public final func request(_ endpoint: E, type: T.Type) -> AnyPublisher where T: Decodable { 21 | performRequest(endpoint) 22 | .map(\.data) 23 | .decode(type: type, decoder: decoder) 24 | .eraseToAnyPublisher() 25 | } 26 | } 27 | 28 | private extension BaseRepository { 29 | @discardableResult 30 | final func performRequest(_ endpoint: E) -> AnyPublisher { 31 | Deferred { 32 | Future { [weak self] promise in 33 | guard let self else { return } 34 | Task(priority: .background) { 35 | do { 36 | let result = try await self.client.request(endpoint) 37 | promise(.success(result)) 38 | } catch { 39 | promise(.failure(error)) 40 | } 41 | } 42 | } 43 | } 44 | .eraseToAnyPublisher() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Projects/Domains/BaseDomain/Sources/Utils/Secrets.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - KeyKind 4 | 5 | public enum KeyKind: String { 6 | case secretKey = "SECRET_KEY" 7 | case accessKey = "ACCESS_KEY" 8 | case cdnDomain = "CDN_DOMAIN" 9 | case profileID = "PROFILE_ID" 10 | case cdnInstanceNo = "CDN_INSTANCE_NO" 11 | case port = "PORT" 12 | case host = "HOST" 13 | } 14 | 15 | public func config(key: KeyKind) -> String { 16 | guard let secrets = Bundle.main.object(forInfoDictionaryKey: "SECRETS") as? [String: Any] else { 17 | return "" 18 | } 19 | return secrets[key.rawValue] as? String ?? "not found key" 20 | } 21 | -------------------------------------------------------------------------------- /Projects/Domains/BaseDomain/Sources/Utils/ServiceUrlType.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum ServiceUrlType: String { 4 | case general = "GENERAL" 5 | case timemachine = "TIMEMACHINE" 6 | case thumbnail = "THUMBNAIL" 7 | } 8 | -------------------------------------------------------------------------------- /Projects/Domains/BaseDomain/Testing/Testing.swift: -------------------------------------------------------------------------------- 1 | // Empty 2 | -------------------------------------------------------------------------------- /Projects/Domains/BaseDomain/Tests/BaseDomainTest.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class BaseDomainTests: XCTestCase { 4 | override func setUpWithError() throws {} 5 | 6 | override func tearDownWithError() throws {} 7 | 8 | func testExample() { 9 | XCTAssertEqual(1, 1) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Projects/Domains/BroadcastDomain/Interface/Entity/BroadcastInfoEntity.swift: -------------------------------------------------------------------------------- 1 | public struct BroadcastInfoEntity { 2 | public let id: String 3 | public let title: String 4 | public let owner: String 5 | public let description: String 6 | 7 | public init(id: String, title: String, owner: String, description: String) { 8 | self.id = id 9 | self.title = title 10 | self.owner = owner 11 | self.description = description 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Projects/Domains/BroadcastDomain/Interface/Repository/BroadcastRepository.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | public protocol BroadcastRepository { 5 | func makeBroadcast(id: String, title: String, owner: String, description: String) -> AnyPublisher 6 | func fetchAllBroadcast() -> AnyPublisher<[BroadcastInfoEntity], Error> 7 | func deleteBroadcast(id: String) -> AnyPublisher 8 | } 9 | -------------------------------------------------------------------------------- /Projects/Domains/BroadcastDomain/Interface/Usecase/DeleteBroadcastUsecase.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | public protocol DeleteBroadcastUsecase { 5 | func execute(id: String) -> AnyPublisher 6 | } 7 | -------------------------------------------------------------------------------- /Projects/Domains/BroadcastDomain/Interface/Usecase/FetchAllBroadcastUsecase.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | public protocol FetchAllBroadcastUsecase { 5 | func execute() -> AnyPublisher<[BroadcastInfoEntity], Error> 6 | } 7 | -------------------------------------------------------------------------------- /Projects/Domains/BroadcastDomain/Interface/Usecase/MakeBroadcastUsecase.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | public protocol MakeBroadcastUsecase { 5 | func execute(id: String, title: String, owner: String, description: String) -> AnyPublisher 6 | } 7 | -------------------------------------------------------------------------------- /Projects/Domains/BroadcastDomain/Project.swift: -------------------------------------------------------------------------------- 1 | import DependencyPlugin 2 | import ProjectDescription 3 | import ProjectDescriptionHelpers 4 | 5 | let project = Project.module( 6 | name: ModulePaths.Domain.BroadcastDomain.rawValue, 7 | targets: [ 8 | .interface(module: .domain(.BroadcastDomain)), 9 | .implements(module: .domain(.BroadcastDomain), dependencies: [ 10 | .domain(target: .BroadcastDomain, type: .interface), 11 | .domain(target: .BaseDomain) 12 | ]), 13 | .testing(module: .domain(.BroadcastDomain), dependencies: [ 14 | .domain(target: .BroadcastDomain, type: .interface) 15 | ]), 16 | .tests(module: .domain(.BroadcastDomain), dependencies: [ 17 | .domain(target: .BroadcastDomain), 18 | .domain(target: .BroadcastDomain, type: .testing) 19 | ]) 20 | ] 21 | ) 22 | -------------------------------------------------------------------------------- /Projects/Domains/BroadcastDomain/Sources/DTO/BaseDTO.swift: -------------------------------------------------------------------------------- 1 | struct BaseDTO: Decodable { 2 | let statusCode: Int32 3 | let message: String 4 | } 5 | -------------------------------------------------------------------------------- /Projects/Domains/BroadcastDomain/Sources/DTO/BroadcastDTO.swift: -------------------------------------------------------------------------------- 1 | struct BroadcastDTO: Codable { 2 | let id: String 3 | let title: String 4 | let owner: String 5 | let description: String 6 | } 7 | -------------------------------------------------------------------------------- /Projects/Domains/BroadcastDomain/Sources/Endpoint/BroadcastEndpoint.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | import BaseDomain 4 | import FastNetwork 5 | 6 | // MARK: - BroadcastEndpoint 7 | 8 | public enum BroadcastEndpoint { 9 | case make(id: String, title: String, owner: String, description: String) 10 | case fetchAll 11 | case delete(id: String) 12 | } 13 | 14 | // MARK: Endpoint 15 | 16 | extension BroadcastEndpoint: Endpoint { 17 | public var method: FastNetwork.HTTPMethod { 18 | switch self { 19 | case .make: .post 20 | case .fetchAll: .get 21 | case .delete: .delete 22 | } 23 | } 24 | 25 | public var header: [String: String]? { 26 | [ 27 | "Content-Type": "application/json" 28 | ] 29 | } 30 | 31 | public var scheme: String { 32 | "http" 33 | } 34 | 35 | public var host: String { 36 | config(key: .host) 37 | } 38 | 39 | public var port: Int? { 40 | Int(config(key: .port)) ?? 0 41 | } 42 | 43 | public var path: String { 44 | switch self { 45 | case .make: "/broadcast" 46 | case .fetchAll: "/broadcast/all" 47 | case let .delete(id): "/broadcast/delete/\(id)" 48 | } 49 | } 50 | 51 | public var requestTask: FastNetwork.RequestTask { 52 | switch self { 53 | case let .make(id, title, owner, description): .withObject( 54 | body: BroadcastDTO( 55 | id: id, 56 | title: title, 57 | owner: owner, 58 | description: description 59 | ) 60 | ) 61 | case .fetchAll: .empty 62 | case .delete: .empty 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Projects/Domains/BroadcastDomain/Sources/Repository/BroadcastRepositoryImpl.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | import BaseDomain 4 | import BroadcastDomainInterface 5 | 6 | public final class BroadcastRepositoryImpl: BaseRepository, BroadcastRepository { 7 | public func makeBroadcast(id: String, title: String, owner: String, description: String) -> AnyPublisher { 8 | request(.make(id: id, title: title, owner: owner, description: description), type: BaseDTO.self) 9 | .map { _ in () } 10 | .eraseToAnyPublisher() 11 | } 12 | 13 | public func fetchAllBroadcast() -> AnyPublisher<[BroadcastInfoEntity], any Error> { 14 | request(.fetchAll, type: [BroadcastDTO].self) 15 | .map { $0.map { BroadcastInfoEntity(id: $0.id, title: $0.title, owner: $0.owner, description: $0.description) } } 16 | .eraseToAnyPublisher() 17 | } 18 | 19 | public func deleteBroadcast(id: String) -> AnyPublisher { 20 | request(.delete(id: id), type: BaseDTO.self) 21 | .map { _ in () } 22 | .eraseToAnyPublisher() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Projects/Domains/BroadcastDomain/Sources/Usecase/DeleteBroadcastUsecaseImpl.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | import BroadcastDomainInterface 5 | 6 | public struct DeleteBroadcastUsecaseImpl: DeleteBroadcastUsecase { 7 | private let repository: any BroadcastRepository 8 | 9 | public init(repository: any BroadcastRepository) { 10 | self.repository = repository 11 | } 12 | 13 | public func execute(id: String) -> AnyPublisher { 14 | repository.deleteBroadcast(id: id) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Projects/Domains/BroadcastDomain/Sources/Usecase/FetchAllBroadcastUsecaseImpl.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | import BroadcastDomainInterface 5 | 6 | public struct FetchAllBroadcastUsecaseImpl: FetchAllBroadcastUsecase { 7 | private let repository: any BroadcastRepository 8 | 9 | public init(repository: any BroadcastRepository) { 10 | self.repository = repository 11 | } 12 | 13 | public func execute() -> AnyPublisher<[BroadcastInfoEntity], any Error> { 14 | repository.fetchAllBroadcast() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Projects/Domains/BroadcastDomain/Sources/Usecase/MakeBroadcastUsecaseImpl.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | import BroadcastDomainInterface 5 | 6 | public struct MakeBroadcastUsecaseImpl: MakeBroadcastUsecase { 7 | private let repository: any BroadcastRepository 8 | 9 | public init(repository: any BroadcastRepository) { 10 | self.repository = repository 11 | } 12 | 13 | public func execute(id: String, title: String, owner: String, description: String) -> AnyPublisher { 14 | repository.makeBroadcast(id: id, title: title, owner: owner, description: description) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Projects/Domains/BroadcastDomain/Testing/Testing.swift: -------------------------------------------------------------------------------- 1 | // Empty 2 | -------------------------------------------------------------------------------- /Projects/Domains/BroadcastDomain/Tests/BroadcastDomainTest.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class BroadcastDomainTests: XCTestCase { 4 | override func setUpWithError() throws {} 5 | 6 | override func tearDownWithError() throws {} 7 | 8 | func testExample() { 9 | XCTAssertEqual(1, 1) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Projects/Domains/ChattingDomain/Interface/Repository/ChatRepository.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | public protocol ChatRepository { 5 | func makeChatRoom(_ id: String) -> AnyPublisher 6 | func deleteChatRoom(_ id: String) -> AnyPublisher 7 | } 8 | -------------------------------------------------------------------------------- /Projects/Domains/ChattingDomain/Interface/UseCase/DeleteChatRoomUseCase.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | public protocol DeleteChatRoomUseCase { 5 | func execute(id: String) -> AnyPublisher 6 | } 7 | -------------------------------------------------------------------------------- /Projects/Domains/ChattingDomain/Interface/UseCase/MakeChatRoomUseCase.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | public protocol MakeChatRoomUseCase { 5 | func execute(id: String) -> AnyPublisher 6 | } 7 | -------------------------------------------------------------------------------- /Projects/Domains/ChattingDomain/Sources/DTO/Request/MakeRoomRequestDTO.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct MakeRoomRequestDTO: Encodable { 4 | let id: String 5 | } 6 | -------------------------------------------------------------------------------- /Projects/Domains/ChattingDomain/Sources/DTO/Response/BaseChatDTO.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct BaseChatDTO: Decodable { 4 | let message: String 5 | } 6 | -------------------------------------------------------------------------------- /Projects/Domains/ChattingDomain/Sources/Endpoint/ChatEndpoint.swift: -------------------------------------------------------------------------------- 1 | import CommonCrypto 2 | import Foundation 3 | 4 | import BaseDomain 5 | import NetworkModule 6 | 7 | // MARK: - ChatEndpoint 8 | 9 | public enum ChatEndpoint { 10 | case makeRoom(String) 11 | case deleteRoom(String) 12 | } 13 | 14 | // MARK: Endpoint 15 | 16 | extension ChatEndpoint: Endpoint { 17 | public var method: NetworkModule.HTTPMethod { 18 | switch self { 19 | case .makeRoom: 20 | .post 21 | 22 | case .deleteRoom: 23 | .delete 24 | } 25 | } 26 | 27 | public var header: [String: String]? { 28 | [ 29 | "Content-Type": "application/json", 30 | ] 31 | } 32 | 33 | public var scheme: String { 34 | "http" 35 | } 36 | 37 | public var host: String { 38 | config(key: .host) 39 | } 40 | 41 | public var port: Int? { 42 | Int(config(key: .port)) ?? 0 43 | } 44 | 45 | public var path: String { 46 | switch self { 47 | case .makeRoom: 48 | "/chat" 49 | 50 | case let .deleteRoom(id): 51 | "/chat/delete/\(id)" 52 | } 53 | } 54 | 55 | public var requestTask: NetworkModule.RequestTask { 56 | switch self { 57 | case let .makeRoom(id): 58 | .withObject(body: MakeRoomRequestDTO(id: id)) 59 | 60 | case .deleteRoom: 61 | .empty 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Projects/Domains/ChattingDomain/Sources/Repository/ChatRepositoryImpl.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | import BaseDomain 4 | import ChattingDomainInterface 5 | 6 | public final class ChatRepositoryImpl: BaseRepository, ChatRepository { 7 | public func makeChatRoom(_ id: String) -> AnyPublisher { 8 | request(.makeRoom(id), type: BaseChatDTO.self) 9 | .map { _ in () } 10 | .eraseToAnyPublisher() 11 | } 12 | 13 | public func deleteChatRoom(_ id: String) -> AnyPublisher { 14 | request(.deleteRoom(id), type: BaseChatDTO.self) 15 | .map { _ in () } 16 | .eraseToAnyPublisher() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Projects/Domains/ChattingDomain/Sources/Usecase/DeleteChatRoomUseCaseImpl.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | import ChattingDomainInterface 5 | 6 | public struct DeleteChatRoomUseCaseImpl: DeleteChatRoomUseCase { 7 | private let repository: any ChatRepository 8 | 9 | public init(repository: any ChatRepository) { 10 | self.repository = repository 11 | } 12 | 13 | public func execute(id: String) -> AnyPublisher { 14 | repository.deleteChatRoom(id) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Projects/Domains/ChattingDomain/Sources/Usecase/MakeChatRoomUseCaseImpl.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | import ChattingDomainInterface 5 | 6 | public struct MakeChatRoomUseCaseImpl: MakeChatRoomUseCase { 7 | private let repository: any ChatRepository 8 | 9 | public init(repository: any ChatRepository) { 10 | self.repository = repository 11 | } 12 | 13 | public func execute(id: String) -> AnyPublisher { 14 | repository.makeChatRoom(id) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Projects/Domains/ChattingDomain/Testing/Testing.swift: -------------------------------------------------------------------------------- 1 | // Empty 2 | -------------------------------------------------------------------------------- /Projects/Domains/ChattingDomain/Tests/ChattingDomainTest.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class ChattingDomainTests: XCTestCase { 4 | override func setUpWithError() throws {} 5 | 6 | override func tearDownWithError() throws {} 7 | 8 | func testExample() { 9 | XCTAssertEqual(1, 1) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Projects/Domains/LiveStationDomain/Interface/Entity/BroadcastEntity.swift: -------------------------------------------------------------------------------- 1 | public struct BroadcastEntity { 2 | public let name: String 3 | public let urlString: String 4 | 5 | public init(name: String, urlString: String) { 6 | self.name = name 7 | self.urlString = urlString 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Projects/Domains/LiveStationDomain/Interface/Entity/ChannelEntity.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public struct ChannelEntity { 4 | public let id: String 5 | public let name: String 6 | public var imageURLString: String 7 | 8 | public init(id: String, name: String) { 9 | self.id = id 10 | self.name = name 11 | imageURLString = "" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Projects/Domains/LiveStationDomain/Interface/Entity/ChannelInfoEntity.swift: -------------------------------------------------------------------------------- 1 | public struct ChannelInfoEntity { 2 | public let id: String 3 | public let name: String 4 | public let streamKey: String 5 | public let rtmpUrl: String 6 | 7 | public init(id: String, name: String, streamKey: String, rtmpUrl: String) { 8 | self.id = id 9 | self.name = name 10 | self.streamKey = streamKey 11 | self.rtmpUrl = rtmpUrl 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Projects/Domains/LiveStationDomain/Interface/Entity/VideoEntity.swift: -------------------------------------------------------------------------------- 1 | public struct VideoEntity { 2 | public let name: String 3 | public let videoURLString: String 4 | 5 | public init(name: String, videoURLString: String) { 6 | self.name = name 7 | self.videoURLString = videoURLString 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Projects/Domains/LiveStationDomain/Interface/Repository/LiveStationRepository.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | public protocol LiveStationRepository { 4 | func fetchChannelList() -> AnyPublisher<[ChannelEntity], Error> 5 | func fetchThumbnail(channelId: String) -> AnyPublisher 6 | func fetchBroadcast(channelId: String) -> AnyPublisher<[VideoEntity], any Error> 7 | func createChannel(name: String) -> AnyPublisher 8 | func deleteChannel(id: String) -> AnyPublisher 9 | func fetchChannelInfo(id: String) -> AnyPublisher 10 | } 11 | -------------------------------------------------------------------------------- /Projects/Domains/LiveStationDomain/Interface/UseCase/CreateChannelUsecase.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | public protocol CreateChannelUsecase { 4 | func execute(name: String) -> AnyPublisher 5 | } 6 | -------------------------------------------------------------------------------- /Projects/Domains/LiveStationDomain/Interface/UseCase/DeleteChannelUsecase.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | public protocol DeleteChannelUsecase { 4 | func execute(channelID: String) -> AnyPublisher 5 | } 6 | -------------------------------------------------------------------------------- /Projects/Domains/LiveStationDomain/Interface/UseCase/FetchChannelInfoUsecase.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | public protocol FetchChannelInfoUsecase { 4 | func execute(channelID: String) -> AnyPublisher 5 | } 6 | -------------------------------------------------------------------------------- /Projects/Domains/LiveStationDomain/Interface/UseCase/FetchChannelListUsecase.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | public protocol FetchChannelListUsecase { 4 | func execute() -> AnyPublisher<[ChannelEntity], Error> 5 | } 6 | -------------------------------------------------------------------------------- /Projects/Domains/LiveStationDomain/Interface/UseCase/FetchVideoListUsecase.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | public protocol FetchVideoListUsecase { 4 | func execute(channelID: String) -> AnyPublisher<[VideoEntity], Error> 5 | } 6 | -------------------------------------------------------------------------------- /Projects/Domains/LiveStationDomain/Project.swift: -------------------------------------------------------------------------------- 1 | import DependencyPlugin 2 | import ProjectDescription 3 | import ProjectDescriptionHelpers 4 | 5 | let project = Project.module( 6 | name: ModulePaths.Domain.LiveStationDomain.rawValue, 7 | targets: [ 8 | .interface(module: .domain(.LiveStationDomain)), 9 | .implements(module: .domain(.LiveStationDomain), dependencies: [ 10 | .domain(target: .LiveStationDomain, type: .interface), 11 | .domain(target: .BaseDomain) 12 | ]), 13 | .testing(module: .domain(.LiveStationDomain), dependencies: [ 14 | .domain(target: .LiveStationDomain, type: .interface) 15 | ]) 16 | ] 17 | ) 18 | -------------------------------------------------------------------------------- /Projects/Domains/LiveStationDomain/Sources/DTO/Response/BroadcastResponseDTO.swift: -------------------------------------------------------------------------------- 1 | import LiveStationDomainInterface 2 | 3 | // MARK: - BroadcastResponseDTO 4 | 5 | public struct BroadcastResponseDTO: Decodable { 6 | let content: [BroadcastResponse] 7 | } 8 | 9 | // MARK: - BroadcastResponse 10 | 11 | public struct BroadcastResponse: Decodable { 12 | let name, url, resolution, videoBitrate, audioBitrate: String 13 | } 14 | 15 | public extension BroadcastResponse { 16 | func toDomain() -> BroadcastEntity { 17 | BroadcastEntity(name: name, urlString: url) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Projects/Domains/LiveStationDomain/Sources/DTO/Response/ChannelInfoResponseDTO.swift: -------------------------------------------------------------------------------- 1 | import LiveStationDomainInterface 2 | 3 | struct ChannelInfoResponseDTO: Decodable { 4 | struct ContentResponseDTO: Decodable { 5 | let channelId: String 6 | let channelName: String 7 | let streamKey: String 8 | let publishUrl: String 9 | } 10 | 11 | let content: ContentResponseDTO 12 | 13 | var toDomain: ChannelInfoEntity { 14 | ChannelInfoEntity( 15 | id: content.channelId, 16 | name: content.channelName, 17 | streamKey: content.streamKey, 18 | rtmpUrl: content.publishUrl 19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Projects/Domains/LiveStationDomain/Sources/DTO/Response/ChannelListResponseDTO.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | import LiveStationDomainInterface 4 | 5 | public struct ChannelListResponseDTO: Decodable { 6 | let content: [ChannelContentResponseDTO] 7 | } 8 | -------------------------------------------------------------------------------- /Projects/Domains/LiveStationDomain/Sources/DTO/Response/ChannelResponseDTO.swift: -------------------------------------------------------------------------------- 1 | import LiveStationDomainInterface 2 | 3 | // MARK: - ChannelResponseDTO 4 | 5 | public struct ChannelResponseDTO: Decodable { 6 | let content: ChannelContentResponseDTO 7 | } 8 | 9 | // MARK: - ChannelContentResponseDTO 10 | 11 | public struct ChannelContentResponseDTO: Decodable { 12 | let channelId: String 13 | let channelName: String 14 | } 15 | 16 | public extension ChannelContentResponseDTO { 17 | func toDomain() -> ChannelEntity { 18 | ChannelEntity(id: channelId, name: channelName) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Projects/Domains/LiveStationDomain/Sources/DTO/Response/ThumbnailResponseDTO.swift: -------------------------------------------------------------------------------- 1 | // MARK: - ThumbnailResponseDTO 2 | 3 | public struct ThumbnailResponseDTO: Decodable { 4 | let content: [ThumbnailResponse] 5 | } 6 | 7 | // MARK: - ThumbnailResponse 8 | 9 | public struct ThumbnailResponse: Decodable { 10 | let name, url: String 11 | } 12 | 13 | public extension ThumbnailResponse { 14 | func toDomain() -> String { 15 | url 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Projects/Domains/LiveStationDomain/Sources/DTO/Response/VideoListResponseDTO.swift: -------------------------------------------------------------------------------- 1 | import LiveStationDomainInterface 2 | 3 | // MARK: - VideoListResponseDTO 4 | 5 | public struct VideoListResponseDTO: Decodable { 6 | let content: [VideoResponse] 7 | } 8 | 9 | // MARK: - VideoResponse 10 | 11 | public struct VideoResponse: Decodable { 12 | let name, url: String 13 | } 14 | 15 | public extension VideoResponse { 16 | func toDomain() -> VideoEntity { 17 | VideoEntity(name: name, videoURLString: url) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Projects/Domains/LiveStationDomain/Sources/Repository/LiveStationRepositoryImpl.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | import BaseDomain 4 | import LiveStationDomainInterface 5 | 6 | public final class LiveStationRepositoryImpl: BaseRepository, LiveStationRepository { 7 | public func fetchChannelList() -> AnyPublisher<[ChannelEntity], any Error> { 8 | request(.fetchChannelList, type: ChannelListResponseDTO.self) 9 | .map { $0.content.map { $0.toDomain() }} 10 | .eraseToAnyPublisher() 11 | } 12 | 13 | public func fetchThumbnail(channelId: String) -> AnyPublisher { 14 | request(.fetchThumbnail(channelId: channelId), type: ThumbnailResponseDTO.self) 15 | .compactMap { $0.content.first?.toDomain() } 16 | .eraseToAnyPublisher() 17 | } 18 | 19 | public func fetchBroadcast(channelId: String) -> AnyPublisher<[VideoEntity], any Error> { 20 | request(.receiveBroadcast(channelId: channelId), type: VideoListResponseDTO.self) 21 | .map { $0.content.map { $0.toDomain() }} 22 | .eraseToAnyPublisher() 23 | } 24 | 25 | public func createChannel(name: String) -> AnyPublisher { 26 | request(.makeChannel(channelName: name), type: ChannelResponseDTO.self) 27 | .map { $0.content.toDomain() } 28 | .eraseToAnyPublisher() 29 | } 30 | 31 | public func deleteChannel(id: String) -> AnyPublisher { 32 | request(.deleteChannel(channelId: id), type: ChannelResponseDTO.self) 33 | .map { $0.content.toDomain() } 34 | .eraseToAnyPublisher() 35 | } 36 | 37 | public func fetchChannelInfo(id: String) -> AnyPublisher { 38 | request(.fetchChannelInfo(channelId: id), type: ChannelInfoResponseDTO.self) 39 | .map(\.toDomain) 40 | .eraseToAnyPublisher() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Projects/Domains/LiveStationDomain/Sources/UseCase/CreateChannelUsecaseImpl.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | import LiveStationDomainInterface 4 | 5 | public struct CreateChannelUsecaseImpl: CreateChannelUsecase { 6 | private let repository: LiveStationRepository 7 | 8 | public init(repository: LiveStationRepository) { 9 | self.repository = repository 10 | } 11 | 12 | public func execute(name: String) -> AnyPublisher { 13 | repository.createChannel(name: name) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Projects/Domains/LiveStationDomain/Sources/UseCase/DeleteChannelUsecaseImpl.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | import LiveStationDomainInterface 4 | 5 | public struct DeleteChannelUsecaseImpl: DeleteChannelUsecase { 6 | private let repository: LiveStationRepository 7 | 8 | public init(repository: LiveStationRepository) { 9 | self.repository = repository 10 | } 11 | 12 | public func execute(channelID: String) -> AnyPublisher { 13 | repository.deleteChannel(id: channelID) 14 | .map { _ in () } 15 | .eraseToAnyPublisher() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Projects/Domains/LiveStationDomain/Sources/UseCase/FetchChannelInfoUsecaseImpl.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | import LiveStationDomainInterface 4 | 5 | public struct FetchChannelInfoUsecaseImpl: FetchChannelInfoUsecase { 6 | private let repository: any LiveStationRepository 7 | 8 | public init(repository: any LiveStationRepository) { 9 | self.repository = repository 10 | } 11 | 12 | public func execute(channelID: String) -> AnyPublisher { 13 | repository.fetchChannelInfo(id: channelID) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Projects/Domains/LiveStationDomain/Sources/UseCase/FetchChannelListUsecaseImpl.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | import LiveStationDomainInterface 4 | 5 | public struct FetchChannelListUsecaseImpl: FetchChannelListUsecase { 6 | private let repository: any LiveStationRepository 7 | 8 | public init(repository: any LiveStationRepository) { 9 | self.repository = repository 10 | } 11 | 12 | public func execute() -> AnyPublisher<[ChannelEntity], any Error> { 13 | repository.fetchChannelList() 14 | .flatMap(processChannelEntities) 15 | .eraseToAnyPublisher() 16 | } 17 | 18 | private func processChannelEntities(_ channelEntities: [ChannelEntity]) -> AnyPublisher<[ChannelEntity], any Error> { 19 | let channels = channelEntities.map { channel in 20 | repository.fetchThumbnail(channelId: channel.id) 21 | .map { thumbnail -> ChannelEntity in 22 | var updatedChannel = channel 23 | updatedChannel.imageURLString = thumbnail 24 | return updatedChannel 25 | } 26 | .eraseToAnyPublisher() 27 | } 28 | 29 | return Publishers.MergeMany(channels) 30 | .collect() 31 | .eraseToAnyPublisher() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Projects/Domains/LiveStationDomain/Sources/UseCase/FetchVideoListUsecaseImpl.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | import LiveStationDomainInterface 4 | 5 | public struct FetchVideoListUsecaseImpl: FetchVideoListUsecase { 6 | private let repository: any LiveStationRepository 7 | 8 | public init(repository: any LiveStationRepository) { 9 | self.repository = repository 10 | } 11 | 12 | public func execute(channelID: String) -> AnyPublisher<[VideoEntity], any Error> { 13 | repository.fetchBroadcast(channelId: channelID) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Projects/Domains/LiveStationDomain/Testing/Testing.swift: -------------------------------------------------------------------------------- 1 | // Empty 2 | -------------------------------------------------------------------------------- /Projects/Features/AuthFeature/Demo/Resources/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Projects/Features/AuthFeature/Demo/Sources/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | import AuthFeature 4 | 5 | @main 6 | final class AppDelegate: UIResponder, UIApplicationDelegate { 7 | var window: UIWindow? 8 | 9 | func application( 10 | _: UIApplication, 11 | didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil 12 | ) -> Bool { 13 | window = UIWindow(frame: UIScreen.main.bounds) 14 | let mockCreateChannelUsecase = MockCreateChannelUsecaseImpl() 15 | let viewModel = SignUpViewModel(createChannelUsecase: mockCreateChannelUsecase) 16 | let viewController = SignUpViewController(viewModel: viewModel) 17 | window?.rootViewController = viewController 18 | window?.makeKeyAndVisible() 19 | 20 | return true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Projects/Features/AuthFeature/Demo/Sources/MockCreateChannelUsecaseImpl.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | import LiveStationDomainInterface 4 | 5 | final class MockCreateChannelUsecaseImpl: CreateChannelUsecase { 6 | func execute(name: String) -> AnyPublisher { 7 | Future { promise in 8 | promise(.success(ChannelEntity(id: "", name: name))) 9 | }.eraseToAnyPublisher() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Projects/Features/AuthFeature/Interface/Interface.swift: -------------------------------------------------------------------------------- 1 | // Empty 2 | -------------------------------------------------------------------------------- /Projects/Features/AuthFeature/Project.swift: -------------------------------------------------------------------------------- 1 | import DependencyPlugin 2 | import ProjectDescription 3 | import ProjectDescriptionHelpers 4 | 5 | let project = Project.module( 6 | name: ModulePaths.Feature.AuthFeature.rawValue, 7 | targets: [ 8 | .interface(module: .feature(.AuthFeature)), 9 | .implements(module: .feature(.AuthFeature), dependencies: [ 10 | .feature(target: .AuthFeature, type: .interface), 11 | .feature(target: .BaseFeature) 12 | ]), 13 | .tests(module: .feature(.AuthFeature), dependencies: [ 14 | .feature(target: .AuthFeature) 15 | ]), 16 | .demo(module: .feature(.AuthFeature), dependencies: [ 17 | .feature(target: .AuthFeature) 18 | ]) 19 | ] 20 | ) 21 | -------------------------------------------------------------------------------- /Projects/Features/AuthFeature/Sources/SignUpGradientView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class SignUpGradientView: UIView { 4 | private let gradientLayer = CAGradientLayer() 5 | 6 | private let colors: [[CGColor]] = [ 7 | [CGColor(red: 31 / 255, green: 52 / 255, blue: 55 / 255, alpha: 1), 8 | CGColor(red: 0 / 255, green: 0 / 255, blue: 0 / 255, alpha: 1)], 9 | 10 | [CGColor(red: 31 / 255, green: 52 / 255, blue: 55 / 255, alpha: 1), 11 | CGColor(red: 22 / 255, green: 23 / 255, blue: 31 / 255, alpha: 1)], 12 | 13 | [CGColor(red: 22 / 255, green: 23 / 255, blue: 31 / 255, alpha: 1), 14 | CGColor(red: 31 / 255, green: 52 / 255, blue: 55 / 255, alpha: 1)], 15 | 16 | [CGColor(red: 0 / 255, green: 0 / 255, blue: 0 / 255, alpha: 1), 17 | CGColor(red: 31 / 255, green: 52 / 255, blue: 55 / 255, alpha: 1)] 18 | ] 19 | 20 | override init(frame: CGRect) { 21 | super.init(frame: frame) 22 | setupGradientLayer() 23 | startDynamicColorAnimation() 24 | } 25 | 26 | required init?(coder: NSCoder) { 27 | super.init(coder: coder) 28 | setupGradientLayer() 29 | startDynamicColorAnimation() 30 | } 31 | 32 | override func layoutSubviews() { 33 | super.layoutSubviews() 34 | gradientLayer.frame = bounds 35 | } 36 | 37 | private func setupGradientLayer() { 38 | gradientLayer.colors = colors.first 39 | gradientLayer.startPoint = CGPoint(x: 0, y: 1) 40 | gradientLayer.endPoint = CGPoint(x: 1, y: 0) 41 | layer.insertSublayer(gradientLayer, at: 0) 42 | } 43 | 44 | private func startDynamicColorAnimation() { 45 | let colorAnimation = CAKeyframeAnimation(keyPath: "colors") 46 | colorAnimation.values = colors 47 | colorAnimation.keyTimes = [0.0, 0.33, 0.66, 1.0] 48 | colorAnimation.duration = 4.0 49 | colorAnimation.autoreverses = true 50 | colorAnimation.repeatCount = .infinity 51 | gradientLayer.add(colorAnimation, forKey: "dynamicColorChange") 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Projects/Features/AuthFeature/Sources/SignUpViewModel.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | import BaseFeatureInterface 5 | import LiveStationDomainInterface 6 | 7 | public class SignUpViewModel: ViewModel { 8 | public struct Input { 9 | let didWriteUserName: PassthroughSubject = .init() 10 | let saveUserName: PassthroughSubject = .init() 11 | } 12 | 13 | public struct Output { 14 | let isValid: PassthroughSubject = .init() 15 | let isSaved: PassthroughSubject = .init() 16 | } 17 | 18 | private let output = Output() 19 | private var cancellables = Set() 20 | 21 | private let createChannelUsecase: any CreateChannelUsecase 22 | 23 | public func transform(input: Input) -> Output { 24 | input.didWriteUserName 25 | .sink { [weak self] name in 26 | if let isValid = self?.validate(with: name) { 27 | self?.output.isValid.send(isValid) 28 | } 29 | } 30 | .store(in: &cancellables) 31 | 32 | input.saveUserName 33 | .debounce(for: .seconds(1), scheduler: RunLoop.main) 34 | .sink { [weak self] name in 35 | self?.save(for: name) 36 | } 37 | .store(in: &cancellables) 38 | 39 | return output 40 | } 41 | 42 | public init(createChannelUsecase: CreateChannelUsecase) { 43 | self.createChannelUsecase = createChannelUsecase 44 | } 45 | 46 | private func validate(with name: String?) -> Bool { 47 | guard let name else { return false } 48 | return name.count >= 2 && name.count <= 10 && name.allSatisfy { $0.isLetter || $0.isNumber } 49 | } 50 | 51 | private func save(for name: String?) { 52 | guard let name else { return } 53 | UserDefaults.standard.set(name, forKey: "USER_NAME") 54 | 55 | let savedName = UserDefaults.standard.string(forKey: "USER_NAME") 56 | 57 | createChannelUsecase.execute(name: "Guest") 58 | .sink { _ in 59 | } receiveValue: { [weak self] channelEntity in 60 | UserDefaults.standard.set(channelEntity.id, forKey: "CHANNEL_ID") 61 | let savedID = UserDefaults.standard.string(forKey: "CHANNEL_ID") 62 | 63 | self?.output.isSaved.send(savedName == name && savedID != nil) 64 | } 65 | .store(in: &cancellables) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Projects/Features/AuthFeature/Tests/AuthFeatureTest.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class AuthFeatureTests: XCTestCase { 4 | override func setUpWithError() throws {} 5 | 6 | override func tearDownWithError() throws {} 7 | 8 | func testExample() { 9 | XCTAssertEqual(1, 1) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Projects/Features/BaseFeature/Demo/Resources/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Projects/Features/BaseFeature/Demo/Sources/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @main 4 | final class AppDelegate: UIResponder, UIApplicationDelegate { 5 | var window: UIWindow? 6 | 7 | func application( 8 | _: UIApplication, 9 | didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil 10 | ) -> Bool { 11 | window = UIWindow(frame: UIScreen.main.bounds) 12 | let viewController = UIViewController() 13 | viewController.view.backgroundColor = .yellow 14 | window?.rootViewController = viewController 15 | window?.makeKeyAndVisible() 16 | 17 | return true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Projects/Features/BaseFeature/Interface/ViewLifeCycle.swift: -------------------------------------------------------------------------------- 1 | /// 뷰 설정, 레이아웃 및 스타일과 관련된 생명주기 메서드를 정의하는 프로토콜입니다. 2 | /// 3 | /// 이 프로토콜을 사용하여 프로젝트 내에서 뷰 설정 및 관리를 표준화할 수 있습니다. 4 | public protocol ViewLifeCycle { 5 | /// 뷰를 초기화하는 메서드입니다. 6 | /// 이 메서드에서 뷰를 생성하고 기본 설정을 추가하세요. 7 | func setupViews() 8 | 9 | /// 레이아웃을 초기화하는 메서드입니다. 10 | /// 이 메서드에서 제약 조건을 정의하거나 뷰의 위치를 설정하세요. 11 | func setupLayouts() 12 | 13 | /// 레이아웃을 업데이트하는 메서드입니다. 14 | /// 레이아웃 요구사항이 변경되었을 때 이 메서드를 호출하여 변경 사항을 반영하세요. 15 | func updateLayouts() 16 | 17 | /// 스타일을 초기화하는 메서드입니다. 18 | /// 이 메서드에서 폰트, 색상, 테마 등 스타일과 관련된 속성을 설정하세요. 19 | func setupStyles() 20 | 21 | /// 스타일을 업데이트하는 메서드입니다. 22 | /// 테마나 외관 설정이 변경되었을 때 이 메서드를 호출하여 변경 사항을 반영하세요. 23 | func updateStyles() 24 | 25 | /// 액션을 초기화하는 메서드입니다. 26 | /// 이 메서드에서 버튼 클릭 이벤트, 제스처, 사용자 상호작용에 대한 동작을 설정하세요. 27 | func setupActions() 28 | } 29 | -------------------------------------------------------------------------------- /Projects/Features/BaseFeature/Interface/ViewModel.swift: -------------------------------------------------------------------------------- 1 | public protocol ViewModel { 2 | associatedtype Input 3 | associatedtype Output 4 | 5 | func transform(input: Input) -> Output 6 | } 7 | -------------------------------------------------------------------------------- /Projects/Features/BaseFeature/Project.swift: -------------------------------------------------------------------------------- 1 | import DependencyPlugin 2 | import ProjectDescription 3 | import ProjectDescriptionHelpers 4 | 5 | let project = Project.module( 6 | name: ModulePaths.Feature.BaseFeature.rawValue, 7 | targets: [ 8 | .interface(module: .feature(.BaseFeature)), 9 | .implements(module: .feature(.BaseFeature), dependencies: [ 10 | .feature(target: .BaseFeature, type: .interface), 11 | .userInterface(target: .DesignSystem), 12 | .module(target: .ThirdPartyLibModule) 13 | ]), 14 | .testing(module: .feature(.BaseFeature), dependencies: [ 15 | .feature(target: .BaseFeature, type: .interface) 16 | ]), 17 | .tests(module: .feature(.BaseFeature), dependencies: [ 18 | .feature(target: .BaseFeature), 19 | .feature(target: .BaseFeature, type: .testing) 20 | ]), 21 | .demo(module: .feature(.BaseFeature), dependencies: [ 22 | .feature(target: .BaseFeature), 23 | .feature(target: .BaseFeature, type: .testing) 24 | ]) 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /Projects/Features/BaseFeature/Sources/BaseCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | import BaseFeatureInterface 4 | 5 | open class BaseCollectionViewCell: UICollectionViewCell, ViewLifeCycle { 6 | public static var identifier: String { 7 | String(describing: Self.self) 8 | } 9 | 10 | override public init(frame: CGRect) { 11 | super.init(frame: frame) 12 | setupViews() 13 | setupStyles() 14 | setupLayouts() 15 | setupActions() 16 | } 17 | 18 | public required init?(coder: NSCoder) { 19 | super.init(coder: coder) 20 | setupViews() 21 | setupStyles() 22 | setupLayouts() 23 | setupActions() 24 | } 25 | 26 | // MARK: - View Life Cycle 27 | 28 | open func setupViews() {} 29 | 30 | open func setupStyles() {} 31 | 32 | open func updateStyles() {} 33 | 34 | open func setupLayouts() {} 35 | 36 | open func updateLayouts() {} 37 | 38 | open func setupActions() {} 39 | } 40 | -------------------------------------------------------------------------------- /Projects/Features/BaseFeature/Sources/BaseNavigationController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | open class BaseNavigationController: UINavigationController { 4 | override open var supportedInterfaceOrientations: UIInterfaceOrientationMask { 5 | topViewController?.supportedInterfaceOrientations ?? .portrait 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Projects/Features/BaseFeature/Sources/BaseTableViewCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | import BaseFeatureInterface 4 | 5 | open class BaseTableViewCell: UITableViewCell, ViewLifeCycle { 6 | public static var identifier: String { 7 | String(describing: Self.self) 8 | } 9 | 10 | override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 11 | super.init(style: style, reuseIdentifier: reuseIdentifier) 12 | setupViews() 13 | setupStyles() 14 | setupLayouts() 15 | setupActions() 16 | } 17 | 18 | public required init?(coder: NSCoder) { 19 | super.init(coder: coder) 20 | setupViews() 21 | setupStyles() 22 | setupLayouts() 23 | setupActions() 24 | } 25 | 26 | // MARK: - View Life Cycle 27 | 28 | open func setupViews() {} 29 | 30 | open func setupLayouts() {} 31 | 32 | open func updateLayouts() {} 33 | 34 | open func setupStyles() {} 35 | 36 | open func updateStyles() {} 37 | 38 | open func setupActions() {} 39 | } 40 | -------------------------------------------------------------------------------- /Projects/Features/BaseFeature/Sources/BaseView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | import BaseFeatureInterface 4 | 5 | open class BaseView: UIView, ViewLifeCycle { 6 | public init() { 7 | super.init(frame: .zero) 8 | setupViews() 9 | setupStyles() 10 | setupLayouts() 11 | setupActions() 12 | } 13 | 14 | override public init(frame: CGRect) { 15 | super.init(frame: frame) 16 | setupViews() 17 | setupStyles() 18 | setupLayouts() 19 | setupActions() 20 | } 21 | 22 | public required init?(coder: NSCoder) { 23 | super.init(coder: coder) 24 | setupViews() 25 | setupStyles() 26 | setupLayouts() 27 | setupActions() 28 | } 29 | 30 | // MARK: - View Life Cycle 31 | 32 | open func setupViews() {} 33 | 34 | open func setupStyles() {} 35 | 36 | open func updateStyles() {} 37 | 38 | open func setupLayouts() {} 39 | 40 | open func updateLayouts() {} 41 | 42 | open func setupActions() {} 43 | } 44 | -------------------------------------------------------------------------------- /Projects/Features/BaseFeature/Sources/BaseViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | import BaseFeatureInterface 4 | 5 | open class BaseViewController: UIViewController, ViewLifeCycle { 6 | public var viewModel: VM 7 | 8 | public init(viewModel: VM) { 9 | self.viewModel = viewModel 10 | super.init(nibName: nil, bundle: nil) 11 | } 12 | 13 | @available(*, unavailable) 14 | public required init?(coder _: NSCoder) { 15 | fatalError("init(coder:) has not been implemented") 16 | } 17 | 18 | override open func viewDidLoad() { 19 | super.viewDidLoad() 20 | setupViews() 21 | setupStyles() 22 | setupLayouts() 23 | setupActions() 24 | setupBind() 25 | } 26 | 27 | override open var supportedInterfaceOrientations: UIInterfaceOrientationMask { 28 | .portrait 29 | } 30 | 31 | // MARK: - View Life Cycle 32 | 33 | open func setupBind() {} 34 | 35 | open func setupViews() {} 36 | 37 | open func setupStyles() {} 38 | 39 | open func updateStyles() {} 40 | 41 | open func setupLayouts() {} 42 | 43 | open func updateLayouts() {} 44 | 45 | open func setupActions() {} 46 | } 47 | -------------------------------------------------------------------------------- /Projects/Features/BaseFeature/Testing/Testing.swift: -------------------------------------------------------------------------------- 1 | // Empty 2 | -------------------------------------------------------------------------------- /Projects/Features/BaseFeature/Tests/BaseFeatureTest.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class BaseFeatureTests: XCTestCase { 4 | override func setUpWithError() throws {} 5 | 6 | override func tearDownWithError() throws {} 7 | 8 | func testExample() { 9 | XCTAssertEqual(1, 1) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Projects/Features/LiveStreamFeature/Demo/Resources/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Projects/Features/LiveStreamFeature/Demo/Sources/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @main 4 | final class AppDelegate: UIResponder, UIApplicationDelegate { 5 | var window: UIWindow? 6 | 7 | func application( 8 | _: UIApplication, 9 | didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil 10 | ) -> Bool { 11 | window = UIWindow(frame: UIScreen.main.bounds) 12 | let viewController = UIViewController() 13 | window?.rootViewController = viewController 14 | window?.makeKeyAndVisible() 15 | return true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Projects/Features/LiveStreamFeature/Interface/LiveStreamViewControllerFactory.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | import BaseFeatureInterface 4 | 5 | public protocol LiveStreamViewControllerFactory { 6 | func make(channelID: String, title: String, owner: String, description: String) -> UIViewController 7 | } 8 | -------------------------------------------------------------------------------- /Projects/Features/LiveStreamFeature/Project.swift: -------------------------------------------------------------------------------- 1 | import DependencyPlugin 2 | import ProjectDescription 3 | import ProjectDescriptionHelpers 4 | 5 | let project = Project.module( 6 | name: ModulePaths.Feature.LiveStreamFeature.rawValue, 7 | targets: [ 8 | .interface(module: .feature(.LiveStreamFeature), dependencies: [ 9 | .feature(target: .BaseFeature, type: .interface) 10 | ]), 11 | .implements(module: .feature(.LiveStreamFeature), dependencies: [ 12 | .feature(target: .LiveStreamFeature, type: .interface), 13 | .feature(target: .BaseFeature), 14 | .domain(target: .LiveStationDomain, type: .interface), 15 | .domain(target: .BroadcastDomain, type: .interface), 16 | .module(target: .ChatSoketModule) 17 | ]), 18 | .tests(module: .feature(.LiveStreamFeature), dependencies: [ 19 | .feature(target: .LiveStreamFeature) 20 | ]), 21 | .demo(module: .feature(.LiveStreamFeature), dependencies: [ 22 | .feature(target: .LiveStreamFeature), 23 | .domain(target: .LiveStationDomain, type: .interface) 24 | ]) 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /Projects/Features/LiveStreamFeature/Sources/Chating/Models/ChatInfo.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - ChatInfo 4 | 5 | struct ChatInfo: Hashable { 6 | let id = UUID() 7 | let owner: ChatOwner 8 | let message: String 9 | } 10 | 11 | // MARK: - ChatOwner 12 | 13 | enum ChatOwner: Hashable { 14 | case user(name: String) 15 | case system 16 | 17 | var name: String { 18 | switch self { 19 | case let .user(name): name 20 | case .system: "System" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Projects/Features/LiveStreamFeature/Sources/Chating/Views/ChatEmptyView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | import BaseFeature 4 | import DesignSystem 5 | import EasyLayout 6 | 7 | final class ChatEmptyView: BaseView { 8 | private let imageView = UIImageView() 9 | private let titleLabel = UILabel() 10 | private let subtitleLabel = UILabel() 11 | 12 | private lazy var textStackView = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel]) 13 | private lazy var stackView = UIStackView(arrangedSubviews: [imageView, textStackView]) 14 | 15 | override func setupViews() { 16 | addSubview(stackView) 17 | 18 | imageView.image = DesignSystemAsset.Image.chat48.image 19 | 20 | titleLabel.text = "여기는 지금 조용하네요!" 21 | 22 | subtitleLabel.text = "첫 댓글로 스트리머와 소통을 시작해보세요!" 23 | } 24 | 25 | override func setupStyles() { 26 | titleLabel.font = .setFont(.body1()) 27 | 28 | subtitleLabel.textColor = .gray 29 | subtitleLabel.font = .setFont(.caption1()) 30 | 31 | stackView.axis = .vertical 32 | stackView.spacing = 13 33 | stackView.alignment = .center 34 | 35 | textStackView.axis = .vertical 36 | textStackView.spacing = 11 37 | textStackView.alignment = .center 38 | } 39 | 40 | override func setupLayouts() { 41 | stackView.ezl.makeConstraint { 42 | $0.center(to: self) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Projects/Features/LiveStreamFeature/Sources/Chating/Views/ChattingCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | import BaseFeature 4 | import DesignSystem 5 | import EasyLayout 6 | 7 | final class ChattingCell: BaseTableViewCell { 8 | private let nameLabel = UILabel() 9 | private let detailLabel = UILabel() 10 | 11 | override func setupViews() { 12 | contentView.addSubview(nameLabel) 13 | contentView.addSubview(detailLabel) 14 | 15 | nameLabel.setContentHuggingPriority(.required, for: .horizontal) 16 | } 17 | 18 | override func setupStyles() { 19 | backgroundColor = .clear 20 | 21 | nameLabel.font = .setFont(.caption1(weight: .bold)) 22 | 23 | detailLabel.font = .setFont(.caption1()) 24 | detailLabel.numberOfLines = 0 25 | } 26 | 27 | override func setupLayouts() { 28 | nameLabel.ezl.makeConstraint { 29 | $0.leading(to: contentView, offset: 20) 30 | .vertical(to: contentView, padding: 6) 31 | } 32 | 33 | detailLabel.ezl.makeConstraint { 34 | $0.leading(to: nameLabel.ezl.trailing, offset: 15) 35 | .vertical(to: contentView, padding: 6) 36 | .trailing(to: contentView, offset: -20) 37 | } 38 | } 39 | 40 | func configure(chat: ChatInfo) { 41 | nameLabel.text = chat.owner.name 42 | detailLabel.text = chat.message 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Projects/Features/LiveStreamFeature/Sources/Chating/Views/SystemAlarmCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | import BaseFeature 4 | import DesignSystem 5 | import EasyLayout 6 | 7 | final class SystemAlarmCell: BaseTableViewCell { 8 | private let contentLabel = UILabel() 9 | 10 | override func setupViews() { 11 | contentView.addSubview(contentLabel) 12 | } 13 | 14 | override func setupStyles() { 15 | backgroundColor = .clear 16 | 17 | contentLabel.textAlignment = .center 18 | contentLabel.textColor = DesignSystemAsset.Color.gray.color 19 | contentLabel.font = .setFont(.caption1()) 20 | } 21 | 22 | override func setupLayouts() { 23 | contentLabel.ezl.makeConstraint { 24 | $0.diagonal(to: contentView) 25 | } 26 | } 27 | 28 | func configure(content: String) { 29 | contentLabel.text = content 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Projects/Features/LiveStreamFeature/Sources/Factory/LiveStreamViewControllerFactoryImpl.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | import BaseFeatureInterface 4 | import LiveStationDomainInterface 5 | import LiveStreamFeatureInterface 6 | 7 | public struct LiveStreamViewControllerFactoryImpl: LiveStreamViewControllerFactory { 8 | private let fetchBroadcastUseCase: any FetchVideoListUsecase 9 | 10 | public init( 11 | fetchBroadcastUseCase: any FetchVideoListUsecase 12 | ) { 13 | self.fetchBroadcastUseCase = fetchBroadcastUseCase 14 | } 15 | 16 | public func make(channelID: String, title: String, owner: String, description: String) -> UIViewController { 17 | let viewModel = LiveStreamViewModel(channelID: channelID, fetchVideoListUsecase: fetchBroadcastUseCase) 18 | return LiveStreamViewController(title: title, owner: owner, description: description, viewModel: viewModel) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Projects/Features/LiveStreamFeature/Sources/Player/Views/LiveStreamInfoView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | import BaseFeature 4 | import DesignSystem 5 | import EasyLayout 6 | 7 | // MARK: - LiveStreamInfoView 8 | 9 | final class LiveStreamInfoView: BaseView { 10 | private let titleLabel = UILabel() 11 | private let descriptionLabel = UILabel() 12 | 13 | private lazy var stackView: UIStackView = { 14 | let stackView = UIStackView() 15 | stackView.addArrangedSubview(titleLabel) 16 | stackView.addArrangedSubview(descriptionLabel) 17 | stackView.axis = .vertical 18 | stackView.distribution = .fill 19 | stackView.spacing = 10 20 | return stackView 21 | }() 22 | 23 | override func setupViews() { 24 | addSubview(stackView) 25 | backgroundColor = DesignSystemAsset.Color.darkGray.color 26 | } 27 | 28 | override func setupLayouts() { 29 | stackView.ezl.makeConstraint { 30 | $0.diagonal(to: self, padding: 20) 31 | } 32 | } 33 | 34 | override func setupStyles() { 35 | titleLabel.font = .setFont(.body2()) 36 | titleLabel.numberOfLines = 0 37 | titleLabel.textColor = .white 38 | 39 | descriptionLabel.font = .setFont(.caption1()) 40 | descriptionLabel.textColor = .white 41 | descriptionLabel.numberOfLines = 1 42 | } 43 | } 44 | 45 | extension LiveStreamInfoView { 46 | public func configureUI(with model: (String, String)) { 47 | titleLabel.text = model.0 48 | descriptionLabel.text = model.1 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Projects/Features/LiveStreamFeature/Tests/LiveStreamFeatureTest.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class LiveStreamFeatureTests: XCTestCase { 4 | override func setUpWithError() throws {} 5 | 6 | override func tearDownWithError() throws {} 7 | 8 | func testExample() { 9 | XCTAssertEqual(1, 1) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Projects/Features/MainFeature/BroadcastUploadExtension/SampleHandler.swift: -------------------------------------------------------------------------------- 1 | import ReplayKit 2 | 3 | final class SampleHandler: RPBroadcastSampleHandler, @unchecked Sendable { 4 | override func broadcastStarted(withSetupInfo _: [String: NSObject]?) {} 5 | } 6 | -------------------------------------------------------------------------------- /Projects/Features/MainFeature/Demo/Resources/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Projects/Features/MainFeature/Demo/Sources/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import ReplayKit 2 | import UIKit 3 | 4 | import MainFeature 5 | 6 | @main 7 | final class AppDelegate: UIResponder, UIApplicationDelegate { 8 | var window: UIWindow? 9 | 10 | func application( 11 | _: UIApplication, 12 | didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil 13 | ) -> Bool { 14 | window = UIWindow(frame: UIScreen.main.bounds) 15 | let mockFetchChannelListUsecase = MockFetchChannelListUsecaseImpl() 16 | let mockFetchChannelInfoUsecase = MockFetchChannelInfoUsecaseImpl() 17 | let mockmakeBroadcastUsecase = MockMakeBroadcastUsecaseImpl() 18 | let mockFetchAllBroadcastUsecase = MockFetchAllBroadcastUsecaseImpl() 19 | let mockDeleteBroadcastUsecase = MockDeleteBroadcastUsecaseImpl() 20 | let viewModel = BroadcastCollectionViewModel( 21 | fetchChannelListUsecase: mockFetchChannelListUsecase, 22 | fetchAllBroadcastUsecase: mockFetchAllBroadcastUsecase 23 | ) 24 | let mockFactory = MockLiveStreamViewControllerFractoryImpl() 25 | let viewController = BroadcastCollectionViewController(viewModel: viewModel, factory: mockFactory) 26 | let navigationController = UINavigationController(rootViewController: viewController) 27 | window?.rootViewController = navigationController 28 | window?.makeKeyAndVisible() 29 | 30 | return true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Projects/Features/MainFeature/Demo/Sources/MockCreateChannelUsecaseImpl.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | import LiveStationDomainInterface 4 | 5 | final class MockCreateChannelUsecaseImpl: CreateChannelUsecase { 6 | func execute(name: String) -> AnyPublisher { 7 | Future { promise in 8 | promise(.success(ChannelEntity(id: "", name: name))) 9 | }.eraseToAnyPublisher() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Projects/Features/MainFeature/Demo/Sources/MockDeleteBroadcastUsecaseImpl.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | import BroadcastDomainInterface 4 | 5 | final class MockDeleteBroadcastUsecaseImpl: DeleteBroadcastUsecase { 6 | func execute(id _: String) -> AnyPublisher { 7 | Future { promise in 8 | promise(.success(())) 9 | }.eraseToAnyPublisher() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Projects/Features/MainFeature/Demo/Sources/MockDeleteChannelUsecaseImpl.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | import LiveStationDomainInterface 4 | 5 | final class MockDeleteChannelUsecaseImpl: DeleteChannelUsecase { 6 | func execute(channelID _: String) -> AnyPublisher { 7 | Future { promise in 8 | promise(.success(())) 9 | }.eraseToAnyPublisher() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Projects/Features/MainFeature/Demo/Sources/MockFetchAllBroadcastUsecaseImpl.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | import BroadcastDomainInterface 4 | 5 | final class MockFetchAllBroadcastUsecaseImpl: FetchAllBroadcastUsecase { 6 | func execute() -> AnyPublisher<[BroadcastInfoEntity], any Error> { 7 | Future { promise in 8 | promise(.success([.init(id: "1", title: "title", owner: "owner", description: "description")])) 9 | }.eraseToAnyPublisher() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Projects/Features/MainFeature/Demo/Sources/MockFetchChannelInfoUsecaseImpl.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | import LiveStationDomainInterface 4 | 5 | final class MockFetchChannelInfoUsecaseImpl: FetchChannelInfoUsecase { 6 | func execute(channelID: String) -> AnyPublisher { 7 | Future { promise in 8 | promise(.success(ChannelInfoEntity(id: channelID, name: "", streamKey: "", rtmpUrl: ""))) 9 | }.eraseToAnyPublisher() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Projects/Features/MainFeature/Demo/Sources/MockFetchChannelListUsecaseImpl.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import UIKit 3 | 4 | import LiveStationDomainInterface 5 | 6 | // MARK: - MockFetchChannelListUsecaseImpl 7 | 8 | struct MockFetchChannelListUsecaseImpl: FetchChannelListUsecase { 9 | func execute() -> AnyPublisher<[ChannelEntity], any Error> { 10 | let fetcher = MockChannelListFetcher() 11 | 12 | return Future<[ChannelEntity], Error> { promise in 13 | Task { 14 | let channels = await fetcher.fetch() 15 | promise(.success(channels)) 16 | } 17 | } 18 | .eraseToAnyPublisher() 19 | } 20 | } 21 | 22 | // MARK: - MockChannelListFetcher 23 | 24 | final class MockChannelListFetcher { 25 | enum Image { 26 | case ratio16x9 27 | case ratio4x3 28 | 29 | func fetch() async -> UIImage? { 30 | let size: (width: Int, height: Int) = switch self { 31 | case .ratio16x9: (1920, 1080) 32 | case .ratio4x3: (1440, 1080) 33 | } 34 | return await fetchImage(width: size.width, height: size.height) 35 | } 36 | 37 | private func fetchImage(width: Int, height: Int) async -> UIImage? { 38 | guard let url = URL(string: "https://picsum.photos/\(width)/\(height)") else { return nil } 39 | 40 | let data = await (try? URLSession.shared.data(from: url).0) ?? Data() 41 | return UIImage(data: data) 42 | } 43 | } 44 | 45 | func fetch() async -> [ChannelEntity] { 46 | let random = Int.random(in: 3 ... 7) 47 | var channels: [ChannelEntity] = [] 48 | 49 | for _ in 0 ..< random { 50 | let nameLength = Int.random(in: 4 ... 50) 51 | let name = String((0 ..< nameLength).map { _ in 52 | "가나다라마바사아자차카타파하".randomElement()! 53 | }) 54 | 55 | let randomBool = Bool.random() 56 | let image: Image = randomBool ? .ratio16x9 : .ratio4x3 57 | let fetchedImage = await image.fetch() 58 | 59 | channels.append(ChannelEntity(id: UUID().uuidString, name: name)) 60 | } 61 | 62 | return channels 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Projects/Features/MainFeature/Demo/Sources/MockLiveStreamViewControllerFactory.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | import LiveStreamFeatureInterface 4 | 5 | public struct MockLiveStreamViewControllerFractoryImpl: LiveStreamViewControllerFactory { 6 | public init() {} 7 | 8 | public func make(channelID _: String, title _: String, owner _: String, description _: String) -> UIViewController { 9 | let viewModel = MockLiveStreamViewModel() 10 | return MockLiveStreamViewController(viewModel: viewModel) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Projects/Features/MainFeature/Demo/Sources/MockMakeBroadcastUsecaseImpl.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | import BroadcastDomainInterface 4 | 5 | final class MockMakeBroadcastUsecaseImpl: MakeBroadcastUsecase { 6 | func execute(id _: String, title _: String, owner _: String, description _: String) -> AnyPublisher { 7 | Future { promise in 8 | promise(.success(())) 9 | }.eraseToAnyPublisher() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Projects/Features/MainFeature/Demo/Sources/MockShookPlayerView.swift: -------------------------------------------------------------------------------- 1 | import AVFoundation 2 | import UIKit 3 | 4 | import BaseFeature 5 | import EasyLayout 6 | 7 | public final class MockShookPlayerView: BaseView { 8 | private let player: AVPlayer = .init() 9 | private var playerItem: AVPlayerItem 10 | 11 | private lazy var playerLayer: AVPlayerLayer = { 12 | let layer = AVPlayerLayer(player: player) 13 | layer.videoGravity = .resizeAspectFill 14 | return layer 15 | }() 16 | 17 | private lazy var videoContainerView: UIView = { 18 | let view = UIView() 19 | view.layer.addSublayer(playerLayer) 20 | return view 21 | }() 22 | 23 | init(with url: URL) { 24 | playerItem = AVPlayerItem(url: url) 25 | player.replaceCurrentItem(with: playerItem) 26 | player.play() 27 | super.init(frame: .zero) 28 | videoContainerView.backgroundColor = .darkGray 29 | } 30 | 31 | @available(*, unavailable) 32 | required init?(coder _: NSCoder) { 33 | fatalError("init(coder:) has not been implemented") 34 | } 35 | 36 | override public func setupViews() { 37 | super.setupViews() 38 | backgroundColor = .systemBackground 39 | addSubview(videoContainerView) 40 | } 41 | 42 | override public func setupLayouts() { 43 | super.setupLayouts() 44 | 45 | videoContainerView.ezl.makeConstraint { 46 | $0.diagonal(to: self) 47 | } 48 | } 49 | 50 | override public func layoutSubviews() { 51 | super.layoutSubviews() 52 | playerLayer.frame = videoContainerView.bounds 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Projects/Features/MainFeature/Interface/BroadcastViewControllerFactory.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public protocol BroadcastViewControllerFactory { 4 | func make() -> UIViewController 5 | } 6 | -------------------------------------------------------------------------------- /Projects/Features/MainFeature/Interface/SettingViewControllerFactory.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public protocol SettingViewControllerFactory { 4 | func make() -> UIViewController 5 | } 6 | -------------------------------------------------------------------------------- /Projects/Features/MainFeature/Project.swift: -------------------------------------------------------------------------------- 1 | import DependencyPlugin 2 | import EnvironmentPlugin 3 | import ProjectDescription 4 | import ProjectDescriptionHelpers 5 | 6 | let project = Project.module( 7 | name: ModulePaths.Feature.MainFeature.rawValue, 8 | targets: [ 9 | .interface(module: .feature(.MainFeature)), 10 | .implements(module: .feature(.MainFeature), dependencies: [ 11 | .feature(target: .MainFeature, type: .interface), 12 | .feature(target: .BaseFeature), 13 | .feature(target: .LiveStreamFeature, type: .interface), 14 | .domain(target: .LiveStationDomain, type: .interface), 15 | .domain(target: .BroadcastDomain, type: .interface) 16 | ]), 17 | .tests(module: .feature(.MainFeature), dependencies: [ 18 | .feature(target: .MainFeature) 19 | ]), 20 | .demo(module: .feature(.MainFeature), dependencies: [ 21 | .feature(target: .MainFeature) 22 | ]) 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /Projects/Features/MainFeature/Sources/Factory/BroadcastViewControllerFactoryImpl.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | import BroadcastDomainInterface 4 | import LiveStationDomainInterface 5 | import MainFeatureInterface 6 | 7 | public struct BroadcastViewControllerFactoryImpl: BroadcastViewControllerFactory { 8 | private let fetchChannelInfoUsecase: any FetchChannelInfoUsecase 9 | private let makeBroadcastUsecase: any MakeBroadcastUsecase 10 | private let deleteBroadCastUsecase: any DeleteBroadcastUsecase 11 | 12 | public init(fetchChannelInfoUsecase: any FetchChannelInfoUsecase, makeBroadcastUsecase: any MakeBroadcastUsecase, deleteBroadCastUsecase: any DeleteBroadcastUsecase) { 13 | self.fetchChannelInfoUsecase = fetchChannelInfoUsecase 14 | self.makeBroadcastUsecase = makeBroadcastUsecase 15 | self.deleteBroadCastUsecase = deleteBroadCastUsecase 16 | } 17 | 18 | public func make() -> UIViewController { 19 | let viewModel = SettingViewModel( 20 | fetchChannelInfoUsecase: fetchChannelInfoUsecase, 21 | makeBroadcastUsecase: makeBroadcastUsecase, 22 | deleteBroadCastUsecase: deleteBroadCastUsecase 23 | ) 24 | 25 | return BroadcastViewController(viewModel: viewModel) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Projects/Features/MainFeature/Sources/Factory/SettingViewControllerFactoryImpl.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | import BroadcastDomainInterface 4 | import LiveStationDomainInterface 5 | import MainFeatureInterface 6 | 7 | public struct SettingViewControllerFactoryImpl: SettingViewControllerFactory { 8 | private let fetchChannelInfoUsecase: any FetchChannelInfoUsecase 9 | private let makeBroadcastUsecase: any MakeBroadcastUsecase 10 | private let deleteBroadCastUsecase: any DeleteBroadcastUsecase 11 | 12 | public init(fetchChannelInfoUsecase: any FetchChannelInfoUsecase, makeBroadcastUsecase: any MakeBroadcastUsecase, deleteBroadCastUsecase: any DeleteBroadcastUsecase) { 13 | self.fetchChannelInfoUsecase = fetchChannelInfoUsecase 14 | self.makeBroadcastUsecase = makeBroadcastUsecase 15 | self.deleteBroadCastUsecase = deleteBroadCastUsecase 16 | } 17 | 18 | public func make() -> UIViewController { 19 | let viewModel = SettingViewModel( 20 | fetchChannelInfoUsecase: fetchChannelInfoUsecase, 21 | makeBroadcastUsecase: makeBroadcastUsecase, 22 | deleteBroadCastUsecase: deleteBroadCastUsecase 23 | ) 24 | 25 | return SettingUIViewController(viewModel: viewModel) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Projects/Features/MainFeature/Sources/Models/Channel.swift: -------------------------------------------------------------------------------- 1 | public struct Channel: Hashable { 2 | let id: String 3 | let name: String 4 | var thumbnailImageURLString: String 5 | let owner: String 6 | let description: String 7 | 8 | public init( 9 | id: String, 10 | title: String, 11 | thumbnailImageURLString: String = "", 12 | owner: String = "", 13 | description: String = "" 14 | ) { 15 | self.id = id 16 | name = title 17 | self.thumbnailImageURLString = thumbnailImageURLString 18 | self.owner = owner 19 | self.description = description 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Projects/Features/MainFeature/Sources/Utilities/NotificationName.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum NotificationName { 4 | static let startStreaming = NSNotification.Name("DidTapStartStreaming") 5 | static let finishStreaming = NSNotification.Name("DidTapFinishStreaming") 6 | } 7 | -------------------------------------------------------------------------------- /Projects/Features/MainFeature/Sources/ViewModels/BroadcastCollectionViewModel.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import UIKit 3 | 4 | import BaseFeatureInterface 5 | import BroadcastDomainInterface 6 | import LiveStationDomainInterface 7 | import MainFeatureInterface 8 | 9 | public class BroadcastCollectionViewModel: ViewModel { 10 | public struct Input { 11 | let fetch: PassthroughSubject = .init() 12 | } 13 | 14 | public struct Output { 15 | let channels: PassthroughSubject<[Channel], Never> = .init() 16 | } 17 | 18 | private let output = Output() 19 | 20 | private let fetchChannelListUsecase: any FetchChannelListUsecase 21 | private let fetchAllBroadcastUsecase: any FetchAllBroadcastUsecase 22 | 23 | private var cancellables = Set() 24 | 25 | private let extensionBundleID = "kr.codesquad.boostcamp9.Shook.BroadcastUploadExtension" 26 | private let isStreamingKey = "IS_STREAMING" 27 | private let channelID = UserDefaults.standard.string(forKey: "CHANNEL_ID") 28 | 29 | public init( 30 | fetchChannelListUsecase: FetchChannelListUsecase, 31 | fetchAllBroadcastUsecase: FetchAllBroadcastUsecase 32 | ) { 33 | self.fetchChannelListUsecase = fetchChannelListUsecase 34 | self.fetchAllBroadcastUsecase = fetchAllBroadcastUsecase 35 | } 36 | 37 | public func transform(input: Input) -> Output { 38 | input.fetch 39 | .sink { [weak self] in 40 | self?.fetchData() 41 | } 42 | .store(in: &cancellables) 43 | 44 | return output 45 | } 46 | 47 | private func fetchData() { 48 | fetchChannelListUsecase.execute() 49 | .zip(fetchAllBroadcastUsecase.execute()) 50 | .map { channelEntities, broadcastInfoEntities in 51 | channelEntities.map { channelEntity in 52 | let broadcast = broadcastInfoEntities.first { $0.id == channelEntity.id } 53 | return Channel( 54 | id: channelEntity.id, 55 | title: broadcast?.title ?? "Unknown", 56 | thumbnailImageURLString: channelEntity.imageURLString, 57 | owner: broadcast?.owner ?? "Unknown", 58 | description: broadcast?.description ?? "" 59 | ) 60 | } 61 | } 62 | .sink( 63 | receiveCompletion: { _ in }, 64 | receiveValue: { [weak self] channels in 65 | let filteredChannels = channels.filter { !($0.id == self?.channelID) } 66 | self?.output.channels.send(filteredChannels) 67 | } 68 | ) 69 | .store(in: &cancellables) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Projects/Features/MainFeature/Sources/Views/BroadcastCollectionLoadView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | import BaseFeature 4 | import DesignSystem 5 | import Lottie 6 | 7 | // MARK: - BroadcastCollectionLoadView 8 | 9 | final class BroadcastCollectionLoadView: BaseView { 10 | private let shookAnimationView = LottieAnimationView(name: "shook", bundle: Bundle(for: DesignSystemResources.self)) 11 | 12 | private let titleLabel = UILabel() 13 | private let subtitleLabel = UILabel() 14 | 15 | private lazy var stackView = UIStackView(arrangedSubviews: [shookAnimationView, titleLabel, subtitleLabel]) 16 | 17 | override func layoutSubviews() { 18 | super.layoutSubviews() 19 | shookAnimationView.play() 20 | } 21 | 22 | override func setupViews() { 23 | addSubview(stackView) 24 | 25 | titleLabel.text = "방송을 불러오는 중이에요!" 26 | 27 | subtitleLabel.text = "잠시만 기다려주세요!" 28 | } 29 | 30 | override func setupStyles() { 31 | shookAnimationView.contentMode = .scaleAspectFit 32 | shookAnimationView.loopMode = .loop 33 | shookAnimationView.animationSpeed = 0.4 34 | 35 | titleLabel.font = .setFont(.title()) 36 | 37 | subtitleLabel.textColor = .gray 38 | subtitleLabel.font = .setFont(.body2()) 39 | 40 | stackView.axis = .vertical 41 | stackView.spacing = 12 42 | stackView.alignment = .center 43 | } 44 | 45 | override func setupLayouts() { 46 | stackView.ezl.makeConstraint { 47 | $0.center(to: self) 48 | } 49 | 50 | shookAnimationView.ezl.makeConstraint { 51 | $0.width(220) 52 | .height(80) 53 | } 54 | } 55 | } 56 | 57 | #if DEBUG 58 | import SwiftUI 59 | 60 | struct BroadcastCollectionLoadViewPreview: UIViewRepresentable { 61 | func makeUIView(context _: Context) -> BroadcastCollectionLoadView { 62 | BroadcastCollectionLoadView() 63 | } 64 | 65 | func updateUIView(_: BroadcastCollectionLoadView, context _: Context) {} 66 | } 67 | 68 | struct BroadcastCollectionLoadViewPreview_Previews: PreviewProvider { 69 | static var previews: some View { 70 | BroadcastCollectionLoadViewPreview() 71 | .background(Color.black) 72 | } 73 | } 74 | #endif 75 | -------------------------------------------------------------------------------- /Projects/Features/MainFeature/Sources/Views/BroadcastCollectionViewCell/EmptyBroadcastCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | import BaseFeature 4 | import DesignSystem 5 | import EasyLayout 6 | 7 | final class EmptyBroadcastCollectionViewCell: BaseCollectionViewCell { 8 | private let imageView = UIImageView() 9 | private let titleLabel = UILabel() 10 | private let subtitleLabel = UILabel() 11 | 12 | private lazy var textStackView = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel]) 13 | private lazy var stackView = UIStackView(arrangedSubviews: [imageView, textStackView]) 14 | 15 | override func setupViews() { 16 | contentView.addSubview(stackView) 17 | 18 | imageView.image = DesignSystemAsset.Image.tv48.image 19 | 20 | titleLabel.text = "아직 라이브 방송이 없어요!" 21 | 22 | subtitleLabel.text = "잠시 후 다시 확인해 주세요!" 23 | } 24 | 25 | override func setupStyles() { 26 | titleLabel.font = .setFont(.title()) 27 | 28 | subtitleLabel.textColor = .gray 29 | subtitleLabel.font = .setFont(.body2()) 30 | 31 | textStackView.axis = .vertical 32 | textStackView.spacing = 12 33 | textStackView.alignment = .center 34 | 35 | stackView.axis = .vertical 36 | stackView.spacing = 7 37 | stackView.alignment = .center 38 | 39 | contentView.backgroundColor = .systemBackground 40 | } 41 | 42 | override func setupLayouts() { 43 | stackView.ezl.makeConstraint { 44 | $0.center(to: contentView) 45 | } 46 | 47 | imageView.ezl.makeConstraint { 48 | $0.size(with: 117) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Projects/Features/MainFeature/Sources/Views/BroadcastCollectionViewCell/LargeBroadcastCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | import BaseFeature 4 | import DesignSystem 5 | import EasyLayout 6 | 7 | final class LargeBroadcastCollectionViewCell: BaseCollectionViewCell, ThumbnailViewContainer { 8 | let thumbnailView = ThumbnailView(with: .large) 9 | 10 | private let titleLabel = UILabel() 11 | private let descriptionLabel = UILabel() 12 | private let liveBadgeLabel = PaddingLabel() 13 | 14 | override func setupViews() { 15 | liveBadgeLabel.text = "L I V E" 16 | 17 | contentView.addSubview(thumbnailView) 18 | contentView.addSubview(titleLabel) 19 | contentView.addSubview(descriptionLabel) 20 | contentView.addSubview(liveBadgeLabel) 21 | } 22 | 23 | override func setupLayouts() { 24 | thumbnailView.ezl.makeConstraint { 25 | $0.top(to: contentView) 26 | .horizontal(to: contentView) 27 | .height(contentView.frame.width * 0.5625) 28 | } 29 | 30 | titleLabel.ezl.makeConstraint { 31 | $0.top(to: thumbnailView.imageView.ezl.bottom, offset: 6) 32 | .horizontal(to: contentView, padding: 20) 33 | } 34 | 35 | descriptionLabel.ezl.makeConstraint { 36 | $0.top(to: titleLabel.ezl.bottom, offset: 6) 37 | .horizontal(to: contentView, padding: 20) 38 | .bottom(to: contentView) 39 | } 40 | 41 | liveBadgeLabel.ezl.makeConstraint { 42 | $0.top(to: thumbnailView.imageView, offset: 12) 43 | .leading(to: thumbnailView.imageView, offset: 12) 44 | } 45 | } 46 | 47 | override func setupStyles() { 48 | liveBadgeLabel.textInsets = UIEdgeInsets(top: 8, left: 12, bottom: 8, right: 12) 49 | liveBadgeLabel.backgroundColor = DesignSystemAsset.Color.mainGreen.color 50 | liveBadgeLabel.textAlignment = .center 51 | liveBadgeLabel.font = .setFont(.caption1(weight: .bold)) 52 | liveBadgeLabel.layer.cornerRadius = 16 53 | liveBadgeLabel.clipsToBounds = true 54 | 55 | titleLabel.font = .setFont(.body1()) 56 | titleLabel.numberOfLines = 2 57 | titleLabel.lineBreakMode = .byWordWrapping 58 | 59 | descriptionLabel.font = .setFont(.body2()) 60 | descriptionLabel.textColor = .gray 61 | descriptionLabel.numberOfLines = 2 62 | descriptionLabel.lineBreakMode = .byWordWrapping 63 | } 64 | 65 | func configure(channel: Channel) { 66 | thumbnailView.configure(with: channel.thumbnailImageURLString) 67 | titleLabel.text = channel.name 68 | descriptionLabel.text = channel.owner + (channel.description.isEmpty ? "" : " • \(channel.description)") 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Projects/Features/MainFeature/Sources/Views/PaddingLabel.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class PaddingLabel: UILabel { 4 | var textInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) 5 | 6 | override func drawText(in rect: CGRect) { 7 | let insetRect = rect.inset(by: textInsets) 8 | super.drawText(in: insetRect) 9 | } 10 | 11 | override var intrinsicContentSize: CGSize { 12 | let size = super.intrinsicContentSize 13 | return CGSize(width: size.width + textInsets.left + textInsets.right, 14 | height: size.height + textInsets.top + textInsets.bottom) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Projects/Features/MainFeature/Tests/MainFeatureTest.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class MainFeatureTests: XCTestCase { 4 | override func setUpWithError() throws {} 5 | 6 | override func tearDownWithError() throws {} 7 | 8 | func testExample() { 9 | XCTAssertEqual(1, 1) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Projects/Modules/ChatSoketModule/Demo/Resources/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Projects/Modules/ChatSoketModule/Demo/Sources/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | import ChatSoketModule 4 | 5 | @main 6 | final class AppDelegate: UIResponder, UIApplicationDelegate { 7 | var window: UIWindow? 8 | 9 | func application( 10 | _: UIApplication, 11 | didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil 12 | ) -> Bool { 13 | window = UIWindow(frame: UIScreen.main.bounds) 14 | let viewController = SoketTestViewController() 15 | window?.rootViewController = viewController 16 | window?.makeKeyAndVisible() 17 | 18 | return true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Projects/Modules/ChatSoketModule/Interface/Interface.swift: -------------------------------------------------------------------------------- 1 | // Empty 2 | -------------------------------------------------------------------------------- /Projects/Modules/ChatSoketModule/Project.swift: -------------------------------------------------------------------------------- 1 | import DependencyPlugin 2 | import ProjectDescription 3 | import ProjectDescriptionHelpers 4 | 5 | let project = Project.module( 6 | name: ModulePaths.Module.ChatSoketModule.rawValue, 7 | targets: [ 8 | .interface(module: .module(.ChatSoketModule)), 9 | .implements(module: .module(.ChatSoketModule), dependencies: [ 10 | .module(target: .ChatSoketModule, type: .interface) 11 | ]), 12 | .testing(module: .module(.ChatSoketModule), dependencies: [ 13 | .module(target: .ChatSoketModule, type: .interface) 14 | ]), 15 | .tests(module: .module(.ChatSoketModule), dependencies: [ 16 | .module(target: .ChatSoketModule), 17 | .module(target: .ChatSoketModule, type: .testing) 18 | ]), 19 | .demo(module: .module(.ChatSoketModule), dependencies: [ 20 | .module(target: .ChatSoketModule), 21 | .module(target: .ChatSoketModule, type: .testing) 22 | ]) 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /Projects/Modules/ChatSoketModule/Sources/Message/ChatMessage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct ChatMessage: Codable { 4 | public let type: MessageType 5 | public let content: String? 6 | public let sender: String 7 | public let roomId: String 8 | 9 | public init(type: MessageType, content: String?, sender: String, roomId: String) { 10 | self.type = type 11 | self.content = content 12 | self.sender = sender 13 | self.roomId = roomId 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Projects/Modules/ChatSoketModule/Sources/Message/MessageType.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum MessageType: String, Codable { 4 | case ENTER 5 | case CHAT 6 | case TERMINATE 7 | } 8 | -------------------------------------------------------------------------------- /Projects/Modules/ChatSoketModule/Testing/Testing.swift: -------------------------------------------------------------------------------- 1 | // Empty 2 | -------------------------------------------------------------------------------- /Projects/Modules/ChatSoketModule/Tests/ChatSoketModuleTest.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class ChatSoketModuleTests: XCTestCase { 4 | override func setUpWithError() throws {} 5 | 6 | override func tearDownWithError() throws {} 7 | 8 | func testExample() { 9 | XCTAssertEqual(1, 1) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Projects/Modules/EasyLayout/Demo/Resources/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Projects/Modules/EasyLayout/Demo/Sources/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @main 4 | final class AppDelegate: UIResponder, UIApplicationDelegate { 5 | var window: UIWindow? 6 | 7 | func application( 8 | _: UIApplication, 9 | didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil 10 | ) -> Bool { 11 | window = UIWindow(frame: UIScreen.main.bounds) 12 | let viewController = EasyLayoutDemoViewController() 13 | viewController.view.backgroundColor = .white 14 | window?.rootViewController = viewController 15 | window?.makeKeyAndVisible() 16 | 17 | return true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Projects/Modules/EasyLayout/Demo/Sources/ViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | import EasyLayout 4 | 5 | final class EasyLayoutDemoViewController: UIViewController { 6 | private let firstView: UIView = { 7 | let view = UIView() 8 | view.backgroundColor = .red 9 | view.frame.origin = CGPoint(x: 0, y: 0) 10 | return view 11 | }() 12 | 13 | private let secondView: UIView = { 14 | let view = UIView() 15 | view.frame.origin = CGPoint(x: 0, y: 0) 16 | return view 17 | }() 18 | 19 | private let thirdView: UIView = { 20 | let view = UIView() 21 | view.backgroundColor = .blue 22 | view.frame.origin = CGPoint(x: 0, y: 0) 23 | return view 24 | }() 25 | 26 | init() { 27 | super.init(nibName: nil, bundle: nil) 28 | } 29 | 30 | @available(*, unavailable) 31 | required init?(coder _: NSCoder) { 32 | fatalError("init(coder:) has not been implemented") 33 | } 34 | 35 | override func viewDidLoad() { 36 | super.viewDidLoad() 37 | setupAttribute() 38 | setupSubViewsConstraints() 39 | } 40 | 41 | private func setupAttribute() {} 42 | 43 | private func setupSubViewsConstraints() { 44 | view.addSubview(firstView) 45 | view.addSubview(secondView) 46 | view.addSubview(thirdView) 47 | 48 | firstView.ezl.makeConstraint { 49 | $0.top(to: view.safeAreaLayoutGuide) 50 | .horizontal(to: view) 51 | .height(200) 52 | } 53 | 54 | secondView.ezl.makeConstraint { 55 | $0.top(to: firstView.ezl.bottom) 56 | .horizontal(to: view, padding: 40) 57 | .height(200) 58 | } 59 | 60 | thirdView.ezl.makeConstraint { 61 | $0.diagonal(to: view.safeAreaLayoutGuide, padding: 50) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Projects/Modules/EasyLayout/Project.swift: -------------------------------------------------------------------------------- 1 | import DependencyPlugin 2 | import ProjectDescription 3 | import ProjectDescriptionHelpers 4 | 5 | let project = Project.module( 6 | name: ModulePaths.Module.EasyLayout.rawValue, 7 | targets: [ 8 | .implements(module: .module(.EasyLayout)), 9 | .demo(module: .module(.EasyLayout), dependencies: [ 10 | .module(target: .EasyLayout) 11 | ]) 12 | ] 13 | ) 14 | -------------------------------------------------------------------------------- /Projects/Modules/EasyLayout/Sources/Anchor.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | // MARK: - YAnchor 4 | 5 | public struct YAnchor { 6 | enum Edge { 7 | case top(Anchorable) 8 | case bottom(Anchorable) 9 | 10 | var standard: NSLayoutYAxisAnchor { 11 | switch self { 12 | case let .top(view): view.topAnchor 13 | case let .bottom(view): view.bottomAnchor 14 | } 15 | } 16 | } 17 | 18 | let edge: Edge 19 | 20 | static func top(_ view: Anchorable) -> Self { 21 | YAnchor(edge: .top(view)) 22 | } 23 | 24 | static func bottom(_ view: Anchorable) -> Self { 25 | YAnchor(edge: .bottom(view)) 26 | } 27 | } 28 | 29 | // MARK: - XAnchor 30 | 31 | public struct XAnchor { 32 | enum Edge { 33 | case leading(Anchorable) 34 | case trailing(Anchorable) 35 | 36 | var standard: NSLayoutXAxisAnchor { 37 | switch self { 38 | case let .leading(view): view.leadingAnchor 39 | case let .trailing(view): view.trailingAnchor 40 | } 41 | } 42 | } 43 | 44 | let edge: Edge 45 | 46 | static func leading(_ view: Anchorable) -> Self { 47 | XAnchor(edge: .leading(view)) 48 | } 49 | 50 | static func trailing(_ view: Anchorable) -> Self { 51 | XAnchor(edge: .trailing(view)) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Projects/Modules/EasyLayout/Sources/EasyLayout.swift: -------------------------------------------------------------------------------- 1 | public struct EasyLayout { 2 | private let constraint: EasyConstraint 3 | 4 | init(_ constraint: EasyConstraint) { 5 | self.constraint = constraint 6 | } 7 | 8 | public func makeConstraint(handler: (EasyConstraint) -> Void) { 9 | handler(constraint) 10 | } 11 | 12 | public var top: YAnchor { 13 | .top(constraint.baseView) 14 | } 15 | 16 | public var bottom: YAnchor { 17 | .bottom(constraint.baseView) 18 | } 19 | 20 | public var leading: XAnchor { 21 | .leading(constraint.baseView) 22 | } 23 | 24 | public var trailing: XAnchor { 25 | .trailing(constraint.baseView) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Projects/Modules/EasyLayout/Sources/Protocol/Anchorable.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | // MARK: - Anchorable 4 | 5 | public protocol Anchorable { 6 | var bottomAnchor: NSLayoutYAxisAnchor { get } 7 | var leadingAnchor: NSLayoutXAxisAnchor { get } 8 | var topAnchor: NSLayoutYAxisAnchor { get } 9 | var trailingAnchor: NSLayoutXAxisAnchor { get } 10 | var widthAnchor: NSLayoutDimension { get } 11 | var heightAnchor: NSLayoutDimension { get } 12 | var centerXAnchor: NSLayoutXAxisAnchor { get } 13 | var centerYAnchor: NSLayoutYAxisAnchor { get } 14 | } 15 | 16 | public extension Anchorable { 17 | var ezl: EasyLayout { 18 | EasyLayout(EasyConstraint(self)) 19 | } 20 | } 21 | 22 | // MARK: - UIView + Anchorable 23 | 24 | extension UIView: Anchorable {} 25 | 26 | // MARK: - UILayoutGuide + Anchorable 27 | 28 | extension UILayoutGuide: Anchorable {} 29 | -------------------------------------------------------------------------------- /Projects/Modules/FastNetwork/Demo/Resources/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Projects/Modules/FastNetwork/Demo/Sources/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @main 4 | final class AppDelegate: UIResponder, UIApplicationDelegate { 5 | var window: UIWindow? 6 | 7 | func application( 8 | _: UIApplication, 9 | didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil 10 | ) -> Bool { 11 | window = UIWindow(frame: UIScreen.main.bounds) 12 | let viewController = UIViewController() 13 | viewController.view.backgroundColor = .yellow 14 | window?.rootViewController = viewController 15 | window?.makeKeyAndVisible() 16 | 17 | return true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Projects/Modules/FastNetwork/Interface/Interface.swift: -------------------------------------------------------------------------------- 1 | // Empty 2 | -------------------------------------------------------------------------------- /Projects/Modules/FastNetwork/Project.swift: -------------------------------------------------------------------------------- 1 | import DependencyPlugin 2 | import ProjectDescription 3 | import ProjectDescriptionHelpers 4 | 5 | let project = Project.module( 6 | name: ModulePaths.Module.FastNetwork.rawValue, 7 | targets: [ 8 | .interface(module: .module(.FastNetwork)), 9 | .implements(module: .module(.FastNetwork), dependencies: [ 10 | .module(target: .FastNetwork, type: .interface) 11 | ]), 12 | .testing(module: .module(.FastNetwork), dependencies: [ 13 | .module(target: .FastNetwork, type: .interface) 14 | ]), 15 | .tests(module: .module(.FastNetwork), dependencies: [ 16 | .module(target: .FastNetwork), 17 | .module(target: .FastNetwork, type: .testing) 18 | ]), 19 | .demo(module: .module(.FastNetwork), dependencies: [ 20 | .module(target: .FastNetwork), 21 | .module(target: .FastNetwork, type: .testing) 22 | ]) 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /Projects/Modules/FastNetwork/Sources/Client/NetworkClient.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | import FastNetworkInterface 4 | 5 | // MARK: - NetworkClient 6 | 7 | public final class NetworkClient: Requestable { 8 | private let session: URLSession 9 | private var interceptors: [any Interceptor] 10 | 11 | public init(session: URLSession = URLSession.shared, interceptors: [any Interceptor] = []) { 12 | self.session = session 13 | self.interceptors = interceptors 14 | } 15 | 16 | public func request(_ endpoint: E) async throws -> Response { 17 | var request = try configureURLRequest(from: endpoint) 18 | request = try interceptRequest(with: request, from: endpoint) 19 | return try await requestNetworkTask(with: request, from: endpoint) 20 | } 21 | } 22 | 23 | private extension NetworkClient { 24 | func configureURLRequest(from endpoint: E) throws -> URLRequest { 25 | let requestURL = try URL(from: endpoint) 26 | #warning("캐싱 정책 나중에 설정") 27 | var request = URLRequest(url: requestURL, cachePolicy: .reloadIgnoringCacheData, timeoutInterval: endpoint.timeout) 28 | request.httpMethod = endpoint.method.description 29 | 30 | try endpoint.requestTask.configureRequest(request: &request) 31 | request.allHTTPHeaderFields = endpoint.header 32 | 33 | return request 34 | } 35 | 36 | func requestNetworkTask(with request: URLRequest, from endpoint: E) async throws -> Response { 37 | let (data, urlResponse) = try await session.data(for: request) 38 | let response = Response(request: request, data: data, response: urlResponse) 39 | try interceptResponse(with: response, from: endpoint) 40 | 41 | guard let httpResponse = urlResponse as? HTTPURLResponse else { throw NetworkError.invaildResponse } 42 | 43 | let statusCode = httpResponse.statusCode 44 | 45 | if !(endpoint.validationCode ~= statusCode) { 46 | throw HTTPError(statuscode: statusCode) 47 | } 48 | 49 | return response 50 | } 51 | 52 | func interceptRequest(with request: URLRequest, from endpoint: E) throws -> URLRequest { 53 | var request = request 54 | 55 | for interceptor in interceptors { 56 | try interceptor.willRequest(request, from: endpoint) 57 | request = try interceptor.prepare(request, from: endpoint) 58 | } 59 | 60 | return request 61 | } 62 | 63 | func interceptResponse(with response: Response, from endpoint: E) throws { 64 | for interceptor in interceptors { 65 | try interceptor.didReceive(response, from: endpoint) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Projects/Modules/FastNetwork/Sources/Client/Requestable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol Requestable { 4 | associatedtype E: Endpoint 5 | 6 | func request(_ endpoint: E) async throws -> Response 7 | } 8 | -------------------------------------------------------------------------------- /Projects/Modules/FastNetwork/Sources/Endpoint.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - Endpoint 4 | 5 | public protocol Endpoint { 6 | var method: HTTPMethod { get } 7 | var header: [String: String]? { get } 8 | var scheme: String { get } 9 | var host: String { get } 10 | var path: String { get } 11 | var requestTask: RequestTask { get } 12 | var validationCode: ClosedRange { get } 13 | var timeout: TimeInterval { get } 14 | var port: Int? { get } 15 | } 16 | 17 | public extension Endpoint { 18 | var scheme: String { "https" } 19 | var validationCode: ClosedRange { 200 ... 500 } 20 | var timeout: TimeInterval { 300 } 21 | var port: Int? { nil } 22 | } 23 | -------------------------------------------------------------------------------- /Projects/Modules/FastNetwork/Sources/Error/HTTPError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum HTTPError: String, LocalizedError { 4 | // MARK: 400..<500 , Client Error 5 | 6 | case badRequest // 400 7 | case unauthorized // 401 8 | case paymentRequired // 402 9 | case forbidden // 403 10 | case notFound // 404 11 | case methodNotAllowed // 405 12 | case conflict // 409 13 | 14 | // MARK: 500..<600 Server Error 15 | 16 | case internalServerError // 500 17 | case badGateway // 502 18 | 19 | // MARK: Extra 20 | 21 | case underlying 22 | 23 | init(statuscode: Int) { 24 | switch statuscode { 25 | case 400: self = .badRequest 26 | case 401: self = .unauthorized 27 | case 402: self = .paymentRequired 28 | case 403: self = .forbidden 29 | case 404: self = .notFound 30 | case 405: self = .methodNotAllowed 31 | case 409: self = .conflict 32 | case 500: self = .internalServerError 33 | case 502: self = .badGateway 34 | default: self = .underlying 35 | } 36 | } 37 | 38 | public var errorDescription: String? { rawValue } 39 | } 40 | -------------------------------------------------------------------------------- /Projects/Modules/FastNetwork/Sources/Error/NetworkError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum NetworkError: LocalizedError { 4 | case invaildURL 5 | case invaildResponse 6 | case jsonEncodingFailed(Error) 7 | 8 | var errorDescription: String? { 9 | switch self { 10 | case .invaildURL: "유효한 URL을 찾을 수 없습니다." 11 | case .invaildResponse: "유효한 응답이 아닙니다." 12 | case let .jsonEncodingFailed(error): "JSON 인코딩에 실패했습니다: \(error.localizedDescription)" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Projects/Modules/FastNetwork/Sources/Extensions/URL+Extension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension URL { 4 | init(from endpoint: Endpoint) throws { 5 | var urlComponents = URLComponents() 6 | urlComponents.scheme = endpoint.scheme 7 | urlComponents.host = endpoint.host 8 | urlComponents.path = endpoint.path 9 | urlComponents.port = endpoint.port 10 | #warning("포트 번호 추후 삭제") 11 | 12 | guard let url = urlComponents.url else { 13 | throw NetworkError.invaildURL 14 | } 15 | 16 | self = url 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Projects/Modules/FastNetwork/Sources/Interceptor/Interceptor.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - Interceptor 4 | 5 | public protocol Interceptor { 6 | func prepare(_ request: URLRequest, from endpoint: Endpoint) throws -> URLRequest 7 | func willRequest(_ request: URLRequest, from endpoint: Endpoint) throws 8 | func didReceive(_ response: Response, from endpoint: Endpoint) throws 9 | } 10 | 11 | public extension Interceptor { 12 | func prepare(_ request: URLRequest, from _: Endpoint) throws -> URLRequest { request } 13 | func willRequest(_: URLRequest, from _: Endpoint) throws {} 14 | func didReceive(_: Response, from _: Endpoint) throws {} 15 | } 16 | -------------------------------------------------------------------------------- /Projects/Modules/FastNetwork/Sources/Request/Components/HTTPMethod.swift: -------------------------------------------------------------------------------- 1 | public enum HTTPMethod: String, CustomStringConvertible { 2 | case get 3 | case head 4 | case post 5 | case put 6 | case delete 7 | case connect 8 | case options 9 | case trace 10 | case patch 11 | 12 | public var description: String { rawValue.uppercased() } 13 | } 14 | -------------------------------------------------------------------------------- /Projects/Modules/FastNetwork/Sources/Request/Components/RequestTask.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - RequestTask 4 | 5 | public enum RequestTask { 6 | case empty 7 | case withParameters( 8 | body: Parameters? = nil, 9 | query: Parameters? = nil, 10 | bodyEncoder: any RequestParameterEncodable = .json, 11 | urlQueryEncoder: any RequestParameterEncodable = .query 12 | ) 13 | case withObject( 14 | body: any Encodable, 15 | query: Parameters? = nil, 16 | urlQueryEncoder: any RequestParameterEncodable = .query 17 | ) 18 | } 19 | 20 | extension RequestTask { 21 | func configureRequest(request: inout URLRequest) throws { 22 | switch self { 23 | case .empty: 24 | request.setValue("application/json", forHTTPHeaderField: "Content-Type") 25 | 26 | case let .withParameters(body, query, bodyEncoder, urlQueryEncoder): 27 | try configureParam( 28 | request: &request, 29 | body: body, 30 | query: query, 31 | bodyEncoder: bodyEncoder, 32 | urlQueryEncoder: urlQueryEncoder 33 | ) 34 | 35 | case let .withObject(body, query, urlQueryEncoder): 36 | try configureObject( 37 | request: &request, 38 | body: body, 39 | query: query, 40 | urlQueryEncoder: urlQueryEncoder 41 | ) 42 | } 43 | } 44 | 45 | func configureParam( 46 | request: inout URLRequest, 47 | body: Parameters?, 48 | query: Parameters?, 49 | bodyEncoder: any RequestParameterEncodable, 50 | urlQueryEncoder: any RequestParameterEncodable 51 | ) throws { 52 | if let body { 53 | try bodyEncoder.encode(request: &request, with: body) 54 | } 55 | 56 | if let query { 57 | try urlQueryEncoder.encode(request: &request, with: query) 58 | } 59 | } 60 | 61 | func configureObject( 62 | request: inout URLRequest, 63 | body: any Encodable, 64 | query: Parameters?, 65 | urlQueryEncoder: any RequestParameterEncodable 66 | ) throws { 67 | request.setValue("application/json", forHTTPHeaderField: "Content-Type") 68 | request.httpBody = try JSONEncoder().encode(body) 69 | 70 | if let query { 71 | try urlQueryEncoder.encode(request: &request, with: query) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Projects/Modules/FastNetwork/Sources/Request/Encoding/ParamterJSONEncoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - ParamterJSONEncoder 4 | 5 | public struct ParamterJSONEncoder: RequestParameterEncodable { 6 | public func encode(request: inout URLRequest, with parameters: Parameters) throws { 7 | do { 8 | let jsonData = try JSONSerialization.data(withJSONObject: parameters, options: []) 9 | request.httpBody = jsonData 10 | request.addValue("application/json", forHTTPHeaderField: "Content-Type") 11 | } catch { 12 | throw NetworkError.jsonEncodingFailed(error) 13 | } 14 | } 15 | } 16 | 17 | public extension RequestParameterEncodable where Self == ParamterJSONEncoder { 18 | static var json: ParamterJSONEncoder { 19 | ParamterJSONEncoder() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Projects/Modules/FastNetwork/Sources/Request/Encoding/RequestParameterEncodable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public typealias Parameters = [String: Any] 4 | 5 | // MARK: - RequestParameterEncodable 6 | 7 | public protocol RequestParameterEncodable { 8 | func encode(request: inout URLRequest, with parameters: Parameters) throws 9 | } 10 | -------------------------------------------------------------------------------- /Projects/Modules/FastNetwork/Sources/Request/Encoding/URLQueryEncoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - URLQueryEncoder 4 | 5 | public struct URLQueryEncoder: RequestParameterEncodable { 6 | #warning("배열 query value는 추후 구현") 7 | public func encode(request: inout URLRequest, with parameters: Parameters) throws { 8 | guard let url = request.url else { throw NetworkError.invaildURL } 9 | 10 | if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), !parameters.isEmpty { 11 | urlComponents.queryItems = parameters.map { 12 | URLQueryItem(name: $0.key, value: "\($0.value)".urlQueryAllowed) 13 | } 14 | request.url = urlComponents.url 15 | } 16 | } 17 | } 18 | 19 | private extension String { 20 | var urlQueryAllowed: String? { 21 | addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) 22 | } 23 | } 24 | 25 | public extension RequestParameterEncodable where Self == URLQueryEncoder { 26 | static var query: URLQueryEncoder { 27 | URLQueryEncoder() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Projects/Modules/FastNetwork/Sources/Response/Response.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct Response: Hashable { 4 | public let request: URLRequest 5 | public let data: Data 6 | public let response: URLResponse 7 | } 8 | -------------------------------------------------------------------------------- /Projects/Modules/FastNetwork/Testing/MockData.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | let mockData = Data( 4 | """ 5 | [ 6 | { 7 | "title": "Black Coffee", 8 | "description": "Svart kaffe är så enkelt som det kan bli med malda kaffebönor dränkta i hett vatten, serverat varmt. Och om du vill låta fancy kan du kalla svart kaffe med sitt rätta namn: café noir.", 9 | "ingredients": [ 10 | "Coffee" 11 | ], 12 | "image": "https://images.unsplash.com/photo-1494314671902-399b18174975?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", 13 | "id": 1 14 | }, 15 | { 16 | "title": "Latte", 17 | "description": "Som den mest populära kaffedrycken där ute består latte av en skvätt espresso och ångad mjölk med bara en gnutta skum. Den kan beställas utan smak eller med smak av allt från vanilj till pumpa kryddor.", 18 | "ingredients": [ 19 | "Espresso", 20 | "Ångad mjölk" 21 | ], 22 | "image": "https://images.unsplash.com/photo-1561882468-9110e03e0f78?auto=format&fit=crop&q=60&w=800&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTl8fGxhdHRlfGVufDB8fDB8fHww", 23 | "id": 2 24 | } 25 | ] 26 | """.utf8 27 | ) 28 | -------------------------------------------------------------------------------- /Projects/Modules/FastNetwork/Testing/MockResponse.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | let mockSuccessResponse = HTTPURLResponse( 4 | url: URL(string: "www.example.com")!, 5 | statusCode: 200, 6 | httpVersion: nil, 7 | headerFields: nil 8 | )! 9 | 10 | let mockBadRequestResponse = HTTPURLResponse( 11 | url: URL(string: "www.example.com")!, 12 | statusCode: 400, 13 | httpVersion: nil, 14 | headerFields: nil 15 | )! 16 | 17 | let mockBadGatewayResponse = HTTPURLResponse( 18 | url: URL(string: "www.example.com")!, 19 | statusCode: 502, 20 | httpVersion: nil, 21 | headerFields: nil 22 | )! 23 | -------------------------------------------------------------------------------- /Projects/Modules/FastNetwork/Testing/MockURLProtocol.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class MockURLProtocol: URLProtocol { 4 | static var mockData: Data? 5 | static var mockResponse: HTTPURLResponse? 6 | 7 | private(set) static var mockRequest: URLRequest? 8 | 9 | override static func canInit(with _: URLRequest) -> Bool { true } 10 | override static func canonicalRequest(for request: URLRequest) -> URLRequest { request } 11 | 12 | override func startLoading() { 13 | MockURLProtocol.mockRequest = request 14 | 15 | if let response = MockURLProtocol.mockResponse { 16 | client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) 17 | } 18 | 19 | if let data = MockURLProtocol.mockData { 20 | client?.urlProtocol(self, didLoad: data) 21 | } 22 | 23 | client?.urlProtocolDidFinishLoading(self) 24 | } 25 | 26 | override func stopLoading() {} 27 | } 28 | -------------------------------------------------------------------------------- /Projects/Modules/FastNetwork/Tests/MockEndpoint.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | import FastNetwork 4 | 5 | // MARK: - MockEndpoint 6 | 7 | enum MockEndpoint { 8 | case fetch 9 | case getwithParameters(queryParams: [String: Any], bodyParams: [String: Any]) 10 | } 11 | 12 | // MARK: Endpoint 13 | 14 | extension MockEndpoint: Endpoint { 15 | var method: FastNetwork.HTTPMethod { 16 | switch self { 17 | case .fetch, .getwithParameters: .get 18 | } 19 | } 20 | 21 | var header: [String: String]? { 22 | switch self { 23 | case .fetch: nil 24 | 25 | case .getwithParameters: 26 | ["임시 헤더": "shookHeader"] 27 | } 28 | } 29 | 30 | var host: String { "www.example.com" } 31 | 32 | var path: String { 33 | switch self { 34 | case .fetch, .getwithParameters: "/fetch" 35 | } 36 | } 37 | 38 | var requestTask: FastNetwork.RequestTask { 39 | switch self { 40 | case .fetch: .empty 41 | 42 | case let .getwithParameters(queryParams, bodyParams): 43 | .withParameters(body: bodyParams, query: queryParams) 44 | } 45 | } 46 | 47 | var validationCode: ClosedRange { 200 ... 299 } 48 | } 49 | -------------------------------------------------------------------------------- /Projects/Modules/FastNetwork/Tests/NetworkEncoderTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import FastNetwork 4 | 5 | // MARK: - Body 6 | 7 | private struct Body: Encodable { 8 | let name: String 9 | } 10 | 11 | // MARK: - NetworkEncoderTests 12 | 13 | final class NetworkEncoderTests: XCTestCase { 14 | let jsonEncoder: JSONEncoder = .init() 15 | 16 | func test_query_encoder() throws { 17 | var request = URLRequest(url: URL(string: "https://example.com")!) 18 | let encoder = URLQueryEncoder() 19 | 20 | try encoder.encode(request: &request, with: ["key1": "value1 ! #!@$**", "key2": 123, "key3": true]) 21 | var components = URLComponents(string: "https://example.com") 22 | components?.queryItems = [ 23 | URLQueryItem(name: "key1", value: "value1 ! #!@$**".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)), 24 | URLQueryItem(name: "key2", value: "\(123)".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)), 25 | URLQueryItem(name: "key3", value: "\(true)".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)) 26 | ] 27 | XCTAssertEqual(request.url?.absoluteString.sorted(), components?.url?.absoluteString.sorted()) 28 | } 29 | 30 | func test_json_encoder() throws { 31 | // 우리가 만든 인코더 이용 32 | var request1 = URLRequest(url: URL(string: "https://example.com")!) 33 | let encoder = ParamterJSONEncoder() 34 | try encoder.encode(request: &request1, with: ["name": "shook"]) 35 | 36 | // struct + json ecoder 이용 37 | var request2 = URLRequest(url: URL(string: "https://example.com")!) 38 | 39 | let data = try jsonEncoder.encode(Body(name: "shook")) 40 | request2.addValue("application/json", forHTTPHeaderField: "Content-Type") 41 | request2.httpBody = data 42 | 43 | XCTAssertEqual(request1, request2) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Projects/Modules/ThirdPartyLibModule/Interface/Interface.swift: -------------------------------------------------------------------------------- 1 | // Empty 2 | -------------------------------------------------------------------------------- /Projects/Modules/ThirdPartyLibModule/Project.swift: -------------------------------------------------------------------------------- 1 | import DependencyPlugin 2 | import ProjectDescription 3 | import ProjectDescriptionHelpers 4 | 5 | let project = Project.module( 6 | name: ModulePaths.Module.ThirdPartyLibModule.rawValue, 7 | targets: [ 8 | .interface(module: .module(.ThirdPartyLibModule)), 9 | .implements(module: .module(.ThirdPartyLibModule), dependencies: [ 10 | .module(target: .ThirdPartyLibModule, type: .interface), 11 | .module(target: .EasyLayout) 12 | ]), 13 | .tests(module: .module(.ThirdPartyLibModule), dependencies: [ 14 | .module(target: .ThirdPartyLibModule) 15 | ]) 16 | ] 17 | ) 18 | -------------------------------------------------------------------------------- /Projects/Modules/ThirdPartyLibModule/Sources/Sources.swift: -------------------------------------------------------------------------------- 1 | // Empty 2 | -------------------------------------------------------------------------------- /Projects/Modules/ThirdPartyLibModule/Tests/ThirdPartyLibModuleTest.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class ThirdPartyLibModuleTests: XCTestCase { 4 | override func setUpWithError() throws {} 5 | 6 | override func tearDownWithError() throws {} 7 | 8 | func testExample() { 9 | XCTAssertEqual(1, 1) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Interface/Interface.swift: -------------------------------------------------------------------------------- 1 | // Empty 2 | -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Project.swift: -------------------------------------------------------------------------------- 1 | import DependencyPlugin 2 | import ProjectDescription 3 | import ProjectDescriptionHelpers 4 | import TemplatePlugin 5 | 6 | let project = Project.module( 7 | name: ModulePaths.UserInterface.DesignSystem.rawValue, 8 | targets: [ 9 | .implements( 10 | module: .userInterface(.DesignSystem), 11 | product: .framework, 12 | spec: .init( 13 | resources: .resources, 14 | dependencies: [ 15 | .userInterface(target: .DesignSystem, type: .interface), 16 | .SPM.Lottie 17 | ] 18 | ) 19 | ), 20 | .interface(module: .userInterface(.DesignSystem)) 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Color.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Color.xcassets/DarkGray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x1E", 9 | "green" : "0x1C", 10 | "red" : "0x1C" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x1E", 27 | "green" : "0x1C", 28 | "red" : "0x1C" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Color.xcassets/ErrorRed.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x41", 9 | "green" : "0x58", 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" : "0x41", 27 | "green" : "0x58", 28 | "red" : "0xFF" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Color.xcassets/Gray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xBE", 9 | "green" : "0xBE", 10 | "red" : "0xBE" 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" : "0xBE", 27 | "green" : "0xBE", 28 | "red" : "0xBE" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Color.xcassets/MainBlack.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x37", 9 | "green" : "0x2F", 10 | "red" : "0x2A" 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" : "0x37", 27 | "green" : "0x2F", 28 | "red" : "0x2A" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Color.xcassets/MainBlue.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xEE", 9 | "green" : "0x4E", 10 | "red" : "0x1C" 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" : "0xEE", 27 | "green" : "0x4E", 28 | "red" : "0x1C" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Color.xcassets/MainGreen.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x8D", 9 | "green" : "0xC9", 10 | "red" : "0x34" 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" : "0x8D", 27 | "green" : "0xC9", 28 | "red" : "0x34" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Color.xcassets/PointYellow.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x3A", 9 | "green" : "0xE6", 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" : "0x3A", 27 | "green" : "0xE6", 28 | "red" : "0xFF" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Color.xcassets/White.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" : "0xFF", 27 | "green" : "0xFF", 28 | "red" : "0xFF" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/appIcon_small.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "appIcon_small@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "appIcon_small@3x.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/appIcon_small.imageset/appIcon_small@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS08-Shook/667278070168ce27a9a03922cc176aca93952eb6/Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/appIcon_small.imageset/appIcon_small@2x.png -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/appIcon_small.imageset/appIcon_small@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS08-Shook/667278070168ce27a9a03922cc176aca93952eb6/Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/appIcon_small.imageset/appIcon_small@3x.png -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/chat_48.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "chat@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "chat@3x.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/chat_48.imageset/chat@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS08-Shook/667278070168ce27a9a03922cc176aca93952eb6/Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/chat_48.imageset/chat@2x.png -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/chat_48.imageset/chat@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS08-Shook/667278070168ce27a9a03922cc176aca93952eb6/Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/chat_48.imageset/chat@3x.png -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/chevronDown_24.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "chevronDownCircle-1.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "chevronDownCircle.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/chevronDown_24.imageset/chevronDownCircle-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS08-Shook/667278070168ce27a9a03922cc176aca93952eb6/Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/chevronDown_24.imageset/chevronDownCircle-1.png -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/chevronDown_24.imageset/chevronDownCircle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS08-Shook/667278070168ce27a9a03922cc176aca93952eb6/Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/chevronDown_24.imageset/chevronDownCircle.png -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/heart_24.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "Property 1=Emoji@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "heart.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/heart_24.imageset/Property 1=Emoji@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS08-Shook/667278070168ce27a9a03922cc176aca93952eb6/Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/heart_24.imageset/Property 1=Emoji@2x.png -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/heart_24.imageset/heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS08-Shook/667278070168ce27a9a03922cc176aca93952eb6/Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/heart_24.imageset/heart.png -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/main_logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "main_logo@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "main_logo@3x.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/main_logo.imageset/main_logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS08-Shook/667278070168ce27a9a03922cc176aca93952eb6/Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/main_logo.imageset/main_logo@2x.png -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/main_logo.imageset/main_logo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS08-Shook/667278070168ce27a9a03922cc176aca93952eb6/Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/main_logo.imageset/main_logo@3x.png -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/pause_48.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "stop_48@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "stop_48@3x.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/pause_48.imageset/stop_48@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS08-Shook/667278070168ce27a9a03922cc176aca93952eb6/Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/pause_48.imageset/stop_48@2x.png -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/pause_48.imageset/stop_48@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS08-Shook/667278070168ce27a9a03922cc176aca93952eb6/Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/pause_48.imageset/stop_48@3x.png -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/play_48.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "play_48@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "play_48@3x.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/play_48.imageset/play_48@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS08-Shook/667278070168ce27a9a03922cc176aca93952eb6/Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/play_48.imageset/play_48@2x.png -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/play_48.imageset/play_48@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS08-Shook/667278070168ce27a9a03922cc176aca93952eb6/Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/play_48.imageset/play_48@3x.png -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/rewind_48.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "rewind_48@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "rewind_48@3x.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/rewind_48.imageset/rewind_48@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS08-Shook/667278070168ce27a9a03922cc176aca93952eb6/Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/rewind_48.imageset/rewind_48@2x.png -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/rewind_48.imageset/rewind_48@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS08-Shook/667278070168ce27a9a03922cc176aca93952eb6/Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/rewind_48.imageset/rewind_48@3x.png -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/send_24.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "send.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "Property 1=Send.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/send_24.imageset/Property 1=Send.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS08-Shook/667278070168ce27a9a03922cc176aca93952eb6/Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/send_24.imageset/Property 1=Send.png -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/send_24.imageset/send.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS08-Shook/667278070168ce27a9a03922cc176aca93952eb6/Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/send_24.imageset/send.png -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/tv_48.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "tv_48@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "tv_48@3x.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/tv_48.imageset/tv_48@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS08-Shook/667278070168ce27a9a03922cc176aca93952eb6/Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/tv_48.imageset/tv_48@2x.png -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/tv_48.imageset/tv_48@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS08-Shook/667278070168ce27a9a03922cc176aca93952eb6/Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/tv_48.imageset/tv_48@3x.png -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/xmark_24.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "xmarkCircle@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "xmarkCircle.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/xmark_24.imageset/xmarkCircle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS08-Shook/667278070168ce27a9a03922cc176aca93952eb6/Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/xmark_24.imageset/xmarkCircle.png -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/xmark_24.imageset/xmarkCircle@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS08-Shook/667278070168ce27a9a03922cc176aca93952eb6/Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/xmark_24.imageset/xmarkCircle@2x.png -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/zoomIn_24.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "Property 1=zoomIn@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "zoomIn.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/zoomIn_24.imageset/Property 1=zoomIn@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS08-Shook/667278070168ce27a9a03922cc176aca93952eb6/Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/zoomIn_24.imageset/Property 1=zoomIn@2x.png -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/zoomIn_24.imageset/zoomIn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS08-Shook/667278070168ce27a9a03922cc176aca93952eb6/Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/zoomIn_24.imageset/zoomIn.png -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/zoomOut_24.imageset/48@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS08-Shook/667278070168ce27a9a03922cc176aca93952eb6/Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/zoomOut_24.imageset/48@2x.png -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/zoomOut_24.imageset/48@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS08-Shook/667278070168ce27a9a03922cc176aca93952eb6/Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/zoomOut_24.imageset/48@3x.png -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/Image.xcassets/zoomOut_24.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "48@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "48@3x.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Resources/PretendardVariable.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS08-Shook/667278070168ce27a9a03922cc176aca93952eb6/Projects/UserInterfaces/DesignSystem/Resources/PretendardVariable.ttf -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Sources/SHFontSystem.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | // MARK: - SHFontable 4 | 5 | protocol SHFontable { 6 | var font: UIFont { get } 7 | } 8 | 9 | public extension UIFont { 10 | enum SHFontSystem: SHFontable { 11 | case title(weight: SHFontWeight = .bold) 12 | case body1(weight: SHFontWeight = .semiBold) 13 | case body2(weight: SHFontWeight = .regular) 14 | case body3(weight: SHFontWeight = .medium) 15 | case caption1(weight: SHFontWeight = .regular) 16 | case caption2(weight: SHFontWeight = .regular) 17 | } 18 | 19 | static func setFont(_ style: SHFontSystem) -> UIFont { 20 | style.font 21 | } 22 | } 23 | 24 | public extension UIFont.SHFontSystem { 25 | enum SHFontWeight { 26 | case bold 27 | case semiBold 28 | case regular 29 | case medium 30 | } 31 | 32 | var font: UIFont { 33 | UIFont(font: weight.font, size: size) ?? .init() 34 | } 35 | 36 | var weight: SHFontWeight { 37 | switch self { 38 | case let .body1(weight), 39 | let .body2(weight), 40 | let .body3(weight), 41 | let .caption1(weight), 42 | let .caption2(weight), 43 | let .title(weight): 44 | weight 45 | } 46 | } 47 | 48 | var size: CGFloat { 49 | switch self { 50 | case .title: 22 51 | case .body1: 17 52 | case .body2: 16 53 | case .body3: 14 54 | case .caption1: 12 55 | case .caption2: 10 56 | } 57 | } 58 | } 59 | 60 | private extension UIFont.SHFontSystem.SHFontWeight { 61 | var font: DesignSystemFontConvertible { 62 | switch self { 63 | case .bold: DesignSystemFontFamily.PretendardVariable.bold 64 | case .semiBold: DesignSystemFontFamily.PretendardVariable.semiBold 65 | case .regular: DesignSystemFontFamily.PretendardVariable.regular 66 | case .medium: DesignSystemFontFamily.PretendardVariable.medium 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Sources/SHLoadingView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | import Lottie 4 | 5 | public final class SHLoadingView: UIView { 6 | public var message: String 7 | private let blurEffectView: UIVisualEffectView = { 8 | let blurEffect = UIBlurEffect(style: .systemUltraThinMaterialDark) 9 | let view = UIVisualEffectView(effect: blurEffect) 10 | view.translatesAutoresizingMaskIntoConstraints = false 11 | return view 12 | }() 13 | 14 | private let animationView: LottieAnimationView = { 15 | let animation = LottieAnimationView(name: "loading", bundle: Bundle(for: DesignSystemResources.self)) 16 | animation.loopMode = .loop 17 | animation.translatesAutoresizingMaskIntoConstraints = false 18 | return animation 19 | }() 20 | 21 | private let messageLabel: UILabel = { 22 | let label = UILabel() 23 | label.font = UIFont.systemFont(ofSize: 16, weight: .medium) 24 | label.textColor = .white 25 | label.textAlignment = .center 26 | label.translatesAutoresizingMaskIntoConstraints = false 27 | return label 28 | }() 29 | 30 | public init(message: String) { 31 | self.message = message 32 | super.init(frame: .zero) 33 | setupViews() 34 | setupConstraints() 35 | setupStyles() 36 | animationView.play() 37 | } 38 | 39 | @available(*, unavailable) 40 | required init?(coder _: NSCoder) { 41 | fatalError("init(coder:) has not been implemented") 42 | } 43 | 44 | private func setupViews() { 45 | addSubview(blurEffectView) 46 | addSubview(animationView) 47 | addSubview(messageLabel) 48 | 49 | messageLabel.text = message 50 | } 51 | 52 | private func setupConstraints() { 53 | NSLayoutConstraint.activate([ 54 | blurEffectView.centerXAnchor.constraint(equalTo: centerXAnchor), 55 | blurEffectView.centerYAnchor.constraint(equalTo: centerYAnchor), 56 | blurEffectView.widthAnchor.constraint(equalToConstant: 200), 57 | blurEffectView.heightAnchor.constraint(equalToConstant: 200) 58 | ]) 59 | 60 | NSLayoutConstraint.activate([ 61 | animationView.centerXAnchor.constraint(equalTo: centerXAnchor), 62 | animationView.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -24), 63 | animationView.widthAnchor.constraint(equalToConstant: 300), 64 | animationView.heightAnchor.constraint(equalToConstant: 150) 65 | ]) 66 | 67 | NSLayoutConstraint.activate([ 68 | messageLabel.centerXAnchor.constraint(equalTo: centerXAnchor), 69 | messageLabel.centerYAnchor.constraint(equalTo: centerYAnchor, constant: 56) 70 | ]) 71 | } 72 | 73 | private func setupStyles() { 74 | blurEffectView.layer.cornerRadius = 24 75 | blurEffectView.clipsToBounds = true 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Projects/UserInterfaces/DesignSystem/Sources/SHRefreshControl.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | import Lottie 4 | 5 | public final class SHRefreshControl: UIRefreshControl { 6 | private let animationView = LottieAnimationView(name: "shook", bundle: Bundle(for: DesignSystemResources.self)) 7 | 8 | override public init() { 9 | super.init(frame: .zero) 10 | setupView() 11 | setupLayout() 12 | } 13 | 14 | @available(*, unavailable) 15 | required init?(coder _: NSCoder) { 16 | fatalError("init(coder:) has not been implemented") 17 | } 18 | 19 | override public func beginRefreshing() { 20 | super.beginRefreshing() 21 | animationView.loopMode = .loop 22 | animationView.play() 23 | let generator = UIImpactFeedbackGenerator(style: .medium) 24 | generator.impactOccurred() 25 | } 26 | 27 | override public func endRefreshing() { 28 | animationView.loopMode = .playOnce 29 | animationView.play { isFinished in 30 | if isFinished { 31 | super.endRefreshing() 32 | self.animationView.stop() 33 | } 34 | } 35 | } 36 | 37 | func setupView() { 38 | tintColor = .clear 39 | addSubview(animationView) 40 | addTarget(self, action: #selector(beginRefreshing), for: .valueChanged) 41 | } 42 | 43 | func setupLayout() { 44 | animationView.translatesAutoresizingMaskIntoConstraints = false 45 | NSLayoutConstraint.activate([ 46 | animationView.centerXAnchor.constraint(equalTo: centerXAnchor), 47 | animationView.centerYAnchor.constraint(equalTo: centerYAnchor), 48 | animationView.widthAnchor.constraint(equalToConstant: 100), 49 | animationView.heightAnchor.constraint(equalToConstant: 80) 50 | ]) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Scripts/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - trailing_whitespace 3 | - line_length 4 | - identifier_name 5 | - type_name 6 | 7 | opt_in_rules: 8 | - empty_count 9 | - empty_string 10 | - sorted_imports 11 | - trailing_closure 12 | - vertical_whitespace_between_cases 13 | - yoda_condition 14 | 15 | included: 16 | 17 | excluded: 18 | - /Tuist 19 | - '**/Project.swift' 20 | - '**/Derived/**' 21 | 22 | custom_rules: 23 | no_print_statements: 24 | name: "나야 프린트..." 25 | regex: "print\\s*\\(.*\\)" 26 | message: "You should remove print statements." 27 | severity: warning -------------------------------------------------------------------------------- /Scripts/Setup.sh: -------------------------------------------------------------------------------- 1 | # brew 확인 2 | if test -d "/opt/homebrew/bin/"; then 3 | PATH="/opt/homebrew/bin/:${PATH}" 4 | fi 5 | 6 | if which swiftlint > /dev/null; then 7 | echo "✅ swiftlint 설치 확인" 8 | else 9 | echo "❌ swiftlint 설치 필요, swiftlint를 설치합나다." 10 | brew install swiftlint 11 | fi 12 | 13 | if which swiftformat > /dev/null; then 14 | echo "✅ swiftformat 설치 확인" 15 | else 16 | echo "❌ swiftformat 설치 필요, swiftformat을 설치합나다." 17 | brew install swiftformat 18 | fi 19 | 20 | if which tuist > /dev/null; then 21 | echo "✅ tuist 설치 확인" 22 | else 23 | echo "❌ tuist 설치 필요 tuist를 설치합나다." 24 | 25 | curl https://mise.run | sh 26 | echo 'eval "$(~/.local/bin/mise activate --shims zsh)"' >> ~/.zshrc 27 | source ~/.zshrc 28 | 29 | mise install tuist 30 | tuist version 31 | fi 32 | 33 | echo "✅ 개발 환경 기본 세팅 완료" -------------------------------------------------------------------------------- /Scripts/SwiftLintRunScript.sh: -------------------------------------------------------------------------------- 1 | if test -d "/opt/homebrew/bin/"; then 2 | PATH="/opt/homebrew/bin/:${PATH}" 3 | fi 4 | 5 | export PATH # 환경변수 PATH 사용 6 | 7 | # 현재 디렉토리 절대 경로 8 | YML="$(dirname "$0")/.swiftlint.yml" 9 | 10 | if which swiftlint > /dev/null; then 11 | swiftlint --config ${YML} 12 | else 13 | echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint" 14 | fi -------------------------------------------------------------------------------- /Tuist/Config.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | 3 | let config = Config( 4 | plugins: [ 5 | .local(path: .relativeToRoot("Plugin/TemplatePlugin")), 6 | .local(path: .relativeToRoot("Plugin/EnvironmentPlugin")), 7 | .local(path: .relativeToRoot("Plugin/DependencyPlugin")), 8 | .local(path: .relativeToRoot("Plugin/ConfigurationPlugin")), 9 | ], 10 | generationOptions: .options() 11 | ) 12 | -------------------------------------------------------------------------------- /Tuist/ProjectDescriptionHelpers/Environment/GenerationEnvironment.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ProjectDescription 3 | import ConfigurationPlugin 4 | 5 | public enum GenerationEnvironment : String { 6 | case ci = "CI" 7 | case dev = "DEV" 8 | case prod = "PROD" 9 | } 10 | 11 | // 환경변수 받기 12 | let tuistEnv = ProcessInfo.processInfo.environment["TUIST_ENV"] ?? "" 13 | public let generationEnvironment = GenerationEnvironment(rawValue: tuistEnv) ?? .dev 14 | 15 | public extension GenerationEnvironment { 16 | var scripts: [TargetScript] { // 환경변수에 따라 동작할 스크립트 구분 17 | switch self { 18 | 19 | case .ci ,.prod: 20 | return [] 21 | 22 | case .dev: 23 | return [.swiftLint] 24 | } 25 | } 26 | 27 | var configurations: [Configuration] { 28 | switch self { 29 | case .ci: 30 | return [ 31 | .debug(name: .debug), 32 | .release(name: .release) 33 | ] 34 | case .dev, .prod: 35 | return .default 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Tuist/ProjectDescriptionHelpers/Extensions/InfoPlist+Extension.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | 3 | extension InfoPlist { 4 | static var demoDefulat: InfoPlist = .extendingDefault(with: [ 5 | "UIMainStoryboardFile": "", 6 | "UILaunchStoryboardName": "LaunchScreen", 7 | "ENABLE_TESTS": .boolean(true), 8 | "NSAppTransportSecurity": [ 9 | "NSAllowsArbitraryLoads": .boolean(true) 10 | ], 11 | "SECRETS": [ 12 | "ACCESS_KEY" : "$(ACCESS_KEY)", 13 | "SECRET_KEY": "$(SECRET_KEY)", 14 | "PORT": "${PORT}", 15 | "HOST": "${HOST}" 16 | ] 17 | ]) 18 | 19 | static var projectDefault: InfoPlist = .extendingDefault( 20 | with: [ 21 | "UILaunchStoryboardName": "Resources/LaunchScreen", 22 | "UIApplicationSceneManifest": [ 23 | "UIApplicationSupportsMultipleScenes": false, 24 | "UISceneConfigurations": [ 25 | "UIWindowSceneSessionRoleApplication": [ 26 | [ 27 | "UISceneConfigurationName": "Default Configuration", 28 | "UISceneDelegateClassName": "$(PRODUCT_MODULE_NAME).SceneDelegate" 29 | ], 30 | ] 31 | ] 32 | ], 33 | "UIUserInterfaceStyle": "Dark", 34 | "NSAppTransportSecurity": [ 35 | "NSAllowsArbitraryLoads": .boolean(true) 36 | ], 37 | "CFBundleShortVersionString": "$(MARKETING_VERSION)", 38 | "SECRETS": [ 39 | "ACCESS_KEY": "$(ACCESS_KEY)", 40 | "SECRET_KEY": "$(SECRET_KEY)", 41 | "PORT": "${PORT}", 42 | "HOST": "${HOST}", 43 | "CDN_DOMAIN": "$(CDN_DOMAIN)", 44 | "PROFILE_ID": "$(PROFILE_ID)", 45 | "CDN_INSTANCE_NO": "$(CDN_INSTANCE_NO)" 46 | ] 47 | ] 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /Tuist/ProjectDescriptionHelpers/Extensions/Project+Extension.swift: -------------------------------------------------------------------------------- 1 | import ConfigurationPlugin 2 | import DependencyPlugin 3 | import EnvironmentPlugin 4 | import Foundation 5 | import ProjectDescription 6 | 7 | public extension Project { 8 | 9 | static let project: Project = Project( 10 | name: env.name, 11 | organizationName: env.organizationName, 12 | packages: [], 13 | settings: .settings(base: .makeProjectSetting(), configurations: generationEnvironment.configurations, defaultSettings: .recommended), 14 | targets: [.projectTarget, .projectTestTarget, .broadcastExtension], 15 | schemes: [.projectDebugScheme, .projectReleaseScheme] 16 | ) 17 | 18 | static func module( 19 | name: String, 20 | options: Options = .options(), 21 | packages: [Package] = [], 22 | settings: Settings = .settings( 23 | base: baseSetting, 24 | configurations: generationEnvironment.configurations, 25 | defaultSettings: .recommended 26 | ), 27 | targets: [Target], 28 | fileHeaderTemplate: FileHeaderTemplate? = nil, 29 | additionalFiles: [FileElement] = [], 30 | resourceSynthesizers: [ResourceSynthesizer] = .default 31 | ) -> Project { 32 | return Project( 33 | name: name, 34 | organizationName: env.organizationName, 35 | options: options, 36 | packages: packages, 37 | settings: settings, 38 | targets: targets, 39 | schemes: targets.contains { $0.product == .app } ? 40 | [.makeScheme(configuration: .debug, name: name), .makeDemoScheme(configuration: .debug, name: name)] : 41 | [.makeScheme(configuration: .debug, name: name)], 42 | fileHeaderTemplate: fileHeaderTemplate, 43 | additionalFiles: additionalFiles, 44 | resourceSynthesizers: resourceSynthesizers 45 | ) 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /Tuist/ProjectDescriptionHelpers/Extensions/SettingsDictionary+Extension.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | 3 | public let baseSetting: SettingsDictionary = SettingsDictionary() 4 | 5 | extension SettingsDictionary { 6 | enum SettingProperty { 7 | case alternateAppIconNames(array: [String]) 8 | 9 | var value: SettingsDictionary { 10 | switch self { 11 | case let .alternateAppIconNames(array): 12 | return ["ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES": .array(array)] 13 | } 14 | } 15 | } 16 | } 17 | 18 | extension SettingsDictionary { 19 | 20 | static func makeProjectSetting() -> SettingsDictionary { 21 | return baseSetting 22 | .marketingVersion("1.3") 23 | // .swiftVersion("6.0") 24 | } 25 | 26 | func merging(property: SettingProperty) -> SettingsDictionary { 27 | return self.merging(property.value) 28 | } 29 | 30 | } 31 | 32 | 33 | -------------------------------------------------------------------------------- /Tuist/ProjectDescriptionHelpers/Extensions/TargetScript+Extension.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | import DependencyPlugin 3 | 4 | public extension TargetScript { 5 | static let swiftLint = TargetScript.pre(path: .scripts + "/SwiftLintRunScript.sh" , name: "SwiftLint", basedOnDependencyAnalysis: false) 6 | } 7 | -------------------------------------------------------------------------------- /Tuist/ProjectDescriptionHelpers/Protocol/Configurable.swift: -------------------------------------------------------------------------------- 1 | protocol Configurable { 2 | func with(_ block: (inout Self) throws -> Void) rethrows -> Self 3 | } 4 | 5 | extension Configurable { 6 | @inlinable // 클로저를 이용하여 configure 진행 7 | func with(_ block: (inout Self) throws -> Void) rethrows -> Self { 8 | var copy = self 9 | try block(©) 10 | return copy 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Workspace.swift: -------------------------------------------------------------------------------- 1 | import ProjectDescription 2 | import EnvironmentPlugin 3 | 4 | let workspace = Workspace( 5 | name: env.name, 6 | projects: [ 7 | "Projects/App" 8 | ] 9 | ) 10 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | install: # tuist를 이용해서 SPM 설치 2 | tuist install 3 | 4 | generate: # 프로젝트 만들기 5 | make install 6 | TUIST_ENV=DEV TUIST_ROOT_DIR=${PWD} tuist generate 7 | 8 | setup: # 기본 셋팅 점검 , 린트 설치 ? , swiftFormat 설치, tuist 설치 9 | sh Scripts/Setup.sh 10 | 11 | tp: 12 | swift Scripts/generatePlugin.swift 13 | 14 | module: 15 | swift Scripts/generateModule.swift 16 | 17 | test: 18 | TUIST_ENV=CI TUIST_ROOT_DIR=${PWD} tuist test --platform ios 19 | 20 | clean: 21 | rm -rf **/*.xcodeproj 22 | rm -rf *.xcworkspace 23 | 24 | reset: 25 | tuist clean 26 | rm -rf **/*.xcodeproj 27 | rm -rf *.xcworkspace 28 | 29 | graph: #외부 의존성 그래프에서 제거 30 | tuist graph -d 31 | --------------------------------------------------------------------------------