diff --git a/iOS/Layover/Layover.xcodeproj/project.pbxproj b/iOS/Layover/Layover.xcodeproj/project.pbxproj index 2262b50..21d94bc 100644 --- a/iOS/Layover/Layover.xcodeproj/project.pbxproj +++ b/iOS/Layover/Layover.xcodeproj/project.pbxproj @@ -77,6 +77,20 @@ 19C7AFCE2B02410F003B35F2 /* AuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19C7AFCD2B02410F003B35F2 /* AuthManager.swift */; }; 19C7AFD62B02584D003B35F2 /* KeychainStored.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19C7AFD52B02584D003B35F2 /* KeychainStored.swift */; }; 19E79AC02B0A85D0009EA9ED /* LoopingPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19E79ABF2B0A85D0009EA9ED /* LoopingPlayerView.swift */; }; + 8321A2EE2B1E1026000A12AF /* ReportPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8321A2E82B1E1026000A12AF /* ReportPresenter.swift */; }; + 8321A2EF2B1E1026000A12AF /* ReportWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8321A2E92B1E1026000A12AF /* ReportWorker.swift */; }; + 8321A2F02B1E1026000A12AF /* ReportRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8321A2EA2B1E1026000A12AF /* ReportRouter.swift */; }; + 8321A2F12B1E1026000A12AF /* ReportModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8321A2EB2B1E1026000A12AF /* ReportModels.swift */; }; + 8321A2F22B1E1026000A12AF /* ReportViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8321A2EC2B1E1026000A12AF /* ReportViewController.swift */; }; + 8321A2F32B1E1026000A12AF /* ReportInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8321A2ED2B1E1026000A12AF /* ReportInteractor.swift */; }; + 8321A2F52B1E10DF000A12AF /* ReportConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8321A2F42B1E10DF000A12AF /* ReportConfigurator.swift */; }; + 8321A2F72B1E14A1000A12AF /* LOPopUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8321A2F62B1E14A1000A12AF /* LOPopUpView.swift */; }; + 8321A2F92B1E15F3000A12AF /* LOReportStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8321A2F82B1E15F3000A12AF /* LOReportStackView.swift */; }; + 8321A2FB2B1E1739000A12AF /* LOReportContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8321A2FA2B1E1739000A12AF /* LOReportContentView.swift */; }; + 8321A2FD2B1E4260000A12AF /* ReportEndPointFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8321A2FC2B1E4260000A12AF /* ReportEndPointFactory.swift */; }; + 8321A2FF2B1E428C000A12AF /* ReportDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8321A2FE2B1E428C000A12AF /* ReportDTO.swift */; }; + 8321A3012B1F1EC5000A12AF /* MockReportWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8321A3002B1F1EC5000A12AF /* MockReportWorker.swift */; }; + 8321A3032B1F2041000A12AF /* ReportPlaybackVideo.json in Resources */ = {isa = PBXBuildFile; fileRef = 8321A3022B1F2041000A12AF /* ReportPlaybackVideo.json */; }; 834B7BD52B14F888002BD176 /* MockSignUpWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 834B7BD42B14F888002BD176 /* MockSignUpWorker.swift */; }; 835783C32B14A41600E7D304 /* MockLoginWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835783C22B14A41600E7D304 /* MockLoginWorker.swift */; }; 835783C62B14A5C800E7D304 /* LoginData.json in Resources */ = {isa = PBXBuildFile; fileRef = 835783C52B14A5C800E7D304 /* LoginData.json */; }; @@ -135,6 +149,10 @@ FC3752352B170A620000D439 /* EditVideoInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC37522F2B170A620000D439 /* EditVideoInteractor.swift */; }; FC3752372B170B230000D439 /* EditVideoConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC3752362B170B230000D439 /* EditVideoConfigurator.swift */; }; FC3F3BD82B069EB30080E3A6 /* MapCarouselCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC3F3BD72B069EB30080E3A6 /* MapCarouselCollectionViewCell.swift */; }; + FC4084C42B1F14F600CE4727 /* UploadPostEndPointFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC4084C32B1F14F600CE4727 /* UploadPostEndPointFactory.swift */; }; + FC4084C62B1F1C5B00CE4727 /* UploadPostDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC4084C52B1F1C5B00CE4727 /* UploadPostDTO.swift */; }; + FC4084CA2B1F291200CE4727 /* UploadVideoDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC4084C92B1F291200CE4727 /* UploadVideoDTO.swift */; }; + FC4084CC2B1F2F5D00CE4727 /* UploadPost.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC4084CB2B1F2F5D00CE4727 /* UploadPost.swift */; }; FC42E4142B17AB69005D4956 /* VideoFileWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC42E4132B17AB69005D4956 /* VideoFileWorker.swift */; }; FC49758F2B03432800D8627F /* Pretendard-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = FC4975862B03432700D8627F /* Pretendard-SemiBold.ttf */; }; FC4975932B03432800D8627F /* Pretendard-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = FC49758A2B03432800D8627F /* Pretendard-Bold.ttf */; }; @@ -267,6 +285,20 @@ 19C7AFCD2B02410F003B35F2 /* AuthManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthManager.swift; sourceTree = ""; }; 19C7AFD52B02584D003B35F2 /* KeychainStored.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStored.swift; sourceTree = ""; }; 19E79ABF2B0A85D0009EA9ED /* LoopingPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopingPlayerView.swift; sourceTree = ""; }; + 8321A2E82B1E1026000A12AF /* ReportPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportPresenter.swift; sourceTree = ""; }; + 8321A2E92B1E1026000A12AF /* ReportWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportWorker.swift; sourceTree = ""; }; + 8321A2EA2B1E1026000A12AF /* ReportRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportRouter.swift; sourceTree = ""; }; + 8321A2EB2B1E1026000A12AF /* ReportModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportModels.swift; sourceTree = ""; }; + 8321A2EC2B1E1026000A12AF /* ReportViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportViewController.swift; sourceTree = ""; }; + 8321A2ED2B1E1026000A12AF /* ReportInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportInteractor.swift; sourceTree = ""; }; + 8321A2F42B1E10DF000A12AF /* ReportConfigurator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportConfigurator.swift; sourceTree = ""; }; + 8321A2F62B1E14A1000A12AF /* LOPopUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LOPopUpView.swift; sourceTree = ""; }; + 8321A2F82B1E15F3000A12AF /* LOReportStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LOReportStackView.swift; sourceTree = ""; }; + 8321A2FA2B1E1739000A12AF /* LOReportContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LOReportContentView.swift; sourceTree = ""; }; + 8321A2FC2B1E4260000A12AF /* ReportEndPointFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportEndPointFactory.swift; sourceTree = ""; }; + 8321A2FE2B1E428C000A12AF /* ReportDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportDTO.swift; sourceTree = ""; }; + 8321A3002B1F1EC5000A12AF /* MockReportWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockReportWorker.swift; sourceTree = ""; }; + 8321A3022B1F2041000A12AF /* ReportPlaybackVideo.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = ReportPlaybackVideo.json; sourceTree = ""; }; 834B7BD42B14F888002BD176 /* MockSignUpWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSignUpWorker.swift; sourceTree = ""; }; 835783C22B14A41600E7D304 /* MockLoginWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockLoginWorker.swift; sourceTree = ""; }; 835783C52B14A5C800E7D304 /* LoginData.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = LoginData.json; sourceTree = ""; }; @@ -327,6 +359,10 @@ FC37522F2B170A620000D439 /* EditVideoInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditVideoInteractor.swift; sourceTree = ""; }; FC3752362B170B230000D439 /* EditVideoConfigurator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditVideoConfigurator.swift; sourceTree = ""; }; FC3F3BD72B069EB30080E3A6 /* MapCarouselCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapCarouselCollectionViewCell.swift; sourceTree = ""; }; + FC4084C32B1F14F600CE4727 /* UploadPostEndPointFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadPostEndPointFactory.swift; sourceTree = ""; }; + FC4084C52B1F1C5B00CE4727 /* UploadPostDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadPostDTO.swift; sourceTree = ""; }; + FC4084C92B1F291200CE4727 /* UploadVideoDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadVideoDTO.swift; sourceTree = ""; }; + FC4084CB2B1F2F5D00CE4727 /* UploadPost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadPost.swift; sourceTree = ""; }; FC42E4132B17AB69005D4956 /* VideoFileWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoFileWorker.swift; sourceTree = ""; }; FC4975862B03432700D8627F /* Pretendard-SemiBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Pretendard-SemiBold.ttf"; sourceTree = ""; }; FC49758A2B03432800D8627F /* Pretendard-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Pretendard-Bold.ttf"; sourceTree = ""; }; @@ -498,6 +534,8 @@ 1972CCD72B13A2EC00C3C762 /* SignUpEndPointFactory.swift */, 193686712B15BCA7008902CD /* UserEndPointFactory.swift */, 19A1693F2B17C10300DB34C0 /* PostEndPointFactory.swift */, + 8321A2FC2B1E4260000A12AF /* ReportEndPointFactory.swift */, + FC4084C32B1F14F600CE4727 /* UploadPostEndPointFactory.swift */, ); path = Factories; sourceTree = ""; @@ -531,6 +569,7 @@ 19A1692C2B17750B00DB34C0 /* Post.swift */, 19A169412B17C70C00DB34C0 /* Member.swift */, 19A169432B17C71C00DB34C0 /* Board.swift */, + FC4084CB2B1F2F5D00CE4727 /* UploadPost.swift */, ); path = Models; sourceTree = ""; @@ -553,6 +592,7 @@ 19A169462B17D12500DB34C0 /* MockTagPlayListWorker.swift */, 194C21BA2B1B718B00C62645 /* MockHomeWorker.swift */, 194C21BC2B1B728600C62645 /* StubAuthManager.swift */, + 8321A3002B1F1EC5000A12AF /* MockReportWorker.swift */, ); path = Mocks; sourceTree = ""; @@ -571,6 +611,9 @@ 19A1693B2B17BD1C00DB34C0 /* BoardDTO.swift */, 836C338C2B15D91F00ECAFB0 /* VideoDTO.swift */, 836C338E2B160CC700ECAFB0 /* MemberDTO.swift */, + 8321A2FE2B1E428C000A12AF /* ReportDTO.swift */, + FC4084C52B1F1C5B00CE4727 /* UploadPostDTO.swift */, + FC4084C92B1F291200CE4727 /* UploadVideoDTO.swift */, 1915D6E42B1FB82000CE1DD0 /* CheckSignUpDTO.swift */, ); path = DTOs; @@ -622,6 +665,20 @@ path = Keychain; sourceTree = ""; }; + 8321A2E72B1E1011000A12AF /* Report */ = { + isa = PBXGroup; + children = ( + 8321A2E82B1E1026000A12AF /* ReportPresenter.swift */, + 8321A2E92B1E1026000A12AF /* ReportWorker.swift */, + 8321A2EA2B1E1026000A12AF /* ReportRouter.swift */, + 8321A2EB2B1E1026000A12AF /* ReportModels.swift */, + 8321A2EC2B1E1026000A12AF /* ReportViewController.swift */, + 8321A2ED2B1E1026000A12AF /* ReportInteractor.swift */, + 8321A2F42B1E10DF000A12AF /* ReportConfigurator.swift */, + ); + path = Report; + sourceTree = ""; + }; 835A61962B0680FC002F22A5 /* Playback */ = { isa = PBXGroup; children = ( @@ -775,6 +832,7 @@ 194C21E52B1F7B5100C62645 /* PostListMore.json */, 194C21E92B1F7C1600C62645 /* PostListEnd.json */, 194C21DB2B1F3ECA00C62645 /* GetMember.json */, + 8321A3022B1F2041000A12AF /* ReportPlaybackVideo.json */, 1915D6E82B1FBABA00CE1DD0 /* CheckSignUp.json */, ); path = MockData; @@ -861,6 +919,7 @@ FC7E457B2AFF6F9D004F155A /* Scenes */ = { isa = PBXGroup; children = ( + 8321A2E72B1E1011000A12AF /* Report */, 1945520E2B03AEA400299768 /* Configurator.swift */, 836C33922B18436A00ECAFB0 /* SettingScene */, 19BB8A572B07BEE30070B922 /* UIComponents */, @@ -892,6 +951,9 @@ 835A61932B068096002F22A5 /* LODescriptionView.swift */, FC0E802F2B1A15D500EF56D6 /* LOImageLabel.swift */, FC0E80312B1A280300EF56D6 /* LOTextView.swift */, + 8321A2F62B1E14A1000A12AF /* LOPopUpView.swift */, + 8321A2F82B1E15F3000A12AF /* LOReportStackView.swift */, + 8321A2FA2B1E1739000A12AF /* LOReportContentView.swift */, ); path = DesignSystem; sourceTree = ""; @@ -1059,6 +1121,7 @@ 194C21EA2B1F7C1600C62645 /* PostListEnd.json in Resources */, 835783C62B14A5C800E7D304 /* LoginData.json in Resources */, FC767FA12B12283D0088CF9B /* DeleteUser.json in Resources */, + 8321A3032B1F2041000A12AF /* ReportPlaybackVideo.json in Resources */, FC7E45462AFEB62B004F155A /* LaunchScreen.storyboard in Resources */, 19A169362B178EA500DB34C0 /* PostList.json in Resources */, 1915D6E92B1FBABA00CE1DD0 /* CheckSignUp.json in Resources */, @@ -1109,6 +1172,7 @@ 83C35E1E2B10923C00D8DD5C /* PlaybackCell.swift in Sources */, FC0E80262B1A0BBB00EF56D6 /* UploadPostRouter.swift in Sources */, FC2511A42B045D6C004717BC /* SignUpModels.swift in Sources */, + 8321A2FD2B1E4260000A12AF /* ReportEndPointFactory.swift in Sources */, FC767F932B1220CC0088CF9B /* NicknameDTO.swift in Sources */, FC3752312B170A620000D439 /* EditVideoWorker.swift in Sources */, 19A169272B176C5F00DB34C0 /* TagPlayListViewController.swift in Sources */, @@ -1131,9 +1195,11 @@ FC3752342B170A620000D439 /* EditVideoViewController.swift in Sources */, 19A169442B17C71C00DB34C0 /* Board.swift in Sources */, 194552252B0478B400299768 /* HomeViewController.swift in Sources */, + FC4084C62B1F1C5B00CE4727 /* UploadPostDTO.swift in Sources */, 194552222B0478B400299768 /* HomeWorker.swift in Sources */, 194C21BD2B1B728600C62645 /* StubAuthManager.swift in Sources */, FC5BE1202B148D170036366D /* EditProfileViewController.swift in Sources */, + 8321A2EE2B1E1026000A12AF /* ReportPresenter.swift in Sources */, 194552282B0479B600299768 /* BaseViewController.swift in Sources */, 194552212B0478B400299768 /* HomePresenter.swift in Sources */, 1972CCDA2B13A4BA00C3C762 /* SignUpWorker.swift in Sources */, @@ -1152,6 +1218,7 @@ FC5BE1212B148D170036366D /* EditProfileInteractor.swift in Sources */, FC930E7C2B0CD80800AA48E3 /* ProfileConfigurator.swift in Sources */, 836C33992B1843BE00ECAFB0 /* SettingScenePresenter.swift in Sources */, + 8321A2F12B1E1026000A12AF /* ReportModels.swift in Sources */, FC2511A62B049020004717BC /* SignUpConfigurator.swift in Sources */, 194552392B05230E00299768 /* HomeCarouselCollectionViewCell.swift in Sources */, FC767FAA2B126D080088CF9B /* LOAnnotation.swift in Sources */, @@ -1159,14 +1226,17 @@ 193686722B15BCA7008902CD /* UserEndPointFactory.swift in Sources */, 194551F22B037F2D00299768 /* LoginPresenter.swift in Sources */, 194552242B0478B400299768 /* HomeModels.swift in Sources */, + 8321A3012B1F1EC5000A12AF /* MockReportWorker.swift in Sources */, 19A1692D2B17750B00DB34C0 /* Post.swift in Sources */, 194552022B038B8300299768 /* OSLog+.swift in Sources */, + 8321A2F92B1E15F3000A12AF /* LOReportStackView.swift in Sources */, FC0E80272B1A0BBB00EF56D6 /* UploadPostModels.swift in Sources */, 194552312B04DA1A00299768 /* LOCircleButton.swift in Sources */, 194552262B0478B400299768 /* HomeInteractor.swift in Sources */, FC930E772B0CD75C00AA48E3 /* ProfileRouter.swift in Sources */, FC0E80302B1A15D600EF56D6 /* LOImageLabel.swift in Sources */, FC5BE11F2B148D160036366D /* EditProfileModels.swift in Sources */, + 8321A2EF2B1E1026000A12AF /* ReportWorker.swift in Sources */, 194551F42B037F2D00299768 /* LoginRouter.swift in Sources */, FC4975992B03439000D8627F /* UIFont+.swift in Sources */, FC5BE11E2B148D160036366D /* EditProfileRouter.swift in Sources */, @@ -1186,6 +1256,7 @@ 19C7AFD62B02584D003B35F2 /* KeychainStored.swift in Sources */, 193686742B15C489008902CD /* UserWorker.swift in Sources */, 19A169242B176C5F00DB34C0 /* TagPlayListWorker.swift in Sources */, + 8321A2FB2B1E1739000A12AF /* LOReportContentView.swift in Sources */, FCE52FF82B0FCAF7002CDB75 /* MockURLProtocol.swift in Sources */, FC930E792B0CD75C00AA48E3 /* ProfileViewController.swift in Sources */, 194C21BB2B1B718B00C62645 /* MockHomeWorker.swift in Sources */, @@ -1193,7 +1264,9 @@ FC7E453A2AFEB623004F155A /* AppDelegate.swift in Sources */, FC0E80282B1A0BBB00EF56D6 /* UploadPostViewController.swift in Sources */, 1972CCD82B13A2EC00C3C762 /* SignUpEndPointFactory.swift in Sources */, + 8321A2F32B1E1026000A12AF /* ReportInteractor.swift in Sources */, 19A169262B176C5F00DB34C0 /* TagPlayListModels.swift in Sources */, + 8321A2F22B1E1026000A12AF /* ReportViewController.swift in Sources */, FC767F842B1214A80088CF9B /* MockUserWorker.swift in Sources */, 836C338B2B15D22C00ECAFB0 /* PlaybackConfigurator.swift in Sources */, FC930E782B0CD75C00AA48E3 /* ProfileModels.swift in Sources */, @@ -1206,6 +1279,8 @@ FC68E29D2B02326A001AABFF /* Responsable.swift in Sources */, FC2511A02B045C0A004717BC /* SignUpInteractor.swift in Sources */, FC767F862B1214C10088CF9B /* CheckUserNameDTO.swift in Sources */, + 836C338D2B15D91F00ECAFB0 /* VideoDTO.swift in Sources */, + FC4084C42B1F14F600CE4727 /* UploadPostEndPointFactory.swift in Sources */, FC5BE1232B1490660036366D /* EditProfileConfigurator.swift in Sources */, 1945522A2B04883800299768 /* UIView+.swift in Sources */, FCE52FFA2B0FCB0A002CDB75 /* URLSession+.swift in Sources */, @@ -1217,6 +1292,7 @@ 836C33912B17629400ECAFB0 /* MapRouter.swift in Sources */, 19C7AFCE2B02410F003B35F2 /* AuthManager.swift in Sources */, 836C339D2B1843BE00ECAFB0 /* SettingSceneViewController.swift in Sources */, + 8321A2FF2B1E428C000A12AF /* ReportDTO.swift in Sources */, FC9BB82C2B094E5500EB72A9 /* UICollectionViewLayout+.swift in Sources */, 194552232B0478B400299768 /* HomeRouter.swift in Sources */, 835783C32B14A41600E7D304 /* MockLoginWorker.swift in Sources */, @@ -1225,9 +1301,11 @@ FC0E80322B1A280300EF56D6 /* LOTextView.swift in Sources */, 19A169282B176C5F00DB34C0 /* TagPlayListInteractor.swift in Sources */, FC930E7A2B0CD75C00AA48E3 /* ProfileInteractor.swift in Sources */, + 8321A2F02B1E1026000A12AF /* ReportRouter.swift in Sources */, FC930E752B0CD75C00AA48E3 /* ProfilePresenter.swift in Sources */, 835A61A12B068115002F22A5 /* PlaybackViewController.swift in Sources */, 1945520F2B03AEA400299768 /* Configurator.swift in Sources */, + FC4084CC2B1F2F5D00CE4727 /* UploadPost.swift in Sources */, 835A619F2B068115002F22A5 /* PlaybackRouter.swift in Sources */, 19A1693C2B17BD1C00DB34C0 /* BoardDTO.swift in Sources */, FC2511B12B04EAEC004717BC /* MapModels.swift in Sources */, @@ -1237,6 +1315,7 @@ FCEE0FFA2B03AF8500195BBE /* SignUpViewController.swift in Sources */, 194551F32B037F2D00299768 /* LoginWorker.swift in Sources */, FC0E803D2B1B91C900EF56D6 /* EditTagModels.swift in Sources */, + 8321A2F52B1E10DF000A12AF /* ReportConfigurator.swift in Sources */, 835A61902B067D61002F22A5 /* LOSlider.swift in Sources */, 19E79AC02B0A85D0009EA9ED /* LoopingPlayerView.swift in Sources */, 1972CCCF2B12438900C3C762 /* LoginEndPointFactory.swift in Sources */, @@ -1252,10 +1331,12 @@ 835A61A22B068115002F22A5 /* PlaybackInteractor.swift in Sources */, 19AACFCC2B0F7D730088143E /* LoginDTO.swift in Sources */, FC0E803C2B1B91C900EF56D6 /* EditTagRouter.swift in Sources */, + FC4084CA2B1F291200CE4727 /* UploadVideoDTO.swift in Sources */, 835A61942B068096002F22A5 /* LODescriptionView.swift in Sources */, FC68E2A52B0233D3001AABFF /* Provider.swift in Sources */, FC930E7E2B0CDB2900AA48E3 /* ThumbnailCollectionViewCell.swift in Sources */, 194552112B03AF2B00299768 /* MainTabBarConfigurator.swift in Sources */, + 8321A2F72B1E14A1000A12AF /* LOPopUpView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/iOS/Layover/Layover/DesignSystem/LOPopUpView.swift b/iOS/Layover/Layover/DesignSystem/LOPopUpView.swift new file mode 100644 index 0000000..e27f7be --- /dev/null +++ b/iOS/Layover/Layover/DesignSystem/LOPopUpView.swift @@ -0,0 +1,90 @@ +// +// LOPopUpView.swift +// Layover +// +// Created by 황지웅 on 12/4/23. +// Copyright © 2023 CodeBomber. All rights reserved. +// + +import UIKit + +final class LOPopUpView: UIView { + private let titleLabel: UILabel = { + let label: UILabel = UILabel() + label.text = "콘텐츠 신고" + label.font = .loFont(type: .subtitle) + return label + }() + + private let reportStackView: LOReportStackView = LOReportStackView() + + private let cancelButton: UIButton = { + let button: UIButton = UIButton() + button.setTitle("취소", for: .normal) + button.setTitleColor(.grey200, for: .normal) + return button + }() + + private let reportButton: UIButton = { + let button: UIButton = UIButton() + button.setTitle("신고", for: .normal) + button.setTitleColor(.error, for: .normal) + return button + }() + + weak var delegate: ReportViewControllerDelegate? + + override init(frame: CGRect) { + super.init(frame: frame) + setConstraints() + cancelButton.addTarget(self, action: #selector(cancelButtonDidTap), for: .touchUpInside) + reportButton.addTarget(self, action: #selector(reportButtonDidTap), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setConstraints() + cancelButton.addTarget(self, action: #selector(cancelButtonDidTap), for: .touchUpInside) + reportButton.addTarget(self, action: #selector(reportButtonDidTap), for: .touchUpInside) + } + + func getReportContent() -> String { + reportStackView.reportContent + } + + private func setConstraints() { + addSubviews(titleLabel, reportStackView, cancelButton, reportButton) + subviews.forEach { subView in + subView.translatesAutoresizingMaskIntoConstraints = false + } + + NSLayoutConstraint.activate([ + titleLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: 28), + titleLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 28), + titleLabel.bottomAnchor.constraint(equalTo: reportStackView.topAnchor, constant: -5), + reportStackView.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 0.8), + reportStackView.heightAnchor.constraint(equalToConstant: 336), + reportStackView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 5), + reportStackView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 13.5), + cancelButton.leadingAnchor.constraint(equalTo: self.leadingAnchor), + cancelButton.trailingAnchor.constraint(equalTo: reportButton.leadingAnchor), + cancelButton.widthAnchor.constraint(equalToConstant: 175), + cancelButton.heightAnchor.constraint(equalToConstant: 56), + cancelButton.topAnchor.constraint(equalTo: reportStackView.bottomAnchor), + reportButton.leadingAnchor.constraint(equalTo: cancelButton.trailingAnchor), + reportButton.trailingAnchor.constraint(equalTo: self.trailingAnchor), + reportButton.widthAnchor.constraint(equalToConstant: 175), + reportButton.heightAnchor.constraint(equalToConstant: 56), + reportButton.topAnchor.constraint(equalTo: reportStackView.bottomAnchor) + ]) + + } + + @objc private func cancelButtonDidTap() { + delegate?.dismissReportView() + } + + @objc private func reportButtonDidTap() { + delegate?.reportPlaybackVideo(reportContent: getReportContent()) + } +} diff --git a/iOS/Layover/Layover/DesignSystem/LOReportContentView.swift b/iOS/Layover/Layover/DesignSystem/LOReportContentView.swift new file mode 100644 index 0000000..58c4ba0 --- /dev/null +++ b/iOS/Layover/Layover/DesignSystem/LOReportContentView.swift @@ -0,0 +1,93 @@ +// +// LOReportContentView.swift +// Layover +// +// Created by 황지웅 on 12/4/23. +// Copyright © 2023 CodeBomber. All rights reserved. +// + +import UIKit + +final class LOReportContentView: UIView { + + // MARK: - UI Components + + var index: Int = 0 + + private let whiteCircle: UIView = { + let view: UIView = UIView() + view.backgroundColor = .layoverWhite + view.clipsToBounds = true + view.layer.cornerRadius = 4 + view.isHidden = true + view.isUserInteractionEnabled = false + return view + }() + + private let radioView: UIView = { + let view: UIView = UIView() + view.layer.borderColor = UIColor.grey100.cgColor + view.layer.borderWidth = 0.1 + view.backgroundColor = .clear + view.layer.cornerRadius = 10 + view.clipsToBounds = true + view.isUserInteractionEnabled = false + return view + }() + + let contentLabel: UILabel = { + let label: UILabel = UILabel() + label.text = "스팸 / 홍보 도배글이에요" + label.font = .loFont(type: .body2) + label.textAlignment = .left + label.textColor = .layoverWhite + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + setConstraints() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setConstraints() + } + + func onRadio() { + whiteCircle.isHidden = false + radioView.backgroundColor = .primaryPurple + } + + func offRadio() { + whiteCircle.isHidden = true + radioView.backgroundColor = .clear + } + + func setText(_ content: String) { + contentLabel.text = content + } + + // MARK: - UI Methods + + private func setConstraints() { + radioView.addSubview(whiteCircle) + whiteCircle.translatesAutoresizingMaskIntoConstraints = false + addSubviews(radioView, contentLabel) + subviews.forEach { subView in + subView.translatesAutoresizingMaskIntoConstraints = false + } + NSLayoutConstraint.activate([ + whiteCircle.centerXAnchor.constraint(equalTo: radioView.centerXAnchor), + whiteCircle.centerYAnchor.constraint(equalTo: radioView.centerYAnchor), + whiteCircle.widthAnchor.constraint(equalToConstant: 8), + whiteCircle.heightAnchor.constraint(equalToConstant: 8), + radioView.widthAnchor.constraint(equalToConstant: 20), + radioView.heightAnchor.constraint(equalToConstant: 20), + radioView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 10), + radioView.centerYAnchor.constraint(equalTo: self.centerYAnchor), + radioView.trailingAnchor.constraint(equalTo: contentLabel.leadingAnchor, constant: -12), + contentLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor) + ]) + } +} diff --git a/iOS/Layover/Layover/DesignSystem/LOReportStackView.swift b/iOS/Layover/Layover/DesignSystem/LOReportStackView.swift new file mode 100644 index 0000000..309e686 --- /dev/null +++ b/iOS/Layover/Layover/DesignSystem/LOReportStackView.swift @@ -0,0 +1,66 @@ +// +// LOReportStackView.swift +// Layover +// +// Created by 황지웅 on 12/4/23. +// Copyright © 2023 CodeBomber. All rights reserved. +// + +import UIKit + +final class LOReportStackView: UIStackView { + + var reportViews: [LOReportContentView] = { + let contentArray: [String] = ["스팸 홍보 / 도배글이에요", "부적절한 사진이에요", "청소년에게 유해한 내용이에요", "욕설 / 혐오 / 차별적 표현을 담고있어요", "거짓 정보를 담고있어요", "기타"] + return contentArray.enumerated().map { index, content in + let loReportContentView: LOReportContentView = LOReportContentView() + loReportContentView.setText(content) + loReportContentView.index = index + return loReportContentView + } + }() + + var reportContent: String = "스팸 홍보 / 도배글이에요" + + override init(frame: CGRect) { + super.init(frame: frame) + setUI() + addContent() + } + + required init(coder: NSCoder) { + super.init(coder: coder) + setUI() + addContent() + } + + private func setUI() { + self.alignment = .fill + self.distribution = .fillProportionally + self.axis = .vertical + self.spacing = 8 + guard let firstReportView: LOReportContentView = reportViews.first else { return } + firstReportView.onRadio() + } + + private func addContent() { + reportViews.forEach { reportView in + let addGesutre: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(reportViewDidTap(_:))) + reportView.addGestureRecognizer(addGesutre) + addArrangedSubview(reportView) + } + } + + @objc private func reportViewDidTap(_ sender: UITapGestureRecognizer) { + guard let tempView: LOReportContentView = sender.view as? LOReportContentView else { return } + self.reportViews.forEach { reportView in + if reportView.index == tempView.index { + reportView.onRadio() + guard let reportContentText: String = reportView.contentLabel.text else { return } + reportContent = reportContentText + } else { + reportView.offRadio() + } + } + } +} diff --git a/iOS/Layover/Layover/DesignSystem/LOTagStackView.swift b/iOS/Layover/Layover/DesignSystem/LOTagStackView.swift index 95f5b47..df6e2b8 100644 --- a/iOS/Layover/Layover/DesignSystem/LOTagStackView.swift +++ b/iOS/Layover/Layover/DesignSystem/LOTagStackView.swift @@ -17,6 +17,12 @@ final class LOTagStackView: UIStackView { // MARK: - Properties + var tags: [String] { + arrangedSubviews + .map { $0 as? UIButton } + .compactMap(\.?.titleLabel?.text) + } + private let style: Style // MARK: - Initializer diff --git a/iOS/Layover/Layover/Extensions/Notification.Name+.swift b/iOS/Layover/Layover/Extensions/Notification.Name+.swift index d48a1b0..ebb5e37 100644 --- a/iOS/Layover/Layover/Extensions/Notification.Name+.swift +++ b/iOS/Layover/Layover/Extensions/Notification.Name+.swift @@ -10,4 +10,7 @@ import Foundation extension Notification.Name { static let refreshTokenDidExpired = Notification.Name("refreshTokenDidExpired") + static let uploadTaskStart = Notification.Name("uploadTaskStart") + static let progressChanged = Notification.Name("progressChanged") + static let uploadTaskDidComplete = Notification.Name("uploadTaskDidComplete") } diff --git a/iOS/Layover/Layover/Models/UploadPost.swift b/iOS/Layover/Layover/Models/UploadPost.swift new file mode 100644 index 0000000..3bc48c6 --- /dev/null +++ b/iOS/Layover/Layover/Models/UploadPost.swift @@ -0,0 +1,17 @@ +// +// UploadPost.swift +// Layover +// +// Created by kong on 2023/12/05. +// Copyright © 2023 CodeBomber. All rights reserved. +// + +import Foundation + +struct UploadPost { + let title: String + let content: String? + let latitude, longitude: Double + let tag: [String] + let videoURL: URL +} diff --git a/iOS/Layover/Layover/Network/DTOs/CheckSignUpDTO.swift b/iOS/Layover/Layover/Network/DTOs/CheckSignUpDTO.swift index 3a1ea7c..f50d020 100644 --- a/iOS/Layover/Layover/Network/DTOs/CheckSignUpDTO.swift +++ b/iOS/Layover/Layover/Network/DTOs/CheckSignUpDTO.swift @@ -9,5 +9,5 @@ import Foundation struct CheckSignUpDTO: Decodable { - let isValid: Bool + let isAlreadyExist: Bool } diff --git a/iOS/Layover/Layover/Network/DTOs/ReportDTO.swift b/iOS/Layover/Layover/Network/DTOs/ReportDTO.swift new file mode 100644 index 0000000..8588bd8 --- /dev/null +++ b/iOS/Layover/Layover/Network/DTOs/ReportDTO.swift @@ -0,0 +1,15 @@ +// +// ReportDTO.swift +// Layover +// +// Created by 황지웅 on 12/5/23. +// Copyright © 2023 CodeBomber. All rights reserved. +// + +import Foundation + +struct ReportDTO: Codable { + let memberId: Int? + let boardID: Int + let reportType: String +} diff --git a/iOS/Layover/Layover/Network/DTOs/UploadPostDTO.swift b/iOS/Layover/Layover/Network/DTOs/UploadPostDTO.swift new file mode 100644 index 0000000..62016db --- /dev/null +++ b/iOS/Layover/Layover/Network/DTOs/UploadPostDTO.swift @@ -0,0 +1,24 @@ +// +// UploadPostDTO.swift +// Layover +// +// Created by kong on 2023/12/05. +// Copyright © 2023 CodeBomber. All rights reserved. +// + +import Foundation + +struct UploadPostDTO: Decodable { + let id: Int + let title: String + let content: String? + let latitude, longitude: Double + let tag: [String] +} + +struct UploadPostRequestDTO: Encodable { + let title: String + let content: String? + let latitude, longitude: Double + let tag: [String] +} diff --git a/iOS/Layover/Layover/Network/DTOs/UploadVideoDTO.swift b/iOS/Layover/Layover/Network/DTOs/UploadVideoDTO.swift new file mode 100644 index 0000000..0d8160a --- /dev/null +++ b/iOS/Layover/Layover/Network/DTOs/UploadVideoDTO.swift @@ -0,0 +1,27 @@ +// +// UploadVideoDTO.swift +// Layover +// +// Created by kong on 2023/12/05. +// Copyright © 2023 CodeBomber. All rights reserved. +// + +import Foundation + +struct UploadVideoDTO: Decodable { + let preSignedURL: String + + enum CodingKeys: String, CodingKey { + case preSignedURL = "preSignedUrl" + } +} + +struct UploadVideoRequestDTO: Encodable { + let boardID: Int + let filetype: String + + enum CodingKeys: String, CodingKey { + case boardID = "boardId" + case filetype + } +} diff --git a/iOS/Layover/Layover/Network/EndPoint/Factories/ReportEndPointFactory.swift b/iOS/Layover/Layover/Network/EndPoint/Factories/ReportEndPointFactory.swift new file mode 100644 index 0000000..ac01e83 --- /dev/null +++ b/iOS/Layover/Layover/Network/EndPoint/Factories/ReportEndPointFactory.swift @@ -0,0 +1,27 @@ +// +// ReportEndPointFactory.swift +// Layover +// +// Created by 황지웅 on 12/5/23. +// Copyright © 2023 CodeBomber. All rights reserved. +// + +import Foundation + +protocol ReportEndPointFactory { + func reportPlaybackVideoEndpoint(boardID: Int, reportType: String) -> EndPoint> +} + +struct DefaultReportEndPointFactory: ReportEndPointFactory { + func reportPlaybackVideoEndpoint(boardID: Int, reportType: String) -> EndPoint> { + var bodyParmeters: ReportDTO = ReportDTO( + memberId: nil, + boardID: boardID, + reportType: reportType) + + return EndPoint( + path: "/report", + method: .POST, + bodyParameters: bodyParmeters) + } +} diff --git a/iOS/Layover/Layover/Network/EndPoint/Factories/UploadPostEndPointFactory.swift b/iOS/Layover/Layover/Network/EndPoint/Factories/UploadPostEndPointFactory.swift new file mode 100644 index 0000000..8345709 --- /dev/null +++ b/iOS/Layover/Layover/Network/EndPoint/Factories/UploadPostEndPointFactory.swift @@ -0,0 +1,42 @@ +// +// UploadPostEndPointFactory.swift +// Layover +// +// Created by kong on 2023/12/05. +// Copyright © 2023 CodeBomber. All rights reserved. +// + +import Foundation + +protocol UploadPostEndPointFactory { + func makeUploadPostEndPoint(title: String, + content: String?, + latitude: Double, + longitude: Double, + tag: [String]) -> EndPoint> + func makeUploadVideoEndPoint(boardID: Int, fileType: String) -> EndPoint> +} + +final class DefaultUploadPostEndPointFactory: UploadPostEndPointFactory { + + func makeUploadPostEndPoint(title: String, + content: String?, + latitude: Double, + longitude: Double, + tag: [String]) -> EndPoint> { + return EndPoint(path: "/board", + method: .POST, + bodyParameters: UploadPostRequestDTO(title: title, + content: content, + latitude: latitude, + longitude: longitude, + tag: tag)) + } + + func makeUploadVideoEndPoint(boardID: Int, fileType: String) -> EndPoint> { + return EndPoint(path: "/board/presigned-url", + method: .POST, + bodyParameters: UploadVideoRequestDTO(boardID: boardID, + filetype: fileType)) + } +} diff --git a/iOS/Layover/Layover/Network/Mock/MockData/CheckSignUp.json b/iOS/Layover/Layover/Network/Mock/MockData/CheckSignUp.json index a976fb8..254727a 100644 --- a/iOS/Layover/Layover/Network/Mock/MockData/CheckSignUp.json +++ b/iOS/Layover/Layover/Network/Mock/MockData/CheckSignUp.json @@ -3,6 +3,6 @@ "message": "성공", "statusCode": 200, "data": { - "isValid": true + "isAlreadyExist": true } } diff --git a/iOS/Layover/Layover/Network/Mock/MockData/LoginData.json b/iOS/Layover/Layover/Network/Mock/MockData/LoginData.json index 2fd9362..f378e18 100644 --- a/iOS/Layover/Layover/Network/Mock/MockData/LoginData.json +++ b/iOS/Layover/Layover/Network/Mock/MockData/LoginData.json @@ -1,9 +1,9 @@ { - "customCode": "SUCCESS", - "message": "성공", - "statusCode": 200, - "data": { - "accessToken": "mockAccessToken", - "refreshToken": "mockRefreshToken" - } + "customCode": "SUCCESS", + "message": "성공", + "statusCode": 200, + "data": { + "accessToken": "mockAccessToken", + "refreshToken": "mockRefreshToken" + } } diff --git a/iOS/Layover/Layover/Network/Mock/MockData/ReportPlaybackVideo.json b/iOS/Layover/Layover/Network/Mock/MockData/ReportPlaybackVideo.json new file mode 100644 index 0000000..52e9557 --- /dev/null +++ b/iOS/Layover/Layover/Network/Mock/MockData/ReportPlaybackVideo.json @@ -0,0 +1,10 @@ +{ + "customCode": "SUCCESS", + "message": "성공", + "statusCode": 200, + "data": { + "memberId": 221, + "boardId": 5, + "reportType": "청소년에게 유해한 내용이에요" + } +} diff --git a/iOS/Layover/Layover/Network/Provider/Provider.swift b/iOS/Layover/Layover/Network/Provider/Provider.swift index 0301e13..3f01d1f 100644 --- a/iOS/Layover/Layover/Network/Provider/Provider.swift +++ b/iOS/Layover/Layover/Network/Provider/Provider.swift @@ -12,6 +12,12 @@ protocol ProviderType { func request(with endPoint: E, authenticationIfNeeded: Bool, retryCount: Int) async throws -> R where E.Response == R func request(url: URL) async throws -> Data func request(url: String) async throws -> Data + func upload(data: Data, to url: String, method: HTTPMethod) async throws -> Data + func upload(fromFile: URL, + to url: String, + method: HTTPMethod, + sessionTaskDelegate: URLSessionTaskDelegate?, + delegateQueue: OperationQueue?) async throws -> Data } extension ProviderType { @@ -22,6 +28,25 @@ extension ProviderType { authenticationIfNeeded: authenticationIfNeeded, retryCount: retryCount) } + + func upload(data: Data, to url: String, method: HTTPMethod = .PUT) async throws -> Data { + return try await upload(data: data, + to: url, + method: method) + } + + func upload(fromFile: URL, + to url: String, + method: HTTPMethod = .PUT, + sessionTaskDelegate: URLSessionTaskDelegate? = nil, + delegateQueue: OperationQueue? = nil) async throws -> Data { + return try await upload(fromFile: fromFile, + to: url, + method: method, + sessionTaskDelegate: sessionTaskDelegate, + delegateQueue: delegateQueue) + } + } class Provider: ProviderType { @@ -112,18 +137,18 @@ class Provider: ProviderType { } // 동영상 업로드용 - func backgroundUpload(fromFile: URL, - to url: String, - method: HTTPMethod = .PUT, - sessionTaskDelegate: URLSessionTaskDelegate? = nil, - delegateQueue: OperationQueue? = nil) async throws -> Data { + func upload(fromFile: URL, + to url: String, + method: HTTPMethod = .PUT, + sessionTaskDelegate: URLSessionTaskDelegate? = nil, + delegateQueue: OperationQueue? = nil) async throws -> Data { guard let url = URL(string: url) else { throw NetworkError.components } var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData) request.httpMethod = method.rawValue - let backgroundSession = URLSession(configuration: .background(withIdentifier: UUID().uuidString), - delegate: sessionTaskDelegate, - delegateQueue: delegateQueue) - let (data, response) = try await backgroundSession.upload(for: request, fromFile: fromFile) + request.setValue("video/\(fromFile.pathExtension)", forHTTPHeaderField: "Content-Type") + let (data, response) = try await session.upload(for: request, + fromFile: fromFile, + delegate: sessionTaskDelegate) try self.checkStatusCode(of: response) return data } diff --git a/iOS/Layover/Layover/Resources/Assets.xcassets/down.imageset/Contents.json b/iOS/Layover/Layover/Resources/Assets.xcassets/down.imageset/Contents.json new file mode 100644 index 0000000..d801f8e --- /dev/null +++ b/iOS/Layover/Layover/Resources/Assets.xcassets/down.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "leading-icon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "leading-icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "leading-icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/Layover/Layover/Resources/Assets.xcassets/down.imageset/leading-icon.png b/iOS/Layover/Layover/Resources/Assets.xcassets/down.imageset/leading-icon.png new file mode 100644 index 0000000..59c6763 Binary files /dev/null and b/iOS/Layover/Layover/Resources/Assets.xcassets/down.imageset/leading-icon.png differ diff --git a/iOS/Layover/Layover/Resources/Assets.xcassets/down.imageset/leading-icon@2x.png b/iOS/Layover/Layover/Resources/Assets.xcassets/down.imageset/leading-icon@2x.png new file mode 100644 index 0000000..0c3c1b0 Binary files /dev/null and b/iOS/Layover/Layover/Resources/Assets.xcassets/down.imageset/leading-icon@2x.png differ diff --git a/iOS/Layover/Layover/Resources/Assets.xcassets/down.imageset/leading-icon@3x.png b/iOS/Layover/Layover/Resources/Assets.xcassets/down.imageset/leading-icon@3x.png new file mode 100644 index 0000000..05d490d Binary files /dev/null and b/iOS/Layover/Layover/Resources/Assets.xcassets/down.imageset/leading-icon@3x.png differ diff --git a/iOS/Layover/Layover/SceneDelegate.swift b/iOS/Layover/Layover/SceneDelegate.swift index 41a0756..ac4b83c 100644 --- a/iOS/Layover/Layover/SceneDelegate.swift +++ b/iOS/Layover/Layover/SceneDelegate.swift @@ -11,6 +11,7 @@ import KakaoSDKUser class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var progressView: UIProgressView = UIProgressView(progressViewStyle: .bar) var window: UIWindow? @@ -70,19 +71,68 @@ extension SceneDelegate { selector: #selector(routeToLoginViewController), name: .refreshTokenDidExpired, object: nil) + NotificationCenter.default.addObserver(self, + selector: #selector(showProgressView), + name: .uploadTaskStart, + object: nil) + NotificationCenter.default.addObserver(self, + selector: #selector(progressChanged), + name: .progressChanged, + object: nil) + NotificationCenter.default.addObserver(self, + selector: #selector(removeProgressView), + name: .uploadTaskDidComplete, + object: nil) } private func removeNotificationObservers() { NotificationCenter.default.removeObserver(self, - name: .refreshTokenDidExpired, - object: nil) + name: .refreshTokenDidExpired, + object: nil) + NotificationCenter.default.removeObserver(self, + name: .refreshTokenDidExpired, + object: nil) + NotificationCenter.default.removeObserver(self, + name: .progressChanged, + object: nil) + NotificationCenter.default.removeObserver(self, + name: .uploadTaskDidComplete, + object: nil) } @objc private func routeToLoginViewController() { guard let rootNavigationViewController = window?.rootViewController as? UINavigationController else { return } - // TODO: 세션이 만료되었습니다. Toast 띄우기 + Toast.shared.showToast(message: "세션이 만료되었습니다.") rootNavigationViewController.setNavigationBarHidden(false, animated: false) rootNavigationViewController.setViewControllers([LoginViewController()], animated: true) } + + @objc private func showProgressView() { + guard let progressViewWidth = window?.screen.bounds.width, + let windowHeight = window?.screen.bounds.height, + let tabBarViewController = window?.rootViewController as? UITabBarController else { return } + let tabBarHeight: CGFloat = tabBarViewController.tabBar.frame.height + progressView.progress = 0 + progressView.tintColor = .primaryPurple + progressView.frame = CGRect(x: 0, + y: (windowHeight - tabBarHeight - 2), + width: progressViewWidth, + height: 2) + window?.addSubview(progressView) + } + + + @objc private func progressChanged(_ notification: Notification) { + guard let progress = notification.userInfo?["progress"] as? Float else { return } + progressView.setProgress(progress, animated: true) + if progress == 1 { + Toast.shared.showToast(message: "업로드가 완료되었습니다 ✨") + } + } + + @objc private func removeProgressView() { + progressView.removeFromSuperview() + } + } diff --git a/iOS/Layover/Layover/Scenes/EditTag/EditTagInteractor.swift b/iOS/Layover/Layover/Scenes/EditTag/EditTagInteractor.swift index 9344d61..2dfca70 100644 --- a/iOS/Layover/Layover/Scenes/EditTag/EditTagInteractor.swift +++ b/iOS/Layover/Layover/Scenes/EditTag/EditTagInteractor.swift @@ -9,14 +9,17 @@ import UIKit protocol EditTagBusinessLogic { - + func fetchTags() + func editTag(request: EditTagModels.EditTag.Request) } protocol EditTagDataStore { - + var tags: [String]? { get set } } -class EditTagInteractor: EditTagBusinessLogic, EditTagDataStore { +final class EditTagInteractor: EditTagBusinessLogic, EditTagDataStore { + + var tags: [String]? // MARK: - Properties @@ -24,4 +27,13 @@ class EditTagInteractor: EditTagBusinessLogic, EditTagDataStore { var presenter: EditTagPresentationLogic? + func fetchTags() { + guard let tags else { return } + presenter?.presentTags(with: EditTagModels.FetchTags.Response(tags: tags)) + } + + func editTag(request: EditTagModels.EditTag.Request) { + tags = request.tags + } + } diff --git a/iOS/Layover/Layover/Scenes/EditTag/EditTagModels.swift b/iOS/Layover/Layover/Scenes/EditTag/EditTagModels.swift index 5f4d150..dd0781a 100644 --- a/iOS/Layover/Layover/Scenes/EditTag/EditTagModels.swift +++ b/iOS/Layover/Layover/Scenes/EditTag/EditTagModels.swift @@ -10,4 +10,22 @@ import UIKit enum EditTagModels { + enum FetchTags { + struct Request { + + } + struct Response { + let tags: [String] + } + struct ViewModel { + let tags: [String] + } + } + + enum EditTag { + struct Request { + let tags: [String] + } + } + } diff --git a/iOS/Layover/Layover/Scenes/EditTag/EditTagPresenter.swift b/iOS/Layover/Layover/Scenes/EditTag/EditTagPresenter.swift index 1520638..ed6952a 100644 --- a/iOS/Layover/Layover/Scenes/EditTag/EditTagPresenter.swift +++ b/iOS/Layover/Layover/Scenes/EditTag/EditTagPresenter.swift @@ -9,14 +9,18 @@ import UIKit protocol EditTagPresentationLogic { - + func presentTags(with response: EditTagModels.FetchTags.Response) } -class EditTagPresenter: EditTagPresentationLogic { +final class EditTagPresenter: EditTagPresentationLogic { // MARK: - Properties typealias Models = EditTagModels weak var viewController: EditTagDisplayLogic? + func presentTags(with response: EditTagModels.FetchTags.Response) { + viewController?.displayTags(viewModel: EditTagModels.FetchTags.ViewModel(tags: response.tags)) + } + } diff --git a/iOS/Layover/Layover/Scenes/EditTag/EditTagRouter.swift b/iOS/Layover/Layover/Scenes/EditTag/EditTagRouter.swift index 814ff8b..d499382 100644 --- a/iOS/Layover/Layover/Scenes/EditTag/EditTagRouter.swift +++ b/iOS/Layover/Layover/Scenes/EditTag/EditTagRouter.swift @@ -16,21 +16,24 @@ protocol EditTagDataPassing { var dataStore: EditTagDataStore? { get } } -class EditTagRouter: NSObject, EditTagRoutingLogic, EditTagDataPassing { +final class EditTagRouter: NSObject, EditTagRoutingLogic, EditTagDataPassing { // MARK: - Properties weak var viewController: EditTagViewController? + var dataStore: EditTagDataStore? // MARK: - Routing func routeToBack() { - let destination = viewController?.presentingViewController as? UploadPostViewController - var destinationDataStore = destination?.router?.dataStore - - // data passing - viewController?.navigationController?.popViewController(animated: true) + guard let presentingViewController = viewController?.presentingViewController as? UITabBarController, + let selectedViewController = presentingViewController.selectedViewController as? UINavigationController, + let previousViewController = selectedViewController.viewControllers.last as? UploadPostViewController, + var destination = previousViewController.router?.dataStore + else { return } + destination.tags = dataStore?.tags + viewController?.dismiss(animated: true) } } diff --git a/iOS/Layover/Layover/Scenes/EditTag/EditTagViewController.swift b/iOS/Layover/Layover/Scenes/EditTag/EditTagViewController.swift index 2cf6c93..dd82629 100644 --- a/iOS/Layover/Layover/Scenes/EditTag/EditTagViewController.swift +++ b/iOS/Layover/Layover/Scenes/EditTag/EditTagViewController.swift @@ -9,13 +9,20 @@ import UIKit protocol EditTagDisplayLogic: AnyObject { - + func displayTags(viewModel: EditTagModels.FetchTags.ViewModel) } -final class EditTagViewController: BaseViewController, EditTagDisplayLogic { +final class EditTagViewController: BaseViewController { // MARK: - UI Components + private lazy var closeButton: UIButton = { + let button: UIButton = UIButton() + button.setImage(UIImage.down, for: .normal) + button.addTarget(self, action: #selector(closeButtonDidTap), for: .touchUpInside) + return button + }() + private let tagTextField: LOTextField = { let textField: LOTextField = LOTextField() textField.placeholder = "태그" @@ -55,18 +62,22 @@ final class EditTagViewController: BaseViewController, EditTagDisplayLogic { override func viewDidLoad() { super.viewDidLoad() + interactor?.fetchTags() tagTextField.delegate = self } override func setConstraints() { super.setConstraints() - view.addSubviews(tagTextField, tagStackView) + view.addSubviews(closeButton, tagTextField, tagStackView) view.subviews.forEach { $0.translatesAutoresizingMaskIntoConstraints = false } NSLayoutConstraint.activate([ - tagTextField.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8), + closeButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8), + closeButton.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 21), + + tagTextField.topAnchor.constraint(equalTo: closeButton.bottomAnchor, constant: 8), tagTextField.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16), tagTextField.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16), tagTextField.heightAnchor.constraint(equalToConstant: 44), @@ -77,6 +88,14 @@ final class EditTagViewController: BaseViewController, EditTagDisplayLogic { ]) } + // MARK: - Methods + + @objc private func closeButtonDidTap() { + let request = EditTagModels.EditTag.Request(tags: tagStackView.tags) + interactor?.editTag(request: request) + router?.routeToBack() + } + } // MARK: - UITextFieldDelegate @@ -89,3 +108,12 @@ extension EditTagViewController: UITextFieldDelegate { return true } } + +extension EditTagViewController: EditTagDisplayLogic { + + func displayTags(viewModel: EditTagModels.FetchTags.ViewModel) { + tagStackView.resetTagStackView() + viewModel.tags.forEach { tagStackView.addTag($0) } + } + +} diff --git a/iOS/Layover/Layover/Scenes/EditVideo/EditVideoInteractor.swift b/iOS/Layover/Layover/Scenes/EditVideo/EditVideoInteractor.swift index 814695e..0709dab 100644 --- a/iOS/Layover/Layover/Scenes/EditVideo/EditVideoInteractor.swift +++ b/iOS/Layover/Layover/Scenes/EditVideo/EditVideoInteractor.swift @@ -12,10 +12,12 @@ import UIKit protocol EditVideoBusinessLogic { func fetchVideo(request: EditVideoModels.FetchVideo.Request) func deleteVideo() + func didFinishVideoEditing(request: EditVideoModels.DidFinishViedoEditing.Request) } protocol EditVideoDataStore { var videoURL: URL? { get set } + var isMuted: Bool? { get set } } final class EditVideoInteractor: EditVideoBusinessLogic, EditVideoDataStore { @@ -27,7 +29,10 @@ final class EditVideoInteractor: EditVideoBusinessLogic, EditVideoDataStore { var videoFileWorker: VideoFileWorker? var presenter: EditVideoPresentationLogic? + // MARK: - Data Store + var videoURL: URL? + var isMuted: Bool? func fetchVideo(request: EditVideoModels.FetchVideo.Request) { let isEdited = request.editedVideoURL != nil @@ -51,4 +56,8 @@ final class EditVideoInteractor: EditVideoBusinessLogic, EditVideoDataStore { videoFileWorker?.delete(at: videoURL) } + func didFinishVideoEditing(request: EditVideoModels.DidFinishViedoEditing.Request) { + isMuted = request.isMuted + } + } diff --git a/iOS/Layover/Layover/Scenes/EditVideo/EditVideoModels.swift b/iOS/Layover/Layover/Scenes/EditVideo/EditVideoModels.swift index 3d65206..7160780 100644 --- a/iOS/Layover/Layover/Scenes/EditVideo/EditVideoModels.swift +++ b/iOS/Layover/Layover/Scenes/EditVideo/EditVideoModels.swift @@ -29,4 +29,10 @@ enum EditVideoModels { } } + enum DidFinishViedoEditing { + struct Request { + let isMuted: Bool + } + } + } diff --git a/iOS/Layover/Layover/Scenes/EditVideo/EditVideoRouter.swift b/iOS/Layover/Layover/Scenes/EditVideo/EditVideoRouter.swift index 74855e0..b7dcf75 100644 --- a/iOS/Layover/Layover/Scenes/EditVideo/EditVideoRouter.swift +++ b/iOS/Layover/Layover/Scenes/EditVideo/EditVideoRouter.swift @@ -26,7 +26,16 @@ final class EditVideoRouter: NSObject, EditVideoRoutingLogic, EditVideoDataPassi // MARK: - Routing func routeToNext() { - + let nextViewController = UploadPostViewController() + guard let source = dataStore, + var destination = nextViewController.router?.dataStore + else { return } + + // Data Passing + destination.videoURL = source.videoURL + destination.isMuted = source.isMuted + nextViewController.hidesBottomBarWhenPushed = true + viewController?.navigationController?.pushViewController(nextViewController, animated: true) } } diff --git a/iOS/Layover/Layover/Scenes/EditVideo/EditVideoViewController.swift b/iOS/Layover/Layover/Scenes/EditVideo/EditVideoViewController.swift index d893c44..e62532c 100644 --- a/iOS/Layover/Layover/Scenes/EditVideo/EditVideoViewController.swift +++ b/iOS/Layover/Layover/Scenes/EditVideo/EditVideoViewController.swift @@ -34,9 +34,10 @@ final class EditVideoViewController: BaseViewController { return button }() - private let nextButton: LOButton = { + private lazy var nextButton: LOButton = { let button = LOButton(style: .basic) button.setTitle("다음", for: .normal) + button.addTarget(self, action: #selector(nextButtonDidTap), for: .touchUpInside) return button }() @@ -73,8 +74,12 @@ final class EditVideoViewController: BaseViewController { interactor?.fetchVideo(request: Models.FetchVideo.Request(editedVideoURL: nil)) } + override func viewWillAppear(_ animated: Bool) { + loopingPlayerView.play() + } + override func viewWillDisappear(_ animated: Bool) { - interactor?.deleteVideo() +// interactor?.deleteVideo() } override func setConstraints() { @@ -121,6 +126,12 @@ final class EditVideoViewController: BaseViewController { } } + @objc private func nextButtonDidTap() { + loopingPlayerView.pause() + interactor?.didFinishVideoEditing(request: EditVideoModels.DidFinishViedoEditing.Request(isMuted: soundButton.isSelected)) + router?.routeToNext() + } + } extension EditVideoViewController: UINavigationControllerDelegate, UIVideoEditorControllerDelegate { diff --git a/iOS/Layover/Layover/Scenes/Login/LoginInteractor.swift b/iOS/Layover/Layover/Scenes/Login/LoginInteractor.swift index 1292fbf..0b63af2 100644 --- a/iOS/Layover/Layover/Scenes/Login/LoginInteractor.swift +++ b/iOS/Layover/Layover/Scenes/Login/LoginInteractor.swift @@ -43,7 +43,7 @@ extension LoginInteractor: LoginBusinessLogic { guard let token = await worker?.fetchKakaoLoginToken() else { return } kakaoLoginToken = token if await worker?.isRegisteredKakao(with: token) == true, - await worker?.loginKakao(with: token) == true { + await worker?.loginKakao(with: token) == true { await MainActor.run { presenter?.presentPerformLogin() } @@ -77,10 +77,10 @@ extension LoginInteractor: ASAuthorizationControllerDelegate { } appleLoginToken = identityToken Task { - async let isRegistered: Bool = worker?.isRegisteredApple(with: identityToken) ?? false - async let loginResult: Bool = worker?.loginApple(with: identityToken) ?? false + let isRegistered = await worker?.isRegisteredApple(with: identityToken) + let loginResult = await worker?.loginApple(with: identityToken) - if await !isRegistered, await loginResult { + if isRegistered == true, loginResult == true { await MainActor.run { presenter?.presentPerformLogin() } diff --git a/iOS/Layover/Layover/Scenes/Login/LoginWorker.swift b/iOS/Layover/Layover/Scenes/Login/LoginWorker.swift index ff6acd3..e315645 100644 --- a/iOS/Layover/Layover/Scenes/Login/LoginWorker.swift +++ b/iOS/Layover/Layover/Scenes/Login/LoginWorker.swift @@ -73,7 +73,7 @@ extension LoginWorker: LoginWorkerProtocol { do { let endPoint = loginEndPointFactory.makeCheckKakaoIsSignedUpEndPoint(with: socialToken) let result = try await provider.request(with: endPoint, authenticationIfNeeded: false) - return result.data?.isValid + return result.data?.isAlreadyExist } catch { os_log(.error, log: .data, "%@", error.localizedDescription) return nil @@ -101,7 +101,7 @@ extension LoginWorker: LoginWorkerProtocol { do { let endPoint = loginEndPointFactory.makeCheckAppleIsSignedUpEndPoint(with: identityToken) let result = try await provider.request(with: endPoint, authenticationIfNeeded: false) - return result.data?.isValid + return result.data?.isAlreadyExist } catch { os_log(.error, log: .data, "%@", error.localizedDescription) return nil diff --git a/iOS/Layover/Layover/Scenes/Playback/Cell/PlaybackCell.swift b/iOS/Layover/Layover/Scenes/Playback/Cell/PlaybackCell.swift index d5f54a9..3261108 100644 --- a/iOS/Layover/Layover/Scenes/Playback/Cell/PlaybackCell.swift +++ b/iOS/Layover/Layover/Scenes/Playback/Cell/PlaybackCell.swift @@ -10,6 +10,7 @@ import UIKit final class PlaybackCell: UICollectionViewCell { let playbackView: PlaybackView = PlaybackView() + var boardID: Int = 0 override init(frame: CGRect) { super.init(frame: frame) @@ -22,6 +23,7 @@ final class PlaybackCell: UICollectionViewCell { } func setPlaybackContents(info: PlaybackModels.PlaybackInfo) { + boardID = info.boardID playbackView.descriptionView.titleLabel.text = info.title playbackView.descriptionView.setText(info.content) playbackView.profileLabel.text = info.profileName diff --git a/iOS/Layover/Layover/Scenes/Playback/PlaybackModels.swift b/iOS/Layover/Layover/Scenes/Playback/PlaybackModels.swift index 1dba042..cf19887 100644 --- a/iOS/Layover/Layover/Scenes/Playback/PlaybackModels.swift +++ b/iOS/Layover/Layover/Scenes/Playback/PlaybackModels.swift @@ -21,6 +21,7 @@ enum PlaybackModels { } struct PlaybackInfo: Hashable { + let boardID: Int let title: String let content: String let profileImageURL: URL? @@ -127,4 +128,20 @@ enum PlaybackModels { let curCell: PlaybackCell } } + + // MARK: - UseCase Report Playback Video + + enum ReportPlaybackVideo { + struct Request { + + } + + struct Response { + let boardID: Int + } + + struct ViewModel { + let boardID: Int + } + } } diff --git a/iOS/Layover/Layover/Scenes/Playback/PlaybackPresenter.swift b/iOS/Layover/Layover/Scenes/Playback/PlaybackPresenter.swift index f8a68ea..73a605e 100644 --- a/iOS/Layover/Layover/Scenes/Playback/PlaybackPresenter.swift +++ b/iOS/Layover/Layover/Scenes/Playback/PlaybackPresenter.swift @@ -35,6 +35,7 @@ final class PlaybackPresenter: PlaybackPresentationLogic { return nil } return Models.PlaybackVideo(playbackInfo: PlaybackModels.PlaybackInfo( + boardID: post.board.identifier, title: post.board.title, content: post.board.description ?? "", profileImageURL: post.member.profileImageURL, diff --git a/iOS/Layover/Layover/Scenes/Playback/PlaybackRouter.swift b/iOS/Layover/Layover/Scenes/Playback/PlaybackRouter.swift index 2100869..24c142b 100644 --- a/iOS/Layover/Layover/Scenes/Playback/PlaybackRouter.swift +++ b/iOS/Layover/Layover/Scenes/Playback/PlaybackRouter.swift @@ -9,7 +9,7 @@ import UIKit protocol PlaybackRoutingLogic { - func routeToNext() + func routeToReport() } protocol PlaybackDataPassing { @@ -25,7 +25,13 @@ final class PlaybackRouter: NSObject, PlaybackRoutingLogic, PlaybackDataPassing // MARK: - Routing - func routeToNext() { - + func routeToReport() { + let reportViewController: ReportViewController = ReportViewController() + guard let source = dataStore, + var destination = reportViewController.router?.dataStore + else { return } + destination.boardID = source.prevCell?.boardID + reportViewController.modalPresentationStyle = .fullScreen + viewController?.present(reportViewController, animated: false) } } diff --git a/iOS/Layover/Layover/Scenes/Playback/PlaybackViewController.swift b/iOS/Layover/Layover/Scenes/Playback/PlaybackViewController.swift index 16a8788..c3c3f6a 100644 --- a/iOS/Layover/Layover/Scenes/Playback/PlaybackViewController.swift +++ b/iOS/Layover/Layover/Scenes/Playback/PlaybackViewController.swift @@ -41,6 +41,14 @@ final class PlaybackViewController: BaseViewController { return collectionView }() + private let reportButton: UIBarButtonItem = { + let button: UIButton = UIButton() + button.setImage(UIImage(systemName: "ellipsis"), for: .normal) + let barButtonItem: UIBarButtonItem = UIBarButtonItem(customView: button) + barButtonItem.customView?.transform = CGAffineTransform(rotationAngle: .pi / 2) + return barButtonItem + }() + // MARK: - Properties private var playerSlider: LOSlider = LOSlider() @@ -115,6 +123,10 @@ final class PlaybackViewController: BaseViewController { override func setUI() { super.setUI() addWindowPlayerSlider() + guard let button = reportButton.customView as? UIButton else { return } + button.addTarget(self, action: #selector(reportButtonDidTap), for: .touchUpInside) + self.navigationItem.rightBarButtonItem = reportButton + self.navigationController?.navigationBar.tintColor = .layoverWhite } private func addWindowPlayerSlider() { @@ -157,6 +169,20 @@ final class PlaybackViewController: BaseViewController { let request: Models.SeekVideo.Request = Models.SeekVideo.Request(currentLocation: Float64(sender.value)) interactor?.controlPlaybackMovie(with: request) } + + @objc private func reportButtonDidTap() { + let alert: UIAlertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + let reportAction: UIAlertAction = UIAlertAction(title: "신고", style: .destructive, handler: { + [weak self] _ in + self?.router?.routeToReport() + }) + let cancelAction: UIAlertAction = UIAlertAction(title: "취소", style: .cancel) + alert.addAction(reportAction) + alert.addAction(cancelAction) + self.present(alert, animated: true, completion: { + self.interactor?.leavePlaybackView() + }) + } } extension PlaybackViewController: PlaybackDisplayLogic { @@ -180,9 +206,6 @@ extension PlaybackViewController: PlaybackDisplayLogic { curCell.playbackView.playPlayer() setPlayerSlider(at: curCell.playbackView) // Slider가 원점으로 돌아가는 시간 필요 -// DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) { -// self.playerSlider.isHidden = false -// } Task { await slowShowPlayerSlider() } diff --git a/iOS/Layover/Layover/Scenes/Report/ReportConfigurator.swift b/iOS/Layover/Layover/Scenes/Report/ReportConfigurator.swift new file mode 100644 index 0000000..7de9c41 --- /dev/null +++ b/iOS/Layover/Layover/Scenes/Report/ReportConfigurator.swift @@ -0,0 +1,32 @@ +// +// ReportConfigurator.swift +// Layover +// +// Created by 황지웅 on 12/4/23. +// Copyright © 2023 CodeBomber. All rights reserved. +// + +import Foundation + +final class ReportConfigurator: Configurator { + static let shared = ReportConfigurator() + + private init() { } + + func configure(_ viewController: ReportViewController) { + let viewController: ReportViewController = viewController + let interactor: ReportInteractor = ReportInteractor() + let presenter: ReportPresenter = ReportPresenter() +// let worker: ReportWorker = ReportWorker() + let worker: ReportWorkerProtocol = MockReportWorker() + let router: ReportRouter = ReportRouter() + + router.viewController = viewController + router.dataStore = interactor + viewController.interactor = interactor + viewController.router = router + interactor.presenter = presenter + interactor.worker = worker + presenter.viewController = viewController + } +} diff --git a/iOS/Layover/Layover/Scenes/Report/ReportInteractor.swift b/iOS/Layover/Layover/Scenes/Report/ReportInteractor.swift new file mode 100644 index 0000000..5af9433 --- /dev/null +++ b/iOS/Layover/Layover/Scenes/Report/ReportInteractor.swift @@ -0,0 +1,44 @@ +// +// ReportInteractor.swift +// Layover +// +// Created by 황지웅 on 12/4/23. +// Copyright © 2023 CodeBomber. All rights reserved. +// + +import UIKit + +protocol ReportBusinessLogic { + @discardableResult + func reportPlaybackVideo(with request: ReportModels.ReportPlaybackVideo.Request) -> Task +} + +protocol ReportDataStore { + var boardID: Int? { get set } +} + +final class ReportInteractor: ReportBusinessLogic, ReportDataStore { + + // MARK: - Properties + + typealias Models = ReportModels + + var worker: ReportWorkerProtocol? + var presenter: ReportPresentationLogic? + + var boardID: Int? + + func reportPlaybackVideo(with request: ReportModels.ReportPlaybackVideo.Request) -> Task { + Task { + guard let boardID, + let worker + else { return false } + let result: Bool = await worker.reportPlaybackVideo(boardID: boardID, reportContent: request.reportContent) + let response: Models.ReportPlaybackVideo.Response = Models.ReportPlaybackVideo.Response(reportResult: result) + await MainActor.run { + presenter?.presentReportPlaybackVideo(with: response) + } + return true + } + } +} diff --git a/iOS/Layover/Layover/Scenes/Report/ReportModels.swift b/iOS/Layover/Layover/Scenes/Report/ReportModels.swift new file mode 100644 index 0000000..f7cd3d6 --- /dev/null +++ b/iOS/Layover/Layover/Scenes/Report/ReportModels.swift @@ -0,0 +1,41 @@ +// +// ReportModels.swift +// Layover +// +// Created by 황지웅 on 12/4/23. +// Copyright © 2023 CodeBomber. All rights reserved. +// + +import UIKit + +enum ReportModels { + + // MARK: - Use Cases + + enum ReportPlaybackVideo { + enum ReportMessage { + case success + case fail + + var description: String { + switch self { + case .success: + "신고가 접수되었습니다." + case .fail: + "신고 접수에 실패했습니다. 다시 시도해주세요." + } + } + } + struct Request { + let reportContent: String + } + + struct Response { + let reportResult: Bool + } + + struct ViewModel { + let reportMessage: ReportMessage + } + } +} diff --git a/iOS/Layover/Layover/Scenes/Report/ReportPresenter.swift b/iOS/Layover/Layover/Scenes/Report/ReportPresenter.swift new file mode 100644 index 0000000..7133322 --- /dev/null +++ b/iOS/Layover/Layover/Scenes/Report/ReportPresenter.swift @@ -0,0 +1,27 @@ +// +// ReportPresenter.swift +// Layover +// +// Created by 황지웅 on 12/4/23. +// Copyright © 2023 CodeBomber. All rights reserved. +// + +import Foundation + +protocol ReportPresentationLogic { + func presentReportPlaybackVideo(with response: ReportModels.ReportPlaybackVideo.Response) +} + +final class ReportPresenter: ReportPresentationLogic { + + // MARK: - Properties + + typealias Models = ReportModels + weak var viewController: ReportDisplayLogic? + + func presentReportPlaybackVideo(with response: ReportModels.ReportPlaybackVideo.Response) { + let viewModel: Models.ReportPlaybackVideo.ViewModel + viewModel = Models.ReportPlaybackVideo.ViewModel(reportMessage: response.reportResult ? .success : .fail) + viewController?.displayReportResult(viewModel: viewModel) + } +} diff --git a/iOS/Layover/Layover/Scenes/Report/ReportRouter.swift b/iOS/Layover/Layover/Scenes/Report/ReportRouter.swift new file mode 100644 index 0000000..99b8bd5 --- /dev/null +++ b/iOS/Layover/Layover/Scenes/Report/ReportRouter.swift @@ -0,0 +1,30 @@ +// +// ReportRouter.swift +// Layover +// +// Created by 황지웅 on 12/4/23. +// Copyright © 2023 CodeBomber. All rights reserved. +// + +import UIKit + +protocol ReportRoutingLogic { + func routeToNext() +} + +protocol ReportDataPassing { + var dataStore: ReportDataStore? { get } +} + +final class ReportRouter: NSObject, ReportRoutingLogic, ReportDataPassing { + + // MARK: - Properties + + weak var viewController: ReportViewController? + var dataStore: ReportDataStore? + + // MARK: - Routing + + func routeToNext() { + } +} diff --git a/iOS/Layover/Layover/Scenes/Report/ReportViewController.swift b/iOS/Layover/Layover/Scenes/Report/ReportViewController.swift new file mode 100644 index 0000000..07c8cac --- /dev/null +++ b/iOS/Layover/Layover/Scenes/Report/ReportViewController.swift @@ -0,0 +1,117 @@ +// +// ReportViewController.swift +// Layover +// +// Created by 황지웅 on 12/4/23. +// Copyright © 2023 CodeBomber. All rights reserved. +// + +import UIKit + +protocol ReportViewControllerDelegate: AnyObject { + func reportPlaybackVideo(reportContent: String) + func dismissReportView() +} + +protocol ReportDisplayLogic: AnyObject { + func displayReportResult(viewModel: ReportModels.ReportPlaybackVideo.ViewModel) +} + +final class ReportViewController: BaseViewController { + + // MARK: - UI Components + + private let popUpView: LOPopUpView = { + let view: LOPopUpView = LOPopUpView() + view.layer.cornerRadius = 12 + view.backgroundColor = .darkGrey + return view + }() + + private let backgroundView: UIView = { + let view: UIView = UIView() + view.backgroundColor = .black + view.alpha = 0.5 + return view + }() + + // MARK: - Properties + + typealias Models = ReportModels + var router: (NSObjectProtocol & ReportRoutingLogic & ReportDataPassing)? + var interactor: ReportBusinessLogic? + + // MARK: - Object lifecycle + + override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + setup() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setup() + } + + // MARK: - Setup + + private func setup() { + ReportConfigurator.shared.configure(self) + } + + // MARK: - View Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + } + + override func setUI() { + super.setUI() + popUpView.delegate = self + } + + override func setConstraints() { + super.setConstraints() + view.addSubviews(backgroundView, popUpView) + view.subviews.forEach { subView in + subView.translatesAutoresizingMaskIntoConstraints = false + } + NSLayoutConstraint.activate([ + backgroundView.topAnchor.constraint(equalTo: view.topAnchor), + backgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + backgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + backgroundView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + popUpView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + popUpView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + popUpView.widthAnchor.constraint(equalToConstant: 350), + popUpView.heightAnchor.constraint(equalToConstant: 450) + ]) + } + + @objc private func cancelButtonDidTap() { + dismiss(animated: true) + } +} + +extension ReportViewController: ReportViewControllerDelegate { + func reportPlaybackVideo(reportContent: String) { + let request: Models.ReportPlaybackVideo.Request = Models.ReportPlaybackVideo.Request(reportContent: reportContent) + interactor?.reportPlaybackVideo(with: request) + } + + func dismissReportView() { + dismiss(animated: false) + } +} + +extension ReportViewController: ReportDisplayLogic { + func displayReportResult(viewModel: ReportModels.ReportPlaybackVideo.ViewModel) { + dismiss(animated: false, completion: { + Toast.shared.showToast(message: viewModel.reportMessage.description) + }) + } +} + +//#Preview { +// ReportViewController() +//} diff --git a/iOS/Layover/Layover/Scenes/Report/ReportWorker.swift b/iOS/Layover/Layover/Scenes/Report/ReportWorker.swift new file mode 100644 index 0000000..67d9d34 --- /dev/null +++ b/iOS/Layover/Layover/Scenes/Report/ReportWorker.swift @@ -0,0 +1,45 @@ +// +// ReportWorker.swift +// Layover +// +// Created by 황지웅 on 12/4/23. +// Copyright © 2023 CodeBomber. All rights reserved. +// + +import Foundation + +import OSLog + +protocol ReportWorkerProtocol { + func reportPlaybackVideo(boardID: Int, reportContent: String) async -> Bool +} + +final class ReportWorker: ReportWorkerProtocol { + + // MARK: - Properties + + typealias Models = ReportModels + + private let reportEndPointFactory: ReportEndPointFactory + private let provider: ProviderType + + init(reportEndPointFactory: ReportEndPointFactory = DefaultReportEndPointFactory(), provider: ProviderType = Provider()) { + self.reportEndPointFactory = reportEndPointFactory + self.provider = provider + } + + func reportPlaybackVideo(boardID: Int, reportContent: String) async -> Bool { + let endPoint = reportEndPointFactory.reportPlaybackVideoEndpoint(boardID: boardID, reportType: reportContent) + do { + let responseData = try await provider.request(with: endPoint) + guard let _ = responseData.data else { + os_log(.error, log: .data, "Failed to report with error: %@", responseData.message) + return false + } + } catch { + os_log(.error, log: .data, "Failed to report with error: %@", error.localizedDescription) + return false + } + return true + } +} diff --git a/iOS/Layover/Layover/Scenes/UploadPost/UploadPostConfigurator.swift b/iOS/Layover/Layover/Scenes/UploadPost/UploadPostConfigurator.swift index e9215b2..a31a522 100644 --- a/iOS/Layover/Layover/Scenes/UploadPost/UploadPostConfigurator.swift +++ b/iOS/Layover/Layover/Scenes/UploadPost/UploadPostConfigurator.swift @@ -21,12 +21,14 @@ final class UploadPostConfigurator: Configurator { let interactor = UploadPostInteractor() let presenter = UploadPostPresenter() let router = UploadPostRouter() + let worker = UploadPostWorker() router.viewController = viewController router.dataStore = interactor viewController.interactor = interactor viewController.router = router interactor.presenter = presenter + interactor.worker = worker presenter.viewController = viewController } diff --git a/iOS/Layover/Layover/Scenes/UploadPost/UploadPostInteractor.swift b/iOS/Layover/Layover/Scenes/UploadPost/UploadPostInteractor.swift index 4b638dd..b3d7fdf 100644 --- a/iOS/Layover/Layover/Scenes/UploadPost/UploadPostInteractor.swift +++ b/iOS/Layover/Layover/Scenes/UploadPost/UploadPostInteractor.swift @@ -6,23 +6,169 @@ // Copyright © 2023 CodeBomber. All rights reserved. // +import AVFoundation +import CoreLocation import UIKit +import OSLog + protocol UploadPostBusinessLogic { + func fetchTags() + + @discardableResult + func fetchThumbnailImage() -> Task + + func fetchCurrentAddress() + func canUploadPost(request: UploadPostModels.CanUploadPost.Request) + @discardableResult + func uploadPost(request: UploadPostModels.UploadPost.Request) -> Task } protocol UploadPostDataStore { - + var videoURL: URL? { get set } + var isMuted: Bool? { get set } + var tags: [String]? { get set } } -class UploadPostInteractor: UploadPostBusinessLogic, UploadPostDataStore { +final class UploadPostInteractor: NSObject, UploadPostBusinessLogic, UploadPostDataStore { // MARK: - Properties typealias Models = UploadPostModels - lazy var worker = UploadPostWorker() + var worker: UploadPostWorkerProtocol? var presenter: UploadPostPresentationLogic? + private let fileManager: FileManager + private let locationManager: CLLocationManager + + // MARK: - Data Store + + var videoURL: URL? + var isMuted: Bool? + var tags: [String]? = [] + + // MARK: - Object LifeCycle + + init(fileManager: FileManager = FileManager.default, + locationManager: CLLocationManager = CLLocationManager()) { + self.fileManager = fileManager + self.locationManager = locationManager + } + + // MARK: - Methods + + func fetchTags() { + guard let tags else { return } + presenter?.presentTags(with: Models.FetchTags.Response(tags: tags)) + } + + @discardableResult + func fetchThumbnailImage() -> Task { + Task { + guard let videoURL else { return false } + let asset = AVAsset(url: videoURL) + let generator = AVAssetImageGenerator(asset: asset) + generator.appliesPreferredTrackTransform = true + do { + let image = try await generator.image(at: .zero).image + await MainActor.run { + presenter?.presentThumnailImage(with: Models.FetchThumbnail.Response(thumnailImage: image)) + } + return true + } catch let error { + os_log(.error, log: .default, "Failed to fetch Thumbnail Image with error: %@", error.localizedDescription) + return false + } + } + } + + func fetchCurrentAddress() { + guard let location = getCurrentLocation() else { return } + let localeIdentifier = Locale.preferredLanguages.first != nil ? Locale.preferredLanguages[0] : Locale.current.identifier + let locale = Locale(identifier: localeIdentifier) + + Task { + do { + let address = try await CLGeocoder().reverseGeocodeLocation(location, preferredLocale: locale).last + let administrativeArea = address?.administrativeArea + let locality = address?.locality + let subLocality = address?.subLocality + let response = Models.FetchCurrentAddress.Response(administrativeArea: administrativeArea, + locality: locality, + subLocality: subLocality) + await MainActor.run { + presenter?.presentCurrentAddress(with: response) + } + } catch { + os_log(.error, log: .default, "Failed to fetch Current Address with error: %@", error.localizedDescription) + } + } + } + + func canUploadPost(request: UploadPostModels.CanUploadPost.Request) { + let response = Models.CanUploadPost.Response(isEmpty: request.title == nil) + presenter?.presentUploadButton(with: response) + } + + @discardableResult + func uploadPost(request: UploadPostModels.UploadPost.Request) -> Task { + Task { + guard let worker, + let videoURL, + let isMuted, + let coordinate = getCurrentLocation()?.coordinate else { return false } + if isMuted { + exportVideoWithoutAudio(at: videoURL) + } + let response = await worker.uploadPost(with: UploadPost(title: request.title, + content: request.content, + latitude: coordinate.latitude, + longitude: coordinate.longitude, + tag: request.tags, + videoURL: videoURL)) + return response + } + } + + private func getCurrentLocation() -> CLLocation? { + locationManager.desiredAccuracy = kCLLocationAccuracyBest + locationManager.startUpdatingLocation() + + guard let space = locationManager.location?.coordinate else { return nil } + let location = CLLocation(latitude: space.latitude, longitude: space.longitude) + return location + } + + private func exportVideoWithoutAudio(at url: URL) { + let composition = AVMutableComposition() + let sourceAsset = AVURLAsset(url: url) + guard let compositionVideoTrack = composition.addMutableTrack(withMediaType: AVMediaType.video, + preferredTrackID: kCMPersistentTrackID_Invalid) else { return } + Task { + do { + let sourceAssetduration = try await sourceAsset.load(.duration) + let sourceVideoTrack = try await sourceAsset.load(.tracks)[0] + compositionVideoTrack.preferredTransform = try await sourceVideoTrack.load(.preferredTransform) + + let timeRange: CMTimeRange = CMTimeRangeMake(start: .zero, duration: sourceAssetduration) + try compositionVideoTrack.insertTimeRange(timeRange, + of: sourceVideoTrack, + at: .zero) + + if fileManager.fileExists(atPath: url.path()) { + try fileManager.removeItem(at: url) + } + + let exporter = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality) + exporter?.outputURL = videoURL + exporter?.outputFileType = AVFileType.mov + await exporter?.export() + } catch { + os_log(.error, log: .data, "Failed to extract Video Without Audio with error: %@", error.localizedDescription) + } + } + } + } diff --git a/iOS/Layover/Layover/Scenes/UploadPost/UploadPostModels.swift b/iOS/Layover/Layover/Scenes/UploadPost/UploadPostModels.swift index 6d904f4..e722fff 100644 --- a/iOS/Layover/Layover/Scenes/UploadPost/UploadPostModels.swift +++ b/iOS/Layover/Layover/Scenes/UploadPost/UploadPostModels.swift @@ -10,5 +10,69 @@ import UIKit enum UploadPostModels { - // MARK: - Use Cases + enum CanUploadPost { + struct Request { + let title: String? + } + struct Response { + let isEmpty: Bool + } + struct ViewModel { + let canUpload: Bool + } + } + + enum FetchTags { + struct Request { + + } + struct Response { + let tags: [String] + } + + struct ViewModel { + let tags: [String] + } + } + + enum FetchThumbnail { + struct Request { + + } + struct Response { + let thumnailImage: CGImage + } + struct ViewModel { + let thumnailImage: UIImage + } + } + + enum FetchCurrentAddress { + struct Request { + + } + struct Response { + let administrativeArea: String? + let locality: String? + let subLocality: String? + } + struct ViewModel { + let fullAddress: String + } + } + + enum UploadPost { + struct Request { + let title: String + let content: String? + let tags: [String] + } + struct Response { + + } + struct VideModel { + + } + } + } diff --git a/iOS/Layover/Layover/Scenes/UploadPost/UploadPostPresenter.swift b/iOS/Layover/Layover/Scenes/UploadPost/UploadPostPresenter.swift index 1e7dce2..f87cef8 100644 --- a/iOS/Layover/Layover/Scenes/UploadPost/UploadPostPresenter.swift +++ b/iOS/Layover/Layover/Scenes/UploadPost/UploadPostPresenter.swift @@ -9,14 +9,51 @@ import UIKit protocol UploadPostPresentationLogic { - + func presentTags(with response: UploadPostModels.FetchTags.Response) + func presentThumnailImage(with response: UploadPostModels.FetchThumbnail.Response) + func presentCurrentAddress(with response: UploadPostModels.FetchCurrentAddress.Response) + func presentUploadButton(with response: UploadPostModels.CanUploadPost.Response) +// func presentUploadProgress(with response: UploadPostModels.UploadPost.Response) } -class UploadPostPresenter: UploadPostPresentationLogic { +final class UploadPostPresenter: UploadPostPresentationLogic { // MARK: - Properties typealias Models = UploadPostModels weak var viewController: UploadPostDisplayLogic? + func presentTags(with response: UploadPostModels.FetchTags.Response) { + viewController?.displayTags(viewModel: Models.FetchTags.ViewModel(tags: response.tags)) + } + + func presentThumnailImage(with response: UploadPostModels.FetchThumbnail.Response) { + let image = UIImage(cgImage: response.thumnailImage) + viewController?.displayThumbnail(viewModel: Models.FetchThumbnail.ViewModel(thumnailImage: image)) + } + + func presentCurrentAddress(with response: UploadPostModels.FetchCurrentAddress.Response) { + let addresses: [String] = [ + response.administrativeArea, + response.locality, + response.subLocality] + .compactMap { $0 } + + var fullAddress: [String] = [] + + for address in addresses { + if !fullAddress.contains(address) { + fullAddress.append(address) + } + } + + let viewModel = Models.FetchCurrentAddress.ViewModel(fullAddress: fullAddress.joined(separator: " ")) + viewController?.displayCurrentAddress(viewModel: viewModel) + } + + func presentUploadButton(with response: UploadPostModels.CanUploadPost.Response) { + let viewModel = Models.CanUploadPost.ViewModel(canUpload: !response.isEmpty) + viewController?.displayUploadButton(viewModel: viewModel) + } + } diff --git a/iOS/Layover/Layover/Scenes/UploadPost/UploadPostRouter.swift b/iOS/Layover/Layover/Scenes/UploadPost/UploadPostRouter.swift index 2edebfc..4b9b74f 100644 --- a/iOS/Layover/Layover/Scenes/UploadPost/UploadPostRouter.swift +++ b/iOS/Layover/Layover/Scenes/UploadPost/UploadPostRouter.swift @@ -10,13 +10,14 @@ import UIKit protocol UploadPostRoutingLogic { func routeToNext() + func routeToBack() } protocol UploadPostDataPassing { var dataStore: UploadPostDataStore? { get } } -class UploadPostRouter: NSObject, UploadPostRoutingLogic, UploadPostDataPassing { +final class UploadPostRouter: NSObject, UploadPostRoutingLogic, UploadPostDataPassing { // MARK: - Properties @@ -31,8 +32,13 @@ class UploadPostRouter: NSObject, UploadPostRoutingLogic, UploadPostDataPassing var destination = nextViewController.router?.dataStore else { return } - // Data Passing - viewController?.navigationController?.pushViewController(nextViewController, animated: true) + destination.tags = source.tags + nextViewController.modalPresentationStyle = .fullScreen + viewController?.present(nextViewController, animated: true) + } + + func routeToBack() { + viewController?.navigationController?.popToRootViewController(animated: true) } } diff --git a/iOS/Layover/Layover/Scenes/UploadPost/UploadPostViewController.swift b/iOS/Layover/Layover/Scenes/UploadPost/UploadPostViewController.swift index 1e76e90..f16890e 100644 --- a/iOS/Layover/Layover/Scenes/UploadPost/UploadPostViewController.swift +++ b/iOS/Layover/Layover/Scenes/UploadPost/UploadPostViewController.swift @@ -9,10 +9,13 @@ import UIKit protocol UploadPostDisplayLogic: AnyObject { - + func displayTags(viewModel: UploadPostModels.FetchTags.ViewModel) + func displayThumbnail(viewModel: UploadPostModels.FetchThumbnail.ViewModel) + func displayCurrentAddress(viewModel: UploadPostModels.FetchCurrentAddress.ViewModel) + func displayUploadButton(viewModel: UploadPostModels.CanUploadPost.ViewModel) } -final class UploadPostViewController: BaseViewController, UploadPostDisplayLogic { +final class UploadPostViewController: BaseViewController { // MARK: - UI Components @@ -24,7 +27,7 @@ final class UploadPostViewController: BaseViewController, UploadPostDisplayLogic private let contentView: UIView = UIView() - private let thumnailImageView: UIImageView = { + private let thumbnailImageView: UIImageView = { let imageView = UIImageView() imageView.contentMode = .scaleAspectFill imageView.clipsToBounds = true @@ -39,9 +42,10 @@ final class UploadPostViewController: BaseViewController, UploadPostDisplayLogic return imageLabel }() - private let titleTextField: LOTextField = { + private lazy var titleTextField: LOTextField = { let textField = LOTextField() textField.placeholder = "제목" + textField.addTarget(self, action: #selector(titleTextChanged), for: .editingChanged) return textField }() @@ -70,10 +74,9 @@ final class UploadPostViewController: BaseViewController, UploadPostDisplayLogic return imageLabel }() - private let locationLabel: UILabel = { + private let currentAddressLabel: UILabel = { let label = UILabel() label.font = .loFont(type: .body2) - label.text = "대구시 달서구 유천동" return label }() @@ -90,9 +93,11 @@ final class UploadPostViewController: BaseViewController, UploadPostDisplayLogic return textView }() - private let uploadButton: LOButton = { + private lazy var uploadButton: LOButton = { let button = LOButton(style: .basic) button.setTitle("업로드", for: .normal) + button.addTarget(self, action: #selector(uploadButtonDidTap), for: .touchUpInside) + button.isEnabled = false return button }() @@ -126,6 +131,12 @@ final class UploadPostViewController: BaseViewController, UploadPostDisplayLogic super.viewDidLoad() setConstraints() addTarget() + interactor?.fetchThumbnailImage() + interactor?.fetchCurrentAddress() + } + + override func viewWillAppear(_ animated: Bool) { + interactor?.fetchTags() } override func setConstraints() { @@ -158,18 +169,18 @@ final class UploadPostViewController: BaseViewController, UploadPostDisplayLogic } private func setContentViewSubviewsConstraints() { - contentView.addSubviews(thumnailImageView, titleImageLabel, titleTextField, tagImageLabel, tagStackView, addTagButton, - locationImageLabel, locationLabel, contentImageLabel, contentTextView) + contentView.addSubviews(thumbnailImageView, titleImageLabel, titleTextField, tagImageLabel, tagStackView, addTagButton, + locationImageLabel, currentAddressLabel, contentImageLabel, contentTextView) contentView.subviews.forEach { $0.translatesAutoresizingMaskIntoConstraints = false } NSLayoutConstraint.activate([ - thumnailImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10), - thumnailImageView.widthAnchor.constraint(equalToConstant: 156), - thumnailImageView.heightAnchor.constraint(equalToConstant: 251), - thumnailImageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + thumbnailImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10), + thumbnailImageView.widthAnchor.constraint(equalToConstant: 156), + thumbnailImageView.heightAnchor.constraint(equalToConstant: 251), + thumbnailImageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), - titleImageLabel.topAnchor.constraint(equalTo: thumnailImageView.bottomAnchor, constant: 22), + titleImageLabel.topAnchor.constraint(equalTo: thumbnailImageView.bottomAnchor, constant: 22), titleImageLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), titleImageLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), titleImageLabel.heightAnchor.constraint(equalToConstant: 22), @@ -195,13 +206,13 @@ final class UploadPostViewController: BaseViewController, UploadPostDisplayLogic locationImageLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), locationImageLabel.heightAnchor.constraint(equalToConstant: 22), - locationLabel.centerYAnchor.constraint(equalTo: locationImageLabel.centerYAnchor), - locationLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - locationLabel.leadingAnchor.constraint(equalTo: locationImageLabel.trailingAnchor), + currentAddressLabel.centerYAnchor.constraint(equalTo: locationImageLabel.centerYAnchor), + currentAddressLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + currentAddressLabel.leadingAnchor.constraint(equalTo: locationImageLabel.trailingAnchor), contentImageLabel.topAnchor.constraint(equalTo: locationImageLabel.bottomAnchor, constant: 22), contentImageLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - contentImageLabel.trailingAnchor.constraint(equalTo: locationLabel.leadingAnchor), + contentImageLabel.trailingAnchor.constraint(equalTo: currentAddressLabel.leadingAnchor), contentImageLabel.heightAnchor.constraint(equalToConstant: 22), contentTextView.topAnchor.constraint(equalTo: contentImageLabel.bottomAnchor, constant: 10), @@ -216,6 +227,10 @@ final class UploadPostViewController: BaseViewController, UploadPostDisplayLogic scrollView.addGestureRecognizer(singleTapGestureRecognizer) } + @objc private func titleTextChanged() { + interactor?.canUploadPost(request: Models.CanUploadPost.Request(title: titleTextField.text)) + } + @objc private func viewDidTap() { self.view.endEditing(true) } @@ -224,8 +239,34 @@ final class UploadPostViewController: BaseViewController, UploadPostDisplayLogic router?.routeToNext() } + @objc private func uploadButtonDidTap() { + guard let title = titleTextField.text else { return } + let request = Models.UploadPost.Request(title: title, + content: contentTextView.text, + tags: tagStackView.tags) + interactor?.uploadPost(request: request) + router?.routeToBack() + } + } -#Preview { - UploadPostViewController() +extension UploadPostViewController: UploadPostDisplayLogic { + + func displayTags(viewModel: UploadPostModels.FetchTags.ViewModel) { + tagStackView.resetTagStackView() + viewModel.tags.forEach { tagStackView.addTag($0) } + } + + func displayThumbnail(viewModel: UploadPostModels.FetchThumbnail.ViewModel) { + thumbnailImageView.image = viewModel.thumnailImage + } + + func displayCurrentAddress(viewModel: UploadPostModels.FetchCurrentAddress.ViewModel) { + currentAddressLabel.text = viewModel.fullAddress + } + + func displayUploadButton(viewModel: UploadPostModels.CanUploadPost.ViewModel) { + uploadButton.isEnabled = viewModel.canUpload + } + } diff --git a/iOS/Layover/Layover/Scenes/UploadPost/UploadPostWorker.swift b/iOS/Layover/Layover/Scenes/UploadPost/UploadPostWorker.swift index 08db923..b57c016 100644 --- a/iOS/Layover/Layover/Scenes/UploadPost/UploadPostWorker.swift +++ b/iOS/Layover/Layover/Scenes/UploadPost/UploadPostWorker.swift @@ -8,12 +8,92 @@ import UIKit -class UploadPostWorker { +import OSLog + +protocol UploadPostWorkerProtocol { + func uploadPost(with request: UploadPost) async -> Bool +} + +final class UploadPostWorker: NSObject, UploadPostWorkerProtocol { // MARK: - Properties typealias Models = UploadPostModels + private let provider: ProviderType + private let uploadPostEndPointFactory: UploadPostEndPointFactory // MARK: - Methods + init(provider: ProviderType = Provider(), + uploadPostEndPointFactory: UploadPostEndPointFactory = DefaultUploadPostEndPointFactory()) { + self.provider = provider + self.uploadPostEndPointFactory = uploadPostEndPointFactory + } + + func uploadPost(with request: UploadPost) async -> Bool { + let endPoint = uploadPostEndPointFactory.makeUploadPostEndPoint(title: request.title, + content: request.content, + latitude: request.latitude, + longitude: request.longitude, + tag: request.tag) + do { + let response = try await provider.request(with: endPoint) + guard let boardID = response.data?.id else { return false } + let fileType = request.videoURL.pathExtension + let uploadResponse = await uploadVideo(with: UploadVideoRequestDTO(boardID: boardID, filetype: fileType), + videoURL: request.videoURL) + return uploadResponse + } catch { + os_log(.error, log: .data, "Failed to fetch posts: %@", error.localizedDescription) + return false + } + } + + private func uploadVideo(with request: UploadVideoRequestDTO, videoURL: URL) async -> Bool { + let endPoint = uploadPostEndPointFactory.makeUploadVideoEndPoint(boardID: request.boardID, + fileType: request.filetype) + do { + let response = try await provider.request(with: endPoint) + guard let preSignedURLString = response.data?.preSignedURL else { return false } + _ = try await provider.upload(fromFile: videoURL, + to: preSignedURLString, + sessionTaskDelegate: self) + await MainActor.run { + NotificationCenter.default.post(name: .uploadTaskDidComplete, object: nil) + } + return true + } catch { + os_log(.error, log: .data, "Failed to upload Video: %@", error.localizedDescription) + await MainActor.run { + NotificationCenter.default.post(name: .uploadTaskDidComplete, object: nil) + } + return false + } + } + +} + +extension UploadPostWorker: URLSessionTaskDelegate { + + func urlSession(_ session: URLSession, didCreateTask task: URLSessionTask) { + DispatchQueue.main.async { + NotificationCenter.default.post(name: .uploadTaskStart, object: nil) + } + } + + func urlSession( + _ session: URLSession, + task: URLSessionTask, + didSendBodyData bytesSent: Int64, + totalBytesSent: Int64, + totalBytesExpectedToSend: Int64 + ) { + let uploadProgress: Float = Float(Double(totalBytesSent) / Double(totalBytesExpectedToSend)) + DispatchQueue.main.async { + NotificationQueue.default.enqueue(Notification(name: .progressChanged, + userInfo: ["progress": uploadProgress]), + postingStyle: .asap) + } + } + } diff --git a/iOS/Layover/Layover/Workers/Mocks/MockLoginWorker.swift b/iOS/Layover/Layover/Workers/Mocks/MockLoginWorker.swift index 9aab6ce..b94518c 100644 --- a/iOS/Layover/Layover/Workers/Mocks/MockLoginWorker.swift +++ b/iOS/Layover/Layover/Workers/Mocks/MockLoginWorker.swift @@ -49,7 +49,7 @@ final class MockLoginWorker: LoginWorkerProtocol { method: .POST, bodyParameters: bodyParameters) let response = try await provider.request(with: endPoint, authenticationIfNeeded: false, retryCount: 0) - return response.data?.isValid + return response.data?.isAlreadyExist } catch { os_log(.error, log: .data, "%@", error.localizedDescription) return nil @@ -107,7 +107,7 @@ final class MockLoginWorker: LoginWorkerProtocol { method: .POST, bodyParameters: bodyParameters) let response = try await provider.request(with: endPoint, authenticationIfNeeded: false, retryCount: 0) - return response.data?.isValid + return response.data?.isAlreadyExist } catch { os_log(.error, log: .data, "%@", error.localizedDescription) return nil diff --git a/iOS/Layover/Layover/Workers/Mocks/MockReportWorker.swift b/iOS/Layover/Layover/Workers/Mocks/MockReportWorker.swift new file mode 100644 index 0000000..801a268 --- /dev/null +++ b/iOS/Layover/Layover/Workers/Mocks/MockReportWorker.swift @@ -0,0 +1,51 @@ +// +// MockReportWorker.swift +// Layover +// +// Created by 황지웅 on 12/5/23. +// Copyright © 2023 CodeBomber. All rights reserved. +// + +import Foundation +import OSLog + +final class MockReportWorker: ReportWorkerProtocol { + + // MARK: - Properties + + private let provider: ProviderType = Provider(session: .initMockSession(), authManager: StubAuthManager()) + + // MARK: - Methods + + func reportPlaybackVideo(boardID: Int, reportContent: String) async -> Bool { + guard let mockFileLocation = Bundle.main.url(forResource: "ReportPlaybackVideo", withExtension: "json"), + let mockData = try? Data(contentsOf: mockFileLocation) + else { + os_log(.error, log: .data, "Failed to generate mock with error: %@", "Generate File Error") + return false + } + + MockURLProtocol.requestHandler = { request in + let response = HTTPURLResponse(url: request.url!, + statusCode: 200, + httpVersion: nil, + headerFields: nil) + return (response, mockData, nil) + } + + do { + let bodyParameters = ReportDTO( + memberId: nil, + boardID: 1, + reportType: "청소년에게 유해한 내용입니다.") + let endPoint = EndPoint>(path: "/report", + method: .POST, + bodyParameters: bodyParameters) + let response = try await provider.request(with: endPoint) + return true + } catch { + os_log(.error, log: .data, "%@", error.localizedDescription) + return false + } + } +}