├── .github ├── ISSUE_TEMPLATE │ └── 이슈-템플릿.md ├── pull_request_template.md └── workflows │ └── CI.yml ├── .gitignore ├── PhotoGether ├── BaseFeature │ └── Extension │ │ ├── UIButton+Publisher.swift │ │ └── UIControl+Publisher.swift ├── Core │ └── CoreModule │ │ ├── CoreModule.xcodeproj │ │ └── project.pbxproj │ │ └── CoreModule │ │ ├── Collection+Extension.swift │ │ ├── NotifiactionName+.swift │ │ ├── PTGLogger.swift │ │ └── Utils.swift ├── DataLayer │ ├── DataUtility │ │ └── DataUtility │ │ │ └── Temp.swift │ ├── PhotoGetherData │ │ ├── PhotoGetherData.xcodeproj │ │ │ └── project.pbxproj │ │ └── PhotoGetherData │ │ │ ├── CapturableVideoView.swift │ │ │ ├── ConnectionClientImpl.swift │ │ │ ├── ConnectionRepositoryImpl.swift │ │ │ ├── Disk │ │ │ └── CacheManager.swift │ │ │ ├── EmojiDTO.swift │ │ │ ├── EventConnection │ │ │ ├── EventConnectionGuestRepositoryImpl.swift │ │ │ └── EventConnectionHostRepositoryImpl.swift │ │ │ ├── EventHub │ │ │ └── EventHub.swift │ │ │ ├── Extension │ │ │ ├── Data+toDTO.swift │ │ │ ├── Encodable+toData.swift │ │ │ └── UIView+flipHorizontally.swift │ │ │ ├── Interface │ │ │ ├── DTO │ │ │ │ ├── RequestDTO │ │ │ │ │ ├── RoomRequestDTO.swift │ │ │ │ │ └── SignalingRequestDTO.swift │ │ │ │ └── ResponseDTO │ │ │ │ │ ├── RoomResponseDTO.swift │ │ │ │ │ └── SignalingResponseDTO.swift │ │ │ ├── Delegate │ │ │ │ └── RoomServiceDelegate.swift │ │ │ ├── Message │ │ │ │ ├── CreateRoomResponseMessage.swift │ │ │ │ ├── IceCandidateMessage.swift │ │ │ │ ├── JoinRoomRequestMessage.swift │ │ │ │ ├── JoinRoomResponseMessage.swift │ │ │ │ ├── NotifyNewUserMessage.swift │ │ │ │ └── SessionDescriptionMessage.swift │ │ │ └── Service │ │ │ │ ├── SignalingService.swift │ │ │ │ └── WebRTCService.swift │ │ │ ├── LocalShapeDataSourceImpl.swift │ │ │ ├── PeerConnectionSupport.swift │ │ │ ├── RemoteShapeDataSourceImpl.swift │ │ │ ├── ServiceImpl │ │ │ ├── RoomServiceImpl.swift │ │ │ ├── SignalingServiceImpl.swift │ │ │ └── WebRTCServiceImpl.swift │ │ │ ├── ShapeDataSource.swift │ │ │ └── ShapeRepositoryImpl.swift │ └── PhotoGetherNetwork │ │ ├── PhotoGetherNetwork.xcodeproj │ │ └── project.pbxproj │ │ └── PhotoGetherNetwork │ │ ├── Extension │ │ └── Data+toDTO.swift │ │ ├── HTTP │ │ ├── APIError.swift │ │ ├── EndPoint.swift │ │ └── Request.swift │ │ └── WebSocket │ │ ├── WebSocketClient.swift │ │ ├── WebSocketClientDelegate.swift │ │ ├── WebSocketClientImpl.swift │ │ ├── WebSocketRequestable.swift │ │ └── WebSocketResponsable.swift ├── DomainLayer │ ├── DomainUtility │ │ └── DomainUtility │ │ │ └── Temp.swift │ └── PhotoGetherDomain │ │ ├── CountClientsUseCaseTests │ │ └── CountClientsUseCaseTests.swift │ │ ├── FetchStickerListUseCaseTests │ │ └── FetchEmojiListUseCaseTests.swift │ │ ├── PhotoGetherDomain.xcodeproj │ │ ├── project.pbxproj │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── FetchStickerListUseCaseTests.xcscheme │ │ ├── PhotoGetherDomain │ │ └── UseCaseImpl │ │ │ ├── CaptureVideosUseCaseImpl.swift │ │ │ ├── CountClientsUseCaseImpl.swift │ │ │ ├── CreateRoomUseCaseImpl.swift │ │ │ ├── DidEnterNewUserPublisherUseCaseImpl.swift │ │ │ ├── FetchEmojiListUseCaseImpl.swift │ │ │ ├── GetLocalVideoUseCaseImpl.swift │ │ │ ├── GetRemoteVideoUseCaseImpl.swift │ │ │ ├── GetVoiceInputStateUseCaseImpl.swift │ │ │ ├── JoinRoomUseCaseImpl.swift │ │ │ ├── ReceiveFrameUseCaseImpl.swift │ │ │ ├── ReceiveStickerListUseCaseImpl.swift │ │ │ ├── SendFrameToRepositoryUseCaseImpl.swift │ │ │ ├── SendOfferUseCaseImpl.swift │ │ │ ├── SendStickerToRepositoryUseCaseImpl.swift │ │ │ ├── StopVideoCaptureUseCaseImpl.swift │ │ │ └── ToggleLocalMicStateUseCaseImpl.swift │ │ ├── PhotoGetherDomainInterface │ │ ├── Component │ │ │ └── SharePhotoComponent.swift │ │ ├── ConnectionClient.swift │ │ ├── Entity │ │ │ ├── EmojiEntity.swift │ │ │ ├── EventEntity.swift │ │ │ ├── FrameEntity.swift │ │ │ ├── JoinRoomEntity.swift │ │ │ ├── NotifyNewUserEntity.swift │ │ │ ├── RoomOwnerEntity.swift │ │ │ ├── StickerEntity.swift │ │ │ ├── UserEntity.swift │ │ │ └── UserInfo.swift │ │ ├── Repository │ │ │ ├── ConnectionRepository.swift │ │ │ ├── EventConnectionRepository.swift │ │ │ └── ShapeRepository.swift │ │ ├── Service │ │ │ └── RoomService.swift │ │ └── UseCase │ │ │ ├── CaptureVideosUseCase.swift │ │ │ ├── CountClientsUseCase.swift │ │ │ ├── CreateRoomUseCase.swift │ │ │ ├── DidEnterNewUserPublisherUseCase.swift │ │ │ ├── FetchEmojiListUseCase.swift │ │ │ ├── GetLocalVideoUseCase.swift │ │ │ ├── GetRemoteVideoUseCase.swift │ │ │ ├── GetVoiceInputStateUseCase.swift │ │ │ ├── JoinRoomUseCase.swift │ │ │ ├── ReceiveFrameUseCase.swift │ │ │ ├── ReceiveStickerListUseCase.swift │ │ │ ├── SendFrameToRepositoryUseCase.swift │ │ │ ├── SendOfferUseCase.swift │ │ │ ├── SendStickerToRepositoryUseCase.swift │ │ │ ├── StopVideoCaptureUseCase.swift │ │ │ └── ToggleLocalMicStateUseCase.swift │ │ └── PhotoGetherDomainTesting │ │ ├── ConnectionClientMock.swift │ │ ├── ConnectionRepositoryMock.swift │ │ ├── CountClientsUseCaseMock.swift │ │ ├── FetchEmojiListUseCaseMock.swift │ │ ├── JoinRoomUseCaseMock.swift │ │ ├── Resource │ │ ├── blackHeart.png │ │ ├── bug.png │ │ ├── cat.png │ │ ├── crown.png │ │ ├── dog.png │ │ ├── lips.png │ │ ├── parkBug.png │ │ ├── racoon.png │ │ ├── redHeart.png │ │ ├── star.png │ │ ├── sunglasses.png │ │ └── tree.png │ │ └── ShapeRepositoryMock.swift ├── PhotoGether.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── PhotoGether.xcscheme ├── PhotoGether.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── swiftpm │ │ └── Package.resolved ├── PhotoGether │ ├── App │ │ ├── AppDelegate.swift │ │ └── SceneDelegate.swift │ ├── Resource │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── 12sd.png │ │ │ │ ├── Contents.json │ │ │ │ └── qas12.png │ │ │ └── Contents.json │ │ ├── Base.lproj │ │ │ └── LaunchScreen.storyboard │ │ └── Info.plist │ └── Source │ │ ├── AppPermissionManager.swift │ │ ├── DeepLinkParser.swift │ │ └── Secrets.swift ├── PresentationLayer │ ├── BaseFeature │ │ ├── BaseFeature.xcodeproj │ │ │ └── project.pbxproj │ │ └── BaseFeature │ │ │ ├── BaseViewController.swift │ │ │ ├── Extension │ │ │ ├── UIButton+Publisher.swift │ │ │ ├── UICollectionViewCell+Identifier.swift │ │ │ ├── UIControl+Publisher.swift │ │ │ ├── UIImageView+SetAsyncImage.swift │ │ │ ├── UILabel+SetKern.swift │ │ │ └── UIViewController+ShowToast.swift │ │ │ └── ImageCache │ │ │ ├── CacheableImage.swift │ │ │ └── ImageCache.swift │ ├── DesignSystem │ │ ├── DesignSystem.xcodeproj │ │ │ └── project.pbxproj │ │ └── DesignSystem │ │ │ ├── Resource │ │ │ ├── DesignSystem.xcassets │ │ │ │ ├── Contents.json │ │ │ │ ├── MockIcon │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── blackHeart.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── blackHeart.svg │ │ │ │ │ ├── bug.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── bug.svg │ │ │ │ │ ├── cat.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── cat.svg │ │ │ │ │ ├── crown.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── crown.svg │ │ │ │ │ ├── dog.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── dog.svg │ │ │ │ │ ├── lips.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── lips.svg │ │ │ │ │ ├── parkBug.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── parkBug.svg │ │ │ │ │ ├── racoon.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── racoon.svg │ │ │ │ │ ├── redHeart.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── redHeart.svg │ │ │ │ │ ├── star.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── star.svg │ │ │ │ │ ├── sunglasses.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── sunglasses.svg │ │ │ │ │ └── tree.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── tree.svg │ │ │ │ ├── PTGColor │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── Gray10.colorset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── Gray20.colorset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── Gray30.colorset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── Gray40.colorset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── Gray50.colorset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── Gray60.colorset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── Gray70.colorset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── Gray80.colorset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── Gray85.colorset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── Gray90.colorset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── PrimaryGreen.colorset │ │ │ │ │ │ └── Contents.json │ │ │ │ ├── PTGIcon │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── PTGresize.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── PTGresize.svg │ │ │ │ │ ├── PTGxmark.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── PTGxmark.svg │ │ │ │ │ ├── chevronLeftWhite.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── chevronLeftWhite.svg │ │ │ │ │ ├── chevronRightBlack.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── chevronRightBlack.svg │ │ │ │ │ ├── ellipsisIcon.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── ellipsis.png │ │ │ │ │ ├── filterIcon.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── wand.and.stars.svg │ │ │ │ │ ├── frameIcon.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── frameIcon.svg │ │ │ │ │ ├── stickerIcon.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── stickerIcon.svg │ │ │ │ │ ├── switchIcon.imageset │ │ │ │ │ │ ├── Change.svg │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── temp1.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── temp1.png │ │ │ │ │ ├── temp10.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── ww2.jpg │ │ │ │ │ ├── temp11.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── ee2.jpeg │ │ │ │ │ ├── temp12.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── rr2.jpeg │ │ │ │ │ ├── temp2.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── temp2.png │ │ │ │ │ ├── temp3.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── temp3.png │ │ │ │ │ ├── temp4.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── temp4.png │ │ │ │ │ ├── temp5.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── qq1.jpg │ │ │ │ │ ├── temp6.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── ww1.jpeg │ │ │ │ │ ├── temp7.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── ee1.jpeg │ │ │ │ │ ├── temp8.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── rr1.png │ │ │ │ │ └── temp9.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── qq2.jpg │ │ │ │ └── sampleImage.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── sampleImage.png │ │ │ │ │ ├── sampleImage@2x.png │ │ │ │ │ └── sampleImage@3x.png │ │ │ ├── Fonts │ │ │ │ ├── Pretendard-Black.ttf │ │ │ │ ├── Pretendard-Bold.ttf │ │ │ │ ├── Pretendard-ExtraBold.ttf │ │ │ │ ├── Pretendard-ExtraLight.ttf │ │ │ │ ├── Pretendard-Light.ttf │ │ │ │ ├── Pretendard-Medium.ttf │ │ │ │ ├── Pretendard-Regular.ttf │ │ │ │ ├── Pretendard-SemiBold.ttf │ │ │ │ └── Pretendard-Thin.ttf │ │ │ └── Info.plist │ │ │ └── Source │ │ │ ├── PTGCircleButton.swift │ │ │ ├── PTGColor.swift │ │ │ ├── PTGGrayButton.swift │ │ │ ├── PTGImage.swift │ │ │ ├── PTGMicButton.swift │ │ │ ├── PTGPaddingLabel.swift │ │ │ ├── PTGParticipantsGridView.swift │ │ │ ├── PTGParticipantsView.swift │ │ │ ├── PTGPrimaryButton.swift │ │ │ ├── PlaceHolderView.swift │ │ │ ├── UIColor+.swift │ │ │ └── UIFont+PTGFont.swift │ ├── EditPhotoRoomFeature │ │ ├── EditPhotoRoomFeature.xcodeproj │ │ │ ├── project.pbxproj │ │ │ └── xcshareddata │ │ │ │ └── xcschemes │ │ │ │ ├── EditPhotoRoomFeature.xcscheme │ │ │ │ └── EditPhotoRoomFeatureDemo.xcscheme │ │ ├── EditPhotoRoomFeature │ │ │ └── Source │ │ │ │ ├── CanvasScrollView.swift │ │ │ │ ├── EditPhotoGuestBottomView.swift │ │ │ │ ├── EditPhotoHostBottomView.swift │ │ │ │ ├── EditPhotoRoomGuestViewController.swift │ │ │ │ ├── EditPhotoRoomGuestViewModel.swift │ │ │ │ ├── EditPhotoRoomHostViewController.swift │ │ │ │ ├── EditPhotoRoomHostViewModel.swift │ │ │ │ ├── FrameImageGenerator │ │ │ │ ├── Frame │ │ │ │ │ ├── Common │ │ │ │ │ │ ├── FrameConstants.swift │ │ │ │ │ │ ├── FrameImageView.swift │ │ │ │ │ │ └── FrameView.swift │ │ │ │ │ └── DefaultFrame │ │ │ │ │ │ ├── DefaultFrameImageView.swift │ │ │ │ │ │ └── DefaultFrameView.swift │ │ │ │ ├── FrameImageGenerator.swift │ │ │ │ └── FrameViewRenderable.swift │ │ │ │ ├── StickerBottomSheetViewController.swift │ │ │ │ ├── StickerBottomSheetViewModel.swift │ │ │ │ ├── StickerCollectionView.swift │ │ │ │ ├── StickerCollectionViewCell.swift │ │ │ │ └── View │ │ │ │ └── StickerView.swift │ │ ├── EditPhotoRoomFeatureDemo │ │ │ ├── App │ │ │ │ ├── AppDelegate.swift │ │ │ │ └── SceneDelegate.swift │ │ │ ├── Resource │ │ │ │ ├── Assets.xcassets │ │ │ │ │ ├── AccentColor.colorset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── AppIcon.appiconset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── EditRoomIcon.png │ │ │ │ │ └── Contents.json │ │ │ │ ├── Base.lproj │ │ │ │ │ └── LaunchScreen.storyboard │ │ │ │ └── Info.plist │ │ │ └── Source │ │ │ │ └── OfferTempViewController.swift │ │ └── EditPhotoRoomHostViewModelTests │ │ │ └── EditPhotoRoomHostViewModelTests.swift │ ├── FeatureTesting │ │ └── FeatureTesting │ │ │ └── PhotoRoomViewControllerMock.swift │ ├── PhotoRoomFeature │ │ ├── PhotoRoomFeature.xcodeproj │ │ │ ├── project.pbxproj │ │ │ └── xcshareddata │ │ │ │ └── xcschemes │ │ │ │ ├── PhotoRoomFeature.xcscheme │ │ │ │ └── PhotoRoomFeatureDemo.xcscheme │ │ ├── PhotoRoomFeature │ │ │ └── Source │ │ │ │ ├── View │ │ │ │ ├── CameraButton.swift │ │ │ │ ├── PhotoRoomBottomView.swift │ │ │ │ └── PlaceHolderView.swift │ │ │ │ ├── ViewController │ │ │ │ └── PhotoRoomViewController.swift │ │ │ │ └── ViewModel │ │ │ │ └── PhotoRoomViewModel.swift │ │ └── PhotoRoomFeatureDemo │ │ │ ├── App │ │ │ ├── AppDelegate.swift │ │ │ └── SceneDelegate.swift │ │ │ └── Resource │ │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ └── PhotoRoomIcon.png │ │ │ └── Contents.json │ │ │ ├── Base.lproj │ │ │ └── LaunchScreen.storyboard │ │ │ └── Info.plist │ ├── SharePhotoFeature │ │ ├── SharePhotoFeature.xcodeproj │ │ │ ├── project.pbxproj │ │ │ └── xcshareddata │ │ │ │ └── xcschemes │ │ │ │ └── SharePhotoFeatureDemo.xcscheme │ │ ├── SharePhotoFeature │ │ │ └── Source │ │ │ │ ├── SharePhotoBottomView.swift │ │ │ │ ├── SharePhotoViewController.swift │ │ │ │ ├── SharePhotoViewModel.swift │ │ │ │ └── Utility │ │ │ │ ├── PhotoLibraryHelper.swift │ │ │ │ └── PhotoLibraryPermissionManager.swift │ │ └── SharePhotoFeatureDemo │ │ │ ├── App │ │ │ ├── AppDelegate.swift │ │ │ └── SceneDelegate.swift │ │ │ └── Resource │ │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ └── ShareRoomIcon.png │ │ │ └── Contents.json │ │ │ ├── Base.lproj │ │ │ └── LaunchScreen.storyboard │ │ │ └── Info.plist │ └── WaitingRoomFeature │ │ ├── WaitingRoomFeature.xcodeproj │ │ ├── project.pbxproj │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ ├── WaitingRoomFeature.xcscheme │ │ │ └── WaitingRoomFeatureDemo.xcscheme │ │ ├── WaitingRoomFeature │ │ └── Source │ │ │ ├── View │ │ │ └── WaitingRoomView.swift │ │ │ ├── ViewController │ │ │ ├── EnterLoadingViewController.swift │ │ │ └── WaitingRoomViewController.swift │ │ │ └── ViewModel │ │ │ ├── EnterLoadingViewModel.swift │ │ │ └── WaitingRoomViewModel.swift │ │ └── WaitingRoomFeatureDemo │ │ ├── App │ │ ├── AppDelegate.swift │ │ └── SceneDelegate.swift │ │ └── Resource │ │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ └── WaitRoomIcon.png │ │ └── Contents.json │ │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ │ └── Info.plist └── Scripts │ ├── .swiftlint.yml │ └── SwiftLintRunScript.sh ├── PhotoGetherServer └── PhotoGetherServer │ ├── .dockerignore │ ├── .gitignore │ ├── Dockerfile │ ├── Package.resolved │ ├── Package.swift │ ├── Public │ └── .gitkeep │ ├── Sources │ └── App │ │ ├── Controllers │ │ ├── .gitkeep │ │ └── WebSocketController.swift │ │ ├── DTO │ │ ├── Message │ │ │ ├── IceCandidateMessage.swift │ │ │ ├── JoinRoomRequestMessage.swift │ │ │ └── SessionDescriptionMessage.swift │ │ ├── RequestDTO │ │ │ ├── RoomRequestDTO.swift │ │ │ ├── SignalingRequestDTO.swift │ │ │ └── WebSocketRequestType.swift │ │ └── ResponseDTO │ │ │ ├── CreateRoomResponseDTO.swift │ │ │ ├── JoinRoomResponseDTO.swift │ │ │ ├── NotifyNewUserResponseDTO.swift │ │ │ ├── RoomResponseDTO.swift │ │ │ └── SignalingResponsetDTO.swift │ │ ├── Error │ │ └── RoomError.swift │ │ ├── Model │ │ ├── Room.swift │ │ ├── RoomManager.swift │ │ └── User.swift │ │ ├── Utils │ │ ├── ByteBuffer+toDTO.swift │ │ ├── Data+toDTO.swift │ │ ├── Encodable+toData.swift │ │ ├── WebSocket+decodeDTO.swift │ │ └── WebSocket+sendDTO.swift │ │ ├── configure.swift │ │ ├── entrypoint.swift │ │ └── routes.swift │ ├── Tests │ ├── AppTests │ │ └── AppTests.swift │ └── RoomManagerTests │ │ └── RoomManagerTests.swift │ └── docker-compose.yml └── README.md /.github/ISSUE_TEMPLATE/이슈-템플릿.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 이슈 템플릿 3 | about: 기본_이슈_템플릿 4 | title: "[FEAT/#00] 작업 내용 요약" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | ## 🤔 작업 배경 10 | 11 | - 작업 배경을 적어주세요 12 | 13 | ## 📝 작업 내용 14 | 15 | - 작업 내용을 적어주세요 16 | 17 | ## ✔️ To-Do 18 | 19 | - [ ] 세부적으로 적어주세요 20 | 21 | ## 👀 ETC (추후 개발해야 할 것, 참고자료 등) 22 | 23 | 24 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## 🤔 배경 2 | 5 | 6 | ## 📃 작업 내역 7 | 11 | 12 | ## ✅ 리뷰 노트 13 | 20 | 21 | ## 🎨 스크린샷 22 | | iPhone SE(2세대) | iPhone 14 | iPhone 16 Pro Max | 23 | | -------------- | -------------- | -------------- | 24 | | 스샷 | 스샷 | 스샷 | 25 | 26 | 27 | ## 🚀 테스트 방법 28 | 31 | -------------------------------------------------------------------------------- /PhotoGether/BaseFeature/Extension/UIButton+Publisher.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import UIKit 3 | 4 | public extension UIButton { 5 | var tapPublisher: AnyPublisher { 6 | controlPublisher(for: .touchUpInside) 7 | .map { _ in } 8 | .eraseToAnyPublisher() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /PhotoGether/BaseFeature/Extension/UIControl+Publisher.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import UIKit 3 | 4 | public extension UIControl { 5 | /// Control Publisher 6 | func controlPublisher(for event: UIControl.Event) -> UIControl.EventPublisher { 7 | return UIControl.EventPublisher(control: self, event: event) 8 | } 9 | 10 | /// Event Publisher 11 | struct EventPublisher: Publisher { 12 | public typealias Output = UIControl 13 | public typealias Failure = Never 14 | 15 | let control: UIControl 16 | let event: UIControl.Event 17 | 18 | public func receive(subscriber: T) where T: Subscriber, Never == T.Failure, UIControl == T.Input { 19 | let subscription = EventSubscription(control: control, subscrier: subscriber, event: event) 20 | subscriber.receive(subscription: subscription) 21 | } 22 | } 23 | 24 | /// Event Subscription 25 | private class EventSubscription: Subscription where EventSubscriber.Input == UIControl, EventSubscriber.Failure == Never { 26 | let control: UIControl 27 | let event: UIControl.Event 28 | var subscriber: EventSubscriber? 29 | 30 | init(control: UIControl, subscrier: EventSubscriber, event: UIControl.Event) { 31 | self.control = control 32 | self.subscriber = subscrier 33 | self.event = event 34 | 35 | control.addTarget(self, action: #selector(eventDidOccur), for: event) 36 | } 37 | 38 | func request(_ demand: Subscribers.Demand) {} 39 | 40 | func cancel() { 41 | subscriber = nil 42 | control.removeTarget(self, action: #selector(eventDidOccur), for: event) 43 | } 44 | 45 | @objc func eventDidOccur() { 46 | _ = subscriber?.receive(control) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /PhotoGether/Core/CoreModule/CoreModule/Collection+Extension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension Collection { 4 | subscript(safe index: Index) -> Iterator.Element? { 5 | return indices.contains(index) ? self[index] : nil 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /PhotoGether/Core/CoreModule/CoreModule/NotifiactionName+.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension Notification.Name { 4 | static let navigateToPhotoRoom = Notification.Name("navigateToPhotoRoom") 5 | static let startCountDown = Notification.Name("startCountDown") 6 | static let navigateToShareRoom = Notification.Name("navigateToShareRoom") 7 | static let receiveNavigateToPhotoRoom = Notification.Name("receiveNavigateToPhotoRoom") 8 | static let receiveStartCountDown = Notification.Name("receiveStartCountDown") 9 | static let receiveNavigateToShareRoom = Notification.Name("receiveNavigateToShareRoom") 10 | } 11 | -------------------------------------------------------------------------------- /PhotoGether/Core/CoreModule/CoreModule/PTGLogger.swift: -------------------------------------------------------------------------------- 1 | import OSLog 2 | 3 | public struct PTGLogger { 4 | private let logger: Logger 5 | 6 | public init(subsystem: String = "PhotoGether", category: String) { 7 | self.logger = Logger(subsystem: subsystem, category: category) 8 | } 9 | 10 | public func log( 11 | _ message: String, 12 | level: OSLogType = .error, 13 | file: String = #file, 14 | function: String = #function, 15 | line: Int = #line 16 | ) { 17 | let fileName = (file as NSString).lastPathComponent 18 | logger.log( 19 | level: level, 20 | "[ 🚀 LOG ] \(fileName, privacy: .public):\(line) | \(function) | \(message, privacy: .public)" 21 | ) 22 | } 23 | } 24 | 25 | public extension PTGLogger { 26 | static let `default` = PTGLogger(subsystem: "PhotoGether", category: "Default") 27 | } 28 | -------------------------------------------------------------------------------- /PhotoGether/Core/CoreModule/CoreModule/Utils.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public func APP_HEIGHT() -> CGFloat { 4 | let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene 5 | return windowScene?.screen.bounds.size.height ?? .zero 6 | } 7 | -------------------------------------------------------------------------------- /PhotoGether/DataLayer/DataUtility/DataUtility/Temp.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public func hello() -> String { 4 | return "hello" 5 | } 6 | -------------------------------------------------------------------------------- /PhotoGether/DataLayer/PhotoGetherData/PhotoGetherData/EmojiDTO.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import PhotoGetherDomainInterface 3 | 4 | public struct EmojiDTO: Codable { 5 | let emoji, hexcode: String 6 | let group: EmojiGroup 7 | let subgroup: String 8 | let annotation: String 9 | let tags, shortcodes, emoticons: [String] 10 | let directional, variation: Bool 11 | let variationBase: Bool? 12 | let unicode: Double 13 | let order: Int 14 | let skintone: Int? 15 | let skintoneCombination, skintoneBase: String? 16 | } 17 | 18 | extension EmojiDTO { 19 | func toEntity() -> EmojiEntity { 20 | return .init( 21 | emoji: emoji, 22 | hexCode: hexcode, 23 | group: group, 24 | annotation: annotation 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /PhotoGether/DataLayer/PhotoGetherData/PhotoGetherData/Extension/Data+toDTO.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import PhotoGetherNetwork 3 | import CoreModule 4 | 5 | public extension Data { 6 | func toDTO(type: T.Type, decoder: JSONDecoder = JSONDecoder()) -> T? { 7 | do { 8 | return try decoder.decode(type, from: self) 9 | } catch let decodingError as DecodingError { 10 | PTGLogger.default.log("Decoding Error: \(decodingError.fullDescription)", level: .debug) 11 | } catch { 12 | PTGLogger.default.log("Unknown Decoding error: \(error.localizedDescription)", level: .debug) 13 | } 14 | return nil 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /PhotoGether/DataLayer/PhotoGetherData/PhotoGetherData/Extension/Encodable+toData.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Encodable { 4 | func toData(encoder: JSONEncoder) -> Data? { 5 | return try? encoder.encode(self) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /PhotoGether/DataLayer/PhotoGetherData/PhotoGetherData/Extension/UIView+flipHorizontally.swift: -------------------------------------------------------------------------------- 1 | import WebRTC 2 | 3 | extension RTCVideoRenderer { 4 | /// 좌우반전 5 | func flipHorizontally() -> Self { 6 | (self as! UIView).transform = CGAffineTransform(scaleX: -1.0, y: 1.0) 7 | return self 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /PhotoGether/DataLayer/PhotoGetherData/PhotoGetherData/Interface/DTO/RequestDTO/RoomRequestDTO.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import PhotoGetherNetwork 3 | 4 | struct RoomRequestDTO: WebSocketRequestable { 5 | var messageType: RoomMessageType 6 | var message: Data? 7 | 8 | init(messageType: RoomMessageType, message: Data? = nil) { 9 | self.messageType = messageType 10 | self.message = message 11 | } 12 | 13 | enum RoomMessageType: String, Encodable { 14 | case createRoom 15 | case joinRoom 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /PhotoGether/DataLayer/PhotoGetherData/PhotoGetherData/Interface/DTO/RequestDTO/SignalingRequestDTO.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import PhotoGetherNetwork 3 | 4 | public struct SignalingRequestDTO: WebSocketRequestable { 5 | public var messageType: SignalingMessageType 6 | public var message: Data? 7 | 8 | public init(messageType: SignalingMessageType, message: Data? = nil) { 9 | self.messageType = messageType 10 | self.message = message 11 | } 12 | 13 | public enum SignalingMessageType: String, Encodable { 14 | case offerSDP 15 | case answerSDP 16 | case iceCandidate 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /PhotoGether/DataLayer/PhotoGetherData/PhotoGetherData/Interface/DTO/ResponseDTO/RoomResponseDTO.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import PhotoGetherNetwork 3 | 4 | struct RoomResponseDTO: WebSocketResponsable { 5 | var messageType: RoomMessageType 6 | var message: Data? 7 | 8 | enum RoomMessageType: String, Decodable { 9 | case createRoom 10 | case joinRoom 11 | case notifyNewUser 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /PhotoGether/DataLayer/PhotoGetherData/PhotoGetherData/Interface/DTO/ResponseDTO/SignalingResponseDTO.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import PhotoGetherNetwork 3 | 4 | struct SignalingResponseDTO: WebSocketResponsable { 5 | var messageType: SignalingMessageType 6 | var message: Data? 7 | 8 | init(messageType: SignalingMessageType, message: Data? = nil) { 9 | self.messageType = messageType 10 | self.message = message 11 | } 12 | 13 | enum SignalingMessageType: String, Decodable { 14 | case offerSDP 15 | case answerSDP 16 | case iceCandidate 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /PhotoGether/DataLayer/PhotoGetherData/PhotoGetherData/Interface/Delegate/RoomServiceDelegate.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import PhotoGetherDomainInterface 3 | 4 | public protocol RoomServiceDelegate: AnyObject { 5 | func roomService(_ roomService: RoomService, didReceiveResponseCreateRoom response: String) 6 | func roomService(_ roomService: RoomService, didReceiveResponseJoinRoom response: String) 7 | } 8 | -------------------------------------------------------------------------------- /PhotoGether/DataLayer/PhotoGetherData/PhotoGetherData/Interface/Message/CreateRoomResponseMessage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import PhotoGetherDomainInterface 3 | 4 | public struct CreateRoomResponseMessage: Decodable { 5 | let roomID: String 6 | let hostID: String 7 | 8 | public func toEntity() -> RoomOwnerEntity { 9 | RoomOwnerEntity(roomID: self.roomID, hostID: self.hostID) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /PhotoGether/DataLayer/PhotoGetherData/PhotoGetherData/Interface/Message/IceCandidateMessage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import WebRTC 3 | 4 | public struct IceCandidateMessage: Codable { 5 | public let sdp: String 6 | public let sdpMLineIndex: Int32 7 | public let sdpMid: String? 8 | /// 받는 사람의 ID 9 | public let receiverID: String 10 | /// 보내는 사람의 ID 11 | public let senderID: String 12 | /// 참가하려는 방의 ID 13 | public let roomID: String 14 | 15 | public init( 16 | from iceCandidate: RTCIceCandidate, 17 | receiverID: String, 18 | senderID: String, 19 | roomID: String 20 | ) { 21 | self.sdpMLineIndex = iceCandidate.sdpMLineIndex 22 | self.sdpMid = iceCandidate.sdpMid 23 | self.sdp = iceCandidate.sdp 24 | self.receiverID = receiverID 25 | self.senderID = senderID 26 | self.roomID = roomID 27 | } 28 | 29 | public var rtcIceCandidate: RTCIceCandidate { 30 | return RTCIceCandidate(sdp: self.sdp, sdpMLineIndex: self.sdpMLineIndex, sdpMid: self.sdpMid) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /PhotoGether/DataLayer/PhotoGetherData/PhotoGetherData/Interface/Message/JoinRoomRequestMessage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct JoinRoomRequestMessage: Encodable { 4 | public let roomID: String 5 | } 6 | -------------------------------------------------------------------------------- /PhotoGether/DataLayer/PhotoGetherData/PhotoGetherData/Interface/Message/JoinRoomResponseMessage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import PhotoGetherDomainInterface 3 | 4 | public struct JoinRoomResponseMessage: Decodable { 5 | public let userID: String // MARK: 참가 요청을 보낸 유저 ID 6 | public let roomID: String 7 | public let userList: [UserDTO] // MARK: 참가 요청을 보낸 유저를 포함한 방 유저 리스트 8 | 9 | public init(userID: String, roomID: String, userList: [UserDTO]) { 10 | self.userID = userID 11 | self.roomID = roomID 12 | self.userList = userList 13 | } 14 | 15 | public func toEntity() -> JoinRoomEntity { 16 | let userList = self.userList.map { $0.toEntity() } 17 | return JoinRoomEntity(userID: self.userID, roomID: self.roomID, userList: userList) 18 | } 19 | } 20 | 21 | public struct UserDTO: Decodable { 22 | public let userID: String 23 | public let nickname: String 24 | public let initialPosition: Int 25 | 26 | public func toEntity() -> UserEntity { 27 | UserEntity(userID: self.userID, nickname: self.nickname, initialPosition: self.initialPosition) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /PhotoGether/DataLayer/PhotoGetherData/PhotoGetherData/Interface/Message/NotifyNewUserMessage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import PhotoGetherDomainInterface 3 | 4 | public struct NotifyNewUserMessage: Decodable { 5 | public let newUser: UserDTO 6 | 7 | public init(newUser: UserDTO) { 8 | self.newUser = newUser 9 | } 10 | 11 | public func toEntity() -> NotifyNewUserEntity { 12 | NotifyNewUserEntity(newUser: self.newUser.toEntity()) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /PhotoGether/DataLayer/PhotoGetherData/PhotoGetherData/Interface/Message/SessionDescriptionMessage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import WebRTC 3 | 4 | public enum SdpType: String, Codable { 5 | case offer, prAnswer, answer, rollback 6 | 7 | public var rtcSdpType: RTCSdpType { 8 | switch self { 9 | case .offer: return .offer 10 | case .answer: return .answer 11 | case .prAnswer: return .prAnswer 12 | case .rollback: return .rollback 13 | } 14 | } 15 | } 16 | 17 | public struct SessionDescriptionMessage: Codable { 18 | public let sdp: String 19 | public let type: SdpType 20 | /// 참가하려는 방의 ID 21 | public let roomID: String 22 | /// Offer를 보내는 사람의 ID 23 | public let offerID: String 24 | /// Answer를 보내는 사람의 ID 25 | public let answerID: String? 26 | 27 | public init(from rtcSessionDescription: RTCSessionDescription, roomID: String, offerID: String, answerID: String?) { 28 | self.sdp = rtcSessionDescription.sdp 29 | self.roomID = roomID 30 | self.offerID = offerID 31 | self.answerID = answerID 32 | 33 | switch rtcSessionDescription.type { 34 | case .offer: self.type = .offer 35 | case .prAnswer: self.type = .prAnswer 36 | case .answer: self.type = .answer 37 | case .rollback: self.type = .rollback 38 | @unknown default: 39 | fatalError("Unknown RTCSessionDescription type: \(rtcSessionDescription.type.rawValue)") 40 | } 41 | } 42 | 43 | public var rtcSessionDescription: RTCSessionDescription { 44 | return RTCSessionDescription(type: self.type.rtcSdpType, sdp: self.sdp) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /PhotoGether/DataLayer/PhotoGetherData/PhotoGetherData/Interface/Service/SignalingService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | import WebRTC 4 | import PhotoGetherNetwork 5 | 6 | public protocol SignalingService: WebSocketClientDelegate { 7 | var didConnectPublisher: AnyPublisher { get } 8 | var didDidDisconnectPublisher: AnyPublisher { get } 9 | var didReceiveOfferSdpPublisher: AnyPublisher { get } 10 | var didReceiveAnswerSdpPublisher: AnyPublisher { get } 11 | var didReceiveCandidatePublisher: AnyPublisher { get } 12 | 13 | func connect() 14 | func send( 15 | type: SignalingRequestDTO.SignalingMessageType, 16 | sdp: RTCSessionDescription, 17 | roomID: String, 18 | offerID: String, 19 | answerID: String? 20 | ) 21 | func send( 22 | type: SignalingRequestDTO.SignalingMessageType, 23 | candidate: RTCIceCandidate, 24 | roomID: String, 25 | receiverID: String, 26 | senderID: String 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /PhotoGether/DataLayer/PhotoGetherData/PhotoGetherData/Interface/Service/WebRTCService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | import WebRTC 4 | 5 | public protocol WebRTCService: RTCPeerConnectionDelegate, RTCDataChannelDelegate { 6 | var didGenerateLocalCandidatePublisher: AnyPublisher { get } 7 | var didChangeConnectionStatePublisher: AnyPublisher { get } 8 | var didReceiveDataPublisher: AnyPublisher { get } 9 | 10 | var peerConnection: RTCPeerConnection { get } 11 | 12 | // MARK: SDP 13 | func offer() async throws -> RTCSessionDescription 14 | func answer() async throws -> RTCSessionDescription 15 | func set(remoteSdp: RTCSessionDescription) async throws 16 | func set(localSdp: RTCSessionDescription) async throws 17 | func set(remoteCandidate: RTCIceCandidate) async throws 18 | 19 | func offer(completion: @escaping (_ sdp: RTCSessionDescription) -> Void) 20 | func answer(completion: @escaping (_ sdp: RTCSessionDescription) -> Void) 21 | func set(remoteSdp: RTCSessionDescription, completion: @escaping (Error?) -> Void) 22 | func set(localSdp: RTCSessionDescription, completion: @escaping (Error?) -> Void) 23 | func set(remoteCandidate: RTCIceCandidate, completion: @escaping (Error?) -> Void) 24 | 25 | // MARK: Video 26 | func renderLocalVideo(to renderer: RTCVideoRenderer) 27 | func renderRemoteVideo(to renderer: RTCVideoRenderer) 28 | func connectLocalVideoTrack(videoTrack: RTCVideoTrack) 29 | func connectRemoteVideoTrack() 30 | 31 | // MARK: Data 32 | func sendData(_ data: Data) 33 | 34 | // MARK: Audio 35 | func muteAudio() 36 | func unmuteAudio() 37 | func setLocalAudioState(_ isEnabled: Bool) 38 | } 39 | -------------------------------------------------------------------------------- /PhotoGether/DataLayer/PhotoGetherData/PhotoGetherData/LocalShapeDataSourceImpl.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | import PhotoGetherDomainInterface 4 | import PhotoGetherNetwork 5 | 6 | public final class LocalShapeDataSourceImpl: ShapeDataSource { 7 | public func fetchEmojiData(_ endpoint: EndPoint) -> AnyPublisher<[EmojiDTO], Error> { 8 | guard let url = endpoint.request().url 9 | else { return Empty().eraseToAnyPublisher() } 10 | 11 | return CacheManager(path: CacheManager.Path.emoji).loadPublisher(url: url) 12 | .compactMap { $0 } 13 | .decode(type: [EmojiDTO].self, decoder: JSONDecoder()) 14 | .eraseToAnyPublisher() 15 | } 16 | 17 | public init() { } 18 | } 19 | -------------------------------------------------------------------------------- /PhotoGether/DataLayer/PhotoGetherData/PhotoGetherData/PeerConnectionSupport.swift: -------------------------------------------------------------------------------- 1 | import WebRTC 2 | 3 | enum PeerConnectionSupport { 4 | static let peerConnectionFactory: RTCPeerConnectionFactory = { 5 | RTCInitializeSSL() 6 | let videoEncoderFactory = RTCDefaultVideoEncoderFactory() 7 | let videoDecoderFactory = RTCDefaultVideoDecoderFactory() 8 | return RTCPeerConnectionFactory( 9 | encoderFactory: videoEncoderFactory, 10 | decoderFactory: videoDecoderFactory 11 | ) 12 | }() 13 | 14 | static func configuration(iceServers: [String]) -> RTCConfiguration { 15 | let config = RTCConfiguration() 16 | config.iceServers = [RTCIceServer(urlStrings: iceServers)] 17 | config.sdpSemantics = .unifiedPlan 18 | config.continualGatheringPolicy = .gatherContinually 19 | return config 20 | } 21 | 22 | static func mediaConstraint() -> RTCMediaConstraints { 23 | return RTCMediaConstraints( 24 | mandatoryConstraints: nil, 25 | optionalConstraints: [ 26 | "DtlsSrtpKeyAgreement": kRTCMediaConstraintsValueTrue 27 | ] 28 | ) 29 | } 30 | 31 | static func createAudioTrack() -> RTCAudioTrack { 32 | let audioConstraints = RTCMediaConstraints( 33 | mandatoryConstraints: nil, 34 | optionalConstraints: nil 35 | ) 36 | let audioSource = peerConnectionFactory.audioSource( 37 | with: audioConstraints 38 | ) 39 | let audioTrack = peerConnectionFactory.audioTrack( 40 | with: audioSource, 41 | trackId: "audio0" 42 | ) 43 | return audioTrack 44 | } 45 | 46 | static func createVideoTrack(videoSource: RTCVideoSource) -> RTCVideoTrack { 47 | let videoTrack = peerConnectionFactory.videoTrack( 48 | with: videoSource, 49 | trackId: "video0" 50 | ) 51 | return videoTrack 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /PhotoGether/DataLayer/PhotoGetherData/PhotoGetherData/RemoteShapeDataSourceImpl.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | import PhotoGetherNetwork 4 | 5 | public final class RemoteShapeDataSourceImpl: ShapeDataSource { 6 | // TODO: 페이징 적용 필요 7 | public func fetchEmojiData(_ endpoint: EndPoint) -> AnyPublisher<[EmojiDTO], Error> { 8 | return Request.requestJSON(endpoint) 9 | .map { (emojiDTOs: [EmojiDTO]) -> [EmojiDTO] in 10 | if let data = try? JSONEncoder().encode(emojiDTOs), 11 | let url = endpoint.request().url { 12 | CacheManager(path: CacheManager.Path.emoji).save(url: url, data: data) 13 | } 14 | return emojiDTOs 15 | } 16 | .eraseToAnyPublisher() 17 | } 18 | 19 | public init() { } 20 | } 21 | -------------------------------------------------------------------------------- /PhotoGether/DataLayer/PhotoGetherData/PhotoGetherData/ShapeDataSource.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | import PhotoGetherNetwork 4 | 5 | public protocol ShapeDataSource { 6 | func fetchEmojiData(_ endPoint: EndPoint) -> AnyPublisher<[EmojiDTO], Error> 7 | } 8 | -------------------------------------------------------------------------------- /PhotoGether/DataLayer/PhotoGetherData/PhotoGetherData/ShapeRepositoryImpl.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | import PhotoGetherDomainInterface 4 | import PhotoGetherNetwork 5 | 6 | final public class ShapeRepositoryImpl: ShapeRepository { 7 | public func fetchEmojiList(_ group: EmojiGroup) -> AnyPublisher<[EmojiEntity], Never> { 8 | return localDataSource.fetchEmojiData(EmojiEndPoint(group: group)) 9 | .catch { [weak self] _ -> AnyPublisher<[EmojiDTO], Never> in 10 | guard let self else { return Just([]).eraseToAnyPublisher() } 11 | 12 | return remoteDataSource.fetchEmojiData(EmojiEndPoint(group: group)) 13 | .replaceError(with: []) 14 | .eraseToAnyPublisher() 15 | } 16 | .map { $0.map { $0.toEntity() } } 17 | .eraseToAnyPublisher() 18 | } 19 | 20 | private let localDataSource: ShapeDataSource 21 | private let remoteDataSource: ShapeDataSource 22 | 23 | public init( 24 | localDataSource: ShapeDataSource, 25 | remoteDataSource: ShapeDataSource 26 | ) { 27 | self.localDataSource = localDataSource 28 | self.remoteDataSource = remoteDataSource 29 | } 30 | 31 | } 32 | 33 | 34 | fileprivate struct EmojiEndPoint: EndPoint { 35 | let group: EmojiGroup 36 | 37 | var baseURL: URL { URL(string: "https://www.emoji.family")! } 38 | var path: String { "api/emojis" } 39 | var method: HTTPMethod { .get } 40 | var parameters: [String: Any]? { ["group": group.rawValue] } 41 | var headers: [String: String]? { nil } 42 | var body: Encodable? { nil } 43 | 44 | init(group: EmojiGroup) { 45 | self.group = group 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /PhotoGether/DataLayer/PhotoGetherNetwork/PhotoGetherNetwork/Extension/Data+toDTO.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension Data { 4 | func convert(to type: Decodable.Type) -> Decodable? { 5 | return try? JSONDecoder().decode(type, from: self) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /PhotoGether/DataLayer/PhotoGetherNetwork/PhotoGetherNetwork/HTTP/EndPoint.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum HTTPMethod: String { 4 | case get = "GET" 5 | } 6 | 7 | public protocol EndPoint { 8 | var baseURL: URL { get } 9 | var path: String { get } 10 | var method: HTTPMethod { get } 11 | var parameters: [String: Any]? { get } 12 | var headers: [String: String]? { get } 13 | var body: Encodable? { get } 14 | } 15 | 16 | extension EndPoint { 17 | public func request() -> URLRequest { 18 | var url = baseURL.appendingPathComponent(path) 19 | var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)! 20 | 21 | urlComponents.queryItems = parameters? 22 | .sorted { $0.key < $1.key } 23 | .map { URLQueryItem(name: $0.key, value: "\($0.value)") } 24 | 25 | url = urlComponents.url! 26 | 27 | var request = URLRequest(url: url) 28 | request.httpMethod = method.rawValue 29 | 30 | headers?.forEach { 31 | request.setValue($0.value, forHTTPHeaderField: $0.key) 32 | } 33 | 34 | if let body, let jsonData = try? JSONEncoder().encode(body) { 35 | request.httpBody = jsonData 36 | } 37 | 38 | request.cachePolicy = .reloadIgnoringLocalCacheData 39 | 40 | return request 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /PhotoGether/DataLayer/PhotoGetherNetwork/PhotoGetherNetwork/WebSocket/WebSocketClient.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | 4 | public protocol WebSocketClient { 5 | var delegates: [WebSocketClientDelegate] { get set } 6 | var webSocketDidConnectPublisher: AnyPublisher { get } 7 | var webSocketDidDisconnectPublisher: AnyPublisher { get } 8 | var webSocketdidReceiveDataPublisher: AnyPublisher { get } 9 | 10 | func connect() 11 | func send(data: Data) 12 | } 13 | -------------------------------------------------------------------------------- /PhotoGether/DataLayer/PhotoGetherNetwork/PhotoGetherNetwork/WebSocket/WebSocketClientDelegate.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @available(*, deprecated, message: "Publisher로 교체될 예정입니다.") 4 | public protocol WebSocketClientDelegate: AnyObject { 5 | func webSocketDidConnect(_ webSocket: WebSocketClient) 6 | func webSocketDidDisconnect(_ webSocket: WebSocketClient) 7 | func webSocket(_ webSocket: WebSocketClient, didReceiveData data: Data) 8 | } 9 | -------------------------------------------------------------------------------- /PhotoGether/DataLayer/PhotoGetherNetwork/PhotoGetherNetwork/WebSocket/WebSocketRequestable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol WebSocketRequestable: Encodable { 4 | associatedtype RequestType: Encodable 5 | var messageType: RequestType { get } 6 | var message: Data? { get } 7 | } 8 | -------------------------------------------------------------------------------- /PhotoGether/DataLayer/PhotoGetherNetwork/PhotoGetherNetwork/WebSocket/WebSocketResponsable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol WebSocketResponsable: Decodable { 4 | associatedtype ResponseType: Decodable 5 | var messageType: ResponseType { get } 6 | var message: Data? { get } 7 | } 8 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/DomainUtility/DomainUtility/Temp.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public func hello() -> String { 4 | return "hello" 5 | } 6 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/CountClientsUseCaseTests/CountClientsUseCaseTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import PhotoGetherDomainInterface 3 | import PhotoGetherDomainTesting 4 | 5 | final class CountClientsUseCaseTests: XCTestCase { 6 | var sut: CountClientsUseCase! 7 | 8 | func test_클라이언트_수를_잘_가져오는지() { 9 | for count in 0..<10 { 10 | sut = CountClientsUseCaseMock(clientCount: count) 11 | XCTAssertEqual(sut.execute(), count) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/FetchStickerListUseCaseTests/FetchEmojiListUseCaseTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import PhotoGetherDomainTesting 3 | import PhotoGetherDomainInterface 4 | import PhotoGetherDomain 5 | 6 | final class FetchEmojiListUseCaseTests: XCTestCase { 7 | var sut: FetchEmojiListUseCase! 8 | var shapeRepositoryMock: ShapeRepositoryMock! 9 | 10 | func test_이미지데이터_리스트를_번들에서_잘_가져오는지() { 11 | //Arrange 준비 단계: 테스트 대상 시스템(SUT)와 의존성을 원하는 상태로 만들기 12 | let expectation = XCTestExpectation(description: "이미지 데이터 리스트 가져오기 테스트") 13 | let imageNameList = [ 14 | "blackHeart", 15 | "bug", 16 | "cat", 17 | "crown", 18 | "dog", 19 | "lips", 20 | "parkBug", 21 | "racoon", 22 | "redHeart", 23 | "star", 24 | "sunglasses", 25 | "tree", 26 | ] 27 | let shapeRepositoryMock = ShapeRepositoryMock(imageNameList: imageNameList) 28 | 29 | sut = FetchEmojiListUseCaseImpl(shapeRepository: shapeRepositoryMock) 30 | 31 | var targetEntityList: [EmojiEntity] = [] 32 | let beforeEntityListCount = 0 33 | 34 | //Act 실행 단계: SUT 메소드를 호출하면서 의존성을 전달해서 결과를 저장하기 35 | let cancellable = sut.execute() 36 | .sink { emojiEntities in 37 | targetEntityList.append(contentsOf: emojiEntities) 38 | expectation.fulfill() 39 | } 40 | 41 | //Assert 검증 단계: 결과와 기대치를 비교해서 검증하기 42 | wait(for: [expectation], timeout: 2.0) 43 | XCTAssertEqual(beforeEntityListCount, 0) 44 | XCTAssertEqual(targetEntityList.count, 12) 45 | 46 | cancellable.cancel() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomain/UseCaseImpl/CaptureVideosUseCaseImpl.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import PhotoGetherDomainInterface 3 | 4 | public final class CaptureVideosUseCaseImpl: CaptureVideosUseCase { 5 | public func execute() -> [UIImage] { 6 | let localImage = connectionRepository.capturedLocalVideo ?? UIImage() 7 | 8 | let localUserImageInfo = [(connectionRepository.localUserInfo?.viewPosition, localImage)] 9 | let remoteUserImagesInfo = connectionRepository.clients 10 | .map { ($0.remoteUserInfo?.viewPosition, $0.captureVideo()) } 11 | 12 | return sortImages(localUserImageInfo + remoteUserImagesInfo) 13 | } 14 | 15 | private let connectionRepository: ConnectionRepository 16 | 17 | public init(connectionRepository: ConnectionRepository) { 18 | self.connectionRepository = connectionRepository 19 | } 20 | 21 | private func sortImages(_ images: [(viewPosition: UserInfo.ViewPosition?, image: UIImage)]) -> [UIImage] { 22 | let convertedArray = images.map { 23 | (position: PositionOder(rawValue: $0.viewPosition?.rawValue ?? -1), 24 | image: $0.image) 25 | } 26 | 27 | // 배열의 2번 인덱스가 마지막 자리이기 때문에 nil일 경우 2로 설정했습니다 28 | let sortedByViewPosition = convertedArray.sorted { 29 | let lhs = $0.position?.sequence ?? 2 30 | let rhs = $1.position?.sequence ?? 2 31 | return lhs < rhs 32 | } 33 | 34 | return sortedByViewPosition.map { $0.image } 35 | } 36 | } 37 | 38 | /// case의 순서는 참가자의 참가 순서에 따른 화면 배치이고 sequence는 이미지 데이터 전달할 때의 배열 순서입니다 39 | private enum PositionOder: Int { 40 | case topLeading 41 | case bottomTrailing 42 | case topTrailing 43 | case bottomLeading 44 | 45 | var sequence: Int { 46 | switch self { 47 | case .topLeading: 0 48 | case .topTrailing: 1 49 | case .bottomLeading: 2 50 | case .bottomTrailing: 3 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomain/UseCaseImpl/CountClientsUseCaseImpl.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import PhotoGetherDomainInterface 3 | 4 | public final class CountClientsUseCaseImpl: CountClientsUseCase { 5 | public func execute() -> Int { repository.clients.count } 6 | 7 | private let repository: ConnectionRepository 8 | 9 | init(repository: ConnectionRepository) { 10 | self.repository = repository 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomain/UseCaseImpl/CreateRoomUseCaseImpl.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | import PhotoGetherDomainInterface 4 | 5 | public final class CreateRoomUseCaseImpl: CreateRoomUseCase { 6 | private let inviteMessage = "PhotoGether 앱에서 초대를 보냈습니다.\n같이 사진 찍어요! 📷\n" 7 | 8 | public func execute() -> AnyPublisher { 9 | connectionRepository.createRoom() 10 | .map { [weak self] in 11 | let message = self?.inviteMessage ?? "" 12 | let scheme = "photoGether://createRoom?roomID=\($0.roomID)&hostID=\($0.hostID)" 13 | return message + scheme 14 | } 15 | .eraseToAnyPublisher() 16 | } 17 | 18 | private let connectionRepository: ConnectionRepository 19 | 20 | public init(connectionRepository: ConnectionRepository) { 21 | self.connectionRepository = connectionRepository 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomain/UseCaseImpl/DidEnterNewUserPublisherUseCaseImpl.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Combine 3 | import PhotoGetherDomainInterface 4 | 5 | public final class DidEnterNewUserPublisherUseCaseImpl: DidEnterNewUserPublisherUseCase { 6 | public func publisher() -> AnyPublisher<(UserInfo, UIView), Never> { 7 | return connectionRepository.didEnterNewUserPublisher 8 | } 9 | private let connectionRepository: ConnectionRepository 10 | 11 | public init(connectionRepository: ConnectionRepository) { 12 | self.connectionRepository = connectionRepository 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomain/UseCaseImpl/FetchEmojiListUseCaseImpl.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | import PhotoGetherDomainInterface 4 | 5 | public final class FetchEmojiListUseCaseImpl: FetchEmojiListUseCase { 6 | public func execute(_ group: EmojiGroup) -> AnyPublisher<[EmojiEntity], Never> { 7 | return shapeRepository.fetchEmojiList(group) 8 | } 9 | 10 | private let shapeRepository: ShapeRepository 11 | 12 | public init(shapeRepository: ShapeRepository) { 13 | self.shapeRepository = shapeRepository 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomain/UseCaseImpl/GetLocalVideoUseCaseImpl.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | import PhotoGetherDomainInterface 4 | 5 | public final class GetLocalVideoUseCaseImpl: GetLocalVideoUseCase { 6 | public func execute() -> (UserInfo?, UIView) { 7 | return (connectionRepository.localUserInfo, connectionRepository.localVideoView) 8 | } 9 | 10 | private let connectionRepository: ConnectionRepository 11 | 12 | public init(connectionRepository: ConnectionRepository) { 13 | self.connectionRepository = connectionRepository 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomain/UseCaseImpl/GetRemoteVideoUseCaseImpl.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | import PhotoGetherDomainInterface 4 | 5 | public final class GetRemoteVideoUseCaseImpl: GetRemoteVideoUseCase { 6 | public func execute() -> [(UserInfo?, UIView)] { 7 | return connectionRepository.clients.map { ($0.remoteUserInfo, $0.remoteVideoView) } 8 | } 9 | 10 | private let connectionRepository: ConnectionRepository 11 | 12 | public init(connectionRepository: ConnectionRepository) { 13 | self.connectionRepository = connectionRepository 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomain/UseCaseImpl/GetVoiceInputStateUseCaseImpl.swift: -------------------------------------------------------------------------------- 1 | import PhotoGetherDomainInterface 2 | 3 | public final class GetVoiceInputStateUseCaseImpl: GetVoiceInputStateUseCase { 4 | public func execute() -> Bool { 5 | return connectionRepository.currentLocalVideoInputState 6 | } 7 | 8 | private let connectionRepository: ConnectionRepository 9 | 10 | public init(connectionRepository: ConnectionRepository) { 11 | self.connectionRepository = connectionRepository 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomain/UseCaseImpl/JoinRoomUseCaseImpl.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | import PhotoGetherDomainInterface 4 | 5 | public final class JoinRoomUseCaseImpl: JoinRoomUseCase { 6 | public func execute(roomID: String, hostID: String) -> AnyPublisher { 7 | return connectionRepository.joinRoom(to: roomID, hostID: hostID) 8 | .replaceError(with: false) 9 | .eraseToAnyPublisher() 10 | } 11 | 12 | private let connectionRepository: ConnectionRepository 13 | 14 | public init(connectionRepository: ConnectionRepository) { 15 | self.connectionRepository = connectionRepository 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomain/UseCaseImpl/ReceiveFrameUseCaseImpl.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | import PhotoGetherDomainInterface 4 | 5 | public final class ReceiveFrameUseCaseImpl: ReceiveFrameUseCase { 6 | private let eventConnectionRepository: EventConnectionRepository 7 | 8 | public init(eventConnectionRepository: EventConnectionRepository) { 9 | self.eventConnectionRepository = eventConnectionRepository 10 | } 11 | 12 | public func execute() -> AnyPublisher { 13 | return eventConnectionRepository.receiveFrameEntity().eraseToAnyPublisher() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomain/UseCaseImpl/ReceiveStickerListUseCaseImpl.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | import PhotoGetherDomainInterface 4 | 5 | public final class ReceiveStickerListUseCaseImpl: ReceiveStickerListUseCase { 6 | private let eventConnectionRepository: EventConnectionRepository 7 | 8 | public init(eventConnectionRepository: EventConnectionRepository) { 9 | self.eventConnectionRepository = eventConnectionRepository 10 | } 11 | 12 | public func execute() -> AnyPublisher<[StickerEntity], Never> { 13 | return eventConnectionRepository.receiveStickerList().eraseToAnyPublisher() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomain/UseCaseImpl/SendFrameToRepositoryUseCaseImpl.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import PhotoGetherDomainInterface 3 | 4 | public final class SendFrameToRepositoryUseCaseImpl: SendFrameToRepositoryUseCase { 5 | public func execute(type: EventType, frame: FrameEntity) { 6 | eventConnectionRepository.mergeFrame(type: type, frame: frame) 7 | } 8 | 9 | private let eventConnectionRepository: EventConnectionRepository 10 | 11 | public init(eventConnectionRepository: EventConnectionRepository) { 12 | self.eventConnectionRepository = eventConnectionRepository 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomain/UseCaseImpl/SendOfferUseCaseImpl.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | import PhotoGetherDomainInterface 4 | 5 | public final class SendOfferUseCaseImpl: SendOfferUseCase { 6 | public func execute() -> AnyPublisher { 7 | Future { promise in 8 | Task { 9 | do { 10 | try await self.repository.sendOffer() 11 | promise(.success(())) 12 | } catch { 13 | promise(.failure(error)) 14 | } 15 | } 16 | } 17 | .eraseToAnyPublisher() 18 | } 19 | 20 | private let repository: ConnectionRepository 21 | 22 | public init(repository: ConnectionRepository) { 23 | self.repository = repository 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomain/UseCaseImpl/SendStickerToRepositoryUseCaseImpl.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import PhotoGetherDomainInterface 3 | 4 | public final class SendStickerToRepositoryUseCaseImpl: SendStickerToRepositoryUseCase { 5 | public func execute(type: EventType, sticker: StickerEntity) { 6 | eventConnectionRepository.mergeSticker(type: type, sticker: sticker) 7 | } 8 | 9 | private let eventConnectionRepository: EventConnectionRepository 10 | 11 | public init(eventConnectionRepository: EventConnectionRepository) { 12 | self.eventConnectionRepository = eventConnectionRepository 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomain/UseCaseImpl/StopVideoCaptureUseCaseImpl.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | import PhotoGetherDomainInterface 4 | 5 | public final class StopVideoCaptureUseCaseImpl: StopVideoCaptureUseCase { 6 | @discardableResult 7 | public func execute() -> Bool { 8 | connectionRepository.stopCaptureLocalVideo() 9 | } 10 | 11 | private let connectionRepository: ConnectionRepository 12 | 13 | public init(connectionRepository: ConnectionRepository) { 14 | self.connectionRepository = connectionRepository 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomain/UseCaseImpl/ToggleLocalMicStateUseCaseImpl.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import PhotoGetherDomainInterface 3 | 4 | public final class ToggleLocalMicStateUseCaseImpl: ToggleLocalMicStateUseCase { 5 | public func execute() -> AnyPublisher { 6 | connectionRepository.switchLocalAudioTrackState() 7 | return connectionRepository.didChangeLocalAudioTrackStatePublisher 8 | } 9 | 10 | private let connectionRepository: ConnectionRepository 11 | 12 | public init(connectionRepository: ConnectionRepository) { 13 | self.connectionRepository = connectionRepository 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainInterface/Component/SharePhotoComponent.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct SharePhotoComponent { 4 | public let photoData: Data 5 | 6 | public init(imageData: Data) { 7 | self.photoData = imageData 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainInterface/ConnectionClient.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import WebRTC 3 | import Combine 4 | 5 | public protocol ConnectionClient { 6 | var remoteVideoView: UIView { get } 7 | var remoteUserInfo: UserInfo? { get } 8 | 9 | var receivedDataPublisher: AnyPublisher { get } 10 | var didGenerateLocalCandidatePublisher: AnyPublisher<(receiverID: String, RTCIceCandidate), Never> { get } 11 | 12 | func createOffer() async throws -> RTCSessionDescription 13 | func createAnswer() async throws -> RTCSessionDescription 14 | 15 | func set(remoteSdp: RTCSessionDescription) async throws 16 | func set(localSdp: RTCSessionDescription) async throws 17 | func set(remoteCandidate: RTCIceCandidate) async throws 18 | 19 | func setRemoteUserInfo(_ remoteUserInfo: UserInfo) 20 | func sendData(data: Data) 21 | func captureVideo() -> UIImage 22 | func toggleLocalAudioTrackState(isEnable: Bool) 23 | func bindLocalVideo(videoSource: RTCVideoSource?, _ localVideoView: UIView) 24 | func bindRemoteVideo() 25 | } 26 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainInterface/Entity/EmojiEntity.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct EmojiEntity: Decodable { 4 | public let emoji: String 5 | public let hexCode: String 6 | public let group: EmojiGroup 7 | public let annotation: String 8 | public var emojiURL: URL? { 9 | guard let emojiURL = URL(string: "https://www.emoji.family/api/emojis") 10 | else { return nil } 11 | 12 | let style = EmojiStyle.twemoji.rawValue 13 | let ext = "png" 14 | let size = "128" 15 | 16 | return emojiURL 17 | .appendingPathComponent(hexCode) 18 | .appendingPathComponent(style) 19 | .appendingPathComponent(ext) 20 | .appendingPathComponent(size) 21 | } 22 | 23 | public init( 24 | emoji: String, 25 | hexCode: String, 26 | group: EmojiGroup, 27 | annotation: String 28 | ) { 29 | self.emoji = emoji 30 | self.hexCode = hexCode 31 | self.group = group 32 | self.annotation = annotation 33 | } 34 | 35 | private enum EmojiStyle: String { 36 | case noto, twemoji, openmoji, blobmoji, fluent, fluentflat 37 | } 38 | } 39 | 40 | public enum EmojiGroup: String, Codable { 41 | case smileysEmotion = "smileys-emotion" 42 | case peopleBody = "people-body" 43 | case component = "component" 44 | case animalsNature = "animals-nature" 45 | case foodDrink = "food-drink" 46 | case travelPlaces = "travel-places" 47 | case activities = "activities" 48 | case objects = "objects" 49 | case symbols = "symbols" 50 | } 51 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainInterface/Entity/FrameEntity.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct FrameEntity: Equatable, Codable { 4 | public static func == (lhs: FrameEntity, rhs: FrameEntity) -> Bool { 5 | return lhs.frameType == rhs.frameType 6 | } 7 | 8 | public let id: UUID 9 | public let frameType: FrameType 10 | public private(set) var owner: UserInfo? 11 | public private(set) var latestUpdated: Date 12 | 13 | public init( 14 | id: UUID = UUID(), 15 | frameType: FrameType, 16 | owner: UserInfo?, 17 | latestUpdated: Date 18 | ) { 19 | self.id = id 20 | self.frameType = frameType 21 | self.owner = owner 22 | self.latestUpdated = latestUpdated 23 | } 24 | } 25 | 26 | @frozen 27 | public enum FrameType: Codable { 28 | case defaultBlack 29 | case defaultWhite 30 | } 31 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainInterface/Entity/JoinRoomEntity.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct JoinRoomEntity { 4 | public let userID: String 5 | public let roomID: String 6 | public let userList: [UserEntity] 7 | 8 | public init(userID: String, roomID: String, userList: [UserEntity]) { 9 | self.userID = userID 10 | self.roomID = roomID 11 | self.userList = userList 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainInterface/Entity/NotifyNewUserEntity.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct NotifyNewUserEntity { 4 | public let newUser: UserEntity 5 | 6 | public init(newUser: UserEntity) { 7 | self.newUser = newUser 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainInterface/Entity/RoomOwnerEntity.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct RoomOwnerEntity { 4 | public let roomID: String 5 | public let hostID: String 6 | 7 | public init(roomID: String, hostID: String) { 8 | self.roomID = roomID 9 | self.hostID = hostID 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainInterface/Entity/UserEntity.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct UserEntity { 4 | public let userID: String 5 | public let nickname: String 6 | public let initialPosition: Int 7 | 8 | public init(userID: String, nickname: String, initialPosition: Int) { 9 | self.userID = userID 10 | self.nickname = nickname 11 | self.initialPosition = initialPosition 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainInterface/Entity/UserInfo.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct UserInfo: Identifiable, Equatable, Codable { 4 | public static func == (lhs: UserInfo, rhs: UserInfo) -> Bool { 5 | lhs.id == rhs.id 6 | } 7 | 8 | public var id: String 9 | public var nickname: String 10 | public var isHost: Bool 11 | public var viewPosition: ViewPosition 12 | public var roomID: String 13 | 14 | public enum ViewPosition: Int, Codable { 15 | case topLeading 16 | case bottomTrailing 17 | case topTrailing 18 | case bottomLeading 19 | 20 | public var color: UserColor { 21 | switch self { 22 | case .topLeading: return .orange 23 | case .bottomTrailing: return .brown 24 | case .topTrailing: return .blue 25 | case .bottomLeading: return .gray 26 | } 27 | } 28 | } 29 | 30 | public init( 31 | id: String, 32 | nickname: String, 33 | isHost: Bool, 34 | viewPosition: ViewPosition, 35 | roomID: String 36 | ) { 37 | self.id = id 38 | self.nickname = nickname 39 | self.isHost = isHost 40 | self.viewPosition = viewPosition 41 | self.roomID = roomID 42 | } 43 | 44 | public enum UserColor: String, Codable { 45 | case orange = "#FF7561" 46 | case brown = "#E7C892" 47 | case blue = "#82BBE6" 48 | case gray = "#7D7C84" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainInterface/Repository/ConnectionRepository.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Combine 3 | 4 | public protocol ConnectionRepository { 5 | var didEnterNewUserPublisher: AnyPublisher<(UserInfo, UIView), Never> { get } 6 | var didChangeLocalAudioTrackStatePublisher: AnyPublisher { get } 7 | 8 | var localUserInfo: UserInfo? { get } 9 | 10 | var clients: [ConnectionClient] { get } 11 | var localVideoView: UIView { get } 12 | var capturedLocalVideo: UIImage? { get } 13 | var currentLocalVideoInputState: Bool { get } 14 | 15 | func createRoom() -> AnyPublisher 16 | func joinRoom(to roomID: String, hostID: String) -> AnyPublisher 17 | func sendOffer() async throws 18 | func stopCaptureLocalVideo() -> Bool 19 | func switchLocalAudioTrackState() 20 | } 21 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainInterface/Repository/EventConnectionRepository.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | public protocol EventConnectionRepository { 5 | func receiveStickerList() -> AnyPublisher<[StickerEntity], Never> 6 | func mergeSticker(type: EventType, sticker: StickerEntity) 7 | 8 | func receiveFrameEntity() -> AnyPublisher 9 | func mergeFrame(type: EventType, frame: FrameEntity) 10 | } 11 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainInterface/Repository/ShapeRepository.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | public protocol ShapeRepository { 5 | func fetchEmojiList(_ group: EmojiGroup) -> AnyPublisher<[EmojiEntity], Never> 6 | } 7 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainInterface/Service/RoomService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | 4 | public protocol RoomService { 5 | var createRoomResponsePublisher: AnyPublisher { get } 6 | var notifyRoomResponsePublisher: AnyPublisher { get } 7 | 8 | func createRoom() -> AnyPublisher 9 | func joinRoom(to roomID: String) -> AnyPublisher 10 | } 11 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainInterface/UseCase/CaptureVideosUseCase.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public protocol CaptureVideosUseCase { 4 | func execute() -> [UIImage] 5 | } 6 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainInterface/UseCase/CountClientsUseCase.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol CountClientsUseCase { 4 | func execute() -> Int 5 | } 6 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainInterface/UseCase/CreateRoomUseCase.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | 4 | public protocol CreateRoomUseCase { 5 | func execute() -> AnyPublisher 6 | } 7 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainInterface/UseCase/DidEnterNewUserPublisherUseCase.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Combine 3 | 4 | public protocol DidEnterNewUserPublisherUseCase { 5 | func publisher() -> AnyPublisher<(UserInfo, UIView), Never> 6 | } 7 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainInterface/UseCase/FetchEmojiListUseCase.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | public protocol FetchEmojiListUseCase { 5 | func execute(_ group: EmojiGroup) -> AnyPublisher<[EmojiEntity], Never> 6 | } 7 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainInterface/UseCase/GetLocalVideoUseCase.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | public protocol GetLocalVideoUseCase { 5 | func execute() -> (UserInfo?, UIView) 6 | } 7 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainInterface/UseCase/GetRemoteVideoUseCase.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | public protocol GetRemoteVideoUseCase { 5 | func execute() -> [(UserInfo?, UIView)] 6 | } 7 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainInterface/UseCase/GetVoiceInputStateUseCase.swift: -------------------------------------------------------------------------------- 1 | public protocol GetVoiceInputStateUseCase { 2 | func execute() -> Bool 3 | } 4 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainInterface/UseCase/JoinRoomUseCase.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | public protocol JoinRoomUseCase { 4 | func execute(roomID: String, hostID: String) -> AnyPublisher 5 | } 6 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainInterface/UseCase/ReceiveFrameUseCase.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | public protocol ReceiveFrameUseCase { 4 | func execute() -> AnyPublisher 5 | } 6 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainInterface/UseCase/ReceiveStickerListUseCase.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | public protocol ReceiveStickerListUseCase { 5 | func execute() -> AnyPublisher<[StickerEntity], Never> 6 | } 7 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainInterface/UseCase/SendFrameToRepositoryUseCase.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | public protocol SendFrameToRepositoryUseCase { 5 | func execute(type: EventType, frame: FrameEntity) 6 | } 7 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainInterface/UseCase/SendOfferUseCase.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | 4 | public protocol SendOfferUseCase { 5 | func execute() -> AnyPublisher 6 | } 7 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainInterface/UseCase/SendStickerToRepositoryUseCase.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | public protocol SendStickerToRepositoryUseCase { 5 | func execute(type: EventType, sticker: StickerEntity) 6 | } 7 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainInterface/UseCase/StopVideoCaptureUseCase.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol StopVideoCaptureUseCase { 4 | @discardableResult 5 | func execute() -> Bool 6 | } 7 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainInterface/UseCase/ToggleLocalMicStateUseCase.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | public protocol ToggleLocalMicStateUseCase { 4 | func execute() -> AnyPublisher 5 | } 6 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainTesting/ConnectionClientMock.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Combine 3 | import PhotoGetherDomainInterface 4 | 5 | public final class ConnectionClientMock: ConnectionClient { 6 | public func captureVideos() -> [UIImage] { return [] } 7 | 8 | public var remoteVideoView: UIView = UIView() 9 | 10 | public var localVideoView: UIView = UIView() 11 | 12 | public var receivedDataPublisher: PassthroughSubject = PassthroughSubject() 13 | 14 | public func sendOffer() { } 15 | 16 | public func sendData(data: Data) { } 17 | } 18 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainTesting/ConnectionRepositoryMock.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import PhotoGetherDomainInterface 3 | 4 | public final class ConnectionRepositoryMock: ConnectionRepository { 5 | public var clients: [ConnectionClient] = [] 6 | 7 | public init(count: Int) { 8 | for _ in 0.. Int { repository.clients.count } 6 | 7 | public init(clientCount: Int) { 8 | self.repository = ConnectionRepositoryMock(count: clientCount) 9 | } 10 | 11 | private let repository: ConnectionRepository 12 | } 13 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainTesting/FetchEmojiListUseCaseMock.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | import PhotoGetherDomainInterface 4 | 5 | public final class FetchEmojiListUseCaseMock: FetchEmojiListUseCase { 6 | private let repository: ShapeRepository = ShapeRepositoryMock(imageNameList: [ 7 | "blackHeart", "bug", "cat", 8 | "crown", "dog", "lips", 9 | "parkBug", "racoon", "redHeart", 10 | "star", "sunglasses", "tree", 11 | ]) 12 | 13 | public init() { } 14 | 15 | public func execute() -> AnyPublisher<[EmojiEntity], Never> { 16 | return repository.fetchEmojiList() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainTesting/JoinRoomUseCaseMock.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | import PhotoGetherDomainInterface 4 | 5 | public final class JoinRoomUseCaseMock: JoinRoomUseCase { 6 | public func execute() -> AnyPublisher { 7 | return Just(true) 8 | .delay(for: .seconds(3), scheduler: DispatchQueue.main) 9 | .eraseToAnyPublisher() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainTesting/Resource/blackHeart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainTesting/Resource/blackHeart.png -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainTesting/Resource/bug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainTesting/Resource/bug.png -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainTesting/Resource/cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainTesting/Resource/cat.png -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainTesting/Resource/crown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainTesting/Resource/crown.png -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainTesting/Resource/dog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainTesting/Resource/dog.png -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainTesting/Resource/lips.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainTesting/Resource/lips.png -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainTesting/Resource/parkBug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainTesting/Resource/parkBug.png -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainTesting/Resource/racoon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainTesting/Resource/racoon.png -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainTesting/Resource/redHeart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainTesting/Resource/redHeart.png -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainTesting/Resource/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainTesting/Resource/star.png -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainTesting/Resource/sunglasses.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainTesting/Resource/sunglasses.png -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainTesting/Resource/tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainTesting/Resource/tree.png -------------------------------------------------------------------------------- /PhotoGether/DomainLayer/PhotoGetherDomain/PhotoGetherDomainTesting/ShapeRepositoryMock.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | import PhotoGetherDomainInterface 4 | 5 | public final class ShapeRepositoryMock: ShapeRepository { 6 | private let imageNameList: [String] 7 | 8 | public init(imageNameList: [String]) { 9 | self.imageNameList = imageNameList 10 | } 11 | 12 | public func fetchEmojiList() -> AnyPublisher<[EmojiEntity], Never> { 13 | let emojiEntities: [EmojiEntity] = imageNameList.map { 14 | .init( 15 | image: imagePath(named: $0), // 이미지 주소(or 경로) 16 | name: $0 17 | ) 18 | } 19 | 20 | return Just(emojiEntities).eraseToAnyPublisher() 21 | } 22 | 23 | private func imagePath(named: String) -> String { 24 | let bundle = Bundle(for: Self.self) // 해당 클래스가 존재하는 Bundle을 의미 25 | guard let path = bundle.url(forResource: named, withExtension: "png")?.absoluteString 26 | else { return "" } 27 | 28 | return path 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /PhotoGether/PhotoGether.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /PhotoGether/PhotoGether.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 10 | 12 | 13 | 15 | 16 | 17 | 20 | 22 | 23 | 24 | 27 | 29 | 30 | 32 | 33 | 35 | 36 | 38 | 39 | 41 | 42 | 44 | 45 | 46 | 49 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /PhotoGether/PhotoGether.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "115c9d9799ad28d3aeb31df0dbc480b96400195f7bc53ad657c8daebbd42fe49", 3 | "pins" : [ 4 | { 5 | "identity" : "snapkit", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/SnapKit/SnapKit", 8 | "state" : { 9 | "revision" : "2842e6e84e82eb9a8dac0100ca90d9444b0307f4", 10 | "version" : "5.7.1" 11 | } 12 | }, 13 | { 14 | "identity" : "webrtc", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/stasel/WebRTC", 17 | "state" : { 18 | "revision" : "1048f8396529c10e259f8240d0c2cd607a13defd", 19 | "version" : "130.0.0" 20 | } 21 | } 22 | ], 23 | "version" : 3 24 | } 25 | -------------------------------------------------------------------------------- /PhotoGether/PhotoGether/App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @main 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | func application( 6 | _ application: UIApplication, 7 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 8 | ) -> Bool { 9 | //앱이 시작될 때 카메라 권한을 요청함 10 | AppPermissionManager.requestCameraPermission() 11 | 12 | //앱이 시작될 때 음성 권한을 요청함 13 | AppPermissionManager.requestMicrophonePermission() 14 | 15 | return true 16 | } 17 | 18 | func application( 19 | _ application: UIApplication, 20 | configurationForConnecting connectingSceneSession: UISceneSession, 21 | options: UIScene.ConnectionOptions 22 | ) -> UISceneConfiguration { 23 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /PhotoGether/PhotoGether/Resource/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /PhotoGether/PhotoGether/Resource/Assets.xcassets/AppIcon.appiconset/12sd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/PhotoGether/Resource/Assets.xcassets/AppIcon.appiconset/12sd.png -------------------------------------------------------------------------------- /PhotoGether/PhotoGether/Resource/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "qas12.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | }, 9 | { 10 | "appearances" : [ 11 | { 12 | "appearance" : "luminosity", 13 | "value" : "dark" 14 | } 15 | ], 16 | "filename" : "12sd.png", 17 | "idiom" : "universal", 18 | "platform" : "ios", 19 | "size" : "1024x1024" 20 | }, 21 | { 22 | "appearances" : [ 23 | { 24 | "appearance" : "luminosity", 25 | "value" : "tinted" 26 | } 27 | ], 28 | "idiom" : "universal", 29 | "platform" : "ios", 30 | "size" : "1024x1024" 31 | } 32 | ], 33 | "info" : { 34 | "author" : "xcode", 35 | "version" : 1 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /PhotoGether/PhotoGether/Resource/Assets.xcassets/AppIcon.appiconset/qas12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/PhotoGether/Resource/Assets.xcassets/AppIcon.appiconset/qas12.png -------------------------------------------------------------------------------- /PhotoGether/PhotoGether/Resource/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /PhotoGether/PhotoGether/Resource/Base.lproj/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 | -------------------------------------------------------------------------------- /PhotoGether/PhotoGether/Resource/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPhotoLibraryAddUsageDescription 6 | 사진을 앨범에 저장하려면 접근 권한이 필요합니다. 7 | NSPhotoLibraryUsageDescription 8 | 사진 앱에 대한 접근 권한이 필요합니다. 9 | NSCameraUsageDescription 10 | $(PRODUCT_NAME) 카메라 권한이 필요합니다. 11 | NSMicrophoneUsageDescription 12 | $(PRODUCT_NAME) 마이크 권한이 필요합니다. 13 | BASE_URL 14 | $(BASE_URL) 15 | EMOJI_API_KEY 16 | $(EMOJI_API_KEY) 17 | STUN_SERVERS 18 | $(STUN_SERVERS) 19 | CFBundleURLTypes 20 | 21 | 22 | CFBundleURLSchemes 23 | 24 | photoGether 25 | 26 | CFBundleURLName 27 | kr.codesquad.boostcamp9.PhotoGether 28 | 29 | 30 | NSAppTransportSecurity 31 | 32 | NSAllowsArbitraryLoads 33 | 34 | 35 | UIApplicationSceneManifest 36 | 37 | UIApplicationSupportsMultipleScenes 38 | 39 | UISceneConfigurations 40 | 41 | UIWindowSceneSessionRoleApplication 42 | 43 | 44 | UISceneConfigurationName 45 | Default Configuration 46 | UISceneDelegateClassName 47 | $(PRODUCT_MODULE_NAME).SceneDelegate 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /PhotoGether/PhotoGether/Source/AppPermissionManager.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AVFoundation 3 | 4 | enum AppPermissionManager { 5 | static func requestCameraPermission() { 6 | AVCaptureDevice.requestAccess(for: .video) { granted in 7 | if granted { 8 | print("카메라 권한 허용됨") 9 | } else { 10 | print("카메라 권한 거부됨") 11 | } 12 | } 13 | } 14 | 15 | static func requestMicrophonePermission() { 16 | AVCaptureDevice.requestAccess(for: .audio) { granted in 17 | if granted { 18 | print("마이크 권한 허용됨") 19 | } else { 20 | print("마이크 권한 거부됨") 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /PhotoGether/PhotoGether/Source/DeepLinkParser.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import PhotoGetherDomainInterface 3 | 4 | public enum DeepLinkParser { 5 | public static func parseRoomInfo(from url: URL) -> RoomOwnerEntity? { 6 | guard let queryItems = parsingURLQueryItems(url) else { return nil } 7 | 8 | guard let roomID = queryItems.first(where: { $0.name == "roomID" })?.value, 9 | let hostID = queryItems.first(where: { $0.name == "hostID" })?.value else { 10 | return nil 11 | } 12 | 13 | return RoomOwnerEntity(roomID: roomID, hostID: hostID) 14 | } 15 | 16 | private static func parsingURLQueryItems(_ url: URL) -> [URLQueryItem]? { 17 | guard let urlComponents = URLComponents( 18 | url: url, 19 | resolvingAgainstBaseURL: false 20 | ) else { return nil } 21 | 22 | return urlComponents.queryItems 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /PhotoGether/PhotoGether/Source/Secrets.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | // swiftlint:disable identifier_name 3 | enum Secrets { 4 | static var BASE_URL: URL? { 5 | let urlString = Bundle.main.object(forInfoDictionaryKey: "BASE_URL") as? String ?? "" 6 | return URL(string: urlString) 7 | } 8 | 9 | static var STUN_SERVERS: [String]? { 10 | guard let serversString = Bundle.main.infoDictionary?["STUN_SERVERS"] as? String else { 11 | return nil 12 | } 13 | return serversString.components(separatedBy: ",") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/BaseFeature/BaseFeature/BaseViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Combine 3 | 4 | open class BaseViewController: UIViewController { 5 | public var cancellables = Set() 6 | let customNavigationBar = UIView() 7 | } 8 | 9 | public protocol ViewControllerConfigure { 10 | func addViews() 11 | func setupConstraints() 12 | func configureUI() 13 | func bindInput() 14 | func bindOutput() 15 | } 16 | 17 | public extension ViewControllerConfigure { 18 | func bindInput() { } 19 | func bindOutput() { } 20 | } 21 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/BaseFeature/BaseFeature/Extension/UIButton+Publisher.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import UIKit 3 | 4 | public extension UIButton { 5 | var tapPublisher: AnyPublisher { 6 | controlPublisher(for: .touchUpInside) 7 | .map { _ in } 8 | .eraseToAnyPublisher() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/BaseFeature/BaseFeature/Extension/UICollectionViewCell+Identifier.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public extension UICollectionViewCell { 4 | static var identifier: String { 5 | return String(describing: self) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/BaseFeature/BaseFeature/Extension/UIControl+Publisher.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import UIKit 3 | 4 | public extension UIControl { 5 | /// Control Publisher 6 | func controlPublisher(for event: UIControl.Event) -> UIControl.EventPublisher { 7 | return UIControl.EventPublisher(control: self, event: event) 8 | } 9 | 10 | /// Event Publisher 11 | struct EventPublisher: Publisher { 12 | public typealias Output = UIControl 13 | public typealias Failure = Never 14 | 15 | let control: UIControl 16 | let event: UIControl.Event 17 | 18 | public func receive(subscriber: T) where T: Subscriber, Never == T.Failure, UIControl == T.Input { 19 | let subscription = EventSubscription(control: control, subscrier: subscriber, event: event) 20 | subscriber.receive(subscription: subscription) 21 | } 22 | } 23 | 24 | /// Event Subscription 25 | private class EventSubscription: Subscription where EventSubscriber.Input == UIControl, EventSubscriber.Failure == Never { 26 | let control: UIControl 27 | let event: UIControl.Event 28 | var subscriber: EventSubscriber? 29 | 30 | init(control: UIControl, subscrier: EventSubscriber, event: UIControl.Event) { 31 | self.control = control 32 | self.subscriber = subscrier 33 | self.event = event 34 | 35 | control.addTarget(self, action: #selector(eventDidOccur), for: event) 36 | } 37 | 38 | func request(_ demand: Subscribers.Demand) {} 39 | 40 | func cancel() { 41 | subscriber = nil 42 | control.removeTarget(self, action: #selector(eventDidOccur), for: event) 43 | } 44 | 45 | @objc func eventDidOccur() { 46 | _ = subscriber?.receive(control) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/BaseFeature/BaseFeature/Extension/UIImageView+SetAsyncImage.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public extension UIImageView { 4 | @MainActor 5 | func setAsyncImage(_ url: URL) async { 6 | if let cachedImage = ImageCache.readCache(with: url) { // MARK: 캐시 히트 7 | guard let cachedUIImage = UIImage(data: cachedImage.imageData) else { 8 | debugPrint("캐싱이미지 변환에 실패했습니다. \(url)") 9 | return 10 | } 11 | self.image = cachedUIImage 12 | } else { // MARK: 캐시 히트에 실패한 경우 13 | do { 14 | let (data, response) = try await URLSession.shared.data(from: url) 15 | guard 16 | let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200, 17 | let image = UIImage(data: data) 18 | else { 19 | debugPrint("이미지 다운로드 실패: \(url)") 20 | return 21 | } 22 | 23 | let cachingImage = CacheableImage(imageData: data) 24 | ImageCache.updateCache(with: url, image: cachingImage) 25 | 26 | self.image = image 27 | 28 | } catch { 29 | debugPrint("이미지 다운로드 중 오류 발생: \(error.localizedDescription)") 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/BaseFeature/BaseFeature/Extension/UILabel+SetKern.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public extension UILabel { 4 | /// 자간을 설정하는 메소드입니다. 5 | /// - Parameter kernValue: 자간입니다. 6 | /// - Parameter lineBreakMode: 텍스트가 짤릴 경우 어떻게 처리할 것인지 7 | /// - Parameter alignment: 텍스트를 어떻게 정렬할 것인지 8 | func setKern( 9 | kernValue: Double? = -0.32, 10 | lineBreakMode: NSLineBreakMode = .byTruncatingTail, 11 | alignment: NSTextAlignment = .left 12 | ) { 13 | let paragraphStyle = NSMutableParagraphStyle() 14 | 15 | paragraphStyle.lineBreakMode = lineBreakMode 16 | paragraphStyle.alignment = alignment 17 | 18 | let attributes: [NSAttributedString.Key: Any] = [ 19 | .paragraphStyle: paragraphStyle, 20 | .kern: kernValue ?? 0.0 21 | ] 22 | let attributedString = NSMutableAttributedString(string: text ?? "", attributes: attributes) 23 | 24 | self.attributedText = attributedString 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/BaseFeature/BaseFeature/ImageCache/CacheableImage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public final class CacheableImage: Codable { 4 | let imageData: Data 5 | 6 | public init(imageData: Data) { 7 | self.imageData = imageData 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/MockIcon/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/MockIcon/blackHeart.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "blackHeart.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/MockIcon/blackHeart.imageset/blackHeart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/MockIcon/bug.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "bug.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/MockIcon/cat.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "cat.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/MockIcon/crown.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "crown.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/MockIcon/dog.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "dog.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/MockIcon/lips.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "lips.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/MockIcon/parkBug.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "parkBug.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/MockIcon/parkBug.imageset/parkBug.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/MockIcon/racoon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "racoon.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/MockIcon/redHeart.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "redHeart.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/MockIcon/redHeart.imageset/redHeart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/MockIcon/star.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "star.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/MockIcon/star.imageset/star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/MockIcon/sunglasses.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "sunglasses.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/MockIcon/sunglasses.imageset/sunglasses.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/MockIcon/tree.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "tree.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/MockIcon/tree.imageset/tree.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGColor/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGColor/Gray10.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xF7", 9 | "green" : "0xF7", 10 | "red" : "0xF7" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGColor/Gray20.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xEE", 9 | "green" : "0xEE", 10 | "red" : "0xEE" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGColor/Gray30.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xE4", 9 | "green" : "0xE4", 10 | "red" : "0xE4" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGColor/Gray40.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xBD", 9 | "green" : "0xBD", 10 | "red" : "0xBD" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGColor/Gray50.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x9E", 9 | "green" : "0x9E", 10 | "red" : "0x9E" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGColor/Gray60.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x75", 9 | "green" : "0x75", 10 | "red" : "0x75" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGColor/Gray70.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x61", 9 | "green" : "0x61", 10 | "red" : "0x61" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGColor/Gray80.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x42", 9 | "green" : "0x42", 10 | "red" : "0x42" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGColor/Gray85.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x28", 9 | "green" : "0x28", 10 | "red" : "0x28" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGColor/Gray90.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x11", 9 | "green" : "0x11", 10 | "red" : "0x11" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGColor/PrimaryGreen.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xAB", 9 | "green" : "0xED", 10 | "red" : "0x2A" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/PTGresize.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "PTGresize.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/PTGresize.imageset/PTGresize.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/PTGxmark.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "PTGxmark.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/PTGxmark.imageset/PTGxmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/chevronLeftWhite.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "chevronLeftWhite.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/chevronLeftWhite.imageset/chevronLeftWhite.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/chevronRightBlack.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "chevronRightBlack.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/chevronRightBlack.imageset/chevronRightBlack.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/ellipsisIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ellipsis.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/ellipsisIcon.imageset/ellipsis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/ellipsisIcon.imageset/ellipsis.png -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/filterIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "wand.and.stars.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/frameIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "frameIcon.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/frameIcon.imageset/frameIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/stickerIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "stickerIcon.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/switchIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Change.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "temp1.png", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp1.imageset/temp1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp1.imageset/temp1.png -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp10.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "ww2.jpg", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp10.imageset/ww2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp10.imageset/ww2.jpg -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp11.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "ee2.jpeg", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp11.imageset/ee2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp11.imageset/ee2.jpeg -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp12.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "rr2.jpeg", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp12.imageset/rr2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp12.imageset/rr2.jpeg -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "temp2.png", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp2.imageset/temp2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp2.imageset/temp2.png -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "temp3.png", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp3.imageset/temp3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp3.imageset/temp3.png -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "temp4.png", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp4.imageset/temp4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp4.imageset/temp4.png -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp5.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "qq1.jpg", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp5.imageset/qq1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp5.imageset/qq1.jpg -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp6.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "ww1.jpeg", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp6.imageset/ww1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp6.imageset/ww1.jpeg -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp7.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "ee1.jpeg", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp7.imageset/ee1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp7.imageset/ee1.jpeg -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp8.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "rr1.png", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp8.imageset/rr1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp8.imageset/rr1.png -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp9.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "qq2.jpg", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp9.imageset/qq2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/PTGIcon/temp9.imageset/qq2.jpg -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/sampleImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "sampleImage.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "sampleImage@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "sampleImage@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/sampleImage.imageset/sampleImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/sampleImage.imageset/sampleImage.png -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/sampleImage.imageset/sampleImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/sampleImage.imageset/sampleImage@2x.png -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/sampleImage.imageset/sampleImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/DesignSystem.xcassets/sampleImage.imageset/sampleImage@3x.png -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/Fonts/Pretendard-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/Fonts/Pretendard-Black.ttf -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/Fonts/Pretendard-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/Fonts/Pretendard-Bold.ttf -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/Fonts/Pretendard-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/Fonts/Pretendard-ExtraBold.ttf -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/Fonts/Pretendard-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/Fonts/Pretendard-ExtraLight.ttf -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/Fonts/Pretendard-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/Fonts/Pretendard-Light.ttf -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/Fonts/Pretendard-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/Fonts/Pretendard-Medium.ttf -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/Fonts/Pretendard-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/Fonts/Pretendard-Regular.ttf -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/Fonts/Pretendard-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/Fonts/Pretendard-SemiBold.ttf -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/Fonts/Pretendard-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/Fonts/Pretendard-Thin.ttf -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Resource/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIAppFonts 6 | 7 | Pretendard-Black.ttf 8 | Pretendard-Bold.ttf 9 | Pretendard-ExtraBold.ttf 10 | Pretendard-ExtraLight.ttf 11 | Pretendard-Light.ttf 12 | Pretendard-Medium.ttf 13 | Pretendard-Regular.ttf 14 | Pretendard-SemiBold.ttf 15 | Pretendard-Thin.ttf 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Source/PTGCircleButton.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SnapKit 3 | 4 | public final class PTGCircleButton: UIButton { 5 | private let type: PTGCircleButtonType 6 | private let buttonImage = UIImageView() 7 | 8 | public init(type: PTGCircleButtonType) { 9 | self.type = type 10 | super.init(frame: .zero) 11 | 12 | addViews() 13 | setupConstraints() 14 | configureUI() 15 | } 16 | 17 | required init?(coder: NSCoder) { 18 | fatalError("init(coder:) has not been implemented") 19 | } 20 | 21 | private func addViews() { 22 | addSubview(buttonImage) 23 | } 24 | 25 | private func setupConstraints() { 26 | buttonImage.snp.makeConstraints { 27 | $0.width.height.equalTo(24) 28 | $0.center.equalToSuperview() 29 | } 30 | } 31 | 32 | private func configureUI() { 33 | backgroundColor = .white 34 | buttonImage.image = UIImage(systemName: type.image) 35 | buttonImage.contentMode = .scaleAspectFit 36 | buttonImage.tintColor = .gray90 37 | } 38 | 39 | public override func layoutSubviews() { 40 | super.layoutSubviews() 41 | layer.cornerRadius = bounds.width / 2 42 | } 43 | 44 | public func changeType(to type: PTGCircleButtonType) { 45 | buttonImage.image = UIImage(systemName: type.image) 46 | } 47 | } 48 | 49 | public extension PTGCircleButton { 50 | enum PTGCircleButtonType { 51 | case link 52 | case share 53 | case micOn 54 | case micOff 55 | 56 | var image: String { 57 | switch self { 58 | case .link: 59 | return "link" 60 | case .share: 61 | return "square.and.arrow.up" 62 | case .micOn: 63 | return "mic" 64 | case .micOff: 65 | return "mic.slash" 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Source/PTGColor.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public enum PTGColor { 4 | case gray10 5 | case gray20 6 | case gray30 7 | case gray40 8 | case gray50 9 | case gray60 10 | case gray70 11 | case gray80 12 | case gray85 13 | case gray90 14 | case primaryGreen 15 | 16 | public var color: UIColor { 17 | switch self { 18 | case .gray10: return UIColor(resource: .gray10) 19 | case .gray20: return UIColor(resource: .gray20) 20 | case .gray30: return UIColor(resource: .gray30) 21 | case .gray40: return UIColor(resource: .gray40) 22 | case .gray50: return UIColor(resource: .gray50) 23 | case .gray60: return UIColor(resource: .gray60) 24 | case .gray70: return UIColor(resource: .gray70) 25 | case .gray80: return UIColor(resource: .gray80) 26 | case .gray85: return UIColor(resource: .gray85) 27 | case .gray90: return UIColor(resource: .gray90) 28 | case .primaryGreen: return UIColor(resource: .primaryGreen) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Source/PTGMicButton.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SnapKit 3 | 4 | public final class PTGMicButton: UIButton { 5 | private let buttonImage = UIImageView() 6 | private var micState: PTGMicState 7 | 8 | public init(micState: PTGMicState) { 9 | self.micState = micState 10 | super.init(frame: .zero) 11 | 12 | addViews() 13 | setupConstraints() 14 | configureUI() 15 | } 16 | 17 | required init?(coder: NSCoder) { 18 | fatalError("init(coder:) has not been implemented") 19 | } 20 | 21 | private func addViews() { 22 | addSubview(buttonImage) 23 | } 24 | 25 | private func setupConstraints() { 26 | buttonImage.snp.makeConstraints { 27 | $0.width.height.equalTo(24) 28 | $0.center.equalToSuperview() 29 | } 30 | } 31 | 32 | private func configureUI() { 33 | backgroundColor = .white.withAlphaComponent(0.2) 34 | 35 | buttonImage.contentMode = .scaleAspectFit 36 | buttonImage.image = UIImage(systemName: micState.image) 37 | buttonImage.tintColor = micState.color 38 | } 39 | 40 | public override func layoutSubviews() { 41 | super.layoutSubviews() 42 | layer.cornerRadius = bounds.width / 2 43 | } 44 | 45 | public func toggleMicState(_ isOn: Bool) { 46 | micState = isOn ? .on : .off 47 | 48 | buttonImage.image = UIImage(systemName: micState.image) 49 | buttonImage.tintColor = micState.color 50 | } 51 | } 52 | 53 | public extension PTGMicButton { 54 | enum PTGMicState { 55 | case on 56 | case off 57 | 58 | var image: String { 59 | switch self { 60 | case .on: 61 | return "microphone" 62 | case .off: 63 | return "microphone.slash" 64 | } 65 | } 66 | 67 | var color: UIColor { 68 | switch self { 69 | case .on: 70 | return .white 71 | case .off: 72 | return .red 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Source/PTGPaddingLabel.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public final class PTGPaddingLabel: UILabel { 4 | private let padding: UIEdgeInsets 5 | 6 | public init(padding: UIEdgeInsets = UIEdgeInsets(top: 3.5, left: 8.0, bottom: 3.5, right: 8.0)) { 7 | self.padding = padding 8 | super.init(frame: .zero) 9 | configure() 10 | } 11 | 12 | required init?(coder: NSCoder) { 13 | fatalError("init(coder:) has not been implemented") 14 | } 15 | 16 | public override func drawText(in rect: CGRect) { 17 | super.drawText(in: rect.inset(by: padding)) 18 | } 19 | 20 | // 안에 내재되어있는 콘텐트의 사이즈에 따라 height와 width에 padding값을 더해줌 21 | public override var intrinsicContentSize: CGSize { 22 | var contentSize = super.intrinsicContentSize 23 | contentSize.height += padding.top + padding.bottom 24 | contentSize.width += padding.left + padding.right 25 | 26 | return contentSize 27 | } 28 | 29 | private func configure() { 30 | font = .PTGFont(size: 14, weight: .semibold) 31 | textColor = .white.withAlphaComponent(0.8) 32 | backgroundColor = UIColor.black.withAlphaComponent(0.4) 33 | layer.cornerRadius = 10 34 | clipsToBounds = true 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Source/PTGPrimaryButton.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SnapKit 3 | 4 | public final class PTGPrimaryButton: UIButton { 5 | private let buttonLabel = UILabel() 6 | 7 | public init() { 8 | super.init(frame: .zero) 9 | 10 | addViews() 11 | setupConstraints() 12 | configureUI() 13 | } 14 | 15 | required init?(coder: NSCoder) { 16 | fatalError("init(coder:) has not been implemented") 17 | } 18 | 19 | private func addViews() { 20 | addSubview(buttonLabel) 21 | } 22 | 23 | private func setupConstraints() { 24 | buttonLabel.snp.makeConstraints { 25 | $0.centerY.equalToSuperview() 26 | $0.horizontalEdges.equalToSuperview().inset(16) 27 | } 28 | } 29 | 30 | private func configureUI() { 31 | backgroundColor = .primaryGreen 32 | layer.cornerRadius = 8 33 | 34 | buttonLabel.font = .systemFont(ofSize: 18, weight: .regular) 35 | buttonLabel.textColor = .gray85 36 | buttonLabel.textAlignment = .center 37 | } 38 | 39 | public func setTitle(to title: String) { 40 | buttonLabel.text = title 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Source/PlaceHolderView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SnapKit 3 | 4 | public final class PTGParticipantsPlaceHolderView: UIView { 5 | private let label = UILabel() 6 | 7 | public init() { 8 | super.init(frame: .zero) 9 | addViews() 10 | setupConstraints() 11 | configureUI() 12 | } 13 | 14 | @available(*, unavailable) 15 | required init?(coder: NSCoder) { 16 | fatalError("init(coder:) has not been implemented") 17 | } 18 | 19 | private func addViews() { 20 | self.addSubview(label) 21 | } 22 | 23 | private func setupConstraints() { 24 | label.snp.makeConstraints { 25 | $0.center.equalToSuperview() 26 | } 27 | } 28 | 29 | private func configureUI() { 30 | self.backgroundColor = PTGColor.gray90.color 31 | label.text = "Photo Gether" 32 | label.font = .PTGFont(size: 20, weight: .regular) 33 | label.textColor = .white 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Source/UIColor+.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public extension UIColor { 4 | convenience init(hex: String) { 5 | let hexString = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) 6 | var int = UInt64() 7 | Scanner(string: hexString).scanHexInt64(&int) 8 | 9 | let red, green, blue, alpha: UInt64 10 | switch hexString.count { 11 | case 6: // 6자리 (RGB) 12 | (red, green, blue, alpha) = ((int >> 16) & 0xFF, (int >> 8) & 0xFF, int & 0xFF, 0xFF) 13 | case 8: // 8자리 (RGBA) 14 | (red, green, blue, alpha) = ((int >> 24) & 0xFF, (int >> 16) & 0xFF, (int >> 8) & 0xFF, int & 0xFF) 15 | default: 16 | (red, green, blue, alpha) = (0, 0, 0, 0xFF) // 유효하지 않은 경우 기본값 17 | } 18 | 19 | self.init( 20 | red: CGFloat(red) / 255.0, 21 | green: CGFloat(green) / 255.0, 22 | blue: CGFloat(blue) / 255.0, 23 | alpha: CGFloat(alpha) / 255.0 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/DesignSystem/DesignSystem/Source/UIFont+PTGFont.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public extension UIFont { 4 | public static func PTGFont(size fontSize: CGFloat, weight: UIFont.Weight) -> UIFont { 5 | let familyName = "Pretendard" 6 | 7 | var weightString: String 8 | switch weight { 9 | case .black: 10 | weightString = "Black" 11 | case .bold: 12 | weightString = "Blod" 13 | case .heavy: 14 | weightString = "ExtraBold" 15 | case .ultraLight: 16 | weightString = "ExtraLight" 17 | case .light: 18 | weightString = "Light" 19 | case .medium: 20 | weightString = "Medium" 21 | case .regular: 22 | weightString = "Regular" 23 | case .semibold: 24 | weightString = "SemiBold" 25 | case .thin: 26 | weightString = "Thin" 27 | default: 28 | weightString = "Regular" 29 | } 30 | 31 | return UIFont(name: "\(familyName)-\(weightString)", size: fontSize) ?? .systemFont(ofSize: fontSize, weight: weight) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/EditPhotoRoomFeature/EditPhotoRoomFeature/Source/EditPhotoGuestBottomView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Combine 3 | import DesignSystem 4 | 5 | final class EditPhotoGuestBottomView: UIView { 6 | private let stackView = UIStackView() 7 | private let frameButton = PTGGrayButton(type: .frame) 8 | private let stickerButton = PTGGrayButton(type: .sticker) 9 | 10 | var frameButtonTapped: AnyPublisher { 11 | return frameButton.tapPublisher 12 | } 13 | 14 | var stickerButtonTapped: AnyPublisher { 15 | return stickerButton.tapPublisher 16 | } 17 | 18 | override init(frame: CGRect) { 19 | super.init(frame: frame) 20 | 21 | addViews() 22 | setupConstraints() 23 | configureUI() 24 | } 25 | 26 | @available(*, unavailable) 27 | required init?(coder: NSCoder) { 28 | fatalError("init(coder:) has not been implemented") 29 | } 30 | 31 | private func addViews() { 32 | addSubview(stackView) 33 | 34 | [frameButton, stickerButton].forEach { 35 | stackView.addArrangedSubview($0) 36 | } 37 | } 38 | 39 | private func setupConstraints() { 40 | stackView.snp.makeConstraints { 41 | $0.horizontalEdges.equalToSuperview().inset(32) 42 | $0.verticalEdges.equalToSuperview().inset(14) 43 | } 44 | } 45 | 46 | // TODO: nextButton background primaryColor로 변경 예정 47 | private func configureUI() { 48 | backgroundColor = .clear 49 | 50 | stackView.spacing = 16 51 | stackView.axis = .horizontal 52 | stackView.distribution = .fillEqually 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/EditPhotoRoomFeature/EditPhotoRoomFeature/Source/FrameImageGenerator/Frame/Common/FrameConstants.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum FrameConstants { 4 | static let frameViewSize = CGSize(width: 393, height: 631) 5 | static let frameImageSize = CGSize(width: 369, height: 607) 6 | static let imageSize = CGSize(width: 175.0, height: 233.33) 7 | } 8 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/EditPhotoRoomFeature/EditPhotoRoomFeature/Source/FrameImageGenerator/Frame/Common/FrameImageView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class FrameImageView: UIView { 4 | private(set) var images: [UIImage] 5 | private(set) var imageViews: [UIImageView] = [] 6 | var participants: Participant { Participant(count: images.count) } 7 | 8 | init(images: [UIImage]) { 9 | self.images = images 10 | super.init(frame: .zero) 11 | setupImageViews() 12 | } 13 | 14 | required init?(coder: NSCoder) { 15 | fatalError("init(coder:) has not been implemented") 16 | } 17 | 18 | private func setupImageViews() { 19 | let maxImageCount = participants.rawValue 20 | imageViews = (0.. UIImage { 26 | layoutIfNeeded() 27 | let renderer = UIGraphicsImageRenderer(size: FrameConstants.frameViewSize) 28 | let capturedImage = renderer.image { context in 29 | layer.render(in: context.cgContext) 30 | } 31 | return capturedImage 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/EditPhotoRoomFeature/EditPhotoRoomFeature/Source/FrameImageGenerator/Frame/DefaultFrame/DefaultFrameView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import DesignSystem 3 | 4 | final class DefaultFrameView: FrameView { 5 | private let frameImageView: DefaultFrameImageView 6 | private let label = UILabel() 7 | private let frameColor: FrameColor 8 | 9 | init(images: [UIImage], color: FrameColor) { 10 | self.frameImageView = DefaultFrameImageView(images: images) 11 | self.frameColor = color 12 | super.init() 13 | addViews() 14 | setupConstraints() 15 | configureUI() 16 | } 17 | 18 | required init?(coder: NSCoder) { 19 | fatalError("init(coder:) has not been implemented") 20 | } 21 | 22 | override func addViews() { 23 | addSubview(frameImageView) 24 | addSubview(label) 25 | } 26 | 27 | override func setupConstraints() { 28 | frameImageView.snp.makeConstraints { 29 | $0.top.horizontalEdges.equalToSuperview().inset(16) 30 | $0.height.equalTo(477.67) 31 | } 32 | 33 | label.snp.makeConstraints { 34 | $0.centerX.equalToSuperview() 35 | $0.bottom.equalToSuperview().inset(48) 36 | } 37 | } 38 | 39 | override func configureUI() { 40 | backgroundColor = frameColor.color 41 | 42 | label.text = "PhotoGether" 43 | label.font = .systemFont(ofSize: 36, weight: .bold) 44 | label.textColor = PTGColor.primaryGreen.color 45 | } 46 | } 47 | 48 | extension DefaultFrameView { 49 | enum FrameColor { 50 | case black, white 51 | 52 | var color: UIColor { 53 | switch self { 54 | case .black: 55 | return PTGColor.gray85.color 56 | case .white: 57 | return PTGColor.gray10.color 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/EditPhotoRoomFeature/EditPhotoRoomFeature/Source/FrameImageGenerator/FrameImageGenerator.swift: -------------------------------------------------------------------------------- 1 | import PhotoGetherDomainInterface 2 | import UIKit 3 | 4 | public protocol FrameImageGenerator { 5 | func generate() -> UIImage 6 | func changeFrame(to type: FrameType) 7 | 8 | var frameType: FrameType { get } 9 | var images: [UIImage] { get } 10 | } 11 | 12 | public final class FrameImageGeneratorImpl: FrameImageGenerator { 13 | public private(set) var images: [UIImage] 14 | public private(set) var frameType: FrameType = .defaultBlack { 15 | didSet { 16 | frameView = makeFrameView() 17 | } 18 | } 19 | private var frameView: FrameViewRenderable! 20 | 21 | public init(images: [UIImage]) { 22 | self.images = images 23 | self.frameView = makeFrameView() 24 | } 25 | 26 | // MARK: 전략 패턴으로 프레임 추가 구성 (ex. DefaultWhiteFrameView) 27 | private func makeFrameView() -> FrameViewRenderable { 28 | switch frameType { 29 | case .defaultBlack: 30 | return DefaultFrameView(images: images, color: .black) 31 | case .defaultWhite: 32 | return DefaultFrameView(images: images, color: .white) 33 | } 34 | } 35 | 36 | public func changeFrame(to type: FrameType) { 37 | self.frameType = type 38 | } 39 | 40 | public func generate() -> UIImage { 41 | return frameView.render() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/EditPhotoRoomFeature/EditPhotoRoomFeature/Source/FrameImageGenerator/FrameViewRenderable.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | protocol FrameViewRenderable { 4 | func render() -> UIImage 5 | } 6 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/EditPhotoRoomFeature/EditPhotoRoomFeature/Source/StickerBottomSheetViewModel.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | import PhotoGetherDomainInterface 5 | 6 | public final class StickerBottomSheetViewModel { 7 | enum Input { 8 | case emojiTapped(index: IndexPath) 9 | } 10 | 11 | enum Output { 12 | case emoji(entity: EmojiEntity) 13 | } 14 | 15 | private(set) var emojiList = CurrentValueSubject<[EmojiEntity], Never>([]) 16 | 17 | private let fetchEmojiListUseCase: FetchEmojiListUseCase 18 | 19 | private var cancellables = Set() 20 | private var output = PassthroughSubject() 21 | 22 | public init( 23 | fetchEmojiListUseCase: FetchEmojiListUseCase 24 | ) { 25 | self.fetchEmojiListUseCase = fetchEmojiListUseCase 26 | 27 | self.bind() 28 | } 29 | 30 | private func bind() { 31 | fetchEmojiListUseCase.execute(.objects) 32 | .sink { [weak self] emojiEntities in 33 | self?.emojiList.send(emojiEntities) 34 | } 35 | .store(in: &cancellables) 36 | } 37 | 38 | func transform(input: AnyPublisher) -> AnyPublisher { 39 | input.sink { [weak self] event in 40 | switch event { 41 | case .emojiTapped(let indexPath): 42 | self?.sendEmoji(by: indexPath) 43 | } 44 | } 45 | .store(in: &cancellables) 46 | 47 | return output.eraseToAnyPublisher() 48 | } 49 | 50 | private func sendEmoji(by indexPath: IndexPath) { 51 | let selectedEmoji = emojiList.value[indexPath.item] 52 | output.send(.emoji(entity: selectedEmoji)) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/EditPhotoRoomFeature/EditPhotoRoomFeature/Source/StickerCollectionView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | import DesignSystem 4 | 5 | final class StickerCollectionView: UICollectionView { 6 | override init( 7 | frame: CGRect = .zero, 8 | collectionViewLayout layout: UICollectionViewLayout 9 | ) { 10 | super.init(frame: frame, collectionViewLayout: layout) 11 | 12 | self.congigureUI() 13 | } 14 | 15 | private func congigureUI() { 16 | guard let layout = self.collectionViewLayout 17 | as? UICollectionViewFlowLayout 18 | else { return } 19 | 20 | layout.itemSize = CGSize(width: 64, height: 64) 21 | layout.minimumInteritemSpacing = 16 22 | layout.minimumLineSpacing = 16 23 | 24 | self.backgroundColor = PTGColor.gray10.color 25 | } 26 | 27 | @available(*, unavailable) 28 | required init?(coder: NSCoder) { fatalError() } 29 | } 30 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/EditPhotoRoomFeature/EditPhotoRoomFeature/Source/StickerCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | import BaseFeature 4 | import PhotoGetherDomainInterface 5 | 6 | // TODO: 이미지가 Repo로 부터 도착하면 image 주입 7 | // TODO: Cell 탭할 때 해당 이모지 화면에 추가 + 이벤트 허브 태우기 8 | 9 | final class StickerCollectionViewCell: UICollectionViewCell { 10 | private let imageView = UIImageView() 11 | 12 | override init(frame: CGRect) { 13 | super.init(frame: frame) 14 | 15 | self.addViews() 16 | self.setupConstraints() 17 | self.configureUI() 18 | } 19 | 20 | @available(*, unavailable) 21 | required init?(coder aDecoder: NSCoder) { fatalError() } 22 | 23 | override func prepareForReuse() { 24 | super.prepareForReuse() 25 | 26 | imageView.image = nil 27 | } 28 | 29 | private func addViews() { 30 | [imageView].forEach { self.addSubview($0) } 31 | } 32 | 33 | private func setupConstraints() { 34 | imageView.snp.makeConstraints { 35 | $0.edges.equalToSuperview() 36 | } 37 | } 38 | 39 | private func configureUI() { } 40 | 41 | func setImage(by emoji: EmojiEntity) { 42 | guard let url = emoji.emojiURL else { return } 43 | Task { await imageView.setAsyncImage(url) } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/EditPhotoRoomFeature/EditPhotoRoomFeatureDemo/App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @main 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | func application( 6 | _ application: UIApplication, 7 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 8 | ) -> Bool { 9 | return true 10 | } 11 | 12 | func application( 13 | _ application: UIApplication, 14 | configurationForConnecting connectingSceneSession: UISceneSession, 15 | options: UIScene.ConnectionOptions 16 | ) -> UISceneConfiguration { 17 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/EditPhotoRoomFeature/EditPhotoRoomFeatureDemo/Resource/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/EditPhotoRoomFeature/EditPhotoRoomFeatureDemo/Resource/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "EditRoomIcon.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | }, 9 | { 10 | "appearances" : [ 11 | { 12 | "appearance" : "luminosity", 13 | "value" : "dark" 14 | } 15 | ], 16 | "idiom" : "universal", 17 | "platform" : "ios", 18 | "size" : "1024x1024" 19 | }, 20 | { 21 | "appearances" : [ 22 | { 23 | "appearance" : "luminosity", 24 | "value" : "tinted" 25 | } 26 | ], 27 | "idiom" : "universal", 28 | "platform" : "ios", 29 | "size" : "1024x1024" 30 | } 31 | ], 32 | "info" : { 33 | "author" : "xcode", 34 | "version" : 1 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/EditPhotoRoomFeature/EditPhotoRoomFeatureDemo/Resource/Assets.xcassets/AppIcon.appiconset/EditRoomIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/PresentationLayer/EditPhotoRoomFeature/EditPhotoRoomFeatureDemo/Resource/Assets.xcassets/AppIcon.appiconset/EditRoomIcon.png -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/EditPhotoRoomFeature/EditPhotoRoomFeatureDemo/Resource/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/EditPhotoRoomFeature/EditPhotoRoomFeatureDemo/Resource/Base.lproj/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 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/EditPhotoRoomFeature/EditPhotoRoomFeatureDemo/Resource/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | 19 | 20 | 21 | 22 | EMOJI_API_KEY 23 | $(EMOJI_API_KEY) 24 | BASE_URL 25 | $(BASE_URL) 26 | NSCameraUsageDescription 27 | $(PRODUCT_NAME) 카메라 권한이 필요합니다. 28 | NSMicrophoneUsageDescription 29 | $(PRODUCT_NAME) 마이크 권한이 필요합니다. 30 | NSAppTransportSecurity 31 | 32 | NSAllowsArbitraryLoads 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/EditPhotoRoomFeature/EditPhotoRoomHostViewModelTests/EditPhotoRoomHostViewModelTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import EditPhotoRoomFeature 3 | import FeatureTesting 4 | 5 | final class EditPhotoRoomHostViewModelTests: XCTestCase { 6 | var sut: EditPhotoRoomHostViewModel! 7 | 8 | override func setUp() { 9 | super.setUp() 10 | 11 | let fetchStickerListUseCaseMock = FetchStickerListUseCaseMock() 12 | sut = EditPhotoRoomHostViewModel(fetchStickerListUseCase: fetchStickerListUseCaseMock) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/FeatureTesting/FeatureTesting/PhotoRoomViewControllerMock.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public class TestClassMock { 4 | public init() { } 5 | } 6 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/PhotoRoomFeature/PhotoRoomFeature/Source/View/PlaceHolderView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import DesignSystem 3 | import SnapKit 4 | import BaseFeature 5 | 6 | public final class PlaceHolderView: UIView { 7 | private let label = UILabel() 8 | 9 | public init() { 10 | super.init(frame: .zero) 11 | addViews() 12 | setupConstraints() 13 | configureUI() 14 | } 15 | 16 | @available(*, unavailable) 17 | required init?(coder: NSCoder) { 18 | fatalError("init(coder:) has not been implemented") 19 | } 20 | 21 | public func setText(_ text: String) { 22 | self.label.text = text 23 | } 24 | 25 | private func addViews() { 26 | self.addSubview(label) 27 | } 28 | 29 | private func setupConstraints() { 30 | label.snp.makeConstraints { 31 | $0.center.equalToSuperview() 32 | } 33 | } 34 | 35 | private func configureUI() { 36 | self.backgroundColor = PTGColor.gray90.color 37 | label.font = .systemFont(ofSize: 20) 38 | label.textColor = .white 39 | label.setKern() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/PhotoRoomFeature/PhotoRoomFeatureDemo/App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @main 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | func application( 6 | _ application: UIApplication, 7 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 8 | ) -> Bool { 9 | return true 10 | } 11 | 12 | func application( 13 | _ application: UIApplication, 14 | configurationForConnecting connectingSceneSession: UISceneSession, 15 | options: UIScene.ConnectionOptions 16 | ) -> UISceneConfiguration { 17 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/PhotoRoomFeature/PhotoRoomFeatureDemo/App/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import PhotoRoomFeature 3 | 4 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 5 | var window: UIWindow? 6 | 7 | func scene( 8 | _ scene: UIScene, 9 | willConnectTo session: UISceneSession, 10 | options connectionOptions: UIScene.ConnectionOptions 11 | ) { 12 | guard let windowScene = (scene as? UIWindowScene) else { return } 13 | window = UIWindow(windowScene: windowScene) 14 | window?.rootViewController = PhotoRoomViewController(isHost: true) 15 | window?.makeKeyAndVisible() 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/PhotoRoomFeature/PhotoRoomFeatureDemo/Resource/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/PhotoRoomFeature/PhotoRoomFeatureDemo/Resource/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "PhotoRoomIcon.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | }, 9 | { 10 | "appearances" : [ 11 | { 12 | "appearance" : "luminosity", 13 | "value" : "dark" 14 | } 15 | ], 16 | "idiom" : "universal", 17 | "platform" : "ios", 18 | "size" : "1024x1024" 19 | }, 20 | { 21 | "appearances" : [ 22 | { 23 | "appearance" : "luminosity", 24 | "value" : "tinted" 25 | } 26 | ], 27 | "idiom" : "universal", 28 | "platform" : "ios", 29 | "size" : "1024x1024" 30 | } 31 | ], 32 | "info" : { 33 | "author" : "xcode", 34 | "version" : 1 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/PhotoRoomFeature/PhotoRoomFeatureDemo/Resource/Assets.xcassets/AppIcon.appiconset/PhotoRoomIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/PresentationLayer/PhotoRoomFeature/PhotoRoomFeatureDemo/Resource/Assets.xcassets/AppIcon.appiconset/PhotoRoomIcon.png -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/PhotoRoomFeature/PhotoRoomFeatureDemo/Resource/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/PhotoRoomFeature/PhotoRoomFeatureDemo/Resource/Base.lproj/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 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/PhotoRoomFeature/PhotoRoomFeatureDemo/Resource/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/SharePhotoFeature/SharePhotoFeature/Source/SharePhotoBottomView.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import DesignSystem 3 | import UIKit 4 | 5 | final class SharePhotoBottomView: UIView { 6 | private let shareButton = PTGCircleButton(type: .share) 7 | private let saveButton = PTGPrimaryButton() 8 | 9 | var shareButtonTapped: AnyPublisher { 10 | return shareButton.tapPublisher 11 | } 12 | 13 | var saveButtonTapped: AnyPublisher { 14 | return saveButton.tapPublisher 15 | } 16 | 17 | override init(frame: CGRect) { 18 | super.init(frame: frame) 19 | 20 | addViews() 21 | setupConstraints() 22 | configureUI() 23 | } 24 | 25 | required init?(coder: NSCoder) { 26 | fatalError("init(coder:) has not been implemented") 27 | } 28 | 29 | 30 | private func addViews() { 31 | [shareButton, saveButton].forEach { 32 | addSubview($0) 33 | } 34 | } 35 | 36 | private func setupConstraints() { 37 | shareButton.snp.makeConstraints { 38 | $0.leading.equalToSuperview().inset(16) 39 | $0.verticalEdges.equalToSuperview().inset(14) 40 | $0.width.height.equalTo(52) 41 | } 42 | 43 | saveButton.snp.makeConstraints { 44 | $0.leading.equalTo(shareButton.snp.trailing).offset(16) 45 | $0.verticalEdges.equalTo(shareButton) 46 | $0.trailing.equalToSuperview().inset(16) 47 | } 48 | } 49 | 50 | private func configureUI() { 51 | backgroundColor = .clear 52 | saveButton.setTitle(to: "저장하기") 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/SharePhotoFeature/SharePhotoFeature/Source/SharePhotoViewModel.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | import PhotoGetherDomainInterface 4 | 5 | public final class SharePhotoViewModel { 6 | enum Input { 7 | case shareButtonDidTap 8 | case saveButtonDidTap 9 | } 10 | 11 | enum Output { 12 | case showShareSheet 13 | case showSaveToast 14 | case showFailToast 15 | case showAuthorizationAlert 16 | } 17 | 18 | public private(set) var photoData: Data 19 | 20 | private let output = PassthroughSubject() 21 | 22 | private var cancellables = Set() 23 | 24 | public init(component: SharePhotoComponent) { 25 | self.photoData = component.photoData 26 | } 27 | 28 | func transform(input: AnyPublisher) -> AnyPublisher { 29 | input.sink { [weak self] event in 30 | switch event { 31 | case .shareButtonDidTap: 32 | self?.output.send(.showShareSheet) 33 | case .saveButtonDidTap: 34 | Task { await self?.handleSaveButtonDidTap() } 35 | } 36 | } 37 | .store(in: &cancellables) 38 | 39 | return output.eraseToAnyPublisher() 40 | } 41 | 42 | private func handleSaveButtonDidTap() async { 43 | guard await isAuthorized() else { 44 | output.send(.showAuthorizationAlert) 45 | return 46 | } 47 | 48 | let isSuccess = await savePhoto() 49 | output.send(isSuccess ? .showSaveToast : .showFailToast) 50 | } 51 | 52 | private func savePhoto() async -> Bool { 53 | return await PhotoLibraryHelper.savePhoto(with: photoData) 54 | } 55 | 56 | private func isAuthorized() async -> Bool { 57 | return await PhotoLibraryPermissionManager.checkPhotoPermission() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/SharePhotoFeature/SharePhotoFeature/Source/Utility/PhotoLibraryHelper.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Photos 3 | 4 | struct PhotoLibraryHelper { 5 | static func savePhoto(with data: Data) async -> Bool { 6 | 7 | guard let photoImage = UIImage(data: data) else { return false } 8 | 9 | return await withCheckedContinuation { continuation in 10 | PHPhotoLibrary.shared().performChanges({ 11 | PHAssetChangeRequest.creationRequestForAsset(from: photoImage) 12 | }) { success, error in 13 | if let error = error { 14 | print("Error saving photo: \(error.localizedDescription)") 15 | } else if success { 16 | print("Photo saved successfully!") 17 | } 18 | continuation.resume(returning: success) 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/SharePhotoFeature/SharePhotoFeature/Source/Utility/PhotoLibraryPermissionManager.swift: -------------------------------------------------------------------------------- 1 | import Photos 2 | 3 | struct PhotoLibraryPermissionManager { 4 | static func checkPhotoPermission() async -> Bool { 5 | let status = PHPhotoLibrary.authorizationStatus() 6 | print(status) 7 | switch status { 8 | case .notDetermined: 9 | return await requestAuthorization() 10 | case .authorized, .limited: 11 | return true 12 | case .denied, .restricted: 13 | return false 14 | @unknown default: 15 | return false 16 | } 17 | } 18 | 19 | private static func requestAuthorization() async -> Bool { 20 | return await withCheckedContinuation { continuation in 21 | PHPhotoLibrary.requestAuthorization(for: .readWrite) { newStatus in 22 | let isAuthorized = (newStatus == .authorized || newStatus == .limited) 23 | continuation.resume(returning: isAuthorized) 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/SharePhotoFeature/SharePhotoFeatureDemo/App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @main 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | func application( 6 | _ application: UIApplication, 7 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 8 | ) -> Bool { 9 | return true 10 | } 11 | 12 | func application( 13 | _ application: UIApplication, 14 | configurationForConnecting connectingSceneSession: UISceneSession, 15 | options: UIScene.ConnectionOptions 16 | ) -> UISceneConfiguration { 17 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/SharePhotoFeature/SharePhotoFeatureDemo/App/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | import DesignSystem 2 | import PhotoGetherDomainInterface 3 | import SharePhotoFeature 4 | import UIKit 5 | 6 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 7 | var window: UIWindow? 8 | 9 | func scene( 10 | _ scene: UIScene, 11 | willConnectTo session: UISceneSession, 12 | options connectionOptions: UIScene.ConnectionOptions 13 | ) { 14 | guard let windowScene = (scene as? UIWindowScene) else { return } 15 | window = UIWindow(windowScene: windowScene) 16 | 17 | let image = PTGImage.sampleImage.image 18 | 19 | // MARK: EditPhotoRoom -> SharePhotoRoom 20 | let imageData = image.pngData() ?? Data() 21 | let component = SharePhotoComponent(imageData: imageData) 22 | let sharePhotoViewModel = SharePhotoViewModel(component: component) 23 | let sharePhotoViewController = SharePhotoViewController(viewModel: sharePhotoViewModel) 24 | 25 | window?.rootViewController = sharePhotoViewController 26 | window?.makeKeyAndVisible() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/SharePhotoFeature/SharePhotoFeatureDemo/Resource/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/SharePhotoFeature/SharePhotoFeatureDemo/Resource/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ShareRoomIcon.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | }, 9 | { 10 | "appearances" : [ 11 | { 12 | "appearance" : "luminosity", 13 | "value" : "dark" 14 | } 15 | ], 16 | "idiom" : "universal", 17 | "platform" : "ios", 18 | "size" : "1024x1024" 19 | }, 20 | { 21 | "appearances" : [ 22 | { 23 | "appearance" : "luminosity", 24 | "value" : "tinted" 25 | } 26 | ], 27 | "idiom" : "universal", 28 | "platform" : "ios", 29 | "size" : "1024x1024" 30 | } 31 | ], 32 | "info" : { 33 | "author" : "xcode", 34 | "version" : 1 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/SharePhotoFeature/SharePhotoFeatureDemo/Resource/Assets.xcassets/AppIcon.appiconset/ShareRoomIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/PresentationLayer/SharePhotoFeature/SharePhotoFeatureDemo/Resource/Assets.xcassets/AppIcon.appiconset/ShareRoomIcon.png -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/SharePhotoFeature/SharePhotoFeatureDemo/Resource/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/SharePhotoFeature/SharePhotoFeatureDemo/Resource/Base.lproj/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 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/SharePhotoFeature/SharePhotoFeatureDemo/Resource/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPhotoLibraryUsageDescription 6 | 사진 앱에 대한 접근 권한이 필요합니다. 7 | NSPhotoLibraryAddUsageDescription 8 | 사진을 앨범에 저장하려면 접근 권한이 필요합니다. 9 | UIApplicationSceneManifest 10 | 11 | UIApplicationSupportsMultipleScenes 12 | 13 | UISceneConfigurations 14 | 15 | UIWindowSceneSessionRoleApplication 16 | 17 | 18 | UISceneConfigurationName 19 | Default Configuration 20 | UISceneDelegateClassName 21 | $(PRODUCT_MODULE_NAME).SceneDelegate 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/WaitingRoomFeature/WaitingRoomFeature/Source/ViewModel/EnterLoadingViewModel.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Combine 3 | import PhotoGetherDomainInterface 4 | 5 | public final class EnterLoadingViewModel { 6 | private var cancellables = Set() 7 | 8 | public enum Input { 9 | case viewDidLoad 10 | } 11 | 12 | public enum Output { 13 | case didJoinRoom(isSuccess: Bool) 14 | } 15 | 16 | private let _output = PassthroughSubject() 17 | public var output: AnyPublisher { _output.eraseToAnyPublisher() } 18 | 19 | private let joinRoomUseCase: JoinRoomUseCase 20 | private let roomID: String 21 | private let hostID: String 22 | 23 | public init(joinRoomUseCase: JoinRoomUseCase, roomID: String, hostID: String) { 24 | self.joinRoomUseCase = joinRoomUseCase 25 | self.roomID = roomID 26 | self.hostID = hostID 27 | } 28 | 29 | func transform(input: AnyPublisher) -> AnyPublisher { 30 | input.sink { [weak self] in 31 | guard let self else { return } 32 | 33 | switch $0 { 34 | case .viewDidLoad: 35 | self.requestJoinRoom(roomID: self.roomID, hostID: self.hostID) 36 | } 37 | }.store(in: &cancellables) 38 | 39 | return output 40 | } 41 | 42 | private func requestJoinRoom(roomID: String, hostID: String) { 43 | joinRoomUseCase.execute(roomID: roomID, hostID: hostID) 44 | .sink { [weak self] isSuccess in 45 | self?._output.send(.didJoinRoom(isSuccess: isSuccess)) 46 | } 47 | .store(in: &cancellables) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/WaitingRoomFeature/WaitingRoomFeatureDemo/App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @main 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | func application( 6 | _ application: UIApplication, 7 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 8 | ) -> Bool { 9 | 10 | 11 | return true 12 | } 13 | 14 | func application( 15 | _ application: UIApplication, 16 | configurationForConnecting connectingSceneSession: UISceneSession, 17 | options: UIScene.ConnectionOptions 18 | ) -> UISceneConfiguration { 19 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/WaitingRoomFeature/WaitingRoomFeatureDemo/Resource/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/WaitingRoomFeature/WaitingRoomFeatureDemo/Resource/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "WaitRoomIcon.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | }, 9 | { 10 | "appearances" : [ 11 | { 12 | "appearance" : "luminosity", 13 | "value" : "dark" 14 | } 15 | ], 16 | "idiom" : "universal", 17 | "platform" : "ios", 18 | "size" : "1024x1024" 19 | }, 20 | { 21 | "appearances" : [ 22 | { 23 | "appearance" : "luminosity", 24 | "value" : "tinted" 25 | } 26 | ], 27 | "idiom" : "universal", 28 | "platform" : "ios", 29 | "size" : "1024x1024" 30 | } 31 | ], 32 | "info" : { 33 | "author" : "xcode", 34 | "version" : 1 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/WaitingRoomFeature/WaitingRoomFeatureDemo/Resource/Assets.xcassets/AppIcon.appiconset/WaitRoomIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGether/PresentationLayer/WaitingRoomFeature/WaitingRoomFeatureDemo/Resource/Assets.xcassets/AppIcon.appiconset/WaitRoomIcon.png -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/WaitingRoomFeature/WaitingRoomFeatureDemo/Resource/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/WaitingRoomFeature/WaitingRoomFeatureDemo/Resource/Base.lproj/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 | -------------------------------------------------------------------------------- /PhotoGether/PresentationLayer/WaitingRoomFeature/WaitingRoomFeatureDemo/Resource/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | 19 | 20 | 21 | 22 | BASE_URL 23 | $(BASE_URL) 24 | EMOJI_API_KEY 25 | $(EMOJI_API_KEY) 26 | NSAppTransportSecurity 27 | 28 | NSAllowsArbitraryLoads 29 | 30 | 31 | NSCameraUsageDescription 32 | $(PRODUCT_NAME) 카메라 권한이 필요합니다. 33 | NSMicrophoneUsageDescription 34 | $(PRODUCT_NAME) 마이크 권한이 필요합니다. 35 | 36 | 37 | -------------------------------------------------------------------------------- /PhotoGether/Scripts/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | # .swiftlint.yml 2 | 3 | # 일반적인 설정 4 | opt_in_rules: 5 | # 강제 언래핑 시 경고 6 | - force_unwrapping 7 | 8 | # 강제 캐스팅 시 경고 9 | force_cast: 10 | warning 11 | 12 | # 강제 try 시 경고 13 | force_try: 14 | warning 15 | 16 | disabled_rules: 17 | - trailing_whitespace 18 | - file_length 19 | 20 | # 중괄호 앞 띄어쓰기 안할 시 경고 21 | opening_brace: 22 | severity: warning # 명시적으로 선언 23 | 24 | # 2연속 줄바꿈 제한 25 | vertical_whitespace: 26 | severity: warning 27 | 28 | # 코드 글자 수 한 줄당 120자 제한 29 | line_length: 30 | warning: 120 31 | -------------------------------------------------------------------------------- /PhotoGether/Scripts/SwiftLintRunScript.sh: -------------------------------------------------------------------------------- 1 | if test -d "/opt/homebrew/bin/"; then 2 | PATH="/opt/homebrew/bin/:${PATH}" 3 | fi 4 | 5 | export PATH 6 | 7 | YML="$(dirname "$0")/.swiftlint.yml" 8 | 9 | if which swiftlint > /dev/null; then 10 | swiftlint --config ${YML} 11 | else 12 | echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint" 13 | fi 14 | -------------------------------------------------------------------------------- /PhotoGetherServer/PhotoGetherServer/.dockerignore: -------------------------------------------------------------------------------- 1 | .build/ 2 | .swiftpm/ 3 | -------------------------------------------------------------------------------- /PhotoGetherServer/PhotoGetherServer/.gitignore: -------------------------------------------------------------------------------- 1 | Packages 2 | .build 3 | xcuserdata 4 | *.xcodeproj 5 | DerivedData/ 6 | .DS_Store 7 | db.sqlite 8 | .swiftpm 9 | .env 10 | .env.* 11 | ! .env.example 12 | .vscode 13 | -------------------------------------------------------------------------------- /PhotoGetherServer/PhotoGetherServer/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:6.0 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "PhotoGetherServer", 6 | platforms: [ 7 | .macOS(.v13), .iOS(.v16) 8 | ], 9 | dependencies: [ 10 | // 💧 A server-side Swift web framework. 11 | .package(url: "https://github.com/vapor/vapor.git", from: "4.99.3"), 12 | // 🔵 Non-blocking, event-driven networking for Swift. Used for custom executors 13 | .package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"), 14 | ], 15 | targets: [ 16 | .executableTarget( 17 | name: "App", 18 | dependencies: [ 19 | .product(name: "Vapor", package: "vapor"), 20 | .product(name: "NIOCore", package: "swift-nio"), 21 | .product(name: "NIOPosix", package: "swift-nio"), 22 | ], 23 | swiftSettings: swiftSettings 24 | ), 25 | .testTarget( 26 | name: "AppTests", 27 | dependencies: [ 28 | .target(name: "App"), 29 | .product(name: "XCTVapor", package: "vapor"), 30 | ], 31 | swiftSettings: swiftSettings 32 | ), 33 | .testTarget( 34 | name: "RoomManagerTests", 35 | dependencies: [ 36 | .target(name: "App"), 37 | .product(name: "XCTVapor", package: "vapor"), 38 | ], 39 | swiftSettings: swiftSettings 40 | ) 41 | ], 42 | swiftLanguageModes: [.v5] 43 | ) 44 | 45 | var swiftSettings: [SwiftSetting] { [ 46 | .enableUpcomingFeature("DisableOutwardActorInference"), 47 | .enableExperimentalFeature("StrictConcurrency"), 48 | ] } 49 | -------------------------------------------------------------------------------- /PhotoGetherServer/PhotoGetherServer/Public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGetherServer/PhotoGetherServer/Public/.gitkeep -------------------------------------------------------------------------------- /PhotoGetherServer/PhotoGetherServer/Sources/App/Controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/iOS04-PhotoGether/a1a56b5b4c6bf329a3e79f4804f752c0a8c60604/PhotoGetherServer/PhotoGetherServer/Sources/App/Controllers/.gitkeep -------------------------------------------------------------------------------- /PhotoGetherServer/PhotoGetherServer/Sources/App/DTO/Message/IceCandidateMessage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | package struct IceCandidateMessage: Codable { 4 | let sdp: String 5 | let sdpMLineIndex: Int32 6 | let sdpMid: String? 7 | let receiverID: String // MARK: 받는 사람의 ID 8 | let senderID: String // MARK: 보내는 사람의 ID 9 | let roomID: String // MARK: 참가하려는 방의 ID 10 | 11 | package init(sdp: String, sdpMLineIndex: Int32, sdpMid: String?, receiverID: String, senderID: String, roomID: String) { 12 | self.sdp = sdp 13 | self.sdpMLineIndex = sdpMLineIndex 14 | self.sdpMid = sdpMid 15 | self.receiverID = receiverID 16 | self.senderID = senderID 17 | self.roomID = roomID 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /PhotoGetherServer/PhotoGetherServer/Sources/App/DTO/Message/JoinRoomRequestMessage.swift: -------------------------------------------------------------------------------- 1 | package struct JoinRoomRequestMessage: Decodable { 2 | let roomID: String 3 | } 4 | -------------------------------------------------------------------------------- /PhotoGetherServer/PhotoGetherServer/Sources/App/DTO/Message/SessionDescriptionMessage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | package enum SdpType: String, Codable { 3 | case offer, prAnswer, answer, rollback 4 | } 5 | 6 | package struct SessionDescriptionMessage: Codable { 7 | let sdp: String 8 | let type: SdpType 9 | let roomID: String // MARK: 참가하려는 방의 ID 10 | let offerID: String // MARK: Offer를 보내는 사람의 ID 11 | var answerID: String? // MARK: Anwer를 보내는 사람의 ID 12 | 13 | package init( 14 | sdp: String, 15 | type: SdpType, 16 | offerID: String, 17 | roomID: String, 18 | answerID: String? = nil 19 | ) { 20 | self.sdp = sdp 21 | self.type = type 22 | self.roomID = roomID 23 | self.offerID = offerID 24 | self.answerID = answerID 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /PhotoGetherServer/PhotoGetherServer/Sources/App/DTO/RequestDTO/RoomRequestDTO.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct RoomRequestDTO: Decodable { 4 | var messageType: RoomMessageType 5 | var message: Data? 6 | 7 | init(messageType: RoomMessageType, message: Data? = nil) { 8 | self.messageType = messageType 9 | self.message = message 10 | } 11 | 12 | enum RoomMessageType: String, Decodable { 13 | case createRoom 14 | case joinRoom 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /PhotoGetherServer/PhotoGetherServer/Sources/App/DTO/RequestDTO/SignalingRequestDTO.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct SignalingRequestDTO: Decodable { 4 | var messageType: SignalingMessageType 5 | var message: Data? 6 | 7 | init(messageType: SignalingMessageType, message: Data? = nil) { 8 | self.messageType = messageType 9 | self.message = message 10 | } 11 | 12 | enum SignalingMessageType: String, Decodable { 13 | case offerSDP 14 | case answerSDP 15 | case iceCandidate 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /PhotoGetherServer/PhotoGetherServer/Sources/App/DTO/RequestDTO/WebSocketRequestType.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct WebSocketRequestType: Decodable { 4 | let messageType: MessageType 5 | } 6 | 7 | enum MessageType: String, Decodable { 8 | case offerSDP 9 | case answerSDP 10 | case iceCandidate 11 | case createRoom 12 | case joinRoom 13 | } 14 | -------------------------------------------------------------------------------- /PhotoGetherServer/PhotoGetherServer/Sources/App/DTO/ResponseDTO/CreateRoomResponseDTO.swift: -------------------------------------------------------------------------------- 1 | package struct CreateRoomResponseDTO: Encodable { 2 | let roomID: String 3 | let hostID: String 4 | } 5 | 6 | -------------------------------------------------------------------------------- /PhotoGetherServer/PhotoGetherServer/Sources/App/DTO/ResponseDTO/JoinRoomResponseDTO.swift: -------------------------------------------------------------------------------- 1 | package struct JoinRoomResponseDTO: Encodable { 2 | let userID: String 3 | let roomID: String 4 | let userList: [UserDTO] 5 | 6 | package init(userID: String, roomID: String, userList: [UserDTO]) { 7 | self.userID = userID 8 | self.roomID = roomID 9 | self.userList = userList 10 | } 11 | } 12 | 13 | package struct UserDTO: Encodable { 14 | let userID: String 15 | let nickname: String 16 | let initialPosition: Int 17 | } 18 | -------------------------------------------------------------------------------- /PhotoGetherServer/PhotoGetherServer/Sources/App/DTO/ResponseDTO/NotifyNewUserResponseDTO.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | package struct NotifyNewUserResponseDTO: Encodable { 4 | let newUser: UserDTO 5 | 6 | package init(newUser: UserDTO) { 7 | self.newUser = newUser 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /PhotoGetherServer/PhotoGetherServer/Sources/App/DTO/ResponseDTO/RoomResponseDTO.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct RoomResponseDTO: Encodable { 4 | var messageType: RoomMessageType 5 | var message: Data? 6 | 7 | enum RoomMessageType: String, Encodable { 8 | case createRoom 9 | case joinRoom 10 | case notifyNewUser 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /PhotoGetherServer/PhotoGetherServer/Sources/App/DTO/ResponseDTO/SignalingResponsetDTO.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct SignalingResponseDTO: Encodable { 4 | var messageType: SignalingMessageType 5 | var message: Data? 6 | 7 | init(messageType: SignalingMessageType, message: Data? = nil) { 8 | self.messageType = messageType 9 | self.message = message 10 | } 11 | 12 | enum SignalingMessageType: String, Encodable { 13 | case offerSDP 14 | case answerSDP 15 | case iceCandidate 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /PhotoGetherServer/PhotoGetherServer/Sources/App/Error/RoomError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum RoomError: LocalizedError { 4 | case createFailed 5 | case joinFailed 6 | 7 | var errorDescription: String? { 8 | switch self { 9 | case .createFailed: 10 | return "Failed to create room" 11 | case .joinFailed: 12 | return "Failed to join room" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /PhotoGetherServer/PhotoGetherServer/Sources/App/Model/Room.swift: -------------------------------------------------------------------------------- 1 | final class Room { 2 | private(set) var userList: [User] = [] 3 | private var maxCount: Int = 4 4 | let roomID: String 5 | 6 | init(roomID: String) { 7 | self.roomID = roomID 8 | } 9 | 10 | @discardableResult 11 | func invite(user: User) -> Bool { 12 | guard userList.count < maxCount else { return false } 13 | userList.append(user) 14 | return true 15 | } 16 | 17 | @discardableResult 18 | func kick(userID: String) -> Bool { 19 | let filtered = userList.filter { $0.id != userID } 20 | userList = filtered 21 | return filtered.isEmpty // 필터에 걸렸으면 찾은 것 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /PhotoGetherServer/PhotoGetherServer/Sources/App/Model/User.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | 3 | struct User: Equatable { 4 | let id: String 5 | let client: WebSocket 6 | 7 | static func == (lhs: User, rhs: User) -> Bool { 8 | lhs.id == rhs.id 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /PhotoGetherServer/PhotoGetherServer/Sources/App/Utils/ByteBuffer+toDTO.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Vapor 3 | 4 | package extension ByteBuffer { 5 | func toDTO(type: T.Type, decoder: JSONDecoder) -> T? { 6 | return try? decoder.decode(type, from: self) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /PhotoGetherServer/PhotoGetherServer/Sources/App/Utils/Data+toDTO.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | package extension Data { 4 | func toDTO(type: T.Type, decoder: JSONDecoder) -> T? { 5 | return try? decoder.decode(type, from: self) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /PhotoGetherServer/PhotoGetherServer/Sources/App/Utils/Encodable+toData.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Encodable { 4 | func toData(_ encoder: JSONEncoder) -> Data? { 5 | return try? encoder.encode(self) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /PhotoGetherServer/PhotoGetherServer/Sources/App/Utils/WebSocket+decodeDTO.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | 3 | extension WebSocket { 4 | func decodeDTO( 5 | data: ByteBuffer, 6 | type: T.Type, 7 | decoder: JSONDecoder 8 | ) -> T? { 9 | guard let dto = data.toDTO(type: type, decoder: decoder) else { 10 | print("[DEBUG] :: Decode Failed: \(type) \(data.readableBytes)") 11 | return nil 12 | } 13 | return dto 14 | } 15 | 16 | func decodeDTO( 17 | data: Data, 18 | type: T.Type, 19 | decoder: JSONDecoder 20 | ) -> T? { 21 | guard let dto = data.toDTO(type: type, decoder: decoder) else { 22 | print("[DEBUG] :: Decode Failed: \(type) \(data.count)") 23 | return nil 24 | } 25 | return dto 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /PhotoGetherServer/PhotoGetherServer/Sources/App/Utils/WebSocket+sendDTO.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | 3 | extension WebSocket { 4 | @discardableResult 5 | func sendDTO( 6 | _ dto: T, 7 | encoder: JSONEncoder 8 | ) -> Bool { 9 | guard let data = dto.toData(encoder) else { 10 | print("[DEBUG] :: Encode Failed: \(dto.self)") 11 | return false 12 | } 13 | self.send(data) 14 | return true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /PhotoGetherServer/PhotoGetherServer/Sources/App/configure.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | 3 | public func configure(_ app: Application) async throws { 4 | app.http.server.configuration.hostname = "0.0.0.0" 5 | app.http.server.configuration.port = 8080 6 | try routes(app) 7 | } 8 | -------------------------------------------------------------------------------- /PhotoGetherServer/PhotoGetherServer/Sources/App/entrypoint.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | import Logging 3 | import NIOCore 4 | import NIOPosix 5 | 6 | @main 7 | enum Entrypoint { 8 | static func main() async throws { 9 | var env = try Environment.detect() 10 | try LoggingSystem.bootstrap(from: &env) 11 | 12 | let app = try await Application.make(env) 13 | 14 | do { 15 | try await configure(app) 16 | } catch { 17 | app.logger.report(error: error) 18 | try? await app.asyncShutdown() 19 | throw error 20 | } 21 | try await app.execute() 22 | try await app.asyncShutdown() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /PhotoGetherServer/PhotoGetherServer/Sources/App/routes.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Vapor 3 | 4 | func routes(_ app: Application) throws { 5 | // MARK: 객체 초기화 6 | let roomManager = RoomManager() 7 | let webSocketController = WebSocketController(roomManager: roomManager) 8 | 9 | // MARK: Controller에서 대신 처리 10 | app.webSocket("signaling") { req, client in 11 | await webSocketController.handleConnection(req, client: client) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /PhotoGetherServer/PhotoGetherServer/Tests/AppTests/AppTests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTVapor 3 | import Testing 4 | 5 | @Suite("App Tests") 6 | struct AppTests { 7 | private func withApp(_ test: (Application) async throws -> ()) async throws { 8 | let app = try await Application.make(.testing) 9 | do { 10 | try await configure(app) 11 | try await test(app) 12 | } 13 | catch { 14 | try await app.asyncShutdown() 15 | throw error 16 | } 17 | try await app.asyncShutdown() 18 | } 19 | 20 | @Test("Test Hello World Route") 21 | func helloWorld() async throws { 22 | try await withApp { app in 23 | try await app.test(.GET, "hello", afterResponse: { res async in 24 | #expect(res.status == .ok) 25 | #expect(res.body.string == "Hello, world!") 26 | }) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /PhotoGetherServer/PhotoGetherServer/Tests/RoomManagerTests/RoomManagerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RoomManagerTests.swift 3 | // PhotoGetherServer 4 | // 5 | // Created by YoungK on 11/27/24. 6 | // 7 | 8 | -------------------------------------------------------------------------------- /PhotoGetherServer/PhotoGetherServer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Docker Compose file for Vapor 2 | # 3 | # Install Docker on your system to run and test 4 | # your Vapor app in a production-like environment. 5 | # 6 | # Note: This file is intended for testing and does not 7 | # implement best practices for a production deployment. 8 | # 9 | # Learn more: https://docs.docker.com/compose/reference/ 10 | # 11 | # Build images: docker-compose build 12 | # Start app: docker-compose up app 13 | # Stop all: docker-compose down 14 | # 15 | version: '3.7' 16 | 17 | x-shared_environment: &shared_environment 18 | LOG_LEVEL: ${LOG_LEVEL:-debug} 19 | 20 | services: 21 | app: 22 | image: photo-gether-server:latest 23 | build: 24 | context: . 25 | environment: 26 | <<: *shared_environment 27 | ports: 28 | - '8080:8080' 29 | # user: '0' # uncomment to run as root for testing purposes even though Dockerfile defines 'vapor' user. 30 | command: ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"] 31 | --------------------------------------------------------------------------------