diff --git a/Boolti/Boolti.xcodeproj/project.pbxproj b/Boolti/Boolti.xcodeproj/project.pbxproj index 039de7f1..b949291f 100644 --- a/Boolti/Boolti.xcodeproj/project.pbxproj +++ b/Boolti/Boolti.xcodeproj/project.pbxproj @@ -286,6 +286,7 @@ 8726054F2B79FD49005CD0D4 /* TicketReservationDetailRequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8726054E2B79FD49005CD0D4 /* TicketReservationDetailRequestDTO.swift */; }; 872605512B79FDE8005CD0D4 /* TicketReservationDetailResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 872605502B79FDE8005CD0D4 /* TicketReservationDetailResponseDTO.swift */; }; 872605532B79FEE2005CD0D4 /* TicketReservationDetailEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 872605522B79FEE2005CD0D4 /* TicketReservationDetailEntity.swift */; }; + 8730BD5E2CAE7E110093A00F /* CastTeamListCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8730BD5D2CAE7E110093A00F /* CastTeamListCollectionViewCell.swift */; }; 8730F0102B7B107F00D4F339 /* TicketRefundReasonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8730F00F2B7B107F00D4F339 /* TicketRefundReasonViewController.swift */; }; 8730F0122B7B108F00D4F339 /* TicketRefundReasonViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8730F0112B7B108F00D4F339 /* TicketRefundReasonViewModel.swift */; }; 8730F0142B7B10A300D4F339 /* TicketRefundReasonDIContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8730F0132B7B10A300D4F339 /* TicketRefundReasonDIContainer.swift */; }; @@ -355,6 +356,17 @@ 87A3716F2B76534B0061814E /* TicketReservationItemEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87A3716E2B76534B0061814E /* TicketReservationItemEntity.swift */; }; 87B18FE72BB15A3C005A4800 /* ReversalPolicyConfirmButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B18FE62BB15A3C005A4800 /* ReversalPolicyConfirmButton.swift */; }; 87C7594C2BA93EA40009A83E /* NotificationMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87C7594B2BA93EA40009A83E /* NotificationMessage.swift */; }; + 87C7A5EC2CADABDA0078213E /* UnderlineSegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87C7A5EB2CADABDA0078213E /* UnderlineSegmentedControl.swift */; }; + 87C7A5EE2CADB1CD0078213E /* SegmentedControlContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87C7A5ED2CADB1CD0078213E /* SegmentedControlContainerView.swift */; }; + 87CC3FEB2CB255D00056A974 /* CastTeamListHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CC3FEA2CB255D00056A974 /* CastTeamListHeaderView.swift */; }; + 87CC3FED2CB25E9A0056A974 /* CastTeamListFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CC3FEC2CB25E9A0056A974 /* CastTeamListFooterView.swift */; }; + 87CC3FEF2CB2683E0056A974 /* ConcertCastTeamListRequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CC3FEE2CB2683E0056A974 /* ConcertCastTeamListRequestDTO.swift */; }; + 87CC3FF12CB269390056A974 /* ConcertCastTeamListResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CC3FF02CB269390056A974 /* ConcertCastTeamListResponseDTO.swift */; }; + 87CC3FF32CB26AF60056A974 /* ConcertCastTeamListEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CC3FF22CB26AF60056A974 /* ConcertCastTeamListEntity.swift */; }; + 87CC3FF52CB40F2F0056A974 /* RepositoryType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CC3FF42CB40F2F0056A974 /* RepositoryType.swift */; }; + 87CC3FF72CB415BB0056A974 /* ConcertUserProfileRequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CC3FF62CB415BB0056A974 /* ConcertUserProfileRequestDTO.swift */; }; + 87CC3FF92CB43A240056A974 /* ConcertUserProfileResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CC3FF82CB43A240056A974 /* ConcertUserProfileResponseDTO.swift */; }; + 87CC3FFB2CB44B000056A974 /* EmptyCastTeamListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CC3FFA2CB44B000056A974 /* EmptyCastTeamListView.swift */; }; 87CE4F6A2B8DD9A8007A0C8F /* DeviceTokenRegisterRequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CE4F692B8DD9A8007A0C8F /* DeviceTokenRegisterRequestDTO.swift */; }; 87CE4F6C2B8DD9BB007A0C8F /* DeviceTokenRegisterResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CE4F6B2B8DD9BB007A0C8F /* DeviceTokenRegisterResponseDTO.swift */; }; 87CE4F6F2B8DDA59007A0C8F /* PushNotificationAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CE4F6E2B8DDA59007A0C8F /* PushNotificationAPI.swift */; }; @@ -664,6 +676,7 @@ 8726054E2B79FD49005CD0D4 /* TicketReservationDetailRequestDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TicketReservationDetailRequestDTO.swift; sourceTree = ""; }; 872605502B79FDE8005CD0D4 /* TicketReservationDetailResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TicketReservationDetailResponseDTO.swift; sourceTree = ""; }; 872605522B79FEE2005CD0D4 /* TicketReservationDetailEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TicketReservationDetailEntity.swift; sourceTree = ""; }; + 8730BD5D2CAE7E110093A00F /* CastTeamListCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CastTeamListCollectionViewCell.swift; sourceTree = ""; }; 8730F00F2B7B107F00D4F339 /* TicketRefundReasonViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TicketRefundReasonViewController.swift; sourceTree = ""; }; 8730F0112B7B108F00D4F339 /* TicketRefundReasonViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TicketRefundReasonViewModel.swift; sourceTree = ""; }; 8730F0132B7B10A300D4F339 /* TicketRefundReasonDIContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TicketRefundReasonDIContainer.swift; sourceTree = ""; }; @@ -733,6 +746,17 @@ 87A3716E2B76534B0061814E /* TicketReservationItemEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TicketReservationItemEntity.swift; sourceTree = ""; }; 87B18FE62BB15A3C005A4800 /* ReversalPolicyConfirmButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReversalPolicyConfirmButton.swift; sourceTree = ""; }; 87C7594B2BA93EA40009A83E /* NotificationMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationMessage.swift; sourceTree = ""; }; + 87C7A5EB2CADABDA0078213E /* UnderlineSegmentedControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnderlineSegmentedControl.swift; sourceTree = ""; }; + 87C7A5ED2CADB1CD0078213E /* SegmentedControlContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedControlContainerView.swift; sourceTree = ""; }; + 87CC3FEA2CB255D00056A974 /* CastTeamListHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CastTeamListHeaderView.swift; sourceTree = ""; }; + 87CC3FEC2CB25E9A0056A974 /* CastTeamListFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CastTeamListFooterView.swift; sourceTree = ""; }; + 87CC3FEE2CB2683E0056A974 /* ConcertCastTeamListRequestDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcertCastTeamListRequestDTO.swift; sourceTree = ""; }; + 87CC3FF02CB269390056A974 /* ConcertCastTeamListResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcertCastTeamListResponseDTO.swift; sourceTree = ""; }; + 87CC3FF22CB26AF60056A974 /* ConcertCastTeamListEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcertCastTeamListEntity.swift; sourceTree = ""; }; + 87CC3FF42CB40F2F0056A974 /* RepositoryType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryType.swift; sourceTree = ""; }; + 87CC3FF62CB415BB0056A974 /* ConcertUserProfileRequestDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcertUserProfileRequestDTO.swift; sourceTree = ""; }; + 87CC3FF82CB43A240056A974 /* ConcertUserProfileResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcertUserProfileResponseDTO.swift; sourceTree = ""; }; + 87CC3FFA2CB44B000056A974 /* EmptyCastTeamListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyCastTeamListView.swift; sourceTree = ""; }; 87CE4F692B8DD9A8007A0C8F /* DeviceTokenRegisterRequestDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceTokenRegisterRequestDTO.swift; sourceTree = ""; }; 87CE4F6B2B8DD9BB007A0C8F /* DeviceTokenRegisterResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceTokenRegisterResponseDTO.swift; sourceTree = ""; }; 87CE4F6E2B8DDA59007A0C8F /* PushNotificationAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationAPI.swift; sourceTree = ""; }; @@ -1136,6 +1160,7 @@ 870099B62B77489A001779FB /* ReservationsRepository.swift */, 87CE4F702B8DF7CE007A0C8F /* PushNotificationRepository.swift */, 846F5F3D2B7BBF60000F86AE /* QRRepository.swift */, + 87CC3FF42CB40F2F0056A974 /* RepositoryType.swift */, ); path = Repositories; sourceTree = ""; @@ -1674,6 +1699,7 @@ children = ( 840B39582B7670FC00E7F8C8 /* ConcertEntity.swift */, 842183722B71D75D00A52E1C /* ConcertDetailEntity.swift */, + 87CC3FF22CB26AF60056A974 /* ConcertCastTeamListEntity.swift */, ); path = Concert; sourceTree = ""; @@ -1692,6 +1718,8 @@ children = ( 840B39602B78CE6300E7F8C8 /* ConcertListRequestDTO.swift */, 84DC6F662B728858001F9576 /* ConcertDetailRequestDTO.swift */, + 87CC3FEE2CB2683E0056A974 /* ConcertCastTeamListRequestDTO.swift */, + 87CC3FF62CB415BB0056A974 /* ConcertUserProfileRequestDTO.swift */, ); path = Request; sourceTree = ""; @@ -1700,6 +1728,8 @@ isa = PBXGroup; children = ( 840B39622B78CEF700E7F8C8 /* ConcertListResponseDTO.swift */, + 87CC3FF82CB43A240056A974 /* ConcertUserProfileResponseDTO.swift */, + 87CC3FF02CB269390056A974 /* ConcertCastTeamListResponseDTO.swift */, 84C6E0022B7320280023BAE6 /* ConcertDetailResponseDTO.swift */, ); path = Response; @@ -1797,6 +1827,12 @@ 84886E8D2B70A651005D2329 /* PlaceInfoView.swift */, 84886E8F2B70A7D4005D2329 /* ContentInfoView.swift */, 84886E912B70AA85005D2329 /* OrganizerInfoView.swift */, + 87C7A5EB2CADABDA0078213E /* UnderlineSegmentedControl.swift */, + 87C7A5ED2CADB1CD0078213E /* SegmentedControlContainerView.swift */, + 8730BD5D2CAE7E110093A00F /* CastTeamListCollectionViewCell.swift */, + 87CC3FEA2CB255D00056A974 /* CastTeamListHeaderView.swift */, + 87CC3FEC2CB25E9A0056A974 /* CastTeamListFooterView.swift */, + 87CC3FFA2CB44B000056A974 /* EmptyCastTeamListView.swift */, ); path = Views; sourceTree = ""; @@ -2522,6 +2558,7 @@ 84E5124D2B6E7736002658D1 /* ConcertDetailViewController.swift in Sources */, 221393632C89CC7E00459A20 /* EditLinkView.swift in Sources */, 84FBBDF32B673877009462E9 /* ConcertInfoView.swift in Sources */, + 87C7A5EE2CADB1CD0078213E /* SegmentedControlContainerView.swift in Sources */, 84E5124B2B6E71FD002658D1 /* ConcertDetailDIContainer.swift in Sources */, 870099AE2B77434A001779FB /* TicketDetailRequestDTO.swift in Sources */, 223698FB2C8B3BA3002C8081 /* UploadProfileImageRequestDTO.swift in Sources */, @@ -2541,6 +2578,7 @@ 84886E8C2B70A45F005D2329 /* DatetimeInfoView.swift in Sources */, 84896A512B6A827E00CB3E33 /* ReservedTicketView.swift in Sources */, 84886E922B70AA85005D2329 /* OrganizerInfoView.swift in Sources */, + 87CC3FF12CB269390056A974 /* ConcertCastTeamListResponseDTO.swift in Sources */, 8757BAAC2B5FDF69008503B5 /* AppleOAuth.swift in Sources */, 22CDB2C62BA2635E00D2077D /* BusinessInfoDIContainer.swift in Sources */, 845D88592B877B0E00179F60 /* ResignRequestDTO.swift in Sources */, @@ -2571,6 +2609,7 @@ 226D42BF2B9EBA3E00680198 /* BooltiBusinessInfoView.swift in Sources */, 84781BCD2B5BDAFC00D37921 /* AppDelegate.swift in Sources */, 87D2FAB42B6E977C0027FBE1 /* TicketDetailDIContainer.swift in Sources */, + 87CC3FF72CB415BB0056A974 /* ConcertUserProfileRequestDTO.swift in Sources */, 8730F0122B7B108F00D4F339 /* TicketRefundReasonViewModel.swift in Sources */, 84781BF02B5BE3C000D37921 /* NetworkProviderType.swift in Sources */, 8757BAB82B600B1B008503B5 /* SignUpResponseDTO.swift in Sources */, @@ -2598,6 +2637,7 @@ 8724C4BF2B6221A500484BFC /* UIView+.swift in Sources */, 8730F0102B7B107F00D4F339 /* TicketRefundReasonViewController.swift in Sources */, 8757BAA82B5FDF0F008503B5 /* OAuth.swift in Sources */, + 87C7A5EC2CADABDA0078213E /* UnderlineSegmentedControl.swift in Sources */, 2250FABF2C020BC400CCF487 /* ContactViewController.swift in Sources */, 84FBB2CC2B80DC6B001F4211 /* MypageProfileView.swift in Sources */, 87681BDD2B7A695B008BF59F /* TicketEntryCodeRequestDTO.swift in Sources */, @@ -2647,6 +2687,8 @@ 8757BAA22B5FDE0D008503B5 /* LoginViewModel.swift in Sources */, 87F7DF492B8275D30068A6C9 /* EntryCodeErrorEntity.swift in Sources */, 845D88552B87705700179F60 /* ResignReasonViewController.swift in Sources */, + 87CC3FF32CB26AF60056A974 /* ConcertCastTeamListEntity.swift in Sources */, + 87CC3FEF2CB2683E0056A974 /* ConcertCastTeamListRequestDTO.swift in Sources */, 879E923A2BFB5DC200EC7ED1 /* GradientBackgroundView.swift in Sources */, 840B395F2B76966E00E7F8C8 /* ConcertListMainTitleCollectionViewCell.swift in Sources */, 87F63D752C3F85DB001D7A56 /* GiftReservationDetailEntity.swift in Sources */, @@ -2662,6 +2704,7 @@ 84A0248C2B7B996F0095A56E /* QRScannerDIContainer.swift in Sources */, 84EF916A2B8A435C0073C89A /* PosterCollectionViewCell.swift in Sources */, 84781CBE2B5BF95100D37921 /* MyPageDIContainer.swift in Sources */, + 87CC3FED2CB25E9A0056A974 /* CastTeamListFooterView.swift in Sources */, 87F63D782C3F8A8C001D7A56 /* ReservationDetailDTOProtocol.swift in Sources */, 84781BCF2B5BDAFC00D37921 /* SceneDelegate.swift in Sources */, 842C1C642B7DA8A700EFE5A0 /* ResignInfoDIContainer.swift in Sources */, @@ -2676,6 +2719,7 @@ 842183732B71D75D00A52E1C /* ConcertDetailEntity.swift in Sources */, 84C6E0032B7320280023BAE6 /* ConcertDetailResponseDTO.swift in Sources */, 870099B32B7744B5001779FB /* TicketReservationItemResponseDTO.swift in Sources */, + 87CC3FF52CB40F2F0056A974 /* RepositoryType.swift in Sources */, 8769BF9A2B625DC900DA9A67 /* TermsAgreementDIContainer.swift in Sources */, 22CF3A382BB806560094711C /* MiddlemanPolicyView.swift in Sources */, 87F7DF472B8275980068A6C9 /* EntryCodeErrorResponseDTO.swift in Sources */, @@ -2685,6 +2729,7 @@ 8730F02E2B7BAA2B00D4F339 /* RefundAccountNumberView.swift in Sources */, 87F63D732C3F8504001D7A56 /* GiftReservationDetailResponseDTO.swift in Sources */, 8746AD202B6932B30037A1B1 /* ConcertEnterView.swift in Sources */, + 8730BD5E2CAE7E110093A00F /* CastTeamListCollectionViewCell.swift in Sources */, 8753CA7B2B7225AA002871C7 /* TicketDetailResponseDTO.swift in Sources */, 87F63D6B2C3F7E55001D7A56 /* GiftCompletionViewController.swift in Sources */, 87D2FAC02B7003970027FBE1 /* ReversalPolicyView.swift in Sources */, @@ -2730,6 +2775,7 @@ 870099BE2B7767E7001779FB /* EmptyReservationsView.swift in Sources */, 87F63D692C3F7E46001D7A56 /* GiftCompletionDIContainer.swift in Sources */, 8710D9542B74FAB500309FBF /* LogoutViewController.swift in Sources */, + 87CC3FEB2CB255D00056A974 /* CastTeamListHeaderView.swift in Sources */, 87CE4F6F2B8DDA59007A0C8F /* PushNotificationAPI.swift in Sources */, 872605462B7932BD005CD0D4 /* TicketReservationDetailViewModel.swift in Sources */, 84781CC52B5BF9DE00D37921 /* TicketListViewController.swift in Sources */, @@ -2773,6 +2819,7 @@ 8757BAB02B60069B008503B5 /* OAuthResponse.swift in Sources */, 8710D9562B74FAC100309FBF /* LogoutViewModel.swift in Sources */, 8753CA6C2B70E17B002871C7 /* TicketEntryCodeViewController.swift in Sources */, + 87CC3FFB2CB44B000056A974 /* EmptyCastTeamListView.swift in Sources */, 845D88532B87704A00179F60 /* ResignReasonDIContainer.swift in Sources */, 84A024882B7B8AA70095A56E /* QRScannerEntity.swift in Sources */, 872605492B793A3F005CD0D4 /* ReservationCollapsableStackView.swift in Sources */, @@ -2807,6 +2854,7 @@ 22DEF7852C28397600EA492A /* GiftingDetailViewModel.swift in Sources */, 84A7134C2B7C89A1000BABCB /* QRExpandViewModel.swift in Sources */, 2233EAAB2BD7A1E200A315BF /* OrderPaymentResponseDTO.swift in Sources */, + 87CC3FF92CB43A240056A974 /* ConcertUserProfileResponseDTO.swift in Sources */, 871996F52C2C2B85003F3845 /* SocialServiceButton.swift in Sources */, 877563DA2B6E0CDA001504FE /* TicketListFooterView.swift in Sources */, 878AF4692B60EC3A00C8838C /* ASAuthorizationController+Rx.swift in Sources */, @@ -2991,7 +3039,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.6.3; + MARKETING_VERSION = 1.7.0; OTHER_LDFLAGS = "-ObjC"; PRODUCT_BUNDLE_IDENTIFIER = com.nexters.boolti; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -3033,7 +3081,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.6.3; + MARKETING_VERSION = 1.7.0; OTHER_LDFLAGS = "-ObjC"; PRODUCT_BUNDLE_IDENTIFIER = com.nexters.boolti; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Boolti/Boolti/Sources/Entities/Concert/ConcertCastTeamListEntity.swift b/Boolti/Boolti/Sources/Entities/Concert/ConcertCastTeamListEntity.swift new file mode 100644 index 00000000..9bded28d --- /dev/null +++ b/Boolti/Boolti/Sources/Entities/Concert/ConcertCastTeamListEntity.swift @@ -0,0 +1,26 @@ +// +// ConcertCastTeamListEntity.swift +// Boolti +// +// Created by Miro on 10/6/24. +// + +import Foundation + +struct ConcertCastTeamListEntity { + let id: Int + let name: String + let members: [TeamMember] + let createdAt: String + let modifiedAt: String +} + +struct TeamMember { + let id: Int + let code: String + let imagePath: String + let nickName: String + let roleName: String + let createdAt: String + let modifiedAt: String +} diff --git a/Boolti/Boolti/Sources/Network/APIs/ConcertAPI.swift b/Boolti/Boolti/Sources/Network/APIs/ConcertAPI.swift index fe70dcd0..7bc8ceed 100644 --- a/Boolti/Boolti/Sources/Network/APIs/ConcertAPI.swift +++ b/Boolti/Boolti/Sources/Network/APIs/ConcertAPI.swift @@ -13,6 +13,8 @@ enum ConcertAPI { case list(requesDTO: ConcertListRequestDTO) case detail(requestDTO: ConcertDetailRequestDTO) + case castTeamList(requestDTO: ConcertCastTeamListRequestDTO) + case userProfile(requsetDTO: ConcertUserProfileRequestDTO) } extension ConcertAPI: ServiceAPI { @@ -23,6 +25,10 @@ extension ConcertAPI: ServiceAPI { return "/papi/v1/shows/search" case .detail(let DTO): return "/papi/v1/show/\(DTO.id)" + case .castTeamList(let DTO): + return "/papi/v1/shows/\(DTO.showID)/cast-teams" + case .userProfile(requsetDTO: let DTO): + return "/papi/v1/users/\(DTO.userCode)" } } diff --git a/Boolti/Boolti/Sources/Network/DTO/Auth/Response/UserResponseDTO.swift b/Boolti/Boolti/Sources/Network/DTO/Auth/Response/UserResponseDTO.swift index 403637a0..44de7db6 100644 --- a/Boolti/Boolti/Sources/Network/DTO/Auth/Response/UserResponseDTO.swift +++ b/Boolti/Boolti/Sources/Network/DTO/Auth/Response/UserResponseDTO.swift @@ -7,7 +7,8 @@ import Foundation -struct UserResponseDTO: Decodable { +// TODO: DTO와 Entity 분리하기! +struct UserResponseDTO: UserProfileResponseDTO { let id: Int let nickname: String? @@ -16,7 +17,6 @@ struct UserResponseDTO: Decodable { let imgPath: String? let introduction: String? let link: [LinkEntity]? - } struct LinkEntity: Codable, Equatable { diff --git a/Boolti/Boolti/Sources/Network/DTO/Concert/Request/ConcertCastTeamListRequestDTO.swift b/Boolti/Boolti/Sources/Network/DTO/Concert/Request/ConcertCastTeamListRequestDTO.swift new file mode 100644 index 00000000..63de8fb9 --- /dev/null +++ b/Boolti/Boolti/Sources/Network/DTO/Concert/Request/ConcertCastTeamListRequestDTO.swift @@ -0,0 +1,13 @@ +// +// ConcertCastTeamListRequestDTO.swift +// Boolti +// +// Created by Miro on 10/6/24. +// + +import Foundation + +struct ConcertCastTeamListRequestDTO: Encodable { + + let showID: Int +} diff --git a/Boolti/Boolti/Sources/Network/DTO/Concert/Request/ConcertUserProfileRequestDTO.swift b/Boolti/Boolti/Sources/Network/DTO/Concert/Request/ConcertUserProfileRequestDTO.swift new file mode 100644 index 00000000..f11597f2 --- /dev/null +++ b/Boolti/Boolti/Sources/Network/DTO/Concert/Request/ConcertUserProfileRequestDTO.swift @@ -0,0 +1,13 @@ +// +// ConcertUserProfileRequestDTO.swift +// Boolti +// +// Created by Miro on 10/7/24. +// + +import Foundation + +struct ConcertUserProfileRequestDTO: Encodable { + + let userCode: String +} diff --git a/Boolti/Boolti/Sources/Network/DTO/Concert/Response/ConcertCastTeamListResponseDTO.swift b/Boolti/Boolti/Sources/Network/DTO/Concert/Response/ConcertCastTeamListResponseDTO.swift new file mode 100644 index 00000000..ee4aa177 --- /dev/null +++ b/Boolti/Boolti/Sources/Network/DTO/Concert/Response/ConcertCastTeamListResponseDTO.swift @@ -0,0 +1,47 @@ +// +// ConcertCastTeamListResponseDTO.swift +// Boolti +// +// Created by Miro on 10/6/24. +// + +import Foundation + +struct ConcertCastTeamListResponseDTO: Decodable { + let id: Int + let name: String + let members: [TeamMemberDTO] + let createdAt: String + let modifiedAt: String + + func convertToTeamListEntity() -> ConcertCastTeamListEntity { + let members = self.members.map { DTO in + return TeamMember( + id: DTO.id, + code: DTO.userCode, + imagePath: DTO.userImgPath, + nickName: DTO.userNickname, + roleName: DTO.roleName, + createdAt: DTO.createdAt, + modifiedAt: DTO.modifiedAt + ) + } + return ConcertCastTeamListEntity( + id: self.id, + name: self.name, + members: members, + createdAt: self.createdAt, + modifiedAt: self.modifiedAt + ) + } +} + +struct TeamMemberDTO: Codable { + let id: Int + let userCode: String + let userImgPath: String + let userNickname: String + let roleName: String + let createdAt: String + let modifiedAt: String +} diff --git a/Boolti/Boolti/Sources/Network/DTO/Concert/Response/ConcertUserProfileResponseDTO.swift b/Boolti/Boolti/Sources/Network/DTO/Concert/Response/ConcertUserProfileResponseDTO.swift new file mode 100644 index 00000000..c0dec79e --- /dev/null +++ b/Boolti/Boolti/Sources/Network/DTO/Concert/Response/ConcertUserProfileResponseDTO.swift @@ -0,0 +1,25 @@ +// +// ConcertUserProfileResponseDTO.swift +// Boolti +// +// Created by Miro on 10/8/24. +// + +import Foundation + +protocol UserProfileResponseDTO: Decodable { + var nickname: String? { get } + var userCode: String? { get } + var imgPath: String? { get } + var introduction: String? { get } + var link: [LinkEntity]? { get } +} + +struct ConcertUserProfileResponseDTO: UserProfileResponseDTO { + + let nickname: String? + let userCode: String? + let imgPath: String? + let introduction: String? + let link: [LinkEntity]? +} diff --git a/Boolti/Boolti/Sources/Network/Foundation/NetworkProvider.swift b/Boolti/Boolti/Sources/Network/Foundation/NetworkProvider.swift index d1f851b9..a9df5d5a 100644 --- a/Boolti/Boolti/Sources/Network/Foundation/NetworkProvider.swift +++ b/Boolti/Boolti/Sources/Network/Foundation/NetworkProvider.swift @@ -26,7 +26,6 @@ final class NetworkProvider: NetworkProviderType { let baseURL = "\(api.baseURL)" let requestString = "\(api.path)" let endpoint = MultiTarget.target(api) - return provider.rx.request(endpoint) .do( onSuccess: { response in diff --git a/Boolti/Boolti/Sources/Network/Repositories/Auth/AuthRepository.swift b/Boolti/Boolti/Sources/Network/Repositories/Auth/AuthRepository.swift index 379b503a..d443381b 100644 --- a/Boolti/Boolti/Sources/Network/Repositories/Auth/AuthRepository.swift +++ b/Boolti/Boolti/Sources/Network/Repositories/Auth/AuthRepository.swift @@ -12,7 +12,8 @@ import KakaoSDKUser import RxKakaoSDKUser import SwiftJWT -protocol AuthRepositoryType { +// TODO: Auth와 유저의 정보 관리 API 나누기 +protocol AuthRepositoryType: RepositoryType { var networkService: NetworkProviderType { get } func fetchTokens() -> (String, String) diff --git a/Boolti/Boolti/Sources/Network/Repositories/ConcertRepository.swift b/Boolti/Boolti/Sources/Network/Repositories/ConcertRepository.swift index 6bfee32b..2846345d 100644 --- a/Boolti/Boolti/Sources/Network/Repositories/ConcertRepository.swift +++ b/Boolti/Boolti/Sources/Network/Repositories/ConcertRepository.swift @@ -9,11 +9,12 @@ import Foundation import RxSwift -protocol ConcertRepositoryType { +protocol ConcertRepositoryType: RepositoryType { var networkService: NetworkProviderType { get } func concertList(concertName: String?) -> Single<[ConcertEntity]> func concertDetail(concertId: Int) -> Single func salesTicket(concertId: Int) -> Single<[SelectedTicketEntity]> + func castTeamList(concertId: Int) -> Single<[ConcertCastTeamListEntity]> } final class ConcertRepository: ConcertRepositoryType { @@ -51,4 +52,22 @@ final class ConcertRepository: ConcertRepositoryType { .map { $0.convertToSalesTicketEntities() } } + func castTeamList(concertId: Int) -> Single<[ConcertCastTeamListEntity]> { + let castTeamListRequestDTO = ConcertCastTeamListRequestDTO(showID: concertId) + let api = ConcertAPI.castTeamList(requestDTO: castTeamListRequestDTO) + + return networkService.request(api) + .map([ConcertCastTeamListResponseDTO].self) + .map { return $0.map { dto in + dto.convertToTeamListEntity() + } } + } + + func userProfile(userCode: String) -> Single { + let concertUserProfileRequestDTO = ConcertUserProfileRequestDTO(userCode: userCode) + let api = ConcertAPI.userProfile(requsetDTO: concertUserProfileRequestDTO) + + return networkService.request(api) + .map(ConcertUserProfileResponseDTO.self) + } } diff --git a/Boolti/Boolti/Sources/Network/Repositories/GiftingRepository.swift b/Boolti/Boolti/Sources/Network/Repositories/GiftingRepository.swift index 7d4e966b..7a51d980 100644 --- a/Boolti/Boolti/Sources/Network/Repositories/GiftingRepository.swift +++ b/Boolti/Boolti/Sources/Network/Repositories/GiftingRepository.swift @@ -9,7 +9,7 @@ import Foundation import RxSwift -protocol GiftingRepositoryType { +protocol GiftingRepositoryType: RepositoryType { var networkService: NetworkProviderType { get } func savePaymentInfo(concertId: Int, selectedTicket: SelectedTicketEntity) -> Single diff --git a/Boolti/Boolti/Sources/Network/Repositories/PushNotificationRepository.swift b/Boolti/Boolti/Sources/Network/Repositories/PushNotificationRepository.swift index 069fe1c2..da354f0b 100644 --- a/Boolti/Boolti/Sources/Network/Repositories/PushNotificationRepository.swift +++ b/Boolti/Boolti/Sources/Network/Repositories/PushNotificationRepository.swift @@ -10,7 +10,7 @@ import Foundation import FirebaseMessaging import RxSwift -protocol PushNotificationRepositoryType { +protocol PushNotificationRepositoryType: RepositoryType { var networkService: NetworkProviderType { get } func registerDeviceToken() diff --git a/Boolti/Boolti/Sources/Network/Repositories/QRRepository.swift b/Boolti/Boolti/Sources/Network/Repositories/QRRepository.swift index b1dd527c..08abdd23 100644 --- a/Boolti/Boolti/Sources/Network/Repositories/QRRepository.swift +++ b/Boolti/Boolti/Sources/Network/Repositories/QRRepository.swift @@ -9,7 +9,7 @@ import Foundation import RxSwift -protocol QRRepositoryType { +protocol QRRepositoryType: RepositoryType { var networkService: NetworkProviderType { get } func scannerList() -> Single<[QRScannerEntity]> diff --git a/Boolti/Boolti/Sources/Network/Repositories/RepositoryType.swift b/Boolti/Boolti/Sources/Network/Repositories/RepositoryType.swift new file mode 100644 index 00000000..6439b142 --- /dev/null +++ b/Boolti/Boolti/Sources/Network/Repositories/RepositoryType.swift @@ -0,0 +1,13 @@ +// +// RepositoryType.swift +// Boolti +// +// Created by Miro on 10/7/24. +// + +import Foundation + +protocol RepositoryType { + + var networkService: NetworkProviderType { get } +} diff --git a/Boolti/Boolti/Sources/Network/Repositories/ReservationsRepository.swift b/Boolti/Boolti/Sources/Network/Repositories/ReservationsRepository.swift index 04853c84..2211d777 100644 --- a/Boolti/Boolti/Sources/Network/Repositories/ReservationsRepository.swift +++ b/Boolti/Boolti/Sources/Network/Repositories/ReservationsRepository.swift @@ -9,7 +9,7 @@ import Foundation import RxSwift -protocol ReservationRepositoryType { +protocol ReservationRepositoryType: RepositoryType { var networkService: NetworkProviderType { get } func ticketReservations() -> Single<[TicketReservationItemEntity]> diff --git a/Boolti/Boolti/Sources/Network/Repositories/TicketingRepository.swift b/Boolti/Boolti/Sources/Network/Repositories/TicketingRepository.swift index 22c1af2f..6960743c 100644 --- a/Boolti/Boolti/Sources/Network/Repositories/TicketingRepository.swift +++ b/Boolti/Boolti/Sources/Network/Repositories/TicketingRepository.swift @@ -9,7 +9,7 @@ import Foundation import RxSwift -protocol TicketingRepositoryType { +protocol TicketingRepositoryType: RepositoryType { var networkService: NetworkProviderType { get } func checkInvitationCode(concertId: Int, ticketId: Int, diff --git a/Boolti/Boolti/Sources/UILayer/Concert/ConcertDetail/ConcertDetailDIContainer.swift b/Boolti/Boolti/Sources/UILayer/Concert/ConcertDetail/ConcertDetailDIContainer.swift index d96bb882..da0c247c 100644 --- a/Boolti/Boolti/Sources/UILayer/Concert/ConcertDetail/ConcertDetailDIContainer.swift +++ b/Boolti/Boolti/Sources/UILayer/Concert/ConcertDetail/ConcertDetailDIContainer.swift @@ -13,7 +13,8 @@ final class ConcertDetailDIContainer { typealias Content = String typealias ConcertId = Int typealias PhoneNumber = String - + typealias UserCode = String + private let authRepository: AuthRepository private let concertRepository: ConcertRepository @@ -72,6 +73,13 @@ final class ConcertDetailDIContainer { return viewController } + let profileViewControllerFactory: (UserCode) -> ProfileViewController = { (userCode) in + let DIContainer = self.createProfileDIContainer() + + let viewController = DIContainer.createProfileViewController(userCode: userCode) + return viewController + } + let viewController = ConcertDetailViewController( viewModel: viewModel, loginViewControllerFactory: loginViewControllerFactory, @@ -79,7 +87,8 @@ final class ConcertDetailDIContainer { concertContentExpandViewControllerFactory: concertContentExpandViewControllerFactory, reportViewControllerFactory: reportViewControllerFactory, ticketSelectionViewControllerFactory: ticketSelectionViewControllerFactory, - contactViewControllerFactory: contactViewControllerFactory + contactViewControllerFactory: contactViewControllerFactory, + profileViewControllerFactory: profileViewControllerFactory ) return viewController @@ -118,4 +127,8 @@ final class ConcertDetailDIContainer { concertId: concertId) } + private func createProfileDIContainer() -> ProfileDIContainer { + return ProfileDIContainer(repository: self.concertRepository) + } + } diff --git a/Boolti/Boolti/Sources/UILayer/Concert/ConcertDetail/ConcertDetailViewController.swift b/Boolti/Boolti/Sources/UILayer/Concert/ConcertDetail/ConcertDetailViewController.swift index ba66e529..6fb26f92 100644 --- a/Boolti/Boolti/Sources/UILayer/Concert/ConcertDetail/ConcertDetailViewController.swift +++ b/Boolti/Boolti/Sources/UILayer/Concert/ConcertDetail/ConcertDetailViewController.swift @@ -21,7 +21,8 @@ final class ConcertDetailViewController: BooltiViewController { typealias Posters = [ConcertDetailEntity.Poster] typealias ConcertId = Int typealias PhoneNumber = String - + typealias UserCode = String + private let viewModel: ConcertDetailViewModel private let disposeBag = DisposeBag() @@ -31,7 +32,8 @@ final class ConcertDetailViewController: BooltiViewController { private let reportViewControllerFactory: () -> ReportViewController private let ticketSelectionViewControllerFactory: (ConcertId, TicketingType) -> TicketSelectionViewController private let contactViewControllerFactory: (ContactType, PhoneNumber) -> ContactViewController - + private let profileViewControllerFactory: (UserCode) -> ProfileViewController + // MARK: UI Component private let navigationBar = BooltiNavigationBar(type: .concertDetail) @@ -58,29 +60,73 @@ final class ConcertDetailViewController: BooltiViewController { let stackView = UIStackView() stackView.axis = .vertical - stackView.addArrangedSubviews([self.concertPosterView, - self.ticketingPeriodView, - self.datetimeInfoView, - self.placeInfoView, - self.contentInfoView, - self.organizerInfoView]) - + stackView.addArrangedSubviews([ + self.concertPosterView, + self.ticketingPeriodView, + self.segmentedControlContainerView, + self.concertDetailStackView, + self.castTeamListCollectionView + ]) + stackView.setCustomSpacing(40, after: self.concertPosterView) + stackView.setCustomSpacing(40, after: self.ticketingPeriodView) return stackView }() - + + private lazy var concertDetailStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.isHidden = false + + stackView.addArrangedSubviews([ + self.datetimeInfoView, + self.placeInfoView, + self.contentInfoView, + self.organizerInfoView + ]) + return stackView + }() + + private lazy var castTeamListCollectionView: UICollectionView = { + let flowLayout = UICollectionViewFlowLayout() + flowLayout.scrollDirection = .vertical + flowLayout.minimumLineSpacing = 20 + flowLayout.minimumInteritemSpacing = 0 + + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) + collectionView.dataSource = self + collectionView.delegate = self + collectionView.backgroundColor = .grey95 + collectionView.isScrollEnabled = false + collectionView.isHidden = true + + collectionView.register( + CastTeamListCollectionViewCell.self, + forCellWithReuseIdentifier: CastTeamListCollectionViewCell.className + ) + + collectionView.register(CastTeamListHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: CastTeamListHeaderView.className) + collectionView.register(CastTeamListFooterView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: CastTeamListFooterView.className) + + return collectionView + }() + private let concertPosterView = ConcertPosterView() private let ticketingPeriodView = TicketingPeriodView() private let datetimeInfoView = DatetimeInfoView() - + + private let segmentedControlContainerView = SegmentedControlContainerView(items: ["공연 정보", "출연진"]) + private let placeInfoView = PlaceInfoView() private let contentInfoView = ContentInfoView() private let organizerInfoView = OrganizerInfoView(horizontalInset: 20, verticalInset: 32, height: 170) + private let emptyCastView = EmptyCastTeamListView() + private lazy var buttonBackgroundView: UIView = { let view = UIView() @@ -118,7 +164,9 @@ final class ConcertDetailViewController: BooltiViewController { concertContentExpandViewControllerFactory: @escaping (Content) -> ConcertContentExpandViewController, reportViewControllerFactory: @escaping () -> ReportViewController, ticketSelectionViewControllerFactory: @escaping (ConcertId, TicketingType) -> TicketSelectionViewController, - contactViewControllerFactory: @escaping (ContactType, PhoneNumber) -> ContactViewController) { + contactViewControllerFactory: @escaping (ContactType, PhoneNumber) -> ContactViewController, + profileViewControllerFactory: @escaping (UserCode) -> ProfileViewController + ) { self.viewModel = viewModel self.loginViewControllerFactory = loginViewControllerFactory self.posterExpandViewControllerFactory = posterExpandViewControllerFactory @@ -126,7 +174,8 @@ final class ConcertDetailViewController: BooltiViewController { self.reportViewControllerFactory = reportViewControllerFactory self.ticketSelectionViewControllerFactory = ticketSelectionViewControllerFactory self.contactViewControllerFactory = contactViewControllerFactory - + self.profileViewControllerFactory = profileViewControllerFactory + super.init() } @@ -152,6 +201,12 @@ final class ConcertDetailViewController: BooltiViewController { self.tabBarController?.tabBar.isHidden = true self.dimmedBackgroundView.isHidden = true self.viewModel.fetchConcertDetail() + self.viewModel.fetchCastTeamList() + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + self.updateCollectionViewHeight() } } @@ -227,6 +282,14 @@ extension ConcertDetailViewController { } } .disposed(by: self.disposeBag) + + self.viewModel.output.teamListEntities + .asDriver() + .drive(with: self) { owner, entity in + owner.configureEmptyCastTeamListView() + owner.configureCollectionView() + } + .disposed(by: self.disposeBag) } private func bindUIComponents() { @@ -293,17 +356,14 @@ extension ConcertDetailViewController { guard let concertID = owner.viewModel.output.concertDetail.value?.id else { return } let image = KFImage(URL(string: posterURL)) - guard let longDeepLinkURL = owner.concerDetailDeepLinkURL(concertID) else { return } - - DynamicLinkComponents.shortenURL(longDeepLinkURL, options: nil) { url, warnings, error in - guard let url = url, error == nil else { return } - let activityViewController = UIActivityViewController( - activityItems: [url, image], - applicationActivities: nil - ) - activityViewController.popoverPresentationController?.sourceView = owner.view - owner.present(activityViewController, animated: true, completion: nil) - } + + guard let link = URL(string: "https://preview.boolti.in/show/\(concertID)") else { return } + let activityViewController = UIActivityViewController( + activityItems: [link, image], + applicationActivities: nil + ) + activityViewController.popoverPresentationController?.sourceView = owner.view + owner.present(activityViewController, animated: true, completion: nil) } .disposed(by: self.disposeBag) self.navigationBar.didMoreButtonTap() @@ -340,14 +400,15 @@ extension ConcertDetailViewController { .disposed(by: self.disposeBag) } - private func concerDetailDeepLinkURL(_ concertID: Int) -> URL? { - guard let link = URL(string: "https://preview.boolti.in/show/\(concertID)") else { return nil } - let dynamicLinksDomainURIPrefix = AppInfo.booltiDeepLinkPrefix - guard let linkBuilder = DynamicLinkComponents( - link: link, - domainURIPrefix: dynamicLinksDomainURIPrefix - ) else { return nil } - return linkBuilder.url + private func configureCollectionView() { + self.castTeamListCollectionView.reloadData() + self.castTeamListCollectionView.layoutIfNeeded() + self.updateCollectionViewHeight() + } + + private func configureEmptyCastTeamListView() { + self.stackView.addArrangedSubview(self.emptyCastView) + self.emptyCastView.isHidden = true } } @@ -376,6 +437,7 @@ extension ConcertDetailViewController { self.dimmedBackgroundView]) self.view.backgroundColor = .grey95 + self.configureSegmentedControl() } private func configureConstraints() { @@ -398,7 +460,7 @@ extension ConcertDetailViewController { make.width.equalToSuperview() make.edges.equalTo(self.scrollView) } - + self.buttonBackgroundView.snp.makeConstraints { make in make.bottom.equalTo(self.ticketingButton.snp.top) make.horizontalEdges.equalToSuperview() @@ -409,5 +471,175 @@ extension ConcertDetailViewController { make.bottom.equalTo(self.view.safeAreaLayoutGuide).offset(-8) make.horizontalEdges.equalToSuperview().inset(20) } + + self.castTeamListCollectionView.snp.makeConstraints { make in + make.height.equalTo(100) // 초기 설정 + make.horizontalEdges.equalToSuperview() + } + } + + private func configureSegmentedControl() { + self.segmentedControlContainerView.segmentedControl.setTitleTextAttributes( + [ + NSAttributedString.Key.foregroundColor: UIColor.grey70, + .font: UIFont.subhead1 + ], + for: .normal + ) + self.segmentedControlContainerView.segmentedControl.setTitleTextAttributes( + [ + NSAttributedString.Key.foregroundColor: UIColor.grey10, + .font: UIFont.subhead1 + ], + for: .selected + ) + self.segmentedControlContainerView.segmentedControl.selectedSegmentIndex = 0 + + self.segmentedControlContainerView.segmentedControl.rx.selectedSegmentIndex + .asDriver() + .distinctUntilChanged() + .drive(with: self, onNext: { owner, index in + if index == 1 { + owner.configureCastView() + } else { + owner.configureConcertDetailView() + } + }) + .disposed(by: self.disposeBag) + } + + private func configureCastView() { + self.concertDetailStackView.isHidden = true + guard let listEntities = self.viewModel.output.teamListEntities.value else { return } + if listEntities.isEmpty { + self.emptyCastView.isHidden = false + self.castTeamListCollectionView.isHidden = true + } else { + self.castTeamListCollectionView.isHidden = false + self.emptyCastView.isHidden = true + } } + + private func configureConcertDetailView() { + self.emptyCastView.isHidden = true + self.castTeamListCollectionView.isHidden = true + self.concertDetailStackView.isHidden = false + } +} + +// MARK: CollectionView + +extension ConcertDetailViewController { + + private func updateCollectionViewHeight() { + self.castTeamListCollectionView.snp.updateConstraints { make in + make.height.equalTo(self.castTeamListCollectionView.contentSize.height) + } + } +} + +// MARK: CollectionViewDatasource + +extension ConcertDetailViewController: UICollectionViewDataSource { + + func numberOfSections(in collectionView: UICollectionView) -> Int { + guard let listEntities = self.viewModel.output.teamListEntities.value else { return 0 } + return listEntities.count + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + guard let listEntities = self.viewModel.output.teamListEntities.value else { return 0 } + let members = listEntities[section].members + + return members.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: CastTeamListCollectionViewCell.className, + for: indexPath + ) as? CastTeamListCollectionViewCell else { + fatalError("Failed to load cell!") + } + guard let listEntities = self.viewModel.output.teamListEntities.value else { return UICollectionViewCell() } + + let entity = listEntities[indexPath.section].members[indexPath.row] + cell.configure(with: entity) + return cell + } + + func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { + guard let listEntities = self.viewModel.output.teamListEntities.value else { return UICollectionReusableView() } + + switch kind { + case UICollectionView.elementKindSectionHeader: + guard let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: CastTeamListHeaderView.className, for: indexPath) as? CastTeamListHeaderView else { return UICollectionReusableView() } + let headerTitle = listEntities[indexPath.section].name + headerView.configure(with: headerTitle) + return headerView + case UICollectionView.elementKindSectionFooter: + guard let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: CastTeamListFooterView.className, for: indexPath) as? CastTeamListFooterView else { return UICollectionReusableView() } + return footerView + default: + return UICollectionReusableView() + } + } +} + +// MARK: CollectionViewDelegateFlowLayout + +extension ConcertDetailViewController: UICollectionViewDelegateFlowLayout { + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + let width = collectionView.frame.width / 2 - 40 + let size = CGSize(width: width, height: 48) + return size + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { + let width = collectionView.frame.width + + if section == 0 { + return CGSize(width: width, height: 65) + } else { + return CGSize(width: width, height: 50) + } + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { + guard let listEntities = self.viewModel.output.teamListEntities.value else { return UIEdgeInsets() } + + let isLastSection = section == listEntities.count - 1 + let hasMembers = !listEntities[section].members.isEmpty + + let bottomInset: CGFloat + + if isLastSection { + bottomInset = 40 + } else if hasMembers { + bottomInset = 24 + } else { + bottomInset = 10 + } + + return UIEdgeInsets(top: 20, left: 20, bottom: bottomInset, right: 20) + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { + let width = collectionView.frame.width + return CGSize(width: width, height: 1) + } +} + +// MARK: UICollectionViewDelegate + +extension ConcertDetailViewController: UICollectionViewDelegate { + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard let listEntities = self.viewModel.output.teamListEntities.value else { return } + let user = listEntities[indexPath.section].members[indexPath.row] + let viewController = self.profileViewControllerFactory(user.code) + self.navigationController?.pushViewController(viewController, animated: true) + } + } diff --git a/Boolti/Boolti/Sources/UILayer/Concert/ConcertDetail/ConcertDetailViewModel.swift b/Boolti/Boolti/Sources/UILayer/Concert/ConcertDetail/ConcertDetailViewModel.swift index 18935586..73fd53ea 100644 --- a/Boolti/Boolti/Sources/UILayer/Concert/ConcertDetail/ConcertDetailViewModel.swift +++ b/Boolti/Boolti/Sources/UILayer/Concert/ConcertDetail/ConcertDetailViewModel.swift @@ -65,6 +65,8 @@ final class ConcertDetailViewModel { struct Output { let navigate = PublishRelay() let concertDetail = BehaviorRelay(value: nil) + var teamListEntities = BehaviorRelay<[ConcertCastTeamListEntity]?>(value: nil) + let buttonState = BehaviorRelay(value: .endSale) } @@ -81,7 +83,7 @@ final class ConcertDetailViewModel { self.concertId = concertId self.input = Input() self.output = Output() - + self.bindInputs() self.bindOutputs() } @@ -158,4 +160,11 @@ extension ConcertDetailViewModel { .disposed(by: self.disposeBag) } + func fetchCastTeamList() { + self.concertRepository.castTeamList(concertId: self.concertId) + .asObservable() + .bind(to: self.output.teamListEntities) + .disposed(by: self.disposeBag) + } + } diff --git a/Boolti/Boolti/Sources/UILayer/Concert/ConcertDetail/Views/CastTeamListCollectionViewCell.swift b/Boolti/Boolti/Sources/UILayer/Concert/ConcertDetail/Views/CastTeamListCollectionViewCell.swift new file mode 100644 index 00000000..acc42ff6 --- /dev/null +++ b/Boolti/Boolti/Sources/UILayer/Concert/ConcertDetail/Views/CastTeamListCollectionViewCell.swift @@ -0,0 +1,90 @@ +// +// CastTeamListCollectionViewCell.swift +// Boolti +// +// Created by Miro on 10/3/24. +// + +import UIKit + +import SnapKit + +final class CastTeamListCollectionViewCell: UICollectionViewCell { + + private let profileImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFill + imageView.backgroundColor = .grey80 + imageView.layer.cornerRadius = 24 + imageView.clipsToBounds = true + imageView.layer.borderColor = UIColor.grey80.cgColor + imageView.image = .defaultProfile + + return imageView + }() + + private let profileNameLabel: BooltiUILabel = { + let label = BooltiUILabel() + label.font = .body3 + label.textColor = .grey10 + + return label + }() + + private let roleNameLabel: BooltiUILabel = { + let label = BooltiUILabel() + label.font = .body1 + label.textColor = .grey50 + + return label + }() + + private lazy var profileStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.alignment = .leading + stackView.addArrangedSubviews([self.profileNameLabel,self.roleNameLabel]) + return stackView + }() + + override init(frame: CGRect) { + super.init(frame: frame) + self.configureUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + super.prepareForReuse() + self.profileNameLabel.text = "" + self.profileImageView.image = nil + self.roleNameLabel.text = "" + } + + func configure(with entity: TeamMember) { + self.profileNameLabel.text = entity.nickName + self.roleNameLabel.text = entity.roleName + self.profileImageView.setImage(with: entity.imagePath) + } + + private func configureUI() { + self.contentView.addSubviews([ + self.profileImageView, + self.profileStackView + ]) + + self.profileImageView.snp.makeConstraints { make in + make.leading.equalToSuperview() + make.height.equalTo(self.profileStackView) + make.width.equalTo(self.profileImageView.snp.height) + } + + self.profileStackView.snp.makeConstraints { make in + make.leading.equalTo(self.profileImageView.snp.trailing).offset(8) + make.trailing.equalToSuperview() + make.verticalEdges.equalToSuperview() + } + } +} diff --git a/Boolti/Boolti/Sources/UILayer/Concert/ConcertDetail/Views/CastTeamListFooterView.swift b/Boolti/Boolti/Sources/UILayer/Concert/ConcertDetail/Views/CastTeamListFooterView.swift new file mode 100644 index 00000000..b6b23a8c --- /dev/null +++ b/Boolti/Boolti/Sources/UILayer/Concert/ConcertDetail/Views/CastTeamListFooterView.swift @@ -0,0 +1,39 @@ +// +// CastTeamListFooterView.swift +// Boolti +// +// Created by Miro on 10/6/24. +// +import UIKit + +import SnapKit + +final class CastTeamListFooterView: UICollectionReusableView { + + private let boundaryLineView: UIView = { + let view = UIView() + view.backgroundColor = .grey85 + + return view + }() + + override init(frame: CGRect) { + super.init(frame: frame) + self.configureUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func configureUI() { + self.addSubview(boundaryLineView) + + self.boundaryLineView.snp.makeConstraints { make in + make.height.equalTo(1) + make.horizontalEdges.equalToSuperview().inset(20) + make.centerY.equalToSuperview() + } + } + +} diff --git a/Boolti/Boolti/Sources/UILayer/Concert/ConcertDetail/Views/CastTeamListHeaderView.swift b/Boolti/Boolti/Sources/UILayer/Concert/ConcertDetail/Views/CastTeamListHeaderView.swift new file mode 100644 index 00000000..f2f8f8de --- /dev/null +++ b/Boolti/Boolti/Sources/UILayer/Concert/ConcertDetail/Views/CastTeamListHeaderView.swift @@ -0,0 +1,49 @@ +// +// CastTeamListHeaderView.swift +// Boolti +// +// Created by Miro on 10/6/24. +// + +import UIKit + +import SnapKit + +final class CastTeamListHeaderView: UICollectionReusableView { + + private let headerTitleLabel: UILabel = { + let label = UILabel() + label.font = .subhead2 + label.textColor = .grey10 + + return label + }() + + func configure(with title: String) { + self.headerTitleLabel.text = title + } + + override init(frame: CGRect) { + super.init(frame: frame) + self.configureUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + super.prepareForReuse() + self.headerTitleLabel.text = "" + } + + private func configureUI() { + self.addSubview(headerTitleLabel) + + self.headerTitleLabel.snp.makeConstraints { make in + make.leading.equalToSuperview().inset(20) + make.bottom.equalToSuperview() + } + } + +} diff --git a/Boolti/Boolti/Sources/UILayer/Concert/ConcertDetail/Views/EmptyCastTeamListView.swift b/Boolti/Boolti/Sources/UILayer/Concert/ConcertDetail/Views/EmptyCastTeamListView.swift new file mode 100644 index 00000000..3fd6b3b0 --- /dev/null +++ b/Boolti/Boolti/Sources/UILayer/Concert/ConcertDetail/Views/EmptyCastTeamListView.swift @@ -0,0 +1,63 @@ +// +// EmptyCastTeamListView.swift +// Boolti +// +// Created by Miro on 10/8/24. +// + +import UIKit +import SnapKit + +final class EmptyCastTeamListView: UIView { + + private let headTitleLabel: UILabel = { + let label = UILabel() + label.text = "COMING SOON" + label.font = .aggroM(20) + label.textColor = .grey20 + + return label + }() + + private let subtitleLabel: UILabel = { + let label = UILabel() + label.text = "조금만 기다려주세요!" + label.font = .body3 + label.textColor = .grey30 + + return label + }() + + private lazy var stackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.addArrangedSubviews([self.headTitleLabel, self.subtitleLabel]) + stackView.spacing = 4 + stackView.alignment = .center + + return stackView + }() + + override init(frame: CGRect) { + super.init(frame: frame) + self.configureUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func configureUI() { + self.addSubview(self.stackView) + + self.snp.makeConstraints { make in + make.height.equalTo(290) + } + + self.stackView.snp.makeConstraints { make in + make.center.equalToSuperview() + } + } + + +} diff --git a/Boolti/Boolti/Sources/UILayer/Concert/ConcertDetail/Views/SegmentedControlContainerView.swift b/Boolti/Boolti/Sources/UILayer/Concert/ConcertDetail/Views/SegmentedControlContainerView.swift new file mode 100644 index 00000000..c135fe54 --- /dev/null +++ b/Boolti/Boolti/Sources/UILayer/Concert/ConcertDetail/Views/SegmentedControlContainerView.swift @@ -0,0 +1,52 @@ +// +// BooltiSegmentedControlView.swift +// Boolti +// +// Created by Miro on 10/3/24. +// + +import UIKit + +final class SegmentedControlContainerView: UIView { + + var segmentedControl: UnderlineSegmentedControl + + private var boundaryLineView: UIView = { + let view = UIView() + view.backgroundColor = .grey85 + return view + }() + + init(items: [Any]?) { + self.segmentedControl = UnderlineSegmentedControl(items: items) + super.init(frame: .zero) + + self.configureUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func configureUI() { + self.addSubviews([ + self.boundaryLineView, + self.segmentedControl + ]) + + self.snp.makeConstraints { make in + make.height.equalTo(48) + } + + self.boundaryLineView.snp.makeConstraints { make in + make.height.equalTo(1) + make.bottom.equalToSuperview() + make.horizontalEdges.equalToSuperview() + } + + self.segmentedControl.snp.makeConstraints { make in + make.horizontalEdges.equalToSuperview().inset(20) + make.height.equalToSuperview() + } + } +} diff --git a/Boolti/Boolti/Sources/UILayer/Concert/ConcertDetail/Views/UnderlineSegmentedControl.swift b/Boolti/Boolti/Sources/UILayer/Concert/ConcertDetail/Views/UnderlineSegmentedControl.swift new file mode 100644 index 00000000..7057fb33 --- /dev/null +++ b/Boolti/Boolti/Sources/UILayer/Concert/ConcertDetail/Views/UnderlineSegmentedControl.swift @@ -0,0 +1,61 @@ +// +// UnderlineSegmentedControl.swift +// Boolti +// +// Created by Miro on 10/3/24. +// + +import UIKit + +import SnapKit + +final class UnderlineSegmentedControl: UISegmentedControl { + + private lazy var underlineView: UIView = { + let width = self.bounds.size.width / CGFloat(self.numberOfSegments) + let height = 2.0 + let xPosition = CGFloat(self.selectedSegmentIndex * Int(width)) + let yPosition = self.bounds.size.height - 1.0 + let frame = CGRect(x: xPosition, y: yPosition, width: width, height: height) + let view = UIView(frame: frame) + view.backgroundColor = .grey10 + self.addSubview(view) + return view + }() + + override init(frame: CGRect) { + super.init(frame: frame) + self.removeBackgroundAndDivider() + } + + override init(items: [Any]?) { + super.init(items: items) + self.removeBackgroundAndDivider() + } + + required init?(coder: NSCoder) { + fatalError() + } + + // background와 Divder 제거 + private func removeBackgroundAndDivider() { + let image = UIImage() + self.setBackgroundImage(image, for: .normal, barMetrics: .default) + self.setBackgroundImage(image, for: .selected, barMetrics: .default) + self.setBackgroundImage(image, for: .highlighted, barMetrics: .default) + + self.setDividerImage(image, forLeftSegmentState: .selected, rightSegmentState: .normal, barMetrics: .default) + } + + override func layoutSubviews() { + super.layoutSubviews() + + let underlineFinalXPosition = (self.bounds.width / CGFloat(self.numberOfSegments)) * CGFloat(self.selectedSegmentIndex) + UIView.animate( + withDuration: 0.1, + animations: { + self.underlineView.frame.origin.x = underlineFinalXPosition + } + ) + } +} diff --git a/Boolti/Boolti/Sources/UILayer/Concert/ConcertList/ConcertListViewController.swift b/Boolti/Boolti/Sources/UILayer/Concert/ConcertList/ConcertListViewController.swift index 9939c3ca..d0d6fb83 100644 --- a/Boolti/Boolti/Sources/UILayer/Concert/ConcertList/ConcertListViewController.swift +++ b/Boolti/Boolti/Sources/UILayer/Concert/ConcertList/ConcertListViewController.swift @@ -120,7 +120,7 @@ extension ConcertListViewController { self.viewModel.output.didConcertFetch .asDriver(onErrorJustReturn: ()) .drive(with: self) { owner, concerts in - owner.mainCollectionView.reloadSections([2, 4], animationStyle: .automatic) + owner.mainCollectionView.reloadSections([2, 3, 4], animationStyle: .automatic) } .disposed(by: self.disposeBag) @@ -209,9 +209,11 @@ extension ConcertListViewController: UICollectionViewDataSource { switch section { case .topConcerts: return viewModel.output.topConcerts.count + case .banner: + return viewModel.output.showBanner ? 1 : 0 case .bottomConcerts: return viewModel.output.bottomConcerts.count - case .title, .searchBar, .banner, .information: + case .title, .searchBar, .information: return 1 } } diff --git a/Boolti/Boolti/Sources/UILayer/Concert/ConcertList/ConcertViewModel.swift b/Boolti/Boolti/Sources/UILayer/Concert/ConcertList/ConcertViewModel.swift index 976766e8..5f6019ee 100644 --- a/Boolti/Boolti/Sources/UILayer/Concert/ConcertList/ConcertViewModel.swift +++ b/Boolti/Boolti/Sources/UILayer/Concert/ConcertList/ConcertViewModel.swift @@ -30,6 +30,7 @@ final class ConcertListViewModel { struct Output { let didConcertFetch = PublishRelay() + var showBanner: Bool = false var topConcerts: [ConcertEntity] = [] var bottomConcerts: [ConcertEntity] = [] let showRegisterGiftPopUp = PublishRelay() @@ -83,6 +84,7 @@ extension ConcertListViewModel { .subscribe(with: self) { owner, concerts in owner.output.topConcerts = Array(concerts.prefix(4)) owner.output.bottomConcerts = Array(concerts.dropFirst(4)) + owner.output.showBanner = true owner.output.didConcertFetch.accept(()) } .disposed(by: self.disposeBag) diff --git a/Boolti/Boolti/Sources/UILayer/Concert/Ticketing/Detail/GiftingDetail/Cells/CardImageCollectionViewCell.swift b/Boolti/Boolti/Sources/UILayer/Concert/Ticketing/Detail/GiftingDetail/Cells/CardImageCollectionViewCell.swift index b66b52fa..ca07fe90 100644 --- a/Boolti/Boolti/Sources/UILayer/Concert/Ticketing/Detail/GiftingDetail/Cells/CardImageCollectionViewCell.swift +++ b/Boolti/Boolti/Sources/UILayer/Concert/Ticketing/Detail/GiftingDetail/Cells/CardImageCollectionViewCell.swift @@ -14,9 +14,17 @@ final class CardImageCollectionViewCell: UICollectionViewCell { private let cardImageView: UIImageView = { let imageView = UIImageView() imageView.backgroundColor = .grey50 + imageView.contentMode = .scaleAspectFill return imageView }() + private let dimmedView: UIView = { + let view = UIView() + view.backgroundColor = .black100.withAlphaComponent(0.45) + view.isHidden = true + return view + }() + // MARK: Initailizer override init(frame: CGRect) { @@ -35,9 +43,11 @@ final class CardImageCollectionViewCell: UICollectionViewCell { didSet { if isSelected { self.layer.borderColor = UIColor.orange01.cgColor - self.layer.borderWidth = 1 + self.layer.borderWidth = 2 + self.dimmedView.isHidden = false } else { self.layer.borderWidth = 0 + self.dimmedView.isHidden = true } } } @@ -67,7 +77,8 @@ extension CardImageCollectionViewCell { private func configureUI() { self.layer.cornerRadius = 4 self.clipsToBounds = true - self.addSubview(self.cardImageView) + self.addSubviews([self.cardImageView, + self.dimmedView]) self.configureConstraints() } @@ -76,6 +87,10 @@ extension CardImageCollectionViewCell { self.cardImageView.snp.makeConstraints { make in make.edges.equalToSuperview() } + + self.dimmedView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } } } diff --git a/Boolti/Boolti/Sources/UILayer/Concert/Ticketing/Detail/GiftingDetail/GiftingDetailViewController.swift b/Boolti/Boolti/Sources/UILayer/Concert/Ticketing/Detail/GiftingDetail/GiftingDetailViewController.swift index 3fa20bf2..8b0d662b 100644 --- a/Boolti/Boolti/Sources/UILayer/Concert/Ticketing/Detail/GiftingDetail/GiftingDetailViewController.swift +++ b/Boolti/Boolti/Sources/UILayer/Concert/Ticketing/Detail/GiftingDetail/GiftingDetailViewController.swift @@ -334,13 +334,12 @@ extension GiftingDetailViewController { .bind(to: self.payButton.rx.isEnabled) .disposed(by: self.disposeBag) - // TODO: 현재 서버에서 오는 이미지가 다름 -// self.viewModel.output.selectedCardImageEntity -// .bind(with: self) { owner, image in -// guard let image = image else { return } -// owner.selectCardView.setSelectedImage(with: image.path) -// } -// .disposed(by: self.disposeBag) + self.viewModel.output.selectedCardImageEntity + .bind(with: self) { owner, image in + guard let image = image else { return } + owner.selectCardView.setSelectedImage(with: image.path) + } + .disposed(by: self.disposeBag) self.viewModel.output.navigateToConfirm .asDriver(onErrorJustReturn: ()) diff --git a/Boolti/Boolti/Sources/UILayer/Concert/Ticketing/Detail/GiftingDetail/Views/SelectCardView.swift b/Boolti/Boolti/Sources/UILayer/Concert/Ticketing/Detail/GiftingDetail/Views/SelectCardView.swift index a5979e5a..b537bd03 100644 --- a/Boolti/Boolti/Sources/UILayer/Concert/Ticketing/Detail/GiftingDetail/Views/SelectCardView.swift +++ b/Boolti/Boolti/Sources/UILayer/Concert/Ticketing/Detail/GiftingDetail/Views/SelectCardView.swift @@ -15,23 +15,19 @@ final class SelectCardView: UIView { private let disposeBag = DisposeBag() private let cardWidth: CGFloat = UIScreen.main.bounds.width - 64 - private lazy var cardHeight: CGFloat = cardWidth * 1.23 + private lazy var cardHeight: CGFloat = cardWidth * 1.267 // MARK: UI Component - private lazy var selectedCardBackgroundView: UIView = { - let view = UIView() - view.layer.cornerRadius = 8 - view.layer.borderWidth = 1 - view.layer.borderColor = UIColor.init("FFA883").cgColor - view.clipsToBounds = true - - let gradientLayer = CAGradientLayer() - gradientLayer.frame = .init(x: 0, y: 0, width: self.cardWidth, height: self.cardHeight) - gradientLayer.colors = [UIColor.init("FF5A14").cgColor, - UIColor.init("FFA883").cgColor] - view.layer.addSublayer(gradientLayer) - return view + private let selectedCardBackgroundImageView: UIImageView = { + let imageView = UIImageView() + imageView.layer.cornerRadius = 8 + imageView.layer.borderWidth = 1 + imageView.layer.borderColor = UIColor.white00.withAlphaComponent(0.4).cgColor + imageView.clipsToBounds = true + imageView.backgroundColor = .clear + imageView.contentMode = .scaleAspectFill + return imageView }() let messageTextView: UITextView = { @@ -54,15 +50,6 @@ final class SelectCardView: UIView { return label }() - private let selectedImageView: UIImageView = { - let imageView = UIImageView() -// imageView.backgroundColor = .white00 - imageView.backgroundColor = .clear - imageView.image = .giftcard - imageView.clipsToBounds = true - return imageView - }() - lazy var cardImageCollectionView: UICollectionView = { let layout = UICollectionViewFlowLayout() layout.scrollDirection = .horizontal @@ -96,7 +83,7 @@ final class SelectCardView: UIView { extension SelectCardView { func setSelectedImage(with imageURL: String) { - self.selectedImageView.setImage(with: imageURL) + self.selectedCardBackgroundImageView.setImage(with: imageURL) } private func bindUIComponent() { @@ -133,53 +120,41 @@ extension SelectCardView: UICollectionViewDelegateFlowLayout { extension SelectCardView { private func configureUI() { - self.addSubviews([self.selectedCardBackgroundView, + self.addSubviews([self.selectedCardBackgroundImageView, self.messageTextView, self.messageCountLabel, - self.selectedImageView, - /*self.cardImageCollectionView*/]) + self.cardImageCollectionView]) self.configureConstraints() } private func configureConstraints() { self.snp.makeConstraints { make in -// make.height.equalTo(self.cardHeight + 156) - make.height.equalTo(self.cardHeight + 40) + make.height.equalTo(self.cardHeight + 140) } - self.selectedCardBackgroundView.snp.makeConstraints { make in + self.selectedCardBackgroundImageView.snp.makeConstraints { make in make.top.equalToSuperview().inset(24) make.horizontalEdges.equalToSuperview().inset(32) make.height.equalTo(self.cardHeight) } self.messageTextView.snp.makeConstraints { make in - make.top.equalTo(self.selectedCardBackgroundView).inset(32) - make.horizontalEdges.equalTo(self.selectedCardBackgroundView).inset(20) + make.top.equalTo(self.selectedCardBackgroundImageView).inset(32) + make.horizontalEdges.equalTo(self.selectedCardBackgroundImageView).inset(20) make.height.lessThanOrEqualTo(80) } self.messageCountLabel.snp.makeConstraints { make in make.centerX.equalToSuperview() - make.bottom.equalTo(self.selectedImageView.snp.top).offset(-28) + make.top.equalTo(self.messageTextView.snp.bottom).offset(12) } - self.selectedImageView.snp.makeConstraints { make in -// make.horizontalEdges.equalTo(self.messageTextView) -// make.height.equalTo((self.cardWidth - 40) * (2/3)) -// make.bottom.equalTo(self.selectedCardBackgroundView).inset(32) - - make.centerX.equalToSuperview() - make.size.equalTo(232) - make.bottom.equalTo(self.selectedCardBackgroundView) + self.cardImageCollectionView.snp.makeConstraints { make in + make.horizontalEdges.equalToSuperview() + make.top.equalTo(self.selectedCardBackgroundImageView.snp.bottom).offset(32) + make.bottom.equalToSuperview().inset(32) } - -// self.cardImageCollectionView.snp.makeConstraints { make in -// make.horizontalEdges.equalToSuperview() -// make.top.equalTo(self.selectedCardBackgroundView.snp.bottom).offset(44) -// make.bottom.equalToSuperview().inset(36) -// } } } diff --git a/Boolti/Boolti/Sources/UILayer/MyPage/Main/MyPageDIContainer.swift b/Boolti/Boolti/Sources/UILayer/MyPage/Main/MyPageDIContainer.swift index 33738f40..77f7d191 100644 --- a/Boolti/Boolti/Sources/UILayer/MyPage/Main/MyPageDIContainer.swift +++ b/Boolti/Boolti/Sources/UILayer/MyPage/Main/MyPageDIContainer.swift @@ -46,8 +46,8 @@ final class MyPageDIContainer { } let profileViewControllerFactory = { - let DIContainer = ProfileDIContainer(authRepository: self.authRepository) - let viewController = DIContainer.createProfileViewController() + let DIContainer = ProfileDIContainer(repository: self.authRepository) + let viewController = DIContainer.createMyProfileViewController() return viewController } diff --git a/Boolti/Boolti/Sources/UILayer/MyPage/Profile/EditProfile/EditProfileViewController.swift b/Boolti/Boolti/Sources/UILayer/MyPage/Profile/EditProfile/EditProfileViewController.swift index 5d2972d4..79c2e718 100644 --- a/Boolti/Boolti/Sources/UILayer/MyPage/Profile/EditProfile/EditProfileViewController.swift +++ b/Boolti/Boolti/Sources/UILayer/MyPage/Profile/EditProfile/EditProfileViewController.swift @@ -169,9 +169,10 @@ extension EditProfileViewController { .orEmpty .asDriver() .drive(with: self, onNext: { owner, text in - // TODO: 아래와 같이 placeholder 판단하는 로직 변경하기 - owner.navigationBar.completeButton.isEnabled = !text.isEmpty && (text != "예) 재즈와 펑크락을 좋아해요") - owner.viewModel.input.didIntroductionTyped.accept(text) + owner.navigationBar.completeButton.isEnabled = !text.isEmpty + if !owner.editIntroductionView.isShowingPlaceHolder { + owner.viewModel.input.didIntroductionTyped.accept(text) + } }) .disposed(by: self.disposeBag) diff --git a/Boolti/Boolti/Sources/UILayer/MyPage/Profile/EditProfile/Views/EditIntroductionView.swift b/Boolti/Boolti/Sources/UILayer/MyPage/Profile/EditProfile/Views/EditIntroductionView.swift index 9dbb956f..aa005948 100644 --- a/Boolti/Boolti/Sources/UILayer/MyPage/Profile/EditProfile/Views/EditIntroductionView.swift +++ b/Boolti/Boolti/Sources/UILayer/MyPage/Profile/EditProfile/Views/EditIntroductionView.swift @@ -12,11 +12,22 @@ import RxCocoa // TODO: EditIntroductionView - TextView의 PlaceHolder등 추가해서 리팩토링 진행하기 final class EditIntroductionView: UIView { - + // MARK: Properties - + private let disposeBag = DisposeBag() - + + var isShowingPlaceHolder: Bool = true { + didSet { + switch self.isShowingPlaceHolder { + case true: + self.introductionTextView.textColor = .grey70 + case false: + self.introductionTextView.textColor = .grey10 + } + } + } + // MARK: UI Components private let introductionLabel: BooltiUILabel = { let label = BooltiUILabel() @@ -25,7 +36,7 @@ final class EditIntroductionView: UIView { label.text = "소개" return label }() - + let introductionTextView: UITextView = { let textView = UITextView() textView.backgroundColor = .grey85 @@ -49,31 +60,31 @@ final class EditIntroductionView: UIView { label.textColor = .grey70 return label }() - + // MARK: Initailizer - + override init(frame: CGRect) { super.init(frame: .zero) - + self.configureUI() self.bindTextView() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + } // MARK: - Methods extension EditIntroductionView { - + func setData(with introduction: String?) { guard let introduction = introduction else { return } - + if !introduction.isEmpty { - self.introductionTextView.textColor = .grey10 + self.isShowingPlaceHolder = false self.introductionTextView.text = introduction self.textCountLabel.text = "\(introduction.count)/60자" } @@ -82,50 +93,50 @@ extension EditIntroductionView { private func bindTextView() { self.introductionTextView.rx.didBeginEditing .bind(with: self) { owner, _ in - if owner.introductionTextView.textColor == .grey70 { + if owner.isShowingPlaceHolder { owner.introductionTextView.text = nil - owner.introductionTextView.textColor = .grey10 + owner.isShowingPlaceHolder = false } } .disposed(by: self.disposeBag) - + self.introductionTextView.rx.didEndEditing .bind(with: self) { owner, _ in guard let changedText = owner.introductionTextView.text?.trimmingCharacters(in: .whitespacesAndNewlines) else { return } - + if changedText.isEmpty { - owner.introductionTextView.textColor = .grey70 + owner.isShowingPlaceHolder = true owner.introductionTextView.text = "예) 재즈와 펑크락을 좋아해요" } } .disposed(by: self.disposeBag) - + self.introductionTextView.rx.text .asDriver() .drive(with: self) { owner, changedText in guard let changedText = changedText else { return } - + if changedText.count > 60 { owner.introductionTextView.deleteBackward() } - - if owner.introductionTextView.textColor == .grey10 { - owner.textCountLabel.text = "\(changedText.count)/60자" - } else { + + if owner.isShowingPlaceHolder { owner.textCountLabel.text = "0/60자" + } else { + owner.textCountLabel.text = "\(changedText.count)/60자" } - + owner.introductionTextView.setLineHeight(alignment: .left) } .disposed(by: self.disposeBag) } - + } // MARK: - UI extension EditIntroductionView { - + private func configureUI() { self.backgroundColor = .grey90 self.addSubviews([self.introductionLabel, @@ -134,7 +145,7 @@ extension EditIntroductionView { self.textCountLabel]) self.configureConstraints() } - + private func configureConstraints() { self.introductionLabel.snp.makeConstraints { make in make.top.leading.equalToSuperview().inset(20) @@ -151,7 +162,7 @@ extension EditIntroductionView { make.top.equalTo(self.backgroundView.snp.top).inset(12) make.height.equalTo(72) } - + self.textCountLabel.snp.makeConstraints { make in make.top.equalTo(self.introductionTextView.snp.bottom).offset(8) make.trailing.equalTo(self.backgroundView).inset(12) diff --git a/Boolti/Boolti/Sources/UILayer/MyPage/Profile/Main/ProfileDIContainer.swift b/Boolti/Boolti/Sources/UILayer/MyPage/Profile/Main/ProfileDIContainer.swift index 07f81318..d3689f15 100644 --- a/Boolti/Boolti/Sources/UILayer/MyPage/Profile/Main/ProfileDIContainer.swift +++ b/Boolti/Boolti/Sources/UILayer/MyPage/Profile/Main/ProfileDIContainer.swift @@ -9,25 +9,35 @@ import UIKit final class ProfileDIContainer { - private let authRepository: AuthRepositoryType + private let repository: RepositoryType - init(authRepository: AuthRepositoryType) { - self.authRepository = authRepository + init(repository: RepositoryType) { + self.repository = repository } - func createProfileViewController() -> ProfileViewController { + func createMyProfileViewController() -> ProfileViewController { + let authRepository = self.repository as? AuthRepository ?? AuthRepository(networkService: NetworkProvider()) let editProfileViewControllerFactory = { - let DIContainer = EditProfileDIContainer(authRepository: self.authRepository) + let DIContainer = EditProfileDIContainer( + authRepository: authRepository + ) let viewController = DIContainer.createEditProfileViewController() return viewController } - let viewModel = ProfileViewModel(authRepository: self.authRepository) + let viewModel = ProfileViewModel(repository: self.repository) let viewController = ProfileViewController(viewModel: viewModel, editProfileViewControllerFactory: editProfileViewControllerFactory) return viewController } + func createProfileViewController(userCode: String) -> ProfileViewController { + + let viewModel = ProfileViewModel(repository: self.repository, userCode: userCode) + let viewController = ProfileViewController(viewModel: viewModel) + + return viewController + } } diff --git a/Boolti/Boolti/Sources/UILayer/MyPage/Profile/Main/ProfileViewController.swift b/Boolti/Boolti/Sources/UILayer/MyPage/Profile/Main/ProfileViewController.swift index f97256a0..db2f6874 100644 --- a/Boolti/Boolti/Sources/UILayer/MyPage/Profile/Main/ProfileViewController.swift +++ b/Boolti/Boolti/Sources/UILayer/MyPage/Profile/Main/ProfileViewController.swift @@ -9,6 +9,7 @@ import UIKit import RxSwift import RxCocoa +import RxAppState final class ProfileViewController: BooltiViewController { @@ -17,8 +18,8 @@ final class ProfileViewController: BooltiViewController { private let disposeBag = DisposeBag() private let viewModel: ProfileViewModel - private let editProfileViewControllerFactory: () -> EditProfileViewController - + private let editProfileViewControllerFactory: (() -> EditProfileViewController)? + // MARK: UI Components private let navigationBar = BooltiNavigationBar(type: .backButtonWithTitle(title: "프로필")) @@ -60,13 +61,14 @@ final class ProfileViewController: BooltiViewController { // MARK: Initailizer init(viewModel: ProfileViewModel, - editProfileViewControllerFactory: @escaping () -> EditProfileViewController) { + editProfileViewControllerFactory: (() -> EditProfileViewController)? = nil + ) { self.viewModel = viewModel self.editProfileViewControllerFactory = editProfileViewControllerFactory super.init() } - + required init?(coder: NSCoder) { fatalError() } @@ -75,7 +77,6 @@ final class ProfileViewController: BooltiViewController { override func viewWillAppear(_ animated: Bool) { self.tabBarController?.tabBar.isHidden = true - self.viewModel.fetchLinkList() } override func viewDidLoad() { @@ -84,23 +85,33 @@ final class ProfileViewController: BooltiViewController { self.configureUI() self.configureCollectionView() self.configureToastView(isButtonExisted: false) + self.bindInput() self.bindUIComponents() self.bindViewModel() } - } // MARK: - Methods extension ProfileViewController { - + + private func bindInput() { + self.rx.viewWillAppear + .asDriver(onErrorDriveWith: .never()) + .drive(with: self) { owner, _ in + owner.viewModel.input.viewWillAppearEvent.onNext(()) + } + .disposed(by: self.disposeBag) + } + private func bindViewModel() { + print("🚨 bindViewModel") self.viewModel.output.didProfileFetch - .subscribe(with: self) { owner, introduction in - owner.profileMainView.setData(introduction: introduction) - owner.dataCollectionView.reloadData() - owner.updateCollectionViewHeight() - } + .subscribe(onNext: { [weak self] (entity, isMyProfile) in + self?.profileMainView.setData(entity: entity, isMyProfile: isMyProfile) + self?.dataCollectionView.reloadData() + self?.updateCollectionViewHeight() + }) .disposed(by: self.disposeBag) } @@ -125,7 +136,8 @@ extension ProfileViewController { self.profileMainView.didEditButtonTap() .emit(with: self) { owner, _ in - owner.navigationController?.pushViewController(owner.editProfileViewControllerFactory(), animated: true) + guard let editProfileViewControllerFactory = owner.editProfileViewControllerFactory?() else { return } + owner.navigationController?.pushViewController(editProfileViewControllerFactory, animated: true) } .disposed(by: self.disposeBag) } diff --git a/Boolti/Boolti/Sources/UILayer/MyPage/Profile/Main/ProfileViewModel.swift b/Boolti/Boolti/Sources/UILayer/MyPage/Profile/Main/ProfileViewModel.swift index aa428a1a..60bd2bfd 100644 --- a/Boolti/Boolti/Sources/UILayer/MyPage/Profile/Main/ProfileViewModel.swift +++ b/Boolti/Boolti/Sources/UILayer/MyPage/Profile/Main/ProfileViewModel.swift @@ -9,38 +9,74 @@ import Foundation import RxSwift +typealias isMyProfile = Bool + final class ProfileViewModel { // MARK: Properties private let disposeBag = DisposeBag() - private let authRepository: AuthRepositoryType - + private let repository: RepositoryType + + private let userCode: String? + + struct Input { + let viewWillAppearEvent = PublishSubject() + } + struct Output { var links: [LinkEntity] = [] - var didProfileFetch = PublishSubject() + var didProfileFetch = PublishSubject<(UserProfileResponseDTO, isMyProfile)>() } - + + var input: Input var output: Output // MARK: Initailizer - - init(authRepository: AuthRepositoryType) { + // TODO: 내 프로필 확인과 다른 사람 프로필 확인하는 API 구분하기!.. -> 지금은 하나의 ProfileVM에서 처리중 + init(repository: RepositoryType, userCode: String? = nil) { + self.input = Input() self.output = Output() - self.authRepository = authRepository + self.repository = repository + self.userCode = userCode + + self.bindInputs() + } + + private func bindInputs() { + self.input.viewWillAppearEvent + .subscribe(with: self) { owner, _ in + if let _ = owner.userCode { + owner.fetchProfileInformation() + } else { + owner.fetchMyProfileInformation() + } + } + .disposed(by: self.disposeBag) } - } // MARK: - Network extension ProfileViewModel { - func fetchLinkList() { - self.authRepository.userProfile() + func fetchMyProfileInformation() { + guard let authRepository = self.repository as? AuthRepository else { return } + authRepository.userProfile() + .subscribe(with: self) { owner, profile in + owner.output.links = profile.link ?? [] + owner.output.didProfileFetch.onNext((profile, true)) + } + .disposed(by: self.disposeBag) + } + + func fetchProfileInformation() { + guard let concertRepository = self.repository as? ConcertRepository else { return } + concertRepository.userProfile(userCode: self.userCode ?? "") + .debug() .subscribe(with: self) { owner, profile in owner.output.links = profile.link ?? [] - owner.output.didProfileFetch.onNext(profile.introduction) + owner.output.didProfileFetch.onNext((profile, false)) } .disposed(by: self.disposeBag) } diff --git a/Boolti/Boolti/Sources/UILayer/MyPage/Profile/Main/Views/ProfileMainView.swift b/Boolti/Boolti/Sources/UILayer/MyPage/Profile/Main/Views/ProfileMainView.swift index 1fdd1918..5eb0ea02 100644 --- a/Boolti/Boolti/Sources/UILayer/MyPage/Profile/Main/Views/ProfileMainView.swift +++ b/Boolti/Boolti/Sources/UILayer/MyPage/Profile/Main/Views/ProfileMainView.swift @@ -92,20 +92,21 @@ final class ProfileMainView: UIView { extension ProfileMainView { - func setData(introduction: String?) { - self.profileImageView.setImage(with: UserDefaults.userImageURLPath) - self.nameLabel.text = UserDefaults.userName - self.introductionLabel.text = introduction ?? "" + func setData(entity: UserProfileResponseDTO, isMyProfile: Bool) { + self.profileImageView.setImage(with: entity.imgPath ?? "") + self.nameLabel.text = entity.nickname + self.introductionLabel.text = entity.introduction ?? "" + self.editButton.isHidden = !isMyProfile } func getHeight() -> CGFloat { - return 222 + self.nameLabel.getLabelHeight() + self.introductionLabel.getLabelHeight() + let height = self.editButton.isHidden ? 192 : 222 + return CGFloat(height) + self.nameLabel.getLabelHeight() + self.introductionLabel.getLabelHeight() } func didEditButtonTap() -> Signal { return self.editButton.rx.tap.asSignal() } - } // MARK: - UI