diff --git a/.gitignore b/.gitignore index 5bafdee7..748d4b1c 100644 --- a/.gitignore +++ b/.gitignore @@ -113,5 +113,12 @@ Gemfile.lock report.xml Runnect-iOS/fastlane/.env.default -## dynamicLink -Runnect-iOS/Runnect-iOS/GoogleService-Info.plist \ No newline at end of file +### Runniest project - git ignore setting ### +Runnect-iOS/Runnect-iOS/Info.plist +Runnect-iOS/Info.plist +Info.plist + +### dynamicLink ### +Runnect-iOS/Runnect-iOS/GoogleService-Info.plist +Runnect-iOS/GoogleService-Info.plist +GoogleService-Info.plist \ No newline at end of file diff --git a/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj b/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj index cfc91627..32a3eacc 100644 --- a/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj +++ b/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj @@ -15,7 +15,12 @@ 23EE06D12AC2F44E00CB3FF8 /* TmapAddressSearchingResponseDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EE06D02AC2F44E00CB3FF8 /* TmapAddressSearchingResponseDto.swift */; }; 712F661D2A7B7BAB00D9539B /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 712F661C2A7B7BAB00D9539B /* Config.swift */; }; 7136BF8A2AF921A900679364 /* CustomBottomSheetVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7136BF892AF921A900679364 /* CustomBottomSheetVC.swift */; }; - 71717B072B063E14004EA8DA /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 71DBF23D2ABB255A0013415B /* GoogleService-Info.plist */; }; + 713BA40B2B218AF8009091A8 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 713BA40A2B218AF8009091A8 /* GoogleService-Info.plist */; }; + 717916DA2B13613B009CEF97 /* MarathonListResponseDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 717916D92B13613B009CEF97 /* MarathonListResponseDto.swift */; }; + 717916DE2B137DC3009CEF97 /* TotalPageCountDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 717916DD2B137DC3009CEF97 /* TotalPageCountDto.swift */; }; + 71F7804E2B0893B600B53253 /* MarathonTitleCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F7804D2B0893B600B53253 /* MarathonTitleCollectionViewCell.swift */; }; + 71F780502B0893D700B53253 /* MarathonMapCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F7804F2B0893D700B53253 /* MarathonMapCollectionViewCell.swift */; }; + 71F7BF072B0CDFE300B752B3 /* MarathonCourseListCVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F7BF062B0CDFE300B752B3 /* MarathonCourseListCVC.swift */; }; A3305A97296EF58C000B1A10 /* GoalRewardInfoDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3305A96296EF58C000B1A10 /* GoalRewardInfoDto.swift */; }; A3BC2F2B2962C3D500198261 /* GoalRewardInfoVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3BC2F2A2962C3D500198261 /* GoalRewardInfoVC.swift */; }; A3BC2F2D2962C3F200198261 /* ActivityRecordInfoVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3BC2F2C2962C3F200198261 /* ActivityRecordInfoVC.swift */; }; @@ -177,7 +182,12 @@ 7110A6032AA337DD009A7E99 /* Runnect-iOSDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Runnect-iOSDebug.entitlements"; sourceTree = ""; }; 712F661C2A7B7BAB00D9539B /* Config.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; 7136BF892AF921A900679364 /* CustomBottomSheetVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomBottomSheetVC.swift; sourceTree = ""; }; - 71DBF23D2ABB255A0013415B /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "../../../../Runnect-iOS/Runnect-iOS/Runnect-iOS/GoogleService-Info.plist"; sourceTree = ""; }; + 713BA40A2B218AF8009091A8 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + 717916D92B13613B009CEF97 /* MarathonListResponseDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarathonListResponseDto.swift; sourceTree = ""; }; + 717916DD2B137DC3009CEF97 /* TotalPageCountDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TotalPageCountDto.swift; sourceTree = ""; }; + 71F7804D2B0893B600B53253 /* MarathonTitleCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarathonTitleCollectionViewCell.swift; sourceTree = ""; }; + 71F7804F2B0893D700B53253 /* MarathonMapCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarathonMapCollectionViewCell.swift; sourceTree = ""; }; + 71F7BF062B0CDFE300B752B3 /* MarathonCourseListCVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarathonCourseListCVC.swift; sourceTree = ""; }; A3305A96296EF58C000B1A10 /* GoalRewardInfoDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoalRewardInfoDto.swift; sourceTree = ""; }; A3BC2F2A2962C3D500198261 /* GoalRewardInfoVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoalRewardInfoVC.swift; sourceTree = ""; }; A3BC2F2C2962C3F200198261 /* ActivityRecordInfoVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityRecordInfoVC.swift; sourceTree = ""; }; @@ -723,9 +733,11 @@ CE17F0442961C3D900E1DED0 /* Views */ = { isa = PBXGroup; children = ( - DAD5A3DB296C6DB800C8166B /* MapCollectionViewCell.swift */, - DAD5A3D9296C6DA500C8166B /* TitleCollectionViewCell.swift */, DAD5A3D7296C6D9600C8166B /* AdImageCollectionViewCell.swift */, + 71F7804D2B0893B600B53253 /* MarathonTitleCollectionViewCell.swift */, + 71F7804F2B0893D700B53253 /* MarathonMapCollectionViewCell.swift */, + DAD5A3D9296C6DA500C8166B /* TitleCollectionViewCell.swift */, + DAD5A3DB296C6DB800C8166B /* MapCollectionViewCell.swift */, CE17F0432961C3D600E1DED0 /* VC */, ); path = Views; @@ -822,7 +834,7 @@ CE6655A9295D7FAA00C64E12 /* Network */, CE6655A8295D7F7D00C64E12 /* Presentation */, CE4545D6295D7AF5003201E1 /* Info.plist */, - 71DBF23D2ABB255A0013415B /* GoogleService-Info.plist */, + 713BA40A2B218AF8009091A8 /* GoogleService-Info.plist */, ); path = "Runnect-iOS"; sourceTree = ""; @@ -1156,6 +1168,7 @@ isa = PBXGroup; children = ( CE6B63D2296725E6003F900F /* CourseListCVC.swift */, + 71F7BF062B0CDFE300B752B3 /* MarathonCourseListCVC.swift */, ); path = CVC; sourceTree = ""; @@ -1206,7 +1219,9 @@ DAD5A3E0296D4C2A00C8166B /* ResponseDto */ = { isa = PBXGroup; children = ( + 717916D92B13613B009CEF97 /* MarathonListResponseDto.swift */, DAD5A3E1296D4C6500C8166B /* PickedMapListResponseDto.swift */, + 717916DD2B137DC3009CEF97 /* TotalPageCountDto.swift */, ); path = ResponseDto; sourceTree = ""; @@ -1274,7 +1289,7 @@ files = ( CE665615295D989A00C64E12 /* .swiftlint.yml in Resources */, CE17F0342961BEF800E1DED0 /* Pretendard-Bold.otf in Resources */, - 71717B072B063E14004EA8DA /* GoogleService-Info.plist in Resources */, + 713BA40B2B218AF8009091A8 /* GoogleService-Info.plist in Resources */, CE17F0352961BEF800E1DED0 /* Pretendard-SemiBold.otf in Resources */, CE17F0332961BEF800E1DED0 /* Pretendard-Medium.otf in Resources */, CE6655BF295D82E200C64E12 /* .gitkeep in Resources */, @@ -1363,6 +1378,7 @@ CE4545CD295D7AF4003201E1 /* TaBarController.swift in Sources */, A3305A97296EF58C000B1A10 /* GoalRewardInfoDto.swift in Sources */, CE21C024299E5FE500F62AF5 /* UserRouter.swift in Sources */, + 71F780502B0893D700B53253 /* MarathonMapCollectionViewCell.swift in Sources */, CE6655F4295D898400C64E12 /* UIViewController+.swift in Sources */, CE14677829658C7200DCEA1B /* Stopwatch.swift in Sources */, CE21C026299E5FF300F62AF5 /* CourseRouter.swift in Sources */, @@ -1410,6 +1426,7 @@ CEC2A6922962BE2900160BF7 /* DepartureSearchVC.swift in Sources */, CE6655EE295D88E600C64E12 /* UITextField+.swift in Sources */, CE40BB1C2967E4910030ABCA /* RunningWaitingVC.swift in Sources */, + 71F7804E2B0893B600B53253 /* MarathonTitleCollectionViewCell.swift in Sources */, CE6B63D6296731F9003F900F /* ScrapCourseListView.swift in Sources */, CE6655F8295D90CF00C64E12 /* adjusted+.swift in Sources */, A3C2CAD729E53B2900EC525B /* RNAlertVC.swift in Sources */, @@ -1443,10 +1460,12 @@ A3D1A77E29CF09B600DD54EC /* SignInResponseDto.swift in Sources */, CE21C028299E5FFC00F62AF5 /* PublicCourseRouter.swift in Sources */, CE18E894296C79B900FEB569 /* CourseDrawingRequestDto.swift in Sources */, + 71F7BF072B0CDFE300B752B3 /* MarathonCourseListCVC.swift in Sources */, DA20D84E2966A9B300F1581F /* CourseSearchVC.swift in Sources */, CE1006572968230800FD31FB /* DepartureLocationModel.swift in Sources */, CECA695C296E61D6002AF05C /* PrivateCourseNotUploadedResponseDto.swift in Sources */, CE6655EC295D88D000C64E12 /* UITableView+.swift in Sources */, + 717916DE2B137DC3009CEF97 /* TotalPageCountDto.swift in Sources */, CEEC6B3A2961C4F300D00E1E /* CourseDrawingHomeVC.swift in Sources */, CEB0BCBC29D123350048CCD5 /* GuideView.swift in Sources */, CEC2A6902962B06C00160BF7 /* convertLocationObject.swift in Sources */, @@ -1474,6 +1493,7 @@ A3BC2F4129667A0D00198261 /* NicknameEditorVC.swift in Sources */, CE0C23742966D62A00B45063 /* PagedView.swift in Sources */, CE3A53C5296C6017003D518C /* KeychainManager.swift in Sources */, + 717916DA2B13613B009CEF97 /* MarathonListResponseDto.swift in Sources */, CEEC6B4B2961D89700D00E1E /* CustomNavigationBar.swift in Sources */, CE09037D296E9ED900BEA710 /* ScrapCourseResponseDto.swift in Sources */, CED791B32A2626AF001BFCFB /* ShadowView.swift in Sources */, diff --git a/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_plus_button.imageset/Component 125.png b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_plus_button.imageset/Component 125.png new file mode 100644 index 00000000..3977ac91 Binary files /dev/null and b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_plus_button.imageset/Component 125.png differ diff --git a/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_plus_button.imageset/Component 125@2x.png b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_plus_button.imageset/Component 125@2x.png new file mode 100644 index 00000000..9f726101 Binary files /dev/null and b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_plus_button.imageset/Component 125@2x.png differ diff --git a/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_plus_button.imageset/Component 125@3x.png b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_plus_button.imageset/Component 125@3x.png new file mode 100644 index 00000000..ef3a8038 Binary files /dev/null and b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_plus_button.imageset/Component 125@3x.png differ diff --git a/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_plus_button.imageset/Contents.json b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_plus_button.imageset/Contents.json index 044ff80f..4e217568 100644 --- a/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_plus_button.imageset/Contents.json +++ b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_plus_button.imageset/Contents.json @@ -1,17 +1,17 @@ { "images" : [ { - "filename" : "plus.png", + "filename" : "Component 125.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "plus@2x.png", + "filename" : "Component 125@2x.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "plus@3x.png", + "filename" : "Component 125@3x.png", "idiom" : "universal", "scale" : "3x" } diff --git a/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_plus_button.imageset/plus.png b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_plus_button.imageset/plus.png deleted file mode 100644 index a5483873..00000000 Binary files a/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_plus_button.imageset/plus.png and /dev/null differ diff --git a/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_plus_button.imageset/plus@2x.png b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_plus_button.imageset/plus@2x.png deleted file mode 100644 index 0e2ac0c8..00000000 Binary files a/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_plus_button.imageset/plus@2x.png and /dev/null differ diff --git a/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_plus_button.imageset/plus@3x.png b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_plus_button.imageset/plus@3x.png deleted file mode 100644 index 5462443f..00000000 Binary files a/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_plus_button.imageset/plus@3x.png and /dev/null differ diff --git a/Runnect-iOS/Runnect-iOS/Global/Utils/RNUtils/UserManager.swift b/Runnect-iOS/Runnect-iOS/Global/Utils/RNUtils/UserManager.swift index 50305d6f..a6e58f70 100644 --- a/Runnect-iOS/Runnect-iOS/Global/Utils/RNUtils/UserManager.swift +++ b/Runnect-iOS/Runnect-iOS/Global/Utils/RNUtils/UserManager.swift @@ -64,6 +64,16 @@ final class UserManager { } case .failure(let error): print(error.localizedDescription) + if let response = error.response { + if let responseData = String(data: response.data, encoding: .utf8) { + print("\n ๐Ÿ”ฅ SignIn ๋ฉ”์„ธ์ง€ \(responseData)\n") // ์ด ์ฝ”๋“œ๋Š” 2์ฐจ ์—…๋ฐ์ดํŠธ ํ† ํฐ ๋ถ€๋ถ„ ๋””๋ฒ„๊น… ์šฉ์œผ๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. (์—…๋ฐ์ดํŠธ ์ดํ›„ ์ œ๊ฑฐ) + } else { + print(error.localizedDescription) + } + } else { + print(error.localizedDescription) + } + completion(.failure(.networkFail)) } } @@ -93,6 +103,17 @@ final class UserManager { } case .failure(let error): print(error.localizedDescription) + // ์•„๋ž˜ ์ฝ”๋“œ๋Š” 2์ฐจ ์—…๋ฐ์ดํŠธ ํ† ํฐ ๋ถ€๋ถ„ ๋””๋ฒ„๊น… ์šฉ์œผ๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. (์—…๋ฐ์ดํŠธ ์ดํ›„ ์ œ๊ฑฐ) +// if let response = error.response { +// if let responseData = String(data: response.data, encoding: .utf8) { +// print("\n getNewToken ๋ฉ”์„ธ์ง€ โ€ผ๏ธ๐Ÿ”ฅ\(responseData)\n") +// } else { +// print(error.localizedDescription) +// } +// } else { +// print(error.localizedDescription) +// } + completion(.failure(.networkFail)) } } diff --git a/Runnect-iOS/Runnect-iOS/GoogleSupport/GoogleService-Info.plist b/Runnect-iOS/Runnect-iOS/GoogleSupport/GoogleService-Info.plist deleted file mode 100644 index 2bf115de..00000000 --- a/Runnect-iOS/Runnect-iOS/GoogleSupport/GoogleService-Info.plist +++ /dev/null @@ -1,34 +0,0 @@ - - - - - CLIENT_ID - 772504823881-bp4sinq6869e5jaqv57r3pk2l5mmnefe.apps.googleusercontent.com - REVERSED_CLIENT_ID - com.googleusercontent.apps.772504823881-bp4sinq6869e5jaqv57r3pk2l5mmnefe - API_KEY - AIzaSyAWjFUbR5CKoP8_V5a56aXONโ€”0DkM1faw - GCM_SENDER_ID - 772504823881 - PLIST_VERSION - 1 - BUNDLE_ID - com.runnect.Runnect-iOS - PROJECT_ID - runnect-6c5e8 - STORAGE_BUCKET - runnect-6c5e8.appspot.com - IS_ADS_ENABLED - - IS_ANALYTICS_ENABLED - - IS_APPINVITE_ENABLED - - IS_GCM_ENABLED - - IS_SIGNIN_ENABLED - - GOOGLE_APP_ID - 1:772504823881:ios:e955e328c32c8a864b3798 - - diff --git a/Runnect-iOS/Runnect-iOS/Network/Dto/CourseDetailDto/ResponseDto/UploadedCourseDetailResponseDto.swift b/Runnect-iOS/Runnect-iOS/Network/Dto/CourseDetailDto/ResponseDto/UploadedCourseDetailResponseDto.swift index 9d72ad5c..0c1de183 100644 --- a/Runnect-iOS/Runnect-iOS/Network/Dto/CourseDetailDto/ResponseDto/UploadedCourseDetailResponseDto.swift +++ b/Runnect-iOS/Runnect-iOS/Network/Dto/CourseDetailDto/ResponseDto/UploadedCourseDetailResponseDto.swift @@ -18,7 +18,7 @@ struct UploadedCourseDetailResponseDto: Codable { struct UploadUser: Codable { let nickname: String - let level: String + let level: Int let image: String let isNowUser: Bool? } diff --git a/Runnect-iOS/Runnect-iOS/Network/Dto/CourseDiscoveryDto/ResponseDto/MarathonListResponseDto.swift b/Runnect-iOS/Runnect-iOS/Network/Dto/CourseDiscoveryDto/ResponseDto/MarathonListResponseDto.swift new file mode 100644 index 00000000..f84dc7f0 --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Network/Dto/CourseDiscoveryDto/ResponseDto/MarathonListResponseDto.swift @@ -0,0 +1,31 @@ +// +// MarathonListResponseDto.swift +// Runnect-iOS +// +// Created by ์ด๋ช…์ง„ on 11/26/23. +// + +import Foundation + +// MARK: - MarathonListResponseDto + +struct MarathonListResponseDto: Codable { + let marathonPublicCourses: [marathonCourse] +} + +// MARK: - PublicCourse + +struct marathonCourse: Codable { + let id, courseId: Int + let title: String + let image: String + let scrap: Bool? + let departure: Departure +} + +// MARK: - CourseDiscoveryDeparture + +struct Departure: Codable { + let region: String + let city: String +} diff --git a/Runnect-iOS/Runnect-iOS/Network/Dto/CourseDiscoveryDto/ResponseDto/PickedMapListResponseDto.swift b/Runnect-iOS/Runnect-iOS/Network/Dto/CourseDiscoveryDto/ResponseDto/PickedMapListResponseDto.swift index 37097778..b1d3ec2d 100644 --- a/Runnect-iOS/Runnect-iOS/Network/Dto/CourseDiscoveryDto/ResponseDto/PickedMapListResponseDto.swift +++ b/Runnect-iOS/Runnect-iOS/Network/Dto/CourseDiscoveryDto/ResponseDto/PickedMapListResponseDto.swift @@ -10,6 +10,8 @@ import Foundation // MARK: - PickedMapListResponseDto struct PickedMapListResponseDto: Codable { + let totalPageSize: Int + let isEnd: Bool let publicCourses: [PublicCourse] } @@ -19,7 +21,7 @@ struct PublicCourse: Codable { let id, courseId: Int let title: String let image: String - let scrap: Bool? + var scrap: Bool? let description: String? let distance: Float? let departure: CourseDiscoveryDeparture diff --git a/Runnect-iOS/Runnect-iOS/Network/Dto/CourseDiscoveryDto/ResponseDto/TotalPageCountDto.swift b/Runnect-iOS/Runnect-iOS/Network/Dto/CourseDiscoveryDto/ResponseDto/TotalPageCountDto.swift new file mode 100644 index 00000000..f0f1f3e2 --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Network/Dto/CourseDiscoveryDto/ResponseDto/TotalPageCountDto.swift @@ -0,0 +1,14 @@ +// +// TotalPageCountDto.swift +// Runnect-iOS +// +// Created by ์ด๋ช…์ง„ on 11/26/23. +// + +import Foundation + +// MARK: - TotalPageCountDto + +struct TotalPageCountDto: Codable { + let totalPageCount: Int +} diff --git a/Runnect-iOS/Runnect-iOS/Network/Router/PublicCourseRouter.swift b/Runnect-iOS/Runnect-iOS/Network/Router/PublicCourseRouter.swift index 3c336277..f992f25c 100644 --- a/Runnect-iOS/Runnect-iOS/Network/Router/PublicCourseRouter.swift +++ b/Runnect-iOS/Runnect-iOS/Network/Router/PublicCourseRouter.swift @@ -9,14 +9,16 @@ import Foundation import Moya enum PublicCourseRouter { - case getCourseData(pageNo: Int) + case getCourseData(pageNo: Int, sort: String) case getCourseSearchData(keyword: String) + case getMarathonCourseData + case getTotalPageCount case courseUploadingData(param: CourseUploadingRequestDto) case getUploadedCourseDetail(publicCourseId: Int) case getUploadedCourseInfo case updatePublicCourse(publicCourseId: Int, editCourseRequestDto: EditCourseRequestDto) case deleteUploadedCourse(publicCourseIdList: [Int]) -} +} extension PublicCourseRouter: TargetType { @@ -31,8 +33,12 @@ extension PublicCourseRouter: TargetType { switch self { case .getCourseData, .courseUploadingData: return "/public-course" + case .getMarathonCourseData: + return "/public-course/marathon" case .getCourseSearchData: return "/public-course/search" + case .getTotalPageCount: + return "public-course/total-page-count" case .getUploadedCourseDetail(let publicCourseId): return "/public-course/detail/\(publicCourseId)" case .getUploadedCourseInfo: @@ -46,7 +52,7 @@ extension PublicCourseRouter: TargetType { var method: Moya.Method { switch self { - case .getCourseData, .getCourseSearchData, .getUploadedCourseDetail, .getUploadedCourseInfo: + case .getCourseData, .getCourseSearchData, .getMarathonCourseData, .getUploadedCourseDetail, .getUploadedCourseInfo, .getTotalPageCount: return .get case .courseUploadingData: return .post @@ -59,8 +65,9 @@ extension PublicCourseRouter: TargetType { var task: Moya.Task { switch self { - case .getCourseData(let pageNo): - return .requestParameters(parameters: ["pageNo": pageNo], encoding: URLEncoding.default) + case .getCourseData(let pageNo, let sort): + var parameters: [String: Any] = ["pageNo": pageNo, "sort": sort] + return .requestParameters(parameters: parameters, encoding: URLEncoding.default) case .getCourseSearchData(let keyword): return .requestParameters(parameters: ["keyword": keyword], encoding: URLEncoding.default) case .courseUploadingData(param: let param): @@ -75,7 +82,7 @@ extension PublicCourseRouter: TargetType { fatalError("Encoding ์‹คํŒจ")} case .deleteUploadedCourse(let publicCourseIdList): return .requestParameters(parameters: ["publicCourseIdList": publicCourseIdList], encoding: JSONEncoding.default) - case .getUploadedCourseDetail, .getUploadedCourseInfo: + case .getMarathonCourseData, .getTotalPageCount, .getUploadedCourseDetail, .getUploadedCourseInfo: return .requestPlain } } diff --git a/Runnect-iOS/Runnect-iOS/Network/Service/AuthInterceptor.swift b/Runnect-iOS/Runnect-iOS/Network/Service/AuthInterceptor.swift index 41be8ff1..0f617ef6 100644 --- a/Runnect-iOS/Runnect-iOS/Network/Service/AuthInterceptor.swift +++ b/Runnect-iOS/Runnect-iOS/Network/Service/AuthInterceptor.swift @@ -23,7 +23,7 @@ final class AuthInterceptor: RequestInterceptor { // ๋ฐฉ๋ฌธ์ž์ผ ๊ฒฝ์šฐ if UserManager.shared.userType == .visitor && urlRequest.url?.absoluteString.hasPrefix(Config.baseURL) == true { urlRequest.setValue("visitor", forHTTPHeaderField: "accessToken") - urlRequest.setValue("null", forHTTPHeaderField: "refreshToken") + urlRequest.setValue("visitor", forHTTPHeaderField: "refreshToken") completion(.success(urlRequest)) return } diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseDetail/VC/CourseDetailVC.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseDetail/VC/CourseDetailVC.swift index ed4f7e97..28548e56 100644 --- a/Runnect-iOS/Runnect-iOS/Presentation/CourseDetail/VC/CourseDetailVC.swift +++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseDetail/VC/CourseDetailVC.swift @@ -23,6 +23,8 @@ final class CourseDetailVC: UIViewController { // MARK: - Properties + weak var delegate: ScrapStateDelegate? + private let scrapProvider = Providers.scrapProvider private let PublicCourseProvider = Providers.publicCourseProvider @@ -37,8 +39,6 @@ final class CourseDetailVC: UIViewController { private var publicCourseId: Int? private var isMyCourse: Bool? - private var safariViewController: SFSafariViewController? - // MARK: - UI Components private lazy var navibar = CustomNavigationBar(self, type: .titleWithLeftButton) @@ -144,43 +144,47 @@ extension CourseDetailVC { return } + guard let publicCourseId = publicCourseId else { return } + scrapCourse(scrapTF: !sender.isSelected) + delegate?.didUpdateScrapState(publicCourseId: publicCourseId, isScrapped: !sender.isSelected) + print("CourseDetailVC ์Šคํฌ๋žฉ ํƒญ๐Ÿ”ฅpublicCourseId=\(publicCourseId), isScrapped์€ \(!sender.isSelected)์š”๋ ‡๊ฒŒ ๋ณ€๊ฒฝ ") } @objc func shareButtonTapped() { guard let model = self.uploadedCourseDetailModel else { return } - + let publicCourse = model.publicCourse let title = publicCourse.title let courseId = publicCourse.id // primaryKey let description = publicCourse.description let courseImage = publicCourse.image - + let dynamicLinksDomainURIPrefix = "https://runnect.page.link" guard let link = URL(string: "\(dynamicLinksDomainURIPrefix)/?courseId=\(courseId)") else { return } - + guard let linkBuilder = DynamicLinkComponents(link: link, domainURIPrefix: dynamicLinksDomainURIPrefix) else { return } - + linkBuilder.iOSParameters = DynamicLinkIOSParameters(bundleID: "com.runnect.Runnect-iOS") linkBuilder.iOSParameters?.appStoreID = "1663884202" linkBuilder.iOSParameters?.minimumAppVersion = "1.0.4" - + linkBuilder.socialMetaTagParameters = DynamicLinkSocialMetaTagParameters() linkBuilder.socialMetaTagParameters?.imageURL = URL(string: courseImage) linkBuilder.socialMetaTagParameters?.title = title linkBuilder.socialMetaTagParameters?.descriptionText = description - + guard let longDynamicLink = linkBuilder.url else { return } print("The long URL is: \(longDynamicLink)") - + /// ์งง์€ Dynamic Link๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๋ถ€๋ถ„ ์ž…๋‹ˆ๋‹ค. linkBuilder.shorten { [weak self] url, warnings, error in guard let shortDynamicLink = url else { @@ -189,9 +193,9 @@ extension CourseDetailVC { } return } - + print("๐Ÿ”ฅThe short URL is: \(shortDynamicLink)") - + DispatchQueue.main.async { let activityVC = UIActivityViewController(activityItems: [shortDynamicLink.absoluteString], applicationActivities: nil) activityVC.popoverPresentationController?.sourceView = self?.view @@ -199,7 +203,7 @@ extension CourseDetailVC { } } } - + @objc func startButtonDidTap() { guard handleVisitor() else { return } guard let courseId = self.courseId else { return } @@ -223,7 +227,7 @@ extension CourseDetailVC { $0.dataSource = items $0.textFont = .b3 } - + menu.customCellConfiguration = { (index: Index, _: String, cell: DropDownCell) -> Void in let lastDividerLineRemove = UIView(frame: CGRect(origin: CGPoint(x: 0, y: isMyCourse ? 79 : 39), size: CGSize(width: 170, height: 10))) lastDividerLineRemove.backgroundColor = .white @@ -308,7 +312,6 @@ extension CourseDetailVC { // MARK: - Layout Helpers extension CourseDetailVC { - private func setNavigationBar() { view.addSubview(navibar) view.addSubview(moreButton) diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/AdImageCollectionViewCell.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/AdImageCollectionViewCell.swift index 3d28022b..97142a43 100644 --- a/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/AdImageCollectionViewCell.swift +++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/AdImageCollectionViewCell.swift @@ -29,7 +29,7 @@ class AdImageCollectionViewCell: UICollectionViewCell, UIScrollViewDelegate { // MARK: - Constants final let collectionViewInset = UIEdgeInsets(top: 28, left: 16, bottom: 28, right: 16) - + // MARK: - UI Components var imgBanners: [UIImage] = [ImageLiterals.imgBanner1, ImageLiterals.imgBanner2] var currentPage: Int = 0 @@ -41,7 +41,7 @@ class AdImageCollectionViewCell: UICollectionViewCell, UIScrollViewDelegate { override init(frame: CGRect) { super.init(frame: frame) - layout() + setLayout() setDelegate() startBannerSlide() } @@ -81,10 +81,10 @@ extension AdImageCollectionViewCell { let currentPage = Int(scrollView.contentOffset.x / scrollView.frame.width) pageControl.currentPage = currentPage % imgBanners.count } - + // MARK: - Layout Helpers - func layout() { + private func setLayout() { contentView.backgroundColor = .clear contentView.addSubview(bannerCollectionView) contentView.addSubview(pageControl) @@ -129,18 +129,18 @@ extension AdImageCollectionViewCell: UICollectionViewDelegate, UICollectionViewD cell.contentView.addSubviews(imageView) if indexPath.item == 0 { - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(firstCellTapped(_:))) - imageView.addGestureRecognizer(tapGesture) - } + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(firstCellTapped(_:))) + imageView.addGestureRecognizer(tapGesture) + } return cell } // ์ฒซ ๋ฒˆ์งธ ์…€ ํด๋ฆญ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ - @objc func firstCellTapped(_ gesture: UITapGestureRecognizer) { - // Safari ๋งํฌ๋กœ ์—ฐ๊ฒฐ - if let url = URL(string: "https://docs.google.com/forms/d/1cpgZHNNi1kIvi2ZCwCIcMJcI1PkHBz9a5vWJb7FfIbg/edit") { - UIApplication.shared.open(url) - } - } + @objc func firstCellTapped(_ gesture: UITapGestureRecognizer) { + // Safari ๋งํฌ๋กœ ์—ฐ๊ฒฐ + if let url = URL(string: "https://docs.google.com/forms/d/1cpgZHNNi1kIvi2ZCwCIcMJcI1PkHBz9a5vWJb7FfIbg/edit") { + UIApplication.shared.open(url) + } + } } @@ -150,7 +150,7 @@ extension AdImageCollectionViewCell: UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return self.frame.size } - + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { return 0 } diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/MapCollectionViewCell.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/MapCollectionViewCell.swift index 7bf86702..13a7fb7b 100644 --- a/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/MapCollectionViewCell.swift +++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/MapCollectionViewCell.swift @@ -5,19 +5,23 @@ // Created by YEONOO on 2023/01/10. // +/// ์—ฌ๊ธฐ ์žˆ๋Š” ์ฝ”๋“œ๋Š” ์‚ฌ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. +/// CourseDiscoveryVC์— ๋”ฐ๋กœ ์„ ์–ธ ๋˜์–ด์žˆ์Šต๋‹ˆ๋‹ค. +/// ์ถ”๊ฐ€ ์‚ฌ์šฉ์•ˆํ• ์‹œ ์‚ญ์ œ ์˜ˆ์ • + import UIKit import SnapKit import Then class MapCollectionViewCell: UICollectionViewCell { - + // MARK: - collectionview private lazy var mapCollectionView: UICollectionView = { let layout = UICollectionViewFlowLayout() layout.scrollDirection = .vertical - + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.backgroundColor = .clear collectionView.translatesAutoresizingMaskIntoConstraints = false @@ -31,11 +35,11 @@ class MapCollectionViewCell: UICollectionViewCell { final let collectionViewInset = UIEdgeInsets(top: 28, left: 16, bottom: 28, right: 16) final let itemSpacing: CGFloat = 10 final let lineSpacing: CGFloat = 20 - + // MARK: - Life cycle override init(frame: CGRect) { super.init(frame: frame) - layout() + setLayout() register() setDelegate() } @@ -52,25 +56,26 @@ extension MapCollectionViewCell { } private func register() { mapCollectionView.register(CourseListCVC.self, - forCellWithReuseIdentifier: CourseListCVC.className) + forCellWithReuseIdentifier: CourseListCVC.className) } } -// MARK: - Extensions extension MapCollectionViewCell { // MARK: - Layout Helpers - func layout() { + private func setLayout() { contentView.backgroundColor = .clear contentView.addSubview(mapCollectionView) mapCollectionView.snp.makeConstraints { $0.top.equalToSuperview() $0.leading.trailing.equalTo(contentView.safeAreaLayoutGuide) $0.bottom.equalToSuperview() -// $0.height.equalTo(1000) + // $0.height.equalTo(1000) } } + + } // MARK: - UICollectionViewDelegate, UICollectionViewDataSource @@ -109,5 +114,5 @@ extension MapCollectionViewCell: UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { return self.lineSpacing } - + } diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/MarathonMapCollectionViewCell.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/MarathonMapCollectionViewCell.swift new file mode 100644 index 00000000..03d718a9 --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/MarathonMapCollectionViewCell.swift @@ -0,0 +1,164 @@ +// +// MarathonMapCollectionViewCell.swift +// Runnect-iOS +// +// Created by ์ด๋ช…์ง„ on 11/18/23. +// + +import UIKit +import Combine + +class CourseSelectionPublisher { + static let shared = CourseSelectionPublisher() + + private init() {} + + let didSelectCourse = PassthroughSubject() +} + +class MarathonMapCollectionViewCell: UICollectionViewCell { + private let PublicCourseProvider = Providers.publicCourseProvider + private var marathonCourseList = [marathonCourse]() + + private lazy var marathonCollectionView: UICollectionView = { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .horizontal + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.backgroundColor = .clear + collectionView.isScrollEnabled = true + collectionView.showsHorizontalScrollIndicator = false + return collectionView + }() + + // MARK: - Constants + + private let collectionViewInset = UIEdgeInsets(top: 0, left: 16, bottom: 34, right: 16) + private let itemSpacing: CGFloat = 10 + private let lineSpacing: CGFloat = 10 + + // MARK: - Life cycle + + override init(frame: CGRect) { + super.init(frame: frame) + setLayout() + register() + setDelegate() + getMarathonCourseData() + } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + + // MARK: - Method + +extension MarathonMapCollectionViewCell { + + private func setDelegate() { + marathonCollectionView.delegate = self + marathonCollectionView.dataSource = self + } + private func register() { + marathonCollectionView.register(CourseListCVC.self, + forCellWithReuseIdentifier: CourseListCVC.className) + } +} + +extension MarathonMapCollectionViewCell { + + // MARK: - Layout Helpers + + private func setLayout() { + contentView.backgroundColor = .clear + contentView.addSubview(marathonCollectionView) + + marathonCollectionView.snp.makeConstraints { + $0.top.equalToSuperview() + $0.leading.trailing.equalTo(contentView.safeAreaLayoutGuide) + $0.bottom.equalToSuperview() + } + } + + private func setData(marathonCourseList: [marathonCourse]) { + self.marathonCourseList = marathonCourseList + marathonCollectionView.reloadData() + } + +} + // MARK: - UICollectionViewDelegate, UICollectionViewDataSource + +extension MarathonMapCollectionViewCell: UICollectionViewDelegate, UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return marathonCourseList.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CourseListCVC.className, + for: indexPath) + as? CourseListCVC else { return UICollectionViewCell() } + cell.setCellType(type: .all) + let model = self.marathonCourseList[indexPath.item] + let location = "\(model.departure.region) \(model.departure.city)" + cell.setData(imageURL: model.image, title: model.title, location: location, didLike: model.scrap, indexPath: indexPath.item) + return cell + } +} + + // MARK: - UICollectionViewDelegateFlowLayout + +extension MarathonMapCollectionViewCell: UICollectionViewDelegateFlowLayout { + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + return CGSize(width: 156, height: 160) + } // ์…€์‚ฌ์ด์ฆˆ RecommendedCVC์™€ ์—ฐ๊ฒฐ ๋˜๋ฉด ๋ณ€๊ฒฝ + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { + return self.collectionViewInset + } + + // ์ด๊ฒŒ ๋ฐ˜๋Œ€๋ฐฉํ–ฅ + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { + return self.itemSpacing + } + + // ์ด๊ฒŒ ์Šคํฌ๋กค๋ฐฉํ–ฅ + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { + return self.lineSpacing + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + CourseSelectionPublisher.shared.didSelectCourse.send(indexPath) + // ์ฝ”์Šค ๋ฐœ๊ฒฌ์— ์ด๋ฒคํŠธ ์ „๋‹ฌ + } + +} + + + // MARK: - NetWork + +extension MarathonMapCollectionViewCell { + private func getMarathonCourseData() { + LoadingIndicator.showLoading() + PublicCourseProvider.request(.getMarathonCourseData) { response in + LoadingIndicator.hideLoading() + switch response { + case .success(let result): + let status = result.statusCode + if 200..<300 ~= status { + do { + let responseDto = try result.map(BaseResponse.self) + guard let data = responseDto.data else { return } + self.setData(marathonCourseList: data.marathonPublicCourses) + } catch { + print(error.localizedDescription) + } + } + if status >= 400 { + print("400 error") + } + case .failure(let error): + print(error.localizedDescription) + } + } + } +} diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/MarathonTitleCollectionViewCell.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/MarathonTitleCollectionViewCell.swift new file mode 100644 index 00000000..7ba7c535 --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/MarathonTitleCollectionViewCell.swift @@ -0,0 +1,55 @@ +// +// MarathonTitleCollectionViewCell.swift +// Runnect-iOS +// +// Created by ์ด๋ช…์ง„ on 11/18/23. +// + +import UIKit + +class MarathonTitleCollectionViewCell: UICollectionViewCell { + + // MARK: - UI Components + + private lazy var titleStackView = UIStackView(arrangedSubviews: [mainLabel, subLabel]).then { + $0.axis = .vertical + $0.spacing = 6 + $0.alignment = .leading + } + + private let mainLabel: UILabel = UILabel().then { + $0.text = "2023 ๋งˆ๋ผํ†ค ์ฝ”์Šค" + $0.font = UIFont.h3 + $0.textColor = UIColor.g1 + } + + private let subLabel: UILabel = UILabel().then { + $0.text = "์‹ค์ œ ๋งˆ๋ผํ†ค ์ฝ”์Šค๋ฅผ ๋งŒ๋‚˜๋ณด์„ธ์š”" + $0.font = UIFont.b6 + $0.textColor = UIColor.g2 + } + + // MARK: - Life cycle + + override init(frame: CGRect) { + super.init(frame: frame) + setLayout() + } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + // MARK: - Layout Helpers + +extension MarathonTitleCollectionViewCell { + private func setLayout() { + contentView.backgroundColor = .clear + contentView.addSubview(titleStackView) + + titleStackView.snp.makeConstraints { + $0.centerY.equalToSuperview() + $0.leading.equalToSuperview().inset(16) + } + } +} diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/TitleCollectionViewCell.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/TitleCollectionViewCell.swift index 365c1c34..a52bb527 100644 --- a/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/TitleCollectionViewCell.swift +++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/TitleCollectionViewCell.swift @@ -2,62 +2,125 @@ // TitleCollectionViewCell.swift // Runnect-iOS // -// Created by YEONOO on 2023/01/10. +// Created by ์ด๋ช…์ง„ on 2023/11/21. // import UIKit -import SnapKit +import Combine -import Then +protocol TitleCollectionViewCellDelegate: AnyObject { + func didTapSortButton(ordering: String) +} class TitleCollectionViewCell: UICollectionViewCell { + private var cancellables: Set = [] + weak var delegate: TitleCollectionViewCellDelegate? + // MARK: - UI Components private lazy var titleStackView = UIStackView(arrangedSubviews: [mainLabel, subLabel]).then { $0.axis = .vertical - $0.spacing = 4 + $0.spacing = 6 $0.alignment = .leading } - private let mainLabel: UILabel = { - let label = UILabel() - label.text = "์ด๋Ÿฐ ์ฝ”์Šค ์–ด๋•Œ์š”?" - label.font = UIFont.h4 - label.textColor = UIColor.g1 - return label - }() - private let subLabel: UILabel = { - let label = UILabel() - label.text = "์ƒ์พŒํ•œ ํ•˜๋ฃจ๋ฅผ ์‹œ์ž‘ํ•˜๊ฒŒ ๋งŒ๋“ค์–ด์ฃผ๋Š” ๋Ÿฌ๋‹์ฝ”์Šค" - label.font = UIFont.b6 - label.textColor = UIColor.g1 - return label - }() + private let divideView = UIView().then { + $0.backgroundColor = .g4 + } + + private let mainLabel: UILabel = UILabel().then { + $0.text = "์ด๋Ÿฐ ์ฝ”์Šค ์–ด๋•Œ์š”?" + $0.font = UIFont.h3 + $0.textColor = UIColor.g1 + } + + private let subLabel: UILabel = UILabel().then { + $0.text = "๋‚˜์—๊ฒŒ ์ตœ์ ํ™”๋œ ์ฝ”์Šค๋ฅผ ์ฐพ์•„๋ณด์„ธ์š”" + $0.font = UIFont.b6 + $0.textColor = UIColor.g2 + } + + private lazy var dateSortButton: UIButton = createSortButton(title: "์ตœ์‹ ์ˆœ", ordering: "date").then { + $0.isSelected = true + $0.titleLabel?.font = .h5 + } + + private lazy var scrapSortButton: UIButton = createSortButton(title: "์Šคํฌ๋žฉ์ˆœ", ordering: "scrap") // MARK: - Life cycle override init(frame: CGRect) { super.init(frame: frame) - layout() + setLayout() } + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } -// MARK: - Extensions + +// MARK: - Method extension TitleCollectionViewCell { - - // MARK: - Layout Helpers - - func layout() { - contentView.backgroundColor = .clear - contentView.addSubview(titleStackView) + private func createSortButton(title: String, ordering: String) -> UIButton { + let button = UIButton(type: .custom).then { + $0.setTitle(title, for: .normal) + $0.titleLabel?.font = .b3 + $0.setTitleColor(.g2, for: .normal) + } + + button.setTitleColor(.m1, for: .selected) + + button.tapPublisher + .sink { [weak self] in + guard let self = self else { return } + + self.dateSortButton.isSelected = (button == self.dateSortButton) + self.scrapSortButton.isSelected = (button == self.scrapSortButton) + + self.dateSortButton.setTitleColor(self.dateSortButton.isSelected ? .m1 : .g2, for: .normal) + self.scrapSortButton.setTitleColor(self.scrapSortButton.isSelected ? .m1 : .g2, for: .normal) + self.dateSortButton.titleLabel?.font = self.dateSortButton.isSelected ? .h5 : .b3 + self.scrapSortButton.titleLabel?.font = self.scrapSortButton.isSelected ? .h5 : .b3 + + if button.isSelected { + self.delegate?.didTapSortButton(ordering: ordering) + } + } + .store(in: &cancellables) + + return button + } +} +// MARK: - Layout + +extension TitleCollectionViewCell { + private func setLayout() { + contentView.backgroundColor = .clear + contentView.addSubviews(titleStackView, divideView, dateSortButton, scrapSortButton) + titleStackView.snp.makeConstraints { $0.centerY.equalToSuperview() $0.leading.equalToSuperview().inset(16) } + + divideView.snp.makeConstraints { + $0.top.equalTo(titleStackView.snp.top).offset(-34) + $0.centerX.equalToSuperview().inset(16) + $0.width.equalTo(358) + $0.height.equalTo(1) + } + + dateSortButton.snp.makeConstraints { + $0.top.equalTo(divideView.snp.bottom).offset(54) + $0.leading.equalTo(titleStackView.snp.trailing).offset(57) + } + + scrapSortButton.snp.makeConstraints { + $0.top.equalTo(divideView.snp.bottom).offset(54) + $0.leading.equalTo(dateSortButton.snp.trailing).offset(8) + } } } diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/VC/CourseDiscoveryVC.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/VC/CourseDiscoveryVC.swift index 488528fe..ac7f271d 100644 --- a/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/VC/CourseDiscoveryVC.swift +++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/VC/CourseDiscoveryVC.swift @@ -2,33 +2,42 @@ // CourseDiscoveryVC.swift // Runnect-iOS // -// Created by sejin on 2023/01/01. +// Created by ์ด๋ช…์ง„ on 2023/11/21. // import UIKit - import Then import SnapKit import Combine import Moya +protocol ScrapStateDelegate: AnyObject { + func didUpdateScrapState(publicCourseId: Int, isScrapped: Bool) + // ์ฝ”์Šค ์ƒ์„ธ ์—์„œ ์Šคํฌ๋žฉ ๋ˆ„๋ฅด๋ฉด ์ฝ”์Šค๋ฐœ๊ฒฌ์— ํ•ด๋‹น ๋ถ€๋ถ„ ์Šคํฌ๋žฉ ๋ˆ„๋ฅด๋Š” ์ด๋ฒคํŠธ ์ „๋‹ฌ +} + final class CourseDiscoveryVC: UIViewController { - // MARK: - Properties - private let PublicCourseProvider = Providers.publicCourseProvider + // MARK: - Properties + private let publicCourseProvider = Providers.publicCourseProvider private let scrapProvider = Providers.scrapProvider + private let serverResponseNumber = 10 private var courseList = [PublicCourse]() - - // pagination ์— ๊ผญ ํ•„์š”ํ•œ ์œ„ํ•œ ๋ณ€์ˆ˜๋“ค ์ž…๋‹ˆ๋‹ค. + private var cancelBag = CancelBag() + private var specialList = [String]() + private var totalPageNum = 0 + private var isEnd = false private var pageNo = 1 - + private var sort = "date" private var isDataLoaded = false + private var uploadButtonChanged = false // MARK: - UIComponents private lazy var naviBar = CustomNavigationBar(self, type: .title).setTitle("์ฝ”์Šค ๋ฐœ๊ฒฌ") + private let searchButton = UIButton(type: .system).then { $0.setImage(ImageLiterals.icSearch, for: .normal) $0.tintColor = .g1 @@ -41,6 +50,10 @@ final class CourseDiscoveryVC: UIViewController { $0.titleEdgeInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 0) } + private let miniUploadButton = UIButton(type: .system).then { + $0.setImage(ImageLiterals.icPlusButton, for: .normal) + } + private let emptyView = ListEmptyView(description: "๊ณต์œ ํ•  ์ˆ˜ ์žˆ๋Š” ์ฝ”์Šค๊ฐ€ ์—†์–ด์š”!\n์ฝ”์Šค๋ฅผ ๊ทธ๋ ค์ฃผ์„ธ์š”", buttonTitle: "์ฝ”์Šค ๊ทธ๋ฆฌ๊ธฐ") @@ -49,7 +62,6 @@ final class CourseDiscoveryVC: UIViewController { private lazy var mapCollectionView: UICollectionView = { let layout = UICollectionViewFlowLayout() layout.scrollDirection = .vertical - let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.backgroundColor = .clear collectionView.isScrollEnabled = true @@ -65,14 +77,15 @@ final class CourseDiscoveryVC: UIViewController { register() setNavigationBar() setDelegate() - layout() + setLayout() setAddTarget() + setCombineEvent() + self.getCourseData() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) self.hideTabBar(wantsToHide: false) - setDataLoadIfNeeded() } } @@ -93,29 +106,35 @@ extension CourseDiscoveryVC { } private func register() { - self.mapCollectionView.register(AdImageCollectionViewCell.self, forCellWithReuseIdentifier: AdImageCollectionViewCell.className) - self.mapCollectionView.register(TitleCollectionViewCell.self, forCellWithReuseIdentifier: TitleCollectionViewCell.className) - self.mapCollectionView.register(CourseListCVC.self, forCellWithReuseIdentifier: CourseListCVC.className) + let cellTypes: [UICollectionViewCell.Type] = [AdImageCollectionViewCell.self, + MarathonTitleCollectionViewCell.self, + MarathonMapCollectionViewCell.self, + TitleCollectionViewCell.self, + CourseListCVC.self] + cellTypes.forEach { cellType in + mapCollectionView.register(cellType, forCellWithReuseIdentifier: cellType.className) + } } private func setAddTarget() { self.searchButton.addTarget(self, action: #selector(pushToSearchVC), for: .touchUpInside) self.uploadButton.addTarget(self, action: #selector(pushToDiscoveryVC), for: .touchUpInside) + self.miniUploadButton.addTarget(self, action: #selector(pushToDiscoveryVC), for: .touchUpInside) } - private func setDataLoadIfNeeded() { /// ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›๊ณ  ๋‹ค๋ฅธ ๋ทฐ๋ฅผ ๊ฐ”๋‹ค๊ฐ€ ์™€๋„ ๋ฐ์ดํ„ฐ๊ฐ€ ์œ ์ง€๋˜๊ฒŒ๋” ํ•˜๊ธฐ ์œ„ํ•œ ํ•จ์ˆ˜ ์ž…๋‹ˆ๋‹ค. (ํ•œ๋ฒˆ๋งŒ ํ˜ธ์ถœ๋˜๋ฉด ๋˜๋Š” ํ•จ์ˆ˜!) - if !isDataLoaded { - // ์•ฑ์ด ์‹คํ–‰ ๋ ๋•Œ ์ฒ˜์Œ์—๋งŒ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” - courseList.removeAll() - pageNo = 1 - - // ์ปฌ๋ ‰์…˜ ๋ทฐ๋ฅผ ๋ฆฌ๋กœ๋“œํ•˜์—ฌ ์ดˆ๊ธฐํ™”๋œ ๋ฐ์ดํ„ฐ๋ฅผ ํ™”๋ฉด์— ํ‘œ์‹œ - mapCollectionView.reloadData() - self.getCourseData() - - isDataLoaded = true // ๋ฐ์ดํ„ฐ๊ฐ€ ๋กœ๋“œ๋˜์—ˆ์Œ์„ ํ‘œ์‹œ - } else { - return + private func setCombineEvent() { + CourseSelectionPublisher.shared.didSelectCourse + .sink { [weak self] indexPath in + self?.setMarathonCourseSelection(at: indexPath) + } + .store(in: cancelBag) + } + + private func reloadCellForCourse(publicCourseId: Int) { + if let index = courseList.firstIndex(where: { $0.id == publicCourseId }) { + let indexPath = IndexPath(item: index, section: Section.courseList) + mapCollectionView.reloadItems(at: [indexPath]) + print("\(indexPath) ๋ถ€๋ถ„ ์Šคํฌ๋žฉ ๊ต์ฒด ๋˜์—ˆ์Œ") } } } @@ -146,6 +165,8 @@ extension CourseDiscoveryVC { view.backgroundColor = .w1 mapCollectionView.backgroundColor = .w1 self.emptyView.isHidden = true + self.miniUploadButton.isHidden = true + self.uploadButton.isHidden = false } private func setNavigationBar() { @@ -162,9 +183,10 @@ extension CourseDiscoveryVC { } } - private func layout() { - view.addSubviews(uploadButton, mapCollectionView) + private func setLayout() { + view.addSubviews(mapCollectionView, uploadButton, miniUploadButton) view.bringSubviewToFront(uploadButton) + view.bringSubviewToFront(miniUploadButton) mapCollectionView.addSubview(emptyView) mapCollectionView.snp.makeConstraints { @@ -179,22 +201,38 @@ extension CourseDiscoveryVC { make.width.equalTo(92) } + miniUploadButton.snp.makeConstraints { make in + make.trailing.equalTo(self.view.safeAreaLayoutGuide).inset(38) + make.bottom.equalTo(self.view.safeAreaLayoutGuide).inset(20) + make.width.height.equalTo(41) + } + emptyView.snp.makeConstraints { make in make.top.equalTo(naviBar.snp.bottom).offset(300) make.centerX.equalTo(naviBar) } - let shadowView = ShadowView() - self.view.addSubview(shadowView) - - shadowView.snp.makeConstraints { make in - make.trailing.equalTo(self.view.safeAreaLayoutGuide).inset(16) - make.bottom.equalTo(self.view.safeAreaLayoutGuide).inset(20) - make.height.equalTo(40) - make.width.equalTo(92) - } - self.view.bringSubviewToFront(uploadButton) + self.view.bringSubviewToFront(miniUploadButton) + + } +} + +// MARK: - Constants + +extension CourseDiscoveryVC { + private enum Section { + static let adImage = 0 // ๊ด‘๊ณ  ์ด๋ฏธ์ง€ + static let marathonTitle = 1 // ๋งˆ๋ผํ†ค ์ฝ”์Šค ์„ค๋ช… + static let recommendedList = 2 // ๋งˆ๋ผํ†ค ์ฝ”์Šค + static let title = 3 // ์ถ”์ฒœ ์ฝ”์Šค ์„ค๋ช… + static let courseList = 4 // ์ถ”์ฒœ ์ฝ”์Šค + } + + private enum Layout { + static let cellSpacing: CGFloat = 20 + static let interitemSpacing: CGFloat = 10 + static let sectionInset = UIEdgeInsets(top: 0, left: 16, bottom: 20, right: 16) } } @@ -202,14 +240,14 @@ extension CourseDiscoveryVC { extension CourseDiscoveryVC: UICollectionViewDelegate, UICollectionViewDataSource { func numberOfSections(in collectionView: UICollectionView) -> Int { - return 3 + return 5 } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { switch section { - case 0, 1: + case Section.adImage, Section.marathonTitle, Section.recommendedList, Section.title: return 1 - case 2: + case Section.courseList: return self.courseList.count default: return 0 @@ -217,61 +255,54 @@ extension CourseDiscoveryVC: UICollectionViewDelegate, UICollectionViewDataSourc } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - if indexPath.section == 0 { + switch indexPath.section { + case Section.adImage: guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: AdImageCollectionViewCell.className, for: indexPath) as? AdImageCollectionViewCell else { return UICollectionViewCell() } return cell - } else if indexPath.section == 1 { - guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TitleCollectionViewCell.className, for: indexPath) as? TitleCollectionViewCell else { return UICollectionViewCell() } + case Section.marathonTitle: + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MarathonTitleCollectionViewCell.className, for: indexPath) as? MarathonTitleCollectionViewCell else { return UICollectionViewCell() } return cell - } else { - guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CourseListCVC.className, for: indexPath) as? CourseListCVC else { return UICollectionViewCell() } - cell.setCellType(type: .all) + case Section.recommendedList: + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MarathonMapCollectionViewCell.className, for: indexPath) as? MarathonMapCollectionViewCell else { return UICollectionViewCell() } + return cell + case Section.title: + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TitleCollectionViewCell.className, for: indexPath) as? TitleCollectionViewCell else { return UICollectionViewCell() } cell.delegate = self - let model = self.courseList[indexPath.item] - let location = "\(model.departure.region) \(model.departure.city)" - cell.setData(imageURL: model.image, title: model.title, location: location, didLike: model.scrap, indexPath: indexPath.item) return cell + case Section.courseList: + return courseListCell(collectionView: collectionView, indexPath: indexPath) + default: + return UICollectionViewCell() } } - func scrollViewDidScroll(_ scrollView: UIScrollView) { - let contentOffsetY = scrollView.contentOffset.y - let collectionViewHeight = mapCollectionView.contentSize.height - let paginationY = collectionViewHeight * 0.2 - - // ์Šคํฌ๋กค์ด 80% (0.2) ๊นŒ์ง€ ๋„๋‹ฌํ•˜๋ฉด ๋‹ค์Œ ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ต๋‹ˆ๋‹ค. - if contentOffsetY >= collectionViewHeight - paginationY { - if courseList.count < pageNo * 24 { // ํŽ˜์ด์ง€ ๋์— ๋„๋‹ฌํ•˜๋ฉด ํ˜„์žฌ ํŽ˜์ด์ง€์— ๋” ์ด์ƒ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Œ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. - // ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ค‘๋‹จ ์ฝ”๋“œ - return - } - - // ๋‹ค์Œ ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ๋ฅผ ์ฆ๊ฐ€์‹œํ‚ต๋‹ˆ๋‹ค. - pageNo += 1 - print("๐Ÿ”ฅ๋‹ค์Œ ํŽ˜์ด์ง€ ๋กœ๋“œ: \(pageNo)๐Ÿ”ฅ") - - // ์—ฌ๊ธฐ์—์„œ ๋‹ค์Œ ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜์„ธ์š”. - getCourseData() - } + private func courseListCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CourseListCVC.className, for: indexPath) as? CourseListCVC else { return UICollectionViewCell() } + cell.setCellType(type: .all) + cell.delegate = self + let model = self.courseList[indexPath.item] + let location = "\(model.departure.region) \(model.departure.city)" + cell.setData(imageURL: model.image, title: model.title, location: location, didLike: model.scrap, indexPath: indexPath.item) + return cell } - } // MARK: - UICollectionViewDelegateFlowLayout extension CourseDiscoveryVC: UICollectionViewDelegateFlowLayout { - func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { - _ = UICollectionViewCell() - let screenWidth = UIScreen.main.bounds.width switch indexPath.section { - case 0: - return CGSize(width: screenWidth, height: screenWidth * (183/390)) - case 1: - return CGSize(width: screenWidth, height: 80) - case 2: + case Section.adImage: + return CGSize(width: screenWidth, height: screenWidth * (174/390)) + case Section.marathonTitle: + return CGSize(width: screenWidth, height: 98) + case Section.recommendedList: + return CGSize(width: screenWidth, height: 194) + case Section.title: + return CGSize(width: screenWidth, height: 106) + case Section.courseList: let cellWidth = (screenWidth - 42) / 2 let cellHeight = CourseListCVCType.getCellHeight(type: .all, cellWidth: cellWidth) return CGSize(width: cellWidth, height: cellHeight) @@ -280,39 +311,112 @@ extension CourseDiscoveryVC: UICollectionViewDelegateFlowLayout { } } + /// section ์ด 4์ผ๋•Œ๋งŒ ์ •ํ•ด์ง„ ๋ ˆ์ด์•„์›ƒ ๋ฆฌํ„ด func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { - if section == 2 { - return 20 - } - return 0 + return section == Section.courseList ? Layout.cellSpacing : 0 } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { - if section == 2 { - return 10 - } - return 0 + return section == Section.courseList ? Layout.interitemSpacing : 0 } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { - if section == 2 { - return UIEdgeInsets(top: 0, left: 16, bottom: 20, right: 16) - } - return .zero + return section == Section.courseList ? Layout.sectionInset : .zero } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - if indexPath.section == 2 { + if indexPath.section == Section.courseList { let courseDetailVC = CourseDetailVC() + courseDetailVC.delegate = self let courseModel = courseList[indexPath.item] courseDetailVC.setCourseId(courseId: courseModel.courseId, publicCourseId: courseModel.id) courseDetailVC.hidesBottomBarWhenPushed = true - self.navigationController?.pushViewController(courseDetailVC, animated: true) + navigationController?.pushViewController(courseDetailVC, animated: true) } } + + // ์™ธ๋ถ€์—์„œ Marathon Cell์—์„œ ๋ฐ›์•„์˜ค๋Š” indexPath๋ฅผ ์ฒ˜๋ฆฌ ํ•ฉ๋‹ˆ๋‹ค. + private func setMarathonCourseSelection(at indexPath: IndexPath) { + let courseDetailVC = CourseDetailVC() + let courseModel = courseList[indexPath.item] + courseDetailVC.setCourseId(courseId: courseModel.courseId, publicCourseId: courseModel.id) + courseDetailVC.hidesBottomBarWhenPushed = true + navigationController?.pushViewController(courseDetailVC, animated: true) + } } -// MARK: - CourseListCVCDeleagte +// MARK: - UIScrollViewDelegate + +extension CourseDiscoveryVC: UIScrollViewDelegate { + func scrollViewDidScroll(_ scrollView: UIScrollView) { + performPagination() + changeButtonStyleOnScroll() + } + + private func performPagination() { + let contentOffsetY = mapCollectionView.contentOffset.y // ์šฐ๋ฆฌ๊ฐ€ ๋ณด๋Š” ํ™”๋ฉด + let collectionViewHeight = mapCollectionView.contentSize.height // ์ „์ฒด ์‚ฌ์ด์ฆˆ + let paginationY = mapCollectionView.bounds.size.height // ์œ ์ € ํ™”๋ฉด์˜ ๊ฐ€์žฅ ์•„๋ž˜ y์ถ• ์ด๋ผ๊ณ  ์ƒ๊ฐ + + if contentOffsetY > collectionViewHeight - paginationY { + if courseList.count < pageNo * serverResponseNumber { + // ํŽ˜์ด์ง€ ๋์— ๋„๋‹ฌํ•˜๋ฉด ํ˜„์žฌ ํŽ˜์ด์ง€์— ๋” ์ด์ƒ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Œ์„ ์˜๋ฏธ + // ์ƒˆ๋กœ์˜จ ๋ฐ์ดํ„ฐ์˜ ๊ฐฏ์ˆ˜๊ฐ€ ์›๋ž˜ ์„œ๋ฒ„์—์„œ ์‘๋‹ต์—์„œ ์˜จ ๊ฐฏ์ˆ˜๋ณด๋‹ค ์ž‘์œผ๋ฉด ํŽ˜์ด์ง€๋„ค์ด์…˜ ๊ธˆ์ง€ + // ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ค‘๋‹จ ์ฝ”๋“œ + return + } + print("๐Ÿซ \(pageNo)") + if pageNo < totalPageNum { + if !isDataLoaded { + isDataLoaded = true + getCourseData() + pageNo += 1 + isDataLoaded = false + } + } + } + } + + private func changeButtonStyleOnScroll() { + let contentOffsetY = mapCollectionView.contentOffset.y + let scrollThreshold = mapCollectionView.bounds.size.height * 0.1 // 10% ์Šคํฌ๋กค ํ–ˆ์œผ๋ฉด UI ๋ณ€๊ฒฝ + + if contentOffsetY > scrollThreshold { + handleButtonVisibility(uploadButtonChanged: true, hidden: true, miniHidden: false) + } else { + handleButtonVisibility(uploadButtonChanged: false, hidden: false, miniHidden: true) + } + } + + private func handleButtonVisibility(uploadButtonChanged: Bool, hidden: Bool, miniHidden: Bool) { + guard self.uploadButtonChanged != uploadButtonChanged else { return } + + toggleUploadButtons(hidden: hidden, miniHidden: miniHidden) + self.uploadButtonChanged = uploadButtonChanged + } + + private func toggleUploadButtons(hidden: Bool, miniHidden: Bool) { + animateButtonTransition(button: uploadButton, hidden: hidden, delay: 0) + animateButtonTransition(button: miniUploadButton, hidden: miniHidden, delay: 0.1) // ์˜ˆ์‹œ๋กœ 0.25์ดˆ ๋”œ๋ ˆ์ด ์ ์šฉ + } + + private func animateButtonTransition(button: UIButton, hidden: Bool, delay: TimeInterval) { + let scale: CGFloat = hidden ? 0.1 : 1.0 + let alpha: CGFloat = hidden ? 0.0 : 1.0 + + UIView.animate(withDuration: 0.5, delay: delay, options: .transitionCurlUp, animations: { + button.transform = CGAffineTransform(scaleX: scale, y: scale) + button.alpha = alpha + }) { _ in + button.isHidden = hidden + + // Reset transform after animation completes + button.transform = CGAffineTransform.identity + } + } +} + +// MARK: - CourseListCVCDelegate extension CourseDiscoveryVC: CourseListCVCDeleagte { func likeButtonTapped(wantsTolike: Bool, index: Int) { @@ -322,7 +426,20 @@ extension CourseDiscoveryVC: CourseListCVCDeleagte { } let publicCourseId = courseList[index].id - scrapCourse(publicCourseId: publicCourseId, scrapTF: wantsTolike) + self.scrapCourse(publicCourseId: publicCourseId, scrapTF: wantsTolike) + } +} + +// MARK: - CourseDetailVCDelegate + +extension CourseDiscoveryVC: ScrapStateDelegate { + func didUpdateScrapState(publicCourseId: Int, isScrapped: Bool) { + // CourseDetail์—์„œ id์™€ scrap์ •๋ณด๋ฅผ ๋ฐ›์•„์™€ ์—ฌ๊ธฐ์„œ ์ฒ˜๋ฆฌ + if let index = courseList.firstIndex(where: { $0.id == publicCourseId }) { + courseList[index].scrap = isScrapped + reloadCellForCourse(publicCourseId: publicCourseId) + print("โ€ผ๏ธCourseDiscoveryVC ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ ๋ฐ›์Œ index=\(index)") + } } } @@ -330,23 +447,52 @@ extension CourseDiscoveryVC: CourseListCVCDeleagte { extension CourseDiscoveryVC { private func getCourseData() { + LoadingIndicator.showLoading() // ํ•ญ์ƒ 0.5์ดˆ ๋Šฆ๊ฒŒ ๋กœ๋”ฉ์ด ๋˜์–ด ๋ฒ„๋ฆผ 0.5์ดˆ๋ฅผ ๋„ฃ์€ ์ด์œ ๋Š” pagination์„ ๊ตฌํ˜„ํ• ๋•Œ ํ•œ๋ฒˆ์— ๋‹ค ๋ฐ›์•„์˜ค์ง€ ์•Š๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•จ + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [self] in + publicCourseProvider.request(.getCourseData(pageNo: pageNo, sort: sort)) { response in + LoadingIndicator.hideLoading() + print("โ€ผ๏ธ sort= \(self.sort) โ€ผ๏ธ\n") + switch response { + case .success(let result): + let status = result.statusCode + if 200..<300 ~= status { + do { + let responseDto = try result.map(BaseResponse.self) + guard let data = responseDto.data else { return } + self.totalPageNum = data.totalPageSize + self.isEnd = data.isEnd + self.courseList.append(contentsOf: data.publicCourses) + self.mapCollectionView.reloadData() + print("isEnd= \(self.isEnd), totalPageNum= \(self.totalPageNum)") + } catch { + print(error.localizedDescription) + } + } + if status >= 400 { + print("400 error") + self.showNetworkFailureToast() + } + case .failure(let error): + print(error.localizedDescription) + self.showNetworkFailureToast() + } + } + } + } + + private func getTotalPageNum() { LoadingIndicator.showLoading() - PublicCourseProvider.request(.getCourseData(pageNo: pageNo)) { response in + publicCourseProvider.request(.getTotalPageCount) { response in LoadingIndicator.hideLoading() switch response { case .success(let result): let status = result.statusCode if 200..<300 ~= status { do { - let responseDto = try result.map(BaseResponse.self) + let responseDto = try result.map(BaseResponse.self) guard let data = responseDto.data else { return } - - // ์ƒˆ๋กœ ๋ฐ›์€ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ์กด ๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€ (์Œ“๊ธฐ ์œ„ํ•จ) - self.courseList.append(contentsOf: data.publicCourses) - - // UI๋ฅผ ์—…๋ฐ์ดํŠธํ•˜์—ฌ ์ถ”๊ฐ€๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค. - self.mapCollectionView.reloadData() - + self.totalPageNum = data.totalPageCount + print("์ถ”์ฒœ ์ฝ”์Šค์˜ ์ฝ”์Šค์˜ ์ˆ˜๋Š” \(self.totalPageNum) ์ž…๋‹ˆ๋‹ค. ๐Ÿƒโ€โ™€๏ธ\n") } catch { print(error.localizedDescription) } @@ -361,7 +507,7 @@ extension CourseDiscoveryVC { } } } - + private func scrapCourse(publicCourseId: Int, scrapTF: Bool) { LoadingIndicator.showLoading() scrapProvider.request(.createAndDeleteScrap(publicCourseId: publicCourseId, scrapTF: scrapTF)) { [weak self] response in @@ -392,3 +538,14 @@ extension CourseDiscoveryVC: ListEmptyViewDelegate { self.tabBarController?.selectedIndex = 0 } } + +extension CourseDiscoveryVC: TitleCollectionViewCellDelegate { + func didTapSortButton(ordering: String) { + // ๊ธฐ์กด์˜ getCourseData ํ•จ์ˆ˜ ํ˜ธ์ถœ์„ getSortedCourseData๋กœ ๋ณ€๊ฒฝ + pageNo = 1 + print("โ€ผ๏ธ\(ordering)โ€ผ๏ธ ํ„ฐ์น˜ ํ•˜์…จ์Šต๋‹ˆ๋‹ค. 0.7์ดˆ ํ›„์— โ€ผ๏ธ\(ordering)โ€ผ๏ธ ์œผ๋กœ ์ •๋ ฌ์ด ๋˜๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ๋ถˆ๋Ÿฌ ์˜ต๋‹ˆ๋‹ค.") + sort = ordering + self.courseList.removeAll() + getCourseData() + } +} diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/VC/CourseSearchVC.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/VC/CourseSearchVC.swift index 1e5f66b9..ac5879b2 100644 --- a/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/VC/CourseSearchVC.swift +++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseDiscovery/Views/VC/CourseSearchVC.swift @@ -209,8 +209,8 @@ extension CourseSearchVC: CourseListCVCDeleagte { return } - let pubilcCourseId = courseList[index].id - scrapCourse(publicCourseId: pubilcCourseId, scrapTF: wantsTolike) + let publicCourseId = courseList[index].id + scrapCourse(publicCourseId: publicCourseId, scrapTF: wantsTolike) } } diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/Views/CVC/MarathonCourseListCVC.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/Views/CVC/MarathonCourseListCVC.swift new file mode 100644 index 00000000..97981b83 --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/Views/CVC/MarathonCourseListCVC.swift @@ -0,0 +1,202 @@ +// +// MarathonCourseListCVC.swift +// Runnect-iOS +// +// Created by ์ด๋ช…์ง„ on 11/21/23. +// + +import UIKit + +protocol MarathonCourseListCVCDeleagte: AnyObject { + func likeButtonTapped(wantsTolike: Bool, index: Int) +} + +@frozen +enum MarathonCourseListCVCType { + case title + case titleWithLocation + case all + + static func getCellHeight(type: CourseListCVCType, cellWidth: CGFloat) -> CGFloat { + let imageHeight = cellWidth * (111/156) + switch type { + case .title: + return imageHeight + 24 + case .titleWithLocation, .all: + return imageHeight + 40 + } + } +} + +final class MarathonCourseListCVC: UICollectionViewCell { + + // MARK: - Properties + + weak var delegate: MarathonCourseListCVCDeleagte? + + private var indexPath: Int? + + // MARK: - UI Components + + private let courseImageView = UIImageView().then { + $0.backgroundColor = .g3 + $0.contentMode = .scaleToFill + $0.layer.cornerRadius = 5 + $0.clipsToBounds = true + } + + private let imageCoverView = UIImageView().then { + $0.backgroundColor = .m1.withAlphaComponent(0.2) + $0.layer.cornerRadius = 5 + $0.isHidden = true + } + + private let titleLabel = UILabel().then { + $0.text = "์ œ๋ชฉ" + $0.font = .b4 + $0.textColor = .g1 + } + + private let locationLabel = UILabel().then { + $0.text = "์œ„์น˜" + $0.font = .b6 + $0.textColor = .g2 + } + + private lazy var labelStackView = UIStackView( + arrangedSubviews: [titleLabel, locationLabel] + ).then { + $0.axis = .vertical + $0.alignment = .leading + } + + private let likeButton = UIButton(type: .custom).then { + $0.setImage(ImageLiterals.icHeartFill, for: .selected) + $0.setImage(ImageLiterals.icHeart, for: .normal) + $0.backgroundColor = .w1 + } + + private let selectIndicatorButton = UIButton(type: .custom).then { + $0.setImage(ImageLiterals.icCheckFill, for: .selected) + $0.setImage(ImageLiterals.icCheck, for: .normal) + $0.isSelected = false + $0.isHidden = true + } + + // MARK: - initialization + + override init(frame: CGRect) { + super.init(frame: frame) + self.setUI() + self.setLayout() + self.setAddTarget() + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - Methods + +extension MarathonCourseListCVC { + private func setAddTarget() { + likeButton.addTarget(self, action: #selector(likeButtonDidTap), for: .touchUpInside) + } + + func setData(imageURL: String, title: String, location: String?, didLike: Bool?, indexPath: Int? = nil, isEditMode: Bool = false) { + self.courseImageView.setImage(with: imageURL) + self.titleLabel.text = title + self.indexPath = indexPath + if let location = location { + self.locationLabel.text = location + } + if let didLike = didLike { + self.likeButton.isSelected = didLike + } + self.selectIndicatorButton.isHidden = !isEditMode + } + + func selectCell(didSelect: Bool) { + if didSelect { + courseImageView.layer.borderColor = UIColor.m1.cgColor + courseImageView.layer.borderWidth = 2 + imageCoverView.isHidden = false + selectIndicatorButton.isSelected = true + } else { + courseImageView.layer.borderColor = UIColor.clear.cgColor + imageCoverView.isHidden = true + selectIndicatorButton.isSelected = false + + } + } +} + +// MARK: - @objc Function + +extension MarathonCourseListCVC { + @objc func likeButtonDidTap(_ sender: UIButton) { + guard let indexPath = self.indexPath else { return } + if UserManager.shared.userType != .visitor { + sender.isSelected.toggle() + } + delegate?.likeButtonTapped(wantsTolike: (sender.isSelected == true), index: indexPath) + } +} + +// MARK: - UI & Layout + +extension MarathonCourseListCVC { + private func setUI() { + self.contentView.backgroundColor = .w1 + } + + private func setLayout() { + self.contentView.addSubviews(courseImageView, imageCoverView, labelStackView, likeButton, selectIndicatorButton) + + courseImageView.snp.makeConstraints { make in + make.leading.top.trailing.equalToSuperview() + let imageHeight = contentView.frame.width * (111/156) + make.height.equalTo(imageHeight) + } + + imageCoverView.snp.makeConstraints { make in + make.edges.equalTo(courseImageView) + } + + likeButton.snp.makeConstraints { make in + make.top.equalTo(courseImageView.snp.bottom).offset(4) + make.trailing.equalToSuperview() + make.width.equalTo(22) + make.height.equalTo(20) + } + + selectIndicatorButton.snp.makeConstraints { make in + make.top.leading.equalToSuperview().inset(8) + make.leading.equalToSuperview().offset(8) + make.width.equalTo(20) + make.height.equalTo(20) + } + + labelStackView.snp.makeConstraints { make in + make.top.equalTo(courseImageView.snp.bottom).offset(4) + make.leading.equalToSuperview() + make.width.equalTo(courseImageView.snp.width).multipliedBy(0.7) + } + } + + func setCellType(type: MarathonCourseListCVCType) { + switch type { + case .title: + self.locationLabel.isHidden = true + self.likeButton.isHidden = true + case .titleWithLocation: + self.locationLabel.isHidden = false + self.likeButton.isHidden = true + case .all: + self.locationLabel.isHidden = false + self.likeButton.isHidden = false + } + } +} diff --git a/Runnect-iOS/Runnect-iOS/Presentation/SignIn/VC/SignInSocialLoginVC.swift b/Runnect-iOS/Runnect-iOS/Presentation/SignIn/VC/SignInSocialLoginVC.swift index fa94ab0d..38843ac1 100644 --- a/Runnect-iOS/Runnect-iOS/Presentation/SignIn/VC/SignInSocialLoginVC.swift +++ b/Runnect-iOS/Runnect-iOS/Presentation/SignIn/VC/SignInSocialLoginVC.swift @@ -211,12 +211,15 @@ extension SignInSocialLoginVC: ASAuthorizationControllerPresentationContextProvi print("token : \(String(describing: tokeStr))") UserManager.shared.signIn(token: tokeStr, provider: "APPLE") { [weak self] result in + switch result { case .success(let type): + type == "Signup" ? self?.pushToNickNameSetUpVC() : self?.pushToTabBarController() case .failure(let error): print(error) self?.showNetworkFailureToast() + } } default: